@truss-harness/tui 0.1.16 → 0.1.18
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/bin.js +151 -966
- package/dist/components.d.ts +44 -0
- package/dist/components.js +60 -0
- package/dist/display.d.ts +10 -0
- package/dist/display.js +88 -0
- package/dist/input-controller.d.ts +74 -0
- package/dist/input-controller.js +251 -0
- package/dist/layout.d.ts +23 -0
- package/dist/layout.js +39 -0
- package/dist/processes.d.ts +7 -0
- package/dist/processes.js +65 -0
- package/dist/runtime-session.d.ts +40 -0
- package/dist/runtime-session.js +287 -0
- package/dist/terminal-controller.d.ts +15 -0
- package/dist/terminal-controller.js +57 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +1 -0
- package/dist/workspace-files.d.ts +35 -0
- package/dist/workspace-files.js +169 -0
- package/dist/workspace-view.d.ts +37 -0
- package/dist/workspace-view.js +72 -0
- package/package.json +3 -3
package/dist/bin.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { jsx as _jsx
|
|
3
|
-
import {
|
|
4
|
-
import { execFile as execFileCallback } from "node:child_process";
|
|
5
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
6
|
-
import { join, relative } from "node:path";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
7
4
|
import { cwd } from "node:process";
|
|
8
|
-
import { promisify } from "node:util";
|
|
9
5
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { Box, render, Text, useApp, useInput } from "ink";
|
|
11
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
-
import { cloudProviderDefinition, detectLocalEndpoints, isCloudProviderId, isLocalEndpointKind, listLocalModels, } from "@truss-harness/provider-openai-compatible";
|
|
13
6
|
import { brand } from "@truss-harness/branding";
|
|
14
|
-
import { resolveConfiguration, } from "@truss-harness/cli/config";
|
|
15
|
-
import { createClientRuntime, } from "@truss-harness/cli/runtime";
|
|
16
7
|
import { FileAgentProfileStore } from "@truss-harness/cli/agents";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
8
|
+
import { resolveConfiguration, } from "@truss-harness/cli/config";
|
|
9
|
+
import { detectLocalEndpoints, isLocalEndpointKind, listLocalModels, } from "@truss-harness/provider-openai-compatible";
|
|
10
|
+
import { render, useApp } from "ink";
|
|
11
|
+
import { useEffect, useMemo, useReducer, useState } from "react";
|
|
12
|
+
import { chatDisplayLines, configuredContextWindow, estimateTokens, visibleLines, } from "./display.js";
|
|
13
|
+
import { useTuiInputController } from "./input-controller.js";
|
|
14
|
+
import { calculateLayout, focusReducer } from "./layout.js";
|
|
15
|
+
import { openExternalPreview } from "./processes.js";
|
|
16
|
+
import { useRuntimeSessionController } from "./runtime-session.js";
|
|
17
|
+
import { useTerminalController } from "./terminal-controller.js";
|
|
18
|
+
import { tuiTheme } from "./theme.js";
|
|
19
|
+
import { useWorkspaceFiles } from "./workspace-files.js";
|
|
20
|
+
import { TuiWorkspaceView } from "./workspace-view.js";
|
|
20
21
|
const tuiHelp = `${brand.productName} TUI
|
|
21
22
|
|
|
22
23
|
Full-screen terminal workspace for local coding models.
|
|
@@ -28,41 +29,6 @@ The TUI starts in the current workspace and reads the same configuration
|
|
|
28
29
|
profiles as ${brand.cliCommand}. Press ? inside the TUI for its complete
|
|
29
30
|
keyboard-control reference.
|
|
30
31
|
`;
|
|
31
|
-
const tuiControlHelp = [
|
|
32
|
-
[
|
|
33
|
-
"NAVIGATION",
|
|
34
|
-
"Tab / Shift+Tab move focus; Ctrl+Left / Ctrl+Right move to an adjacent pane.",
|
|
35
|
-
],
|
|
36
|
-
[
|
|
37
|
-
"FILES",
|
|
38
|
-
"/ opens fuzzy file search; Up/Down select; Left/Right collapse or expand; Enter opens.",
|
|
39
|
-
],
|
|
40
|
-
[
|
|
41
|
-
"EDITOR",
|
|
42
|
-
"Up/Down scroll; d toggles Git diff; o opens the detected preview URL externally.",
|
|
43
|
-
],
|
|
44
|
-
[
|
|
45
|
-
"AGENT",
|
|
46
|
-
"Enter sends; a opens saved multi-agent profiles; Up/Down scroll chat; n starts a new conversation outside the chat input.",
|
|
47
|
-
],
|
|
48
|
-
[
|
|
49
|
-
"MCP",
|
|
50
|
-
"c opens credential-safe server and tool status; R tests configured servers.",
|
|
51
|
-
],
|
|
52
|
-
[
|
|
53
|
-
"TERMINAL",
|
|
54
|
-
"Type directly and press Enter; Up/Down scroll output; slash commands run through Truss.",
|
|
55
|
-
],
|
|
56
|
-
[
|
|
57
|
-
"SETTINGS",
|
|
58
|
-
"m opens settings outside chat; Tab changes fields; Theme previews forest, sage, and dusk.",
|
|
59
|
-
],
|
|
60
|
-
["APPROVALS", "Y or Enter allows; N or Escape denies."],
|
|
61
|
-
[
|
|
62
|
-
"CANCEL / EXIT",
|
|
63
|
-
"Escape cancels an active agent run. Ctrl+C stops a process or run, and exits while idle.",
|
|
64
|
-
],
|
|
65
|
-
];
|
|
66
32
|
if (process.argv
|
|
67
33
|
.slice(2)
|
|
68
34
|
.some((argument) => argument === "--help" || argument === "-h")) {
|
|
@@ -97,177 +63,6 @@ function ensureInteractiveTerminal() {
|
|
|
97
63
|
process.exit(1);
|
|
98
64
|
}
|
|
99
65
|
ensureInteractiveTerminal();
|
|
100
|
-
const execFile = promisify(execFileCallback);
|
|
101
|
-
const ignoredDirectories = new Set([
|
|
102
|
-
".git",
|
|
103
|
-
".next",
|
|
104
|
-
".truss-harness",
|
|
105
|
-
"node_modules",
|
|
106
|
-
"dist",
|
|
107
|
-
"coverage",
|
|
108
|
-
]);
|
|
109
|
-
const focusOrder = ["files", "editor", "chat", "terminal"];
|
|
110
|
-
function truncate(value, length) {
|
|
111
|
-
return value.length <= length
|
|
112
|
-
? value
|
|
113
|
-
: `${value.slice(0, Math.max(0, length - 3))}...`;
|
|
114
|
-
}
|
|
115
|
-
function clamp(value, minimum, maximum) {
|
|
116
|
-
return Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
|
|
117
|
-
}
|
|
118
|
-
function visibleLines(items, count, offset) {
|
|
119
|
-
const end = Math.max(0, items.length - clamp(offset, 0, Math.max(0, items.length - count)));
|
|
120
|
-
return items.slice(Math.max(0, end - count), end);
|
|
121
|
-
}
|
|
122
|
-
function estimateTokens(value) {
|
|
123
|
-
const trimmed = value.trim();
|
|
124
|
-
return trimmed ? Math.ceil(trimmed.length / 4) : 0;
|
|
125
|
-
}
|
|
126
|
-
function formatTokenCount(value) {
|
|
127
|
-
if (value < 1_000)
|
|
128
|
-
return String(Math.round(value));
|
|
129
|
-
return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1).replace(/\.0$/, "")}k`;
|
|
130
|
-
}
|
|
131
|
-
function wrapText(value, width) {
|
|
132
|
-
const result = [];
|
|
133
|
-
for (const sourceLine of value.split(/\r?\n/)) {
|
|
134
|
-
let remaining = sourceLine || " ";
|
|
135
|
-
while (remaining.length > width) {
|
|
136
|
-
const breakAt = Math.max(1, remaining.lastIndexOf(" ", width));
|
|
137
|
-
result.push(remaining.slice(0, breakAt).trimEnd());
|
|
138
|
-
remaining = remaining.slice(breakAt).trimStart();
|
|
139
|
-
}
|
|
140
|
-
result.push(remaining);
|
|
141
|
-
}
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
function plainChatText(value) {
|
|
145
|
-
let inCodeBlock = false;
|
|
146
|
-
return value
|
|
147
|
-
.split(/\r?\n/)
|
|
148
|
-
.flatMap((sourceLine) => {
|
|
149
|
-
if (/^\s*```/.test(sourceLine)) {
|
|
150
|
-
inCodeBlock = !inCodeBlock;
|
|
151
|
-
return [];
|
|
152
|
-
}
|
|
153
|
-
if (inCodeBlock)
|
|
154
|
-
return [` ${sourceLine}`];
|
|
155
|
-
const line = sourceLine
|
|
156
|
-
.replace(/^\s{0,3}#{1,6}\s+/, "")
|
|
157
|
-
.replace(/^\s*>\s?/, "")
|
|
158
|
-
.replace(/^\s*[-*+]\s+/, "- ")
|
|
159
|
-
.replace(/^\s*\d+[.)]\s+/, "- ")
|
|
160
|
-
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
161
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
162
|
-
.replace(/(\*\*|__|\*|_|~~)/g, "");
|
|
163
|
-
return /^\s*[-:|]+\s*$/.test(line) ? [] : [line];
|
|
164
|
-
})
|
|
165
|
-
.join("\n");
|
|
166
|
-
}
|
|
167
|
-
function chatDisplayLines(messages, busy, width) {
|
|
168
|
-
const transcript = messages.flatMap((message) => {
|
|
169
|
-
const content = message.content
|
|
170
|
-
? message.role === "assistant"
|
|
171
|
-
? plainChatText(message.content)
|
|
172
|
-
: message.content
|
|
173
|
-
: busy && message.role === "assistant"
|
|
174
|
-
? "Thinking..."
|
|
175
|
-
: "";
|
|
176
|
-
return [
|
|
177
|
-
{
|
|
178
|
-
role: message.role,
|
|
179
|
-
text: message.role === "user" ? "YOU" : "AGENT",
|
|
180
|
-
header: true,
|
|
181
|
-
},
|
|
182
|
-
...wrapText(content, width).map((text) => ({
|
|
183
|
-
role: message.role,
|
|
184
|
-
text,
|
|
185
|
-
header: false,
|
|
186
|
-
})),
|
|
187
|
-
];
|
|
188
|
-
});
|
|
189
|
-
return transcript;
|
|
190
|
-
}
|
|
191
|
-
function configuredContextWindow() {
|
|
192
|
-
const configured = Number.parseInt(process.env.TRUSS_HARNESS_CONTEXT_WINDOW ?? "8192", 10);
|
|
193
|
-
return Number.isFinite(configured)
|
|
194
|
-
? Math.max(512, Math.min(1_000_000, configured))
|
|
195
|
-
: 8_192;
|
|
196
|
-
}
|
|
197
|
-
async function collectFiles(root, current = root, result = []) {
|
|
198
|
-
if (result.length >= 2_000)
|
|
199
|
-
return result;
|
|
200
|
-
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
201
|
-
if (result.length >= 2_000)
|
|
202
|
-
break;
|
|
203
|
-
if (entry.isDirectory() && !ignoredDirectories.has(entry.name))
|
|
204
|
-
await collectFiles(root, join(current, entry.name), result);
|
|
205
|
-
if (entry.isFile())
|
|
206
|
-
result.push({ path: relative(root, join(current, entry.name)) });
|
|
207
|
-
}
|
|
208
|
-
return result.sort((left, right) => left.path.localeCompare(right.path));
|
|
209
|
-
}
|
|
210
|
-
async function runTrackedCommand(command, workspaceRoot, onOutput, onProcess) {
|
|
211
|
-
await new Promise((resolve, reject) => {
|
|
212
|
-
const child = spawn(command, {
|
|
213
|
-
cwd: workspaceRoot,
|
|
214
|
-
shell: true,
|
|
215
|
-
windowsHide: true,
|
|
216
|
-
});
|
|
217
|
-
onProcess?.(child);
|
|
218
|
-
child.stdout.on("data", (data) => onOutput(data.toString()));
|
|
219
|
-
child.stderr.on("data", (data) => onOutput(data.toString()));
|
|
220
|
-
child.on("error", reject);
|
|
221
|
-
child.on("close", (code) => {
|
|
222
|
-
onProcess?.(undefined);
|
|
223
|
-
onOutput(`\n[process exited: ${code ?? "unknown"}]\n`);
|
|
224
|
-
resolve();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
function detectedPreviewUrl(output) {
|
|
229
|
-
const match = output.match(/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(?:\/[^\s]*)?/i);
|
|
230
|
-
return match?.[0].replace(/[),.;]+$/, "").replace("0.0.0.0", "127.0.0.1");
|
|
231
|
-
}
|
|
232
|
-
function normalizedPreviewUrl(value) {
|
|
233
|
-
const normalized = /^[a-z][a-z\d+.-]*:\/\//i.test(value.trim())
|
|
234
|
-
? value.trim()
|
|
235
|
-
: `http://${value.trim()}`;
|
|
236
|
-
const url = new URL(normalized);
|
|
237
|
-
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
238
|
-
throw new Error("Preview URLs must use HTTP or HTTPS.");
|
|
239
|
-
return url.toString();
|
|
240
|
-
}
|
|
241
|
-
function openExternalPreview(value) {
|
|
242
|
-
const url = normalizedPreviewUrl(value);
|
|
243
|
-
const [command, arguments_] = process.platform === "win32"
|
|
244
|
-
? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]]
|
|
245
|
-
: process.platform === "darwin"
|
|
246
|
-
? ["open", [url]]
|
|
247
|
-
: ["xdg-open", [url]];
|
|
248
|
-
const child = spawn(command, arguments_, {
|
|
249
|
-
detached: true,
|
|
250
|
-
stdio: "ignore",
|
|
251
|
-
windowsHide: true,
|
|
252
|
-
});
|
|
253
|
-
child.once("error", () => undefined);
|
|
254
|
-
child.unref();
|
|
255
|
-
}
|
|
256
|
-
async function stopProcessTree(child) {
|
|
257
|
-
if (process.platform === "win32" && child.pid) {
|
|
258
|
-
try {
|
|
259
|
-
await execFile("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
// Fall back to the Node process handle if taskkill is unavailable.
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
child.kill();
|
|
267
|
-
}
|
|
268
|
-
function Panel({ title, active, theme, children, }) {
|
|
269
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: active ? theme.focus : theme.panel, paddingX: 1, width: "100%", height: "100%", overflow: "hidden", children: [_jsx(Text, { color: active ? theme.focus : theme.muted, bold: true, children: title }), children] }));
|
|
270
|
-
}
|
|
271
66
|
function App({ initialConfiguration, }) {
|
|
272
67
|
const { exit } = useApp();
|
|
273
68
|
const [viewport, setViewport] = useState(() => ({
|
|
@@ -275,43 +70,8 @@ function App({ initialConfiguration, }) {
|
|
|
275
70
|
rows: process.stdout.rows || 36,
|
|
276
71
|
}));
|
|
277
72
|
const workspaceRoot = useMemo(() => cwd(), []);
|
|
278
|
-
const [
|
|
279
|
-
const [client, setClient] = useState();
|
|
280
|
-
const [mcpStatuses, setMcpStatuses] = useState(() => Object.entries(initialConfiguration?.mcpServers ?? {}).map(([name, server]) => ({
|
|
281
|
-
name,
|
|
282
|
-
state: server.enabled === false ? "disabled" : "idle",
|
|
283
|
-
toolCount: 0,
|
|
284
|
-
})));
|
|
285
|
-
const [sessionId, setSessionId] = useState();
|
|
286
|
-
const [files, setFiles] = useState([]);
|
|
287
|
-
const [fileIndex, setFileIndex] = useState(0);
|
|
288
|
-
const [collapsedDirectories, setCollapsedDirectories] = useState(() => new Set());
|
|
289
|
-
const [fileSearchInput, setFileSearchInput] = useState("");
|
|
290
|
-
const [fileSearchIndex, setFileSearchIndex] = useState(0);
|
|
291
|
-
const [openFilePath, setOpenFilePath] = useState();
|
|
292
|
-
const [editor, setEditor] = useState("Select a file from the workspace tree.");
|
|
293
|
-
const [editorTitle, setEditorTitle] = useState("Preview");
|
|
294
|
-
const [isDiff, setIsDiff] = useState(false);
|
|
295
|
-
const [focus, setFocus] = useState("chat");
|
|
73
|
+
const [focus, dispatchFocus] = useReducer(focusReducer, "chat");
|
|
296
74
|
const [screen, setScreen] = useState(initialConfiguration ? "workspace" : "settings");
|
|
297
|
-
const [chatInput, setChatInput] = useState("");
|
|
298
|
-
const [chat, setChat] = useState([]);
|
|
299
|
-
const [chatScroll, setChatScroll] = useState(0);
|
|
300
|
-
const [streamMetrics, setStreamMetrics] = useState({
|
|
301
|
-
startedAt: 0,
|
|
302
|
-
generatedTokens: 0,
|
|
303
|
-
});
|
|
304
|
-
const [terminalLines, setTerminalLines] = useState([
|
|
305
|
-
"Terminal ready. Shell commands run in the workspace root; slash commands run locally.",
|
|
306
|
-
]);
|
|
307
|
-
const [terminalScroll, setTerminalScroll] = useState(0);
|
|
308
|
-
const [previewUrl, setPreviewUrl] = useState();
|
|
309
|
-
const [editorScroll, setEditorScroll] = useState(0);
|
|
310
|
-
const [busy, setBusy] = useState(false);
|
|
311
|
-
const [runStatus, setRunStatus] = useState("ready");
|
|
312
|
-
const [activePlan, setActivePlan] = useState();
|
|
313
|
-
const [pendingTool, setPendingTool] = useState();
|
|
314
|
-
const [commandInput, setCommandInput] = useState("");
|
|
315
75
|
const [endpoints, setEndpoints] = useState([]);
|
|
316
76
|
const [serverIndex, setServerIndex] = useState(0);
|
|
317
77
|
const [models, setModels] = useState([]);
|
|
@@ -325,26 +85,10 @@ function App({ initialConfiguration, }) {
|
|
|
325
85
|
const [agentMode] = useState(initialConfiguration?.mode ?? "chat");
|
|
326
86
|
const [permissionMode] = useState(initialConfiguration?.permission ?? "ask");
|
|
327
87
|
const [internetAccess, setInternetAccess] = useState(initialConfiguration?.internetAccess ?? false);
|
|
328
|
-
const approvalResolvers = useRef(new Map());
|
|
329
|
-
const abortController = useRef(undefined);
|
|
330
|
-
const terminalProcess = useRef(undefined);
|
|
331
88
|
const contextWindow = useMemo(configuredContextWindow, []);
|
|
332
|
-
const contextTokens = useMemo(() => chat.reduce((total, message) => total + estimateTokens(message.content), 400), [chat]);
|
|
333
|
-
const tokensPerSecond = streamMetrics.startedAt && streamMetrics.generatedTokens
|
|
334
|
-
? streamMetrics.generatedTokens /
|
|
335
|
-
Math.max((Date.now() - streamMetrics.startedAt) / 1_000, 0.1)
|
|
336
|
-
: undefined;
|
|
337
89
|
const theme = tuiTheme(themeName);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
if (!next.length)
|
|
341
|
-
return;
|
|
342
|
-
const detected = detectedPreviewUrl(output);
|
|
343
|
-
if (detected)
|
|
344
|
-
setPreviewUrl(detected);
|
|
345
|
-
setTerminalScroll(0);
|
|
346
|
-
setTerminalLines((current) => [...current, ...next].slice(-110));
|
|
347
|
-
};
|
|
90
|
+
const terminal = useTerminalController(workspaceRoot);
|
|
91
|
+
const { terminalLines, terminalScroll, setTerminalScroll, commandInput, setCommandInput, previewUrl, appendTerminal, runTerminalInput, interruptProcess, } = terminal;
|
|
348
92
|
const candidates = useMemo(() => [
|
|
349
93
|
...endpoints,
|
|
350
94
|
{
|
|
@@ -355,66 +99,44 @@ function App({ initialConfiguration, }) {
|
|
|
355
99
|
},
|
|
356
100
|
], [endpoints]);
|
|
357
101
|
const selectedEndpoint = candidates[Math.min(serverIndex, Math.max(candidates.length - 1, 0))];
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
102
|
+
const layout = useMemo(() => calculateLayout(viewport), [viewport]);
|
|
103
|
+
const { terminalHeight, chatWidth, editorWidth, compactChatHeight, editorLineCount, overlayWidth, } = layout;
|
|
104
|
+
const workspaceFiles = useWorkspaceFiles({
|
|
105
|
+
workspaceRoot,
|
|
106
|
+
viewportRows: viewport.rows,
|
|
107
|
+
editorWidth,
|
|
108
|
+
editorLineCount,
|
|
109
|
+
focusEditor: () => dispatchFocus({ type: "select", focus: "editor" }),
|
|
110
|
+
closeSearch: () => setScreen("workspace"),
|
|
111
|
+
});
|
|
112
|
+
const { fileIndex, setFileIndex, fileTree, selectedFileTreeEntry, fileSearchInput, setFileSearchInput, fileSearchIndex, setFileSearchIndex, fileSearchResults, openFilePath, editorTitle, setEditorScroll, editorRows, visibleEditorRows, fileTreeStart, visibleFileTree, loadFile, toggleDirectory, openSearchResult, toggleDiff, } = workspaceFiles;
|
|
113
|
+
const runtimeSession = useRuntimeSessionController({
|
|
114
|
+
initialConfiguration,
|
|
115
|
+
workspaceRoot,
|
|
116
|
+
providerKind,
|
|
117
|
+
endpointInput,
|
|
118
|
+
modelInput,
|
|
119
|
+
agentMode,
|
|
120
|
+
permissionMode,
|
|
121
|
+
internetAccess,
|
|
122
|
+
openFilePath,
|
|
123
|
+
contextWindow,
|
|
124
|
+
appendTerminal,
|
|
125
|
+
showScreen: setScreen,
|
|
126
|
+
});
|
|
127
|
+
const { configuration, mcpStatuses, sessionId, chatInput, setChatInput, chat, chatScroll, setChatScroll, busy, runStatus, activePlan, pendingTool, tokensPerSecond, configureRuntime, sendPrompt, resolveApproval, startNewConversation, cancelRun, testMcpConnections, } = runtimeSession;
|
|
128
|
+
const contextTokens = useMemo(() => chat.reduce((total, message) => total + estimateTokens(message.content), 400), [chat]);
|
|
378
129
|
const chatLineCount = Math.max(2, compactChatHeight - (activePlan ? 7 : 0) - 4);
|
|
379
130
|
const chatTranscript = chatDisplayLines(chat, busy, Math.max(12, chatWidth - 5));
|
|
380
131
|
const chatLines = visibleLines(chatTranscript, chatLineCount, chatScroll);
|
|
381
|
-
const editorRows = useMemo(() => {
|
|
382
|
-
const maximumCharacters = Math.max(12, editorWidth - 7);
|
|
383
|
-
return editor.split(/\r?\n/).flatMap((line, sourceIndex) => {
|
|
384
|
-
const color = isDiff
|
|
385
|
-
? line.startsWith("+") && !line.startsWith("+++")
|
|
386
|
-
? "green"
|
|
387
|
-
: line.startsWith("-") && !line.startsWith("---")
|
|
388
|
-
? "red"
|
|
389
|
-
: undefined
|
|
390
|
-
: undefined;
|
|
391
|
-
const tokens = isDiff
|
|
392
|
-
? [{ text: line, color }]
|
|
393
|
-
: syntaxTokens(line, openFilePath ?? "");
|
|
394
|
-
return wrapSyntaxTokens(tokens, maximumCharacters).map((row, rowIndex) => ({
|
|
395
|
-
sourceLine: sourceIndex + 1,
|
|
396
|
-
continuation: rowIndex > 0,
|
|
397
|
-
tokens: row,
|
|
398
|
-
}));
|
|
399
|
-
});
|
|
400
|
-
}, [editor, editorWidth, isDiff, openFilePath]);
|
|
401
|
-
const visibleEditorRows = editorRows.slice(editorScroll, editorScroll + editorLineCount);
|
|
402
132
|
const visibleTerminalLines = visibleLines(terminalLines, Math.max(1, terminalHeight - 4), terminalScroll);
|
|
403
|
-
const fileTreeStart = clamp(fileIndex - Math.floor(editorLineCount / 2), 0, Math.max(0, fileTree.length - editorLineCount));
|
|
404
|
-
const visibleFileTree = fileTree.slice(fileTreeStart, fileTreeStart + editorLineCount);
|
|
405
|
-
const overlayWidth = clamp(viewport.columns - 4, 42, 90);
|
|
406
133
|
useEffect(() => {
|
|
407
|
-
void collectFiles(workspaceRoot)
|
|
408
|
-
.then(setFiles)
|
|
409
|
-
.catch((error) => setEditor(`Unable to list workspace files: ${String(error)}`));
|
|
410
134
|
void new FileAgentProfileStore(workspaceRoot)
|
|
411
135
|
.list()
|
|
412
136
|
.then(setAgentProfiles)
|
|
413
137
|
.catch((error) => appendTerminal(`[agents] ${error instanceof Error ? error.message : String(error)}`));
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
.then(setActivePlan)
|
|
417
|
-
.catch(() => undefined);
|
|
138
|
+
}, [workspaceRoot, appendTerminal]);
|
|
139
|
+
useEffect(() => {
|
|
418
140
|
void detectLocalEndpoints().then((detected) => {
|
|
419
141
|
setEndpoints(detected);
|
|
420
142
|
const preferred = configuration
|
|
@@ -424,20 +146,7 @@ function App({ initialConfiguration, }) {
|
|
|
424
146
|
if (preferred >= 0)
|
|
425
147
|
setServerIndex(preferred);
|
|
426
148
|
});
|
|
427
|
-
}, [
|
|
428
|
-
useEffect(() => {
|
|
429
|
-
setFileIndex((current) => clamp(current, 0, Math.max(0, fileTree.length - 1)));
|
|
430
|
-
}, [fileTree.length]);
|
|
431
|
-
useEffect(() => {
|
|
432
|
-
setEditorScroll((current) => clamp(current, 0, Math.max(0, editorRows.length - editorLineCount)));
|
|
433
|
-
}, [editorRows.length, editorLineCount]);
|
|
434
|
-
useEffect(() => {
|
|
435
|
-
if (!openFilePath)
|
|
436
|
-
return;
|
|
437
|
-
const index = fileTree.findIndex((entry) => entry.kind === "file" && entry.path === openFilePath);
|
|
438
|
-
if (index >= 0)
|
|
439
|
-
setFileIndex(index);
|
|
440
|
-
}, [fileTree, openFilePath]);
|
|
149
|
+
}, [configuration]);
|
|
441
150
|
useEffect(() => {
|
|
442
151
|
const refreshViewport = () => setViewport({
|
|
443
152
|
columns: process.stdout.columns || 120,
|
|
@@ -459,645 +168,121 @@ function App({ initialConfiguration, }) {
|
|
|
459
168
|
baseUrl: endpointInput,
|
|
460
169
|
};
|
|
461
170
|
void listLocalModels(endpoint)
|
|
462
|
-
.then((available) =>
|
|
463
|
-
setModels(available.map((model) => model.name));
|
|
464
|
-
const selected = available.findIndex((model) => model.name === modelInput);
|
|
465
|
-
setModelIndex(Math.max(0, selected));
|
|
466
|
-
})
|
|
171
|
+
.then((available) => setModels(available.map((model) => model.name)))
|
|
467
172
|
.catch(() => setModels([]));
|
|
468
|
-
}, [selectedEndpoint
|
|
173
|
+
}, [selectedEndpoint, endpointInput, providerKind]);
|
|
469
174
|
useEffect(() => {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const unsubscribe = client.events.subscribe((event) => {
|
|
479
|
-
if (event.type === "text_delta") {
|
|
480
|
-
setRunStatus("thinking");
|
|
481
|
-
setChatScroll(0);
|
|
482
|
-
setStreamMetrics((current) => current.startedAt
|
|
483
|
-
? {
|
|
484
|
-
...current,
|
|
485
|
-
generatedTokens: current.generatedTokens + estimateTokens(event.text),
|
|
486
|
-
}
|
|
487
|
-
: current);
|
|
488
|
-
setChat((current) => {
|
|
489
|
-
const last = current.at(-1);
|
|
490
|
-
if (!last || last.role !== "assistant")
|
|
491
|
-
return [...current, { role: "assistant", content: event.text }];
|
|
492
|
-
return [
|
|
493
|
-
...current.slice(0, -1),
|
|
494
|
-
{ role: "assistant", content: last.content + event.text },
|
|
495
|
-
];
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
if (event.type === "tool_call_requested") {
|
|
499
|
-
setRunStatus("tool");
|
|
500
|
-
appendTerminal(`[tool requested] ${event.tool} ${JSON.stringify(event.input)}`);
|
|
501
|
-
}
|
|
502
|
-
if (event.type === "tool_completed") {
|
|
503
|
-
setRunStatus("thinking");
|
|
504
|
-
appendTerminal(`[tool completed] ${event.tool}: ${truncate(event.result.content, 160)}`);
|
|
505
|
-
}
|
|
506
|
-
if (event.type === "plan_updated")
|
|
507
|
-
setActivePlan(event.plan);
|
|
508
|
-
if (event.type === "run_failed") {
|
|
509
|
-
setRunStatus("ready");
|
|
510
|
-
appendTerminal(`[agent error] ${event.error.message}`);
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
return () => {
|
|
514
|
-
unsubscribe();
|
|
515
|
-
unsubscribeMcp();
|
|
516
|
-
void client.dispose();
|
|
517
|
-
};
|
|
518
|
-
}, [client]);
|
|
519
|
-
const loadFile = async (entry) => {
|
|
520
|
-
try {
|
|
521
|
-
setIsDiff(false);
|
|
522
|
-
setEditorScroll(0);
|
|
523
|
-
setOpenFilePath(entry.path);
|
|
524
|
-
setEditorTitle(entry.path);
|
|
525
|
-
setEditor(await readFile(join(workspaceRoot, entry.path), "utf8"));
|
|
526
|
-
setFocus("editor");
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
setEditor(`Unable to read ${entry.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
const toggleDirectory = (path, expand) => {
|
|
533
|
-
setCollapsedDirectories((current) => {
|
|
534
|
-
const next = new Set(current);
|
|
535
|
-
const currentlyExpanded = !next.has(path);
|
|
536
|
-
const shouldExpand = expand ?? !currentlyExpanded;
|
|
537
|
-
if (shouldExpand)
|
|
538
|
-
next.delete(path);
|
|
539
|
-
else
|
|
540
|
-
next.add(path);
|
|
541
|
-
return next;
|
|
542
|
-
});
|
|
175
|
+
const selected = models.indexOf(modelInput);
|
|
176
|
+
setModelIndex(Math.max(0, selected));
|
|
177
|
+
}, [models, modelInput]);
|
|
178
|
+
const refreshAgentProfiles = () => {
|
|
179
|
+
void new FileAgentProfileStore(workspaceRoot)
|
|
180
|
+
.list()
|
|
181
|
+
.then(setAgentProfiles)
|
|
182
|
+
.catch((error) => appendTerminal(`[agents] ${error instanceof Error ? error.message : String(error)}`));
|
|
543
183
|
};
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
parts.pop();
|
|
547
|
-
setCollapsedDirectories((current) => {
|
|
548
|
-
const next = new Set(current);
|
|
549
|
-
let path = "";
|
|
550
|
-
for (const part of parts) {
|
|
551
|
-
path = path ? `${path}/${part}` : part;
|
|
552
|
-
next.delete(path);
|
|
553
|
-
}
|
|
554
|
-
return next;
|
|
555
|
-
});
|
|
556
|
-
setFileSearchInput("");
|
|
557
|
-
setFileSearchIndex(0);
|
|
558
|
-
setScreen("workspace");
|
|
559
|
-
void loadFile(entry);
|
|
184
|
+
const moveFocus = (direction) => {
|
|
185
|
+
dispatchFocus({ type: direction === 1 ? "next" : "previous" });
|
|
560
186
|
};
|
|
561
|
-
const
|
|
562
|
-
if (
|
|
187
|
+
const interruptOrExit = () => {
|
|
188
|
+
if (interruptProcess())
|
|
563
189
|
return;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
setOpenFilePath(entry.path);
|
|
569
|
-
const { stdout } = await execFile("git", ["diff", "--no-ext-diff", "--", entry.path], { cwd: workspaceRoot, maxBuffer: 1_000_000 });
|
|
570
|
-
setEditorTitle(`Diff: ${entry.path}`);
|
|
571
|
-
setEditor(stdout || "No working-tree diff for this file.");
|
|
572
|
-
setIsDiff(true);
|
|
573
|
-
setEditorScroll(0);
|
|
574
|
-
}
|
|
575
|
-
catch (error) {
|
|
576
|
-
setEditor(`Unable to load Git diff: ${error instanceof Error ? error.message : String(error)}`);
|
|
577
|
-
setIsDiff(true);
|
|
578
|
-
}
|
|
190
|
+
if (busy)
|
|
191
|
+
cancelRun();
|
|
192
|
+
else
|
|
193
|
+
exit();
|
|
579
194
|
};
|
|
580
|
-
const
|
|
581
|
-
if (!endpointInput.trim() || !modelInput.trim()) {
|
|
582
|
-
appendTerminal("[configuration] An endpoint and model are required.");
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
const nextConfiguration = {
|
|
586
|
-
workspaceRoot,
|
|
587
|
-
provider: providerKind,
|
|
588
|
-
baseUrl: endpointInput.trim(),
|
|
589
|
-
model: modelInput.trim(),
|
|
590
|
-
apiKey: initialConfiguration?.provider === providerKind
|
|
591
|
-
? initialConfiguration.apiKey
|
|
592
|
-
: (process.env.TRUSS_HARNESS_API_KEY ??
|
|
593
|
-
(isCloudProviderId(providerKind)
|
|
594
|
-
? process.env[cloudProviderDefinition(providerKind)
|
|
595
|
-
.apiKeyEnvironmentVariable]
|
|
596
|
-
: undefined)),
|
|
597
|
-
systemPrompt: process.env.TRUSS_HARNESS_SYSTEM_PROMPT,
|
|
598
|
-
mode: agentMode,
|
|
599
|
-
internetAccess,
|
|
600
|
-
mcpServers: initialConfiguration?.mcpServers,
|
|
601
|
-
approval: {
|
|
602
|
-
approve: async (call) => {
|
|
603
|
-
const readOnly = [
|
|
604
|
-
"read_file",
|
|
605
|
-
"list_directory",
|
|
606
|
-
"search_files",
|
|
607
|
-
"grep",
|
|
608
|
-
].includes(call.name);
|
|
609
|
-
if (permissionMode === "auto-all" ||
|
|
610
|
-
(permissionMode === "auto-read" && readOnly))
|
|
611
|
-
return true;
|
|
612
|
-
return new Promise((resolve) => {
|
|
613
|
-
approvalResolvers.current.set(call.id, resolve);
|
|
614
|
-
setPendingTool(call);
|
|
615
|
-
setRunStatus("waiting");
|
|
616
|
-
setScreen("approval");
|
|
617
|
-
});
|
|
618
|
-
},
|
|
619
|
-
},
|
|
620
|
-
};
|
|
621
|
-
setConfiguration(nextConfiguration);
|
|
622
|
-
setRunStatus("thinking");
|
|
195
|
+
const openPreview = (url) => {
|
|
623
196
|
try {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
for (const server of nextClient.mcpServers) {
|
|
627
|
-
appendTerminal(`[mcp] ${server.name}: ${server.state}${server.error ? ` (${server.error})` : ` (${server.toolCount} tools)`}`);
|
|
628
|
-
}
|
|
197
|
+
openExternalPreview(url);
|
|
198
|
+
appendTerminal(`[preview] Opened ${url}`);
|
|
629
199
|
}
|
|
630
200
|
catch (error) {
|
|
631
|
-
appendTerminal(`[
|
|
632
|
-
setRunStatus("ready");
|
|
633
|
-
return;
|
|
201
|
+
appendTerminal(`[preview error] ${error instanceof Error ? error.message : String(error)}`);
|
|
634
202
|
}
|
|
635
|
-
setSessionId(undefined);
|
|
636
|
-
setChat([]);
|
|
637
|
-
setChatScroll(0);
|
|
638
|
-
setStreamMetrics({ startedAt: 0, generatedTokens: 0 });
|
|
639
|
-
setScreen("workspace");
|
|
640
|
-
appendTerminal(`[configuration] ${nextConfiguration.provider} ${nextConfiguration.model} at ${nextConfiguration.baseUrl}`);
|
|
641
|
-
setRunStatus("ready");
|
|
642
203
|
};
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
: await client.runtime.createSession();
|
|
706
|
-
if (!session)
|
|
707
|
-
throw new Error("Conversation is unavailable.");
|
|
708
|
-
setSessionId(session.id);
|
|
709
|
-
await client.runtime.run(session.id, prompt, controller.signal, await openFileContext());
|
|
710
|
-
}
|
|
711
|
-
catch (error) {
|
|
712
|
-
if (!controller.signal.aborted)
|
|
713
|
-
appendTerminal(`[agent error] ${error instanceof Error ? error.message : String(error)}`);
|
|
714
|
-
}
|
|
715
|
-
finally {
|
|
716
|
-
setBusy(false);
|
|
717
|
-
setRunStatus("ready");
|
|
718
|
-
abortController.current = undefined;
|
|
719
|
-
}
|
|
720
|
-
};
|
|
721
|
-
const resolveApproval = (approved) => {
|
|
722
|
-
if (pendingTool)
|
|
723
|
-
approvalResolvers.current.get(pendingTool.id)?.(approved);
|
|
724
|
-
setPendingTool(undefined);
|
|
725
|
-
setRunStatus(approved ? "thinking" : "ready");
|
|
726
|
-
setScreen("workspace");
|
|
727
|
-
};
|
|
728
|
-
const startNewConversation = () => {
|
|
729
|
-
if (busy) {
|
|
730
|
-
appendTerminal("[conversation] Stop the active run before starting a new conversation.");
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
setSessionId(undefined);
|
|
734
|
-
setChat([]);
|
|
735
|
-
setChatInput("");
|
|
736
|
-
setChatScroll(0);
|
|
737
|
-
setStreamMetrics({ startedAt: 0, generatedTokens: 0 });
|
|
738
|
-
appendTerminal("[conversation] Started a new conversation.");
|
|
739
|
-
};
|
|
740
|
-
const runTerminalInput = async (input) => {
|
|
741
|
-
const command = await executeWorkspaceCommand({ workspaceRoot, input });
|
|
742
|
-
if (command.handled) {
|
|
743
|
-
appendTerminal(`[workspace command] ${command.command ?? input}: ${command.ok ? "completed" : "failed"}`);
|
|
744
|
-
appendTerminal(command.message);
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
await runTrackedCommand(input, workspaceRoot, appendTerminal, (process) => {
|
|
748
|
-
terminalProcess.current = process;
|
|
749
|
-
});
|
|
750
|
-
};
|
|
751
|
-
const moveFocus = (direction) => {
|
|
752
|
-
setFocus((current) => {
|
|
753
|
-
const currentIndex = focusOrder.indexOf(current);
|
|
754
|
-
return focusOrder[(currentIndex + direction + focusOrder.length) % focusOrder.length];
|
|
755
|
-
});
|
|
756
|
-
};
|
|
757
|
-
useInput((input, key) => {
|
|
758
|
-
if (key.ctrl && input === "c") {
|
|
759
|
-
if (terminalProcess.current) {
|
|
760
|
-
const process = terminalProcess.current;
|
|
761
|
-
terminalProcess.current = undefined;
|
|
762
|
-
void stopProcessTree(process).then(() => appendTerminal("[terminal] Process stopped."));
|
|
763
|
-
}
|
|
764
|
-
else if (busy) {
|
|
765
|
-
abortController.current?.abort();
|
|
766
|
-
setRunStatus("ready");
|
|
767
|
-
appendTerminal("[agent] Cancellation requested.");
|
|
768
|
-
}
|
|
769
|
-
else
|
|
770
|
-
exit();
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
if (screen === "approval") {
|
|
774
|
-
if (input.toLowerCase() === "y" || key.return)
|
|
775
|
-
resolveApproval(true);
|
|
776
|
-
if (input.toLowerCase() === "n" || key.escape)
|
|
777
|
-
resolveApproval(false);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
if (screen === "file-search") {
|
|
781
|
-
if (key.escape) {
|
|
782
|
-
setFileSearchInput("");
|
|
783
|
-
setFileSearchIndex(0);
|
|
784
|
-
setScreen("workspace");
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
if (key.upArrow) {
|
|
788
|
-
setFileSearchIndex((current) => Math.max(0, current - 1));
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
if (key.downArrow) {
|
|
792
|
-
setFileSearchIndex((current) => Math.min(Math.max(0, fileSearchResults.length - 1), current + 1));
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
if (key.return) {
|
|
796
|
-
const result = fileSearchResults[Math.min(fileSearchIndex, Math.max(0, fileSearchResults.length - 1))];
|
|
797
|
-
if (result)
|
|
798
|
-
openSearchResult(result);
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
if (key.backspace || key.delete) {
|
|
802
|
-
setFileSearchInput((current) => current.slice(0, -1));
|
|
803
|
-
setFileSearchIndex(0);
|
|
804
|
-
}
|
|
805
|
-
else if (input) {
|
|
806
|
-
setFileSearchInput((current) => current + input);
|
|
807
|
-
setFileSearchIndex(0);
|
|
808
|
-
}
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
if (screen === "help") {
|
|
812
|
-
setScreen("workspace");
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
if (screen === "agents") {
|
|
816
|
-
if (key.escape || input.toLowerCase() === "a")
|
|
817
|
-
setScreen("workspace");
|
|
818
|
-
if (input.toLowerCase() === "r")
|
|
819
|
-
void new FileAgentProfileStore(workspaceRoot)
|
|
820
|
-
.list()
|
|
821
|
-
.then(setAgentProfiles)
|
|
822
|
-
.catch((error) => appendTerminal(`[agents] ${error instanceof Error ? error.message : String(error)}`));
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
if (screen === "mcp") {
|
|
826
|
-
if (key.escape || input.toLowerCase() === "c") {
|
|
827
|
-
setScreen("workspace");
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
if (input.toLowerCase() === "r" && configuration) {
|
|
831
|
-
void createClientRuntime({ ...configuration, mode: "edit" })
|
|
832
|
-
.then(async (inspection) => {
|
|
833
|
-
setMcpStatuses(inspection.mcpServers);
|
|
834
|
-
appendTerminal("[mcp] Tested configured servers.");
|
|
835
|
-
await inspection.dispose();
|
|
836
|
-
})
|
|
837
|
-
.catch((error) => appendTerminal(`[mcp] ${error instanceof Error ? error.message : String(error)}`));
|
|
838
|
-
}
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
if (screen === "settings") {
|
|
842
|
-
if (key.escape) {
|
|
843
|
-
setScreen("workspace");
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
if (key.tab) {
|
|
847
|
-
const fields = [
|
|
848
|
-
"server",
|
|
849
|
-
"endpoint",
|
|
850
|
-
"model",
|
|
851
|
-
"internet",
|
|
852
|
-
"theme",
|
|
853
|
-
];
|
|
854
|
-
setSettingsField((current) => {
|
|
855
|
-
const index = fields.indexOf(current);
|
|
856
|
-
return fields[(index + (key.shift ? -1 : 1) + fields.length) % fields.length];
|
|
857
|
-
});
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
if (settingsField === "server") {
|
|
861
|
-
if (key.upArrow)
|
|
862
|
-
setServerIndex((current) => Math.max(0, current - 1));
|
|
863
|
-
if (key.downArrow)
|
|
864
|
-
setServerIndex((current) => Math.min(candidates.length - 1, current + 1));
|
|
865
|
-
if (key.return && selectedEndpoint) {
|
|
866
|
-
setProviderKind(selectedEndpoint.kind);
|
|
867
|
-
setEndpointInput(selectedEndpoint.baseUrl);
|
|
868
|
-
setSettingsField("model");
|
|
869
|
-
}
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
if (settingsField === "model") {
|
|
873
|
-
if (key.upArrow)
|
|
874
|
-
setModelIndex((current) => Math.max(0, current - 1));
|
|
875
|
-
if (key.downArrow)
|
|
876
|
-
setModelIndex((current) => Math.min(models.length - 1, current + 1));
|
|
877
|
-
if (models[modelIndex] && (key.return || key.rightArrow))
|
|
878
|
-
setModelInput(models[modelIndex]);
|
|
879
|
-
}
|
|
880
|
-
if (settingsField === "internet") {
|
|
881
|
-
if (input === " " || key.leftArrow || key.rightArrow)
|
|
882
|
-
setInternetAccess((current) => !current);
|
|
883
|
-
if (key.return && endpointInput && modelInput)
|
|
884
|
-
void configureRuntime();
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
if (settingsField === "theme") {
|
|
888
|
-
if (key.leftArrow || key.upArrow)
|
|
889
|
-
setThemeName((current) => tuiThemeNames[(tuiThemeNames.indexOf(current) - 1 + tuiThemeNames.length) %
|
|
890
|
-
tuiThemeNames.length]);
|
|
891
|
-
if (key.rightArrow || key.downArrow || input === " ")
|
|
892
|
-
setThemeName((current) => tuiThemeNames[(tuiThemeNames.indexOf(current) + 1) % tuiThemeNames.length]);
|
|
893
|
-
if (key.return && endpointInput && modelInput)
|
|
894
|
-
void configureRuntime();
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
if (key.return && endpointInput && modelInput) {
|
|
898
|
-
void configureRuntime();
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
const setter = settingsField === "endpoint" ? setEndpointInput : setModelInput;
|
|
902
|
-
if (key.backspace || key.delete)
|
|
903
|
-
setter((current) => current.slice(0, -1));
|
|
904
|
-
else if (!key.return && input)
|
|
905
|
-
setter((current) => current + input);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
if (key.ctrl && (key.leftArrow || key.rightArrow)) {
|
|
909
|
-
moveFocus(key.leftArrow ? -1 : 1);
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
if (input.toLowerCase() === "a" &&
|
|
913
|
-
focus !== "chat" &&
|
|
914
|
-
focus !== "terminal") {
|
|
915
|
-
setScreen("agents");
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
if (key.tab) {
|
|
919
|
-
moveFocus(key.shift ? -1 : 1);
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
if (key.escape && busy) {
|
|
923
|
-
abortController.current?.abort();
|
|
924
|
-
setRunStatus("ready");
|
|
925
|
-
appendTerminal("[agent] Cancellation requested.");
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
if (input === "o" &&
|
|
929
|
-
focus !== "chat" &&
|
|
930
|
-
focus !== "terminal" &&
|
|
931
|
-
previewUrl) {
|
|
932
|
-
try {
|
|
933
|
-
openExternalPreview(previewUrl);
|
|
934
|
-
appendTerminal(`[preview] Opened ${previewUrl}`);
|
|
935
|
-
}
|
|
936
|
-
catch (error) {
|
|
937
|
-
appendTerminal(`[preview error] ${error instanceof Error ? error.message : String(error)}`);
|
|
938
|
-
}
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
if (focus === "terminal") {
|
|
942
|
-
if (key.upArrow) {
|
|
943
|
-
setTerminalScroll((current) => clamp(current + 1, 0, Math.max(0, terminalLines.length - Math.max(1, terminalHeight - 4))));
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
if (key.downArrow) {
|
|
947
|
-
setTerminalScroll((current) => clamp(current - 1, 0, Math.max(0, terminalLines.length - Math.max(1, terminalHeight - 4))));
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
if (key.return) {
|
|
951
|
-
const command = commandInput.trim();
|
|
952
|
-
setCommandInput("");
|
|
953
|
-
if (command)
|
|
954
|
-
void runTerminalInput(command).catch((error) => appendTerminal(`[terminal error] ${String(error)}`));
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
if (key.backspace || key.delete)
|
|
958
|
-
setCommandInput((current) => current.slice(0, -1));
|
|
959
|
-
else if (input)
|
|
960
|
-
setCommandInput((current) => current + input);
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
if (input === "?" && focus !== "chat") {
|
|
964
|
-
setScreen("help");
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
if (input === "m" && focus !== "chat") {
|
|
968
|
-
setScreen("settings");
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
if (input.toLowerCase() === "c" && focus !== "chat") {
|
|
972
|
-
setScreen("mcp");
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
if (input === "n" && focus !== "chat") {
|
|
976
|
-
startNewConversation();
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
if (focus === "files") {
|
|
980
|
-
if (input === "/") {
|
|
981
|
-
setFileSearchInput("");
|
|
982
|
-
setFileSearchIndex(0);
|
|
983
|
-
setScreen("file-search");
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
if (key.upArrow)
|
|
987
|
-
setFileIndex((current) => Math.max(0, current - 1));
|
|
988
|
-
if (key.downArrow)
|
|
989
|
-
setFileIndex((current) => Math.min(Math.max(fileTree.length - 1, 0), current + 1));
|
|
990
|
-
if (key.leftArrow && selectedFileTreeEntry?.kind === "directory")
|
|
991
|
-
toggleDirectory(selectedFileTreeEntry.path, false);
|
|
992
|
-
if (key.rightArrow && selectedFileTreeEntry?.kind === "directory")
|
|
993
|
-
toggleDirectory(selectedFileTreeEntry.path, true);
|
|
994
|
-
if (key.return && selectedFileTreeEntry?.kind === "directory")
|
|
995
|
-
toggleDirectory(selectedFileTreeEntry.path);
|
|
996
|
-
if (key.return && selectedFileTreeEntry?.kind === "file")
|
|
997
|
-
void loadFile(selectedFileTreeEntry);
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
if (focus === "editor") {
|
|
1001
|
-
if (key.upArrow) {
|
|
1002
|
-
setEditorScroll((current) => clamp(current - 1, 0, Math.max(0, editorRows.length - editorLineCount)));
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
if (key.downArrow) {
|
|
1006
|
-
setEditorScroll((current) => clamp(current + 1, 0, Math.max(0, editorRows.length - editorLineCount)));
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
if (input === "d")
|
|
1010
|
-
void toggleDiff();
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
if (key.upArrow) {
|
|
1014
|
-
setChatScroll((current) => clamp(current + 1, 0, Math.max(0, chatTranscript.length - chatLineCount)));
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
if (key.downArrow) {
|
|
1018
|
-
setChatScroll((current) => clamp(current - 1, 0, Math.max(0, chatTranscript.length - chatLineCount)));
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
if (key.return) {
|
|
1022
|
-
void sendPrompt();
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
if (key.backspace || key.delete)
|
|
1026
|
-
setChatInput((current) => current.slice(0, -1));
|
|
1027
|
-
else if (input)
|
|
1028
|
-
setChatInput((current) => current + input);
|
|
204
|
+
useTuiInputController({
|
|
205
|
+
state: {
|
|
206
|
+
screen,
|
|
207
|
+
focus,
|
|
208
|
+
busy,
|
|
209
|
+
fileSearchResults,
|
|
210
|
+
fileSearchIndex,
|
|
211
|
+
candidates,
|
|
212
|
+
selectedEndpoint,
|
|
213
|
+
settingsField,
|
|
214
|
+
endpointInput,
|
|
215
|
+
modelInput,
|
|
216
|
+
models,
|
|
217
|
+
modelIndex,
|
|
218
|
+
configurationAvailable: Boolean(configuration),
|
|
219
|
+
previewUrl,
|
|
220
|
+
terminalLineCount: terminalLines.length,
|
|
221
|
+
terminalHeight,
|
|
222
|
+
commandInput,
|
|
223
|
+
fileTree,
|
|
224
|
+
selectedFileTreeEntry,
|
|
225
|
+
editorRows,
|
|
226
|
+
editorLineCount,
|
|
227
|
+
chatTranscriptLength: chatTranscript.length,
|
|
228
|
+
chatLineCount,
|
|
229
|
+
},
|
|
230
|
+
setters: {
|
|
231
|
+
setScreen,
|
|
232
|
+
setFileSearchInput,
|
|
233
|
+
setFileSearchIndex,
|
|
234
|
+
setSettingsField,
|
|
235
|
+
setServerIndex,
|
|
236
|
+
setProviderKind,
|
|
237
|
+
setEndpointInput,
|
|
238
|
+
setModelIndex,
|
|
239
|
+
setModelInput,
|
|
240
|
+
setInternetAccess,
|
|
241
|
+
setThemeName,
|
|
242
|
+
setTerminalScroll,
|
|
243
|
+
setCommandInput,
|
|
244
|
+
setFileIndex,
|
|
245
|
+
setEditorScroll,
|
|
246
|
+
setChatScroll,
|
|
247
|
+
setChatInput,
|
|
248
|
+
},
|
|
249
|
+
actions: {
|
|
250
|
+
interruptOrExit,
|
|
251
|
+
cancelRun,
|
|
252
|
+
resolveApproval,
|
|
253
|
+
openSearchResult,
|
|
254
|
+
refreshAgentProfiles,
|
|
255
|
+
testMcpConnections,
|
|
256
|
+
configureRuntime: () => void configureRuntime(),
|
|
257
|
+
moveFocus,
|
|
258
|
+
openPreview,
|
|
259
|
+
runTerminalInput: (input) => void runTerminalInput(input).catch((error) => appendTerminal(`[terminal error] ${String(error)}`)),
|
|
260
|
+
startNewConversation,
|
|
261
|
+
toggleDirectory,
|
|
262
|
+
loadFile: (entry) => void loadFile(entry),
|
|
263
|
+
toggleDiff: () => void toggleDiff(),
|
|
264
|
+
sendPrompt: () => void sendPrompt(),
|
|
265
|
+
},
|
|
1029
266
|
});
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
const agentPanel = (_jsxs(Panel, { title: chatScroll
|
|
1050
|
-
? `AGENT [${chatScroll} lines above]`
|
|
1051
|
-
: "AGENT [Enter] send", active: focus === "chat", theme: theme, children: [activePlan && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: theme.accent, wrap: "truncate-end", children: ["PLAN: ", truncate(activePlan.title, Math.max(8, chatWidth - 9))] }), activePlan.steps.slice(0, 3).map((step) => (_jsxs(Text, { wrap: "truncate-end", color: step.status === "completed"
|
|
1052
|
-
? theme.success
|
|
1053
|
-
: step.status === "in_progress"
|
|
1054
|
-
? theme.warning
|
|
1055
|
-
: theme.muted, children: [step.status === "completed"
|
|
1056
|
-
? "[x]"
|
|
1057
|
-
: step.status === "in_progress"
|
|
1058
|
-
? "[..]"
|
|
1059
|
-
: "[ ]", " ", truncate(step.content, Math.max(8, chatWidth - 7))] }, step.id)))] })), chatLines.map((line, index) => (_jsx(Text, { color: line.role === "user" ? theme.user : theme.agent, bold: line.header, children: line.text }, `${line.role}-${line.header}-${index}-${line.text}`))), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { wrap: "truncate-end", color: focus === "chat" ? theme.focus : theme.muted, children: [focus === "chat" ? "> " : " ", truncate(chatInput ||
|
|
1060
|
-
(busy ? "Working... Escape cancels" : "Ask about this workspace"), Math.max(12, chatWidth - 5))] })] }));
|
|
1061
|
-
return (_jsxs(Box, { flexDirection: "column", height: viewport.rows, paddingX: 1, overflow: "hidden", children: [_jsxs(Box, { height: 1, justifyContent: "space-between", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.accent, children: brand.productName.toUpperCase() }), _jsx(Text, { color: theme.muted, children: configuration
|
|
1062
|
-
? `${configuration.provider} / ${configuration.model}`
|
|
1063
|
-
: "No model selected" }), _jsx(Text, { color: runStatus === "waiting"
|
|
1064
|
-
? theme.warning
|
|
1065
|
-
: busy
|
|
1066
|
-
? theme.focus
|
|
1067
|
-
: theme.success, children: runStatus === "waiting"
|
|
1068
|
-
? "APPROVAL"
|
|
1069
|
-
: runStatus === "tool"
|
|
1070
|
-
? "TOOL"
|
|
1071
|
-
: busy
|
|
1072
|
-
? "WORKING"
|
|
1073
|
-
: "READY" })] }), _jsxs(Box, { height: 1, justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Text, { color: theme.muted, children: ["CONTEXT", " ", _jsxs(Text, { color: contextTokens / contextWindow >= 0.9
|
|
1074
|
-
? theme.error
|
|
1075
|
-
: contextTokens / contextWindow >= 0.7
|
|
1076
|
-
? theme.warning
|
|
1077
|
-
: theme.accent, children: [formatTokenCount(contextTokens), " /", " ", formatTokenCount(contextWindow)] }), " ", "estimated"] }), _jsxs(Text, { color: theme.muted, children: ["SPEED", " ", _jsx(Text, { color: busy ? theme.success : theme.muted, children: tokensPerSecond
|
|
1078
|
-
? `${tokensPerSecond.toFixed(1)} tok/s`
|
|
1079
|
-
: "-- tok/s" }), previewUrl
|
|
1080
|
-
? ` PREVIEW ${truncate(previewUrl, Math.max(16, Math.floor(viewport.columns * 0.3)))}`
|
|
1081
|
-
: ""] })] }), compactLayout ? (_jsxs(Box, { height: workspaceHeight, flexDirection: "column", gap: 1, overflow: "hidden", children: [_jsxs(Box, { height: editorHeight, flexDirection: "row", gap: 1, overflow: "hidden", children: [_jsx(Box, { width: filesWidth, height: "100%", children: filesPanel }), _jsx(Box, { width: editorWidth, height: "100%", children: editorPanel })] }), _jsx(Box, { height: compactChatHeight, width: "100%", children: agentPanel })] })) : (_jsxs(Box, { height: workspaceHeight, flexDirection: "row", gap: 1, overflow: "hidden", children: [_jsx(Box, { width: filesWidth, height: "100%", children: filesPanel }), _jsx(Box, { width: editorWidth, height: "100%", children: editorPanel }), _jsx(Box, { width: chatWidth, height: "100%", children: agentPanel })] })), _jsx(Box, { height: terminalHeight, marginTop: 1, children: _jsxs(Panel, { title: "TERMINAL [Enter] run", active: focus === "terminal", theme: theme, children: [visibleTerminalLines.map((line, index) => (_jsx(Text, { wrap: "truncate-end", color: line.startsWith("[agent error]") ||
|
|
1082
|
-
line.startsWith("[terminal error]")
|
|
1083
|
-
? theme.error
|
|
1084
|
-
: line.startsWith("[tool")
|
|
1085
|
-
? theme.warning
|
|
1086
|
-
: theme.muted, children: truncate(line, Math.max(20, viewport.columns - 8)) }, `${index}-${line}`))), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { wrap: "truncate-end", color: focus === "terminal" ? theme.focus : theme.muted, children: [focus === "terminal" ? "> " : " ", truncate(commandInput || "Type a workspace command", Math.max(16, viewport.columns - 10))] })] }) }), _jsxs(Box, { height: 1, marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: theme.muted, wrap: "truncate-end", children: "Tab focus | Ctrl+Left/Right pane | / find | a agents | c MCP | m settings | Esc cancel run | Ctrl+C exit idle" }), _jsx(Text, { color: theme.muted, children: sessionId ? `session ${sessionId.slice(0, 8)}` : "new session" })] }), screen === "file-search" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 3, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "FIND FILE" }), _jsxs(Text, { color: theme.text, children: [_jsx(Text, { color: theme.focus, children: "> " }), fileSearchInput || (_jsx(Text, { color: theme.muted, children: "Type part of a file name or path" }))] }), _jsx(Text, { color: theme.muted, children: "Up/Down select Enter open Escape close" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [fileSearchResults.map((entry, index) => {
|
|
1087
|
-
const selected = index ===
|
|
1088
|
-
Math.min(fileSearchIndex, Math.max(0, fileSearchResults.length - 1));
|
|
1089
|
-
const fileName = entry.path.split("/").at(-1) ?? entry.path;
|
|
1090
|
-
return (_jsxs(Text, { color: selected ? theme.focus : theme.text, bold: selected, children: [selected ? "> " : " ", fileName, _jsxs(Text, { color: theme.muted, children: [" ", truncate(entry.path, Math.max(12, overlayWidth - fileName.length - 10))] })] }, entry.path));
|
|
1091
|
-
}), !fileSearchResults.length && (_jsx(Text, { color: theme.warning, children: "No matching files." }))] })] })), screen === "settings" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 3, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "LOCAL MODEL CONFIGURATION" }), _jsx(Text, { color: theme.muted, children: "Tab changes fields. Enter selects an item or saves when endpoint and model are set." }), _jsxs(Text, { color: settingsField === "server" ? theme.focus : theme.text, children: ["SERVER: ", selectedEndpoint?.label ?? "Scanning local servers..."] }), _jsxs(Text, { color: settingsField === "endpoint" ? theme.focus : theme.text, children: ["ENDPOINT: ", endpointInput] }), _jsxs(Text, { color: settingsField === "model" ? theme.focus : theme.text, children: ["MODEL:", " ", modelInput || models[modelIndex] || "Choose or type a model"] }), _jsxs(Text, { color: settingsField === "internet" ? theme.focus : theme.text, children: ["INTERNET RESEARCH: ", internetAccess ? "enabled" : "disabled", " ", settingsField === "internet" ? "[Space toggles]" : ""] }), _jsxs(Text, { color: settingsField === "theme" ? theme.focus : theme.text, children: ["THEME: ", themeName, " ", settingsField === "theme" ? "[Left/Right changes]" : ""] }), _jsxs(Text, { color: theme.muted, children: ["Detected models:", " ", models.slice(0, 5).join(", ") || "none; type one manually"] }), _jsxs(Text, { color: theme.warning, children: ["Provider: ", providerKind, ". Local endpoints use the server selector. Configure cloud BYOK profiles with truss-cli setup, then relaunch the TUI."] })] })), screen === "approval" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.warning, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 8, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.warning, children: "TOOL APPROVAL REQUIRED" }), _jsxs(Text, { color: theme.text, children: [pendingTool?.name, " ", JSON.stringify(pendingTool?.input)] }), _jsx(Text, { color: theme.muted, children: "Press Y or Enter to allow. Press N or Escape to deny." })] })), screen === "agents" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 6, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "MULTI-AGENT PROFILES" }), _jsx(Text, { color: theme.muted, children: "Profiles are shared with truss-cli in .truss-harness/agents.json. Use `truss agents run <id> <task>` for independent concurrent runs." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [agentProfiles.map((profile) => (_jsxs(Text, { color: theme.text, children: [_jsx(Text, { color: theme.focus, children: profile.displayName }), _jsxs(Text, { color: theme.muted, children: [" ", profile.id.slice(0, 8), " ", profile.provider.providerId, "/", profile.provider.modelId, " ", profile.mode, " ", profile.approvalPolicy] })] }, profile.id))), !agentProfiles.length && (_jsx(Text, { color: theme.warning, children: "No profiles yet. Add one with `truss agents add <name>`." }))] }), _jsx(Text, { color: theme.muted, children: "R refreshes. A or Escape closes." })] })), screen === "mcp" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 6, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "MCP CONNECTIONS" }), _jsx(Text, { color: theme.muted, children: "Status and discovered tools only. Commands, environment values, and credentials remain hidden." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [mcpStatuses.map((server) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: server.state === "failed"
|
|
1092
|
-
? theme.error
|
|
1093
|
-
: server.state === "connected"
|
|
1094
|
-
? theme.success
|
|
1095
|
-
: theme.warning, children: [_jsx(Text, { bold: true, children: server.name }), " ", server.state, server.error
|
|
1096
|
-
? ` — ${server.error}`
|
|
1097
|
-
: ` — ${server.toolCount} tool${server.toolCount === 1 ? "" : "s"}`] }), (server.tools ?? []).map((tool) => (_jsxs(Text, { color: theme.muted, children: [" ", tool.name, " [", tool.readOnly ? "read-only" : "approval", "]"] }, `${server.name}:${tool.name}`)))] }, server.name))), !mcpStatuses.length && (_jsx(Text, { color: theme.warning, children: "No MCP servers configured." }))] }), _jsx(Text, { color: theme.muted, children: "R tests configured servers. C or Escape closes." })] })), screen === "help" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 7, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "TUI CONTROLS" }), tuiControlHelp.map(([label, detail]) => (_jsxs(Text, { color: theme.text, wrap: "wrap", children: [_jsxs(Text, { bold: true, color: theme.focus, children: [label, ":", " "] }), detail] }, label))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: theme.accent, children: "WORKSPACE COMMANDS" }) }), workspaceCommandHelp()
|
|
1098
|
-
.split("\n")
|
|
1099
|
-
.slice(1)
|
|
1100
|
-
.map((line) => (_jsx(Text, { color: theme.text, children: line }, line))), _jsx(Text, { color: theme.muted, children: "Press any key to close this reference." })] }))] }));
|
|
267
|
+
return (_jsx(TuiWorkspaceView, { viewport: viewport, layout: layout, theme: theme, configuration: configuration, runStatus: runStatus, busy: busy, contextTokens: contextTokens, contextWindow: contextWindow, tokensPerSecond: tokensPerSecond, previewUrl: previewUrl, focus: focus, visibleFileTree: visibleFileTree, fileTreeLength: fileTree.length, fileTreeStart: fileTreeStart, fileIndex: fileIndex, openFilePath: openFilePath, editorTitle: editorTitle, visibleEditorRows: visibleEditorRows, activePlan: activePlan, chatLines: chatLines, chatScroll: chatScroll, chatInput: chatInput, visibleTerminalLines: visibleTerminalLines, commandInput: commandInput, sessionId: sessionId, overlays: {
|
|
268
|
+
screen,
|
|
269
|
+
overlayWidth,
|
|
270
|
+
fileSearchInput,
|
|
271
|
+
fileSearchResults,
|
|
272
|
+
fileSearchIndex,
|
|
273
|
+
settingsField,
|
|
274
|
+
selectedEndpoint,
|
|
275
|
+
endpointInput,
|
|
276
|
+
modelInput,
|
|
277
|
+
models,
|
|
278
|
+
modelIndex,
|
|
279
|
+
internetAccess,
|
|
280
|
+
themeName,
|
|
281
|
+
providerKind,
|
|
282
|
+
pendingTool,
|
|
283
|
+
agentProfiles,
|
|
284
|
+
mcpStatuses,
|
|
285
|
+
} }));
|
|
1101
286
|
}
|
|
1102
287
|
const initialConfiguration = await resolveConfiguration({
|
|
1103
288
|
workspaceRoot: cwd(),
|