@truss-harness/tui 0.1.7 → 0.1.9
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 +324 -94
- package/package.json +4 -4
package/dist/bin.js
CHANGED
|
@@ -9,35 +9,59 @@ import { promisify } from "node:util";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { Box, render, Text, useApp, useInput } from "ink";
|
|
11
11
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
-
import { cloudProviderDefinition, detectLocalEndpoints, isCloudProviderId, isLocalEndpointKind, listLocalModels } from "@truss-harness/provider-openai-compatible";
|
|
12
|
+
import { cloudProviderDefinition, detectLocalEndpoints, isCloudProviderId, isLocalEndpointKind, listLocalModels, } from "@truss-harness/provider-openai-compatible";
|
|
13
13
|
import { brand } from "@truss-harness/branding";
|
|
14
|
-
import { resolveConfiguration } from "@truss-harness/cli/config";
|
|
15
|
-
import { createClientRuntime } from "@truss-harness/cli/runtime";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
import { resolveConfiguration, } from "@truss-harness/cli/config";
|
|
15
|
+
import { createClientRuntime, } from "@truss-harness/cli/runtime";
|
|
16
|
+
import { FileAgentProfileStore } from "@truss-harness/cli/agents";
|
|
17
|
+
import { executeWorkspaceCommand, FileWorkspacePlanStore, workspaceCommandHelp, } from "@truss-harness/runtime";
|
|
18
|
+
import { buildFileTree, fuzzyFiles, syntaxTokens, wrapSyntaxTokens, } from "./file-browser.js";
|
|
19
|
+
import { tuiTheme, tuiThemeNames, } from "./theme.js";
|
|
20
|
+
const tuiHelp = `${brand.productName} TUI
|
|
21
|
+
|
|
22
|
+
Full-screen terminal workspace for local coding models.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
${brand.tuiCommand}
|
|
26
|
+
|
|
27
|
+
The TUI starts in the current workspace and reads the same configuration
|
|
28
|
+
profiles as ${brand.cliCommand}. Press ? inside the TUI for its complete
|
|
29
|
+
keyboard-control reference.
|
|
29
30
|
`;
|
|
30
31
|
const tuiControlHelp = [
|
|
31
|
-
[
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[
|
|
36
|
-
|
|
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
|
+
"TERMINAL",
|
|
50
|
+
"Type directly and press Enter; Up/Down scroll output; slash commands run through Truss.",
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"SETTINGS",
|
|
54
|
+
"m opens settings outside chat; Tab changes fields; Theme previews forest, sage, and dusk.",
|
|
55
|
+
],
|
|
37
56
|
["APPROVALS", "Y or Enter allows; N or Escape denies."],
|
|
38
|
-
[
|
|
57
|
+
[
|
|
58
|
+
"CANCEL / EXIT",
|
|
59
|
+
"Escape cancels an active agent run. Ctrl+C stops a process or run, and exits while idle.",
|
|
60
|
+
],
|
|
39
61
|
];
|
|
40
|
-
if (process.argv
|
|
62
|
+
if (process.argv
|
|
63
|
+
.slice(2)
|
|
64
|
+
.some((argument) => argument === "--help" || argument === "-h")) {
|
|
41
65
|
process.stdout.write(tuiHelp);
|
|
42
66
|
process.exit(0);
|
|
43
67
|
}
|
|
@@ -46,23 +70,43 @@ function ensureInteractiveTerminal() {
|
|
|
46
70
|
return;
|
|
47
71
|
// Git Bash exposes a pipe to Node for some console programs. winpty creates
|
|
48
72
|
// the Windows console bridge Ink needs for keyboard input.
|
|
49
|
-
if (process.platform === "win32" &&
|
|
50
|
-
|
|
73
|
+
if (process.platform === "win32" &&
|
|
74
|
+
process.env.MSYSTEM &&
|
|
75
|
+
process.env.TRUSS_TUI_WINPTY !== "1") {
|
|
76
|
+
const result = spawnSync("winpty", [
|
|
77
|
+
process.execPath,
|
|
78
|
+
fileURLToPath(import.meta.url),
|
|
79
|
+
...process.argv.slice(2),
|
|
80
|
+
], {
|
|
51
81
|
stdio: "inherit",
|
|
52
|
-
env: { ...process.env, TRUSS_TUI_WINPTY: "1" }
|
|
82
|
+
env: { ...process.env, TRUSS_TUI_WINPTY: "1" },
|
|
53
83
|
});
|
|
54
84
|
if (!result.error)
|
|
55
85
|
process.exit(result.status ?? 1);
|
|
56
86
|
}
|
|
57
|
-
process.stderr.write(brand.productName +
|
|
87
|
+
process.stderr.write(brand.productName +
|
|
88
|
+
" TUI needs an interactive terminal with raw keyboard input. Open PowerShell or Windows Terminal and run " +
|
|
89
|
+
brand.tuiCommand +
|
|
90
|
+
", or run winpty " +
|
|
91
|
+
brand.tuiCommand +
|
|
92
|
+
" from Git Bash.\n");
|
|
58
93
|
process.exit(1);
|
|
59
94
|
}
|
|
60
95
|
ensureInteractiveTerminal();
|
|
61
96
|
const execFile = promisify(execFileCallback);
|
|
62
|
-
const ignoredDirectories = new Set([
|
|
97
|
+
const ignoredDirectories = new Set([
|
|
98
|
+
".git",
|
|
99
|
+
".next",
|
|
100
|
+
".truss-harness",
|
|
101
|
+
"node_modules",
|
|
102
|
+
"dist",
|
|
103
|
+
"coverage",
|
|
104
|
+
]);
|
|
63
105
|
const focusOrder = ["files", "editor", "chat", "terminal"];
|
|
64
106
|
function truncate(value, length) {
|
|
65
|
-
return value.length <= length
|
|
107
|
+
return value.length <= length
|
|
108
|
+
? value
|
|
109
|
+
: `${value.slice(0, Math.max(0, length - 3))}...`;
|
|
66
110
|
}
|
|
67
111
|
function clamp(value, minimum, maximum) {
|
|
68
112
|
return Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
|
|
@@ -95,7 +139,9 @@ function wrapText(value, width) {
|
|
|
95
139
|
}
|
|
96
140
|
function plainChatText(value) {
|
|
97
141
|
let inCodeBlock = false;
|
|
98
|
-
return value
|
|
142
|
+
return value
|
|
143
|
+
.split(/\r?\n/)
|
|
144
|
+
.flatMap((sourceLine) => {
|
|
99
145
|
if (/^\s*```/.test(sourceLine)) {
|
|
100
146
|
inCodeBlock = !inCodeBlock;
|
|
101
147
|
return [];
|
|
@@ -111,23 +157,38 @@ function plainChatText(value) {
|
|
|
111
157
|
.replace(/`([^`]+)`/g, "$1")
|
|
112
158
|
.replace(/(\*\*|__|\*|_|~~)/g, "");
|
|
113
159
|
return /^\s*[-:|]+\s*$/.test(line) ? [] : [line];
|
|
114
|
-
})
|
|
160
|
+
})
|
|
161
|
+
.join("\n");
|
|
115
162
|
}
|
|
116
163
|
function chatDisplayLines(messages, busy, width) {
|
|
117
164
|
const transcript = messages.flatMap((message) => {
|
|
118
165
|
const content = message.content
|
|
119
|
-
? message.role === "assistant"
|
|
120
|
-
|
|
166
|
+
? message.role === "assistant"
|
|
167
|
+
? plainChatText(message.content)
|
|
168
|
+
: message.content
|
|
169
|
+
: busy && message.role === "assistant"
|
|
170
|
+
? "Thinking..."
|
|
171
|
+
: "";
|
|
121
172
|
return [
|
|
122
|
-
{
|
|
123
|
-
|
|
173
|
+
{
|
|
174
|
+
role: message.role,
|
|
175
|
+
text: message.role === "user" ? "YOU" : "AGENT",
|
|
176
|
+
header: true,
|
|
177
|
+
},
|
|
178
|
+
...wrapText(content, width).map((text) => ({
|
|
179
|
+
role: message.role,
|
|
180
|
+
text,
|
|
181
|
+
header: false,
|
|
182
|
+
})),
|
|
124
183
|
];
|
|
125
184
|
});
|
|
126
185
|
return transcript;
|
|
127
186
|
}
|
|
128
187
|
function configuredContextWindow() {
|
|
129
188
|
const configured = Number.parseInt(process.env.TRUSS_HARNESS_CONTEXT_WINDOW ?? "8192", 10);
|
|
130
|
-
return Number.isFinite(configured)
|
|
189
|
+
return Number.isFinite(configured)
|
|
190
|
+
? Math.max(512, Math.min(1_000_000, configured))
|
|
191
|
+
: 8_192;
|
|
131
192
|
}
|
|
132
193
|
async function collectFiles(root, current = root, result = []) {
|
|
133
194
|
if (result.length >= 2_000)
|
|
@@ -144,7 +205,11 @@ async function collectFiles(root, current = root, result = []) {
|
|
|
144
205
|
}
|
|
145
206
|
async function runTrackedCommand(command, workspaceRoot, onOutput, onProcess) {
|
|
146
207
|
await new Promise((resolve, reject) => {
|
|
147
|
-
const child = spawn(command, {
|
|
208
|
+
const child = spawn(command, {
|
|
209
|
+
cwd: workspaceRoot,
|
|
210
|
+
shell: true,
|
|
211
|
+
windowsHide: true,
|
|
212
|
+
});
|
|
148
213
|
onProcess?.(child);
|
|
149
214
|
child.stdout.on("data", (data) => onOutput(data.toString()));
|
|
150
215
|
child.stderr.on("data", (data) => onOutput(data.toString()));
|
|
@@ -161,7 +226,9 @@ function detectedPreviewUrl(output) {
|
|
|
161
226
|
return match?.[0].replace(/[),.;]+$/, "").replace("0.0.0.0", "127.0.0.1");
|
|
162
227
|
}
|
|
163
228
|
function normalizedPreviewUrl(value) {
|
|
164
|
-
const normalized = /^[a-z][a-z\d+.-]*:\/\//i.test(value.trim())
|
|
229
|
+
const normalized = /^[a-z][a-z\d+.-]*:\/\//i.test(value.trim())
|
|
230
|
+
? value.trim()
|
|
231
|
+
: `http://${value.trim()}`;
|
|
165
232
|
const url = new URL(normalized);
|
|
166
233
|
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
167
234
|
throw new Error("Preview URLs must use HTTP or HTTPS.");
|
|
@@ -174,7 +241,11 @@ function openExternalPreview(value) {
|
|
|
174
241
|
: process.platform === "darwin"
|
|
175
242
|
? ["open", [url]]
|
|
176
243
|
: ["xdg-open", [url]];
|
|
177
|
-
const child = spawn(command, arguments_, {
|
|
244
|
+
const child = spawn(command, arguments_, {
|
|
245
|
+
detached: true,
|
|
246
|
+
stdio: "ignore",
|
|
247
|
+
windowsHide: true,
|
|
248
|
+
});
|
|
178
249
|
child.once("error", () => undefined);
|
|
179
250
|
child.unref();
|
|
180
251
|
}
|
|
@@ -190,12 +261,15 @@ async function stopProcessTree(child) {
|
|
|
190
261
|
}
|
|
191
262
|
child.kill();
|
|
192
263
|
}
|
|
193
|
-
function Panel({ title, active, theme, children }) {
|
|
194
|
-
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] });
|
|
264
|
+
function Panel({ title, active, theme, children, }) {
|
|
265
|
+
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] }));
|
|
195
266
|
}
|
|
196
|
-
function App({ initialConfiguration }) {
|
|
267
|
+
function App({ initialConfiguration, }) {
|
|
197
268
|
const { exit } = useApp();
|
|
198
|
-
const [viewport, setViewport] = useState(() => ({
|
|
269
|
+
const [viewport, setViewport] = useState(() => ({
|
|
270
|
+
columns: process.stdout.columns || 120,
|
|
271
|
+
rows: process.stdout.rows || 36,
|
|
272
|
+
}));
|
|
199
273
|
const workspaceRoot = useMemo(() => cwd(), []);
|
|
200
274
|
const [configuration, setConfiguration] = useState(initialConfiguration);
|
|
201
275
|
const [client, setClient] = useState();
|
|
@@ -214,8 +288,13 @@ function App({ initialConfiguration }) {
|
|
|
214
288
|
const [chatInput, setChatInput] = useState("");
|
|
215
289
|
const [chat, setChat] = useState([]);
|
|
216
290
|
const [chatScroll, setChatScroll] = useState(0);
|
|
217
|
-
const [streamMetrics, setStreamMetrics] = useState({
|
|
218
|
-
|
|
291
|
+
const [streamMetrics, setStreamMetrics] = useState({
|
|
292
|
+
startedAt: 0,
|
|
293
|
+
generatedTokens: 0,
|
|
294
|
+
});
|
|
295
|
+
const [terminalLines, setTerminalLines] = useState([
|
|
296
|
+
"Terminal ready. Shell commands run in the workspace root; slash commands run locally.",
|
|
297
|
+
]);
|
|
219
298
|
const [terminalScroll, setTerminalScroll] = useState(0);
|
|
220
299
|
const [previewUrl, setPreviewUrl] = useState();
|
|
221
300
|
const [editorScroll, setEditorScroll] = useState(0);
|
|
@@ -227,6 +306,7 @@ function App({ initialConfiguration }) {
|
|
|
227
306
|
const [endpoints, setEndpoints] = useState([]);
|
|
228
307
|
const [serverIndex, setServerIndex] = useState(0);
|
|
229
308
|
const [models, setModels] = useState([]);
|
|
309
|
+
const [agentProfiles, setAgentProfiles] = useState([]);
|
|
230
310
|
const [modelIndex, setModelIndex] = useState(0);
|
|
231
311
|
const [settingsField, setSettingsField] = useState("server");
|
|
232
312
|
const [themeName, setThemeName] = useState(initialConfiguration?.tuiTheme ?? "forest");
|
|
@@ -242,7 +322,8 @@ function App({ initialConfiguration }) {
|
|
|
242
322
|
const contextWindow = useMemo(configuredContextWindow, []);
|
|
243
323
|
const contextTokens = useMemo(() => chat.reduce((total, message) => total + estimateTokens(message.content), 400), [chat]);
|
|
244
324
|
const tokensPerSecond = streamMetrics.startedAt && streamMetrics.generatedTokens
|
|
245
|
-
? streamMetrics.generatedTokens /
|
|
325
|
+
? streamMetrics.generatedTokens /
|
|
326
|
+
Math.max((Date.now() - streamMetrics.startedAt) / 1_000, 0.1)
|
|
246
327
|
: undefined;
|
|
247
328
|
const theme = tuiTheme(themeName);
|
|
248
329
|
const appendTerminal = (output) => {
|
|
@@ -257,7 +338,12 @@ function App({ initialConfiguration }) {
|
|
|
257
338
|
};
|
|
258
339
|
const candidates = useMemo(() => [
|
|
259
340
|
...endpoints,
|
|
260
|
-
{
|
|
341
|
+
{
|
|
342
|
+
id: "custom",
|
|
343
|
+
label: "Custom compatible endpoint",
|
|
344
|
+
kind: "openai-compatible",
|
|
345
|
+
baseUrl: "",
|
|
346
|
+
},
|
|
261
347
|
], [endpoints]);
|
|
262
348
|
const selectedEndpoint = candidates[Math.min(serverIndex, Math.max(candidates.length - 1, 0))];
|
|
263
349
|
const fileTree = useMemo(() => buildFileTree(files, collapsedDirectories), [files, collapsedDirectories]);
|
|
@@ -267,10 +353,18 @@ function App({ initialConfiguration }) {
|
|
|
267
353
|
const terminalHeight = clamp(Math.floor(viewport.rows * 0.24), 7, 11);
|
|
268
354
|
const workspaceHeight = Math.max(compactLayout ? 14 : 9, viewport.rows - terminalHeight - 6);
|
|
269
355
|
const filesWidth = Math.max(20, Math.min(36, Math.floor(viewport.columns * 0.24)));
|
|
270
|
-
const chatWidth = compactLayout
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
356
|
+
const chatWidth = compactLayout
|
|
357
|
+
? Math.max(40, viewport.columns - 4)
|
|
358
|
+
: Math.max(28, Math.min(48, Math.floor(viewport.columns * 0.31)));
|
|
359
|
+
const editorWidth = compactLayout
|
|
360
|
+
? Math.max(30, viewport.columns - filesWidth - 4)
|
|
361
|
+
: Math.max(30, viewport.columns - filesWidth - chatWidth - 4);
|
|
362
|
+
const compactChatHeight = compactLayout
|
|
363
|
+
? clamp(Math.floor(workspaceHeight * 0.4), 6, 10)
|
|
364
|
+
: workspaceHeight;
|
|
365
|
+
const editorHeight = compactLayout
|
|
366
|
+
? Math.max(6, workspaceHeight - compactChatHeight - 1)
|
|
367
|
+
: workspaceHeight;
|
|
274
368
|
const editorLineCount = Math.max(2, editorHeight - 3);
|
|
275
369
|
const chatLineCount = Math.max(2, compactChatHeight - (activePlan ? 7 : 0) - 4);
|
|
276
370
|
const chatTranscript = chatDisplayLines(chat, busy, Math.max(12, chatWidth - 5));
|
|
@@ -285,11 +379,13 @@ function App({ initialConfiguration }) {
|
|
|
285
379
|
? "red"
|
|
286
380
|
: undefined
|
|
287
381
|
: undefined;
|
|
288
|
-
const tokens = isDiff
|
|
382
|
+
const tokens = isDiff
|
|
383
|
+
? [{ text: line, color }]
|
|
384
|
+
: syntaxTokens(line, openFilePath ?? "");
|
|
289
385
|
return wrapSyntaxTokens(tokens, maximumCharacters).map((row, rowIndex) => ({
|
|
290
386
|
sourceLine: sourceIndex + 1,
|
|
291
387
|
continuation: rowIndex > 0,
|
|
292
|
-
tokens: row
|
|
388
|
+
tokens: row,
|
|
293
389
|
}));
|
|
294
390
|
});
|
|
295
391
|
}, [editor, editorWidth, isDiff, openFilePath]);
|
|
@@ -299,11 +395,23 @@ function App({ initialConfiguration }) {
|
|
|
299
395
|
const visibleFileTree = fileTree.slice(fileTreeStart, fileTreeStart + editorLineCount);
|
|
300
396
|
const overlayWidth = clamp(viewport.columns - 4, 42, 90);
|
|
301
397
|
useEffect(() => {
|
|
302
|
-
void collectFiles(workspaceRoot)
|
|
303
|
-
|
|
398
|
+
void collectFiles(workspaceRoot)
|
|
399
|
+
.then(setFiles)
|
|
400
|
+
.catch((error) => setEditor(`Unable to list workspace files: ${String(error)}`));
|
|
401
|
+
void new FileAgentProfileStore(workspaceRoot)
|
|
402
|
+
.list()
|
|
403
|
+
.then(setAgentProfiles)
|
|
404
|
+
.catch((error) => appendTerminal(`[agents] ${error instanceof Error ? error.message : String(error)}`));
|
|
405
|
+
void new FileWorkspacePlanStore(workspaceRoot)
|
|
406
|
+
.load()
|
|
407
|
+
.then(setActivePlan)
|
|
408
|
+
.catch(() => undefined);
|
|
304
409
|
void detectLocalEndpoints().then((detected) => {
|
|
305
410
|
setEndpoints(detected);
|
|
306
|
-
const preferred = configuration
|
|
411
|
+
const preferred = configuration
|
|
412
|
+
? detected.findIndex((endpoint) => endpoint.kind === configuration.provider &&
|
|
413
|
+
endpoint.baseUrl === configuration.baseUrl)
|
|
414
|
+
: -1;
|
|
307
415
|
if (preferred >= 0)
|
|
308
416
|
setServerIndex(preferred);
|
|
309
417
|
});
|
|
@@ -322,19 +430,32 @@ function App({ initialConfiguration }) {
|
|
|
322
430
|
setFileIndex(index);
|
|
323
431
|
}, [fileTree, openFilePath]);
|
|
324
432
|
useEffect(() => {
|
|
325
|
-
const refreshViewport = () => setViewport({
|
|
433
|
+
const refreshViewport = () => setViewport({
|
|
434
|
+
columns: process.stdout.columns || 120,
|
|
435
|
+
rows: process.stdout.rows || 36,
|
|
436
|
+
});
|
|
326
437
|
process.stdout.on("resize", refreshViewport);
|
|
327
|
-
return () => {
|
|
438
|
+
return () => {
|
|
439
|
+
process.stdout.off("resize", refreshViewport);
|
|
440
|
+
};
|
|
328
441
|
}, []);
|
|
329
442
|
useEffect(() => {
|
|
330
|
-
if (!selectedEndpoint ||
|
|
443
|
+
if (!selectedEndpoint ||
|
|
444
|
+
!endpointInput ||
|
|
445
|
+
!isLocalEndpointKind(providerKind))
|
|
331
446
|
return;
|
|
332
|
-
const endpoint = {
|
|
333
|
-
|
|
447
|
+
const endpoint = {
|
|
448
|
+
...selectedEndpoint,
|
|
449
|
+
kind: providerKind,
|
|
450
|
+
baseUrl: endpointInput,
|
|
451
|
+
};
|
|
452
|
+
void listLocalModels(endpoint)
|
|
453
|
+
.then((available) => {
|
|
334
454
|
setModels(available.map((model) => model.name));
|
|
335
455
|
const selected = available.findIndex((model) => model.name === modelInput);
|
|
336
456
|
setModelIndex(Math.max(0, selected));
|
|
337
|
-
})
|
|
457
|
+
})
|
|
458
|
+
.catch(() => setModels([]));
|
|
338
459
|
}, [selectedEndpoint?.id, endpointInput, providerKind]);
|
|
339
460
|
useEffect(() => {
|
|
340
461
|
if (!client)
|
|
@@ -344,13 +465,19 @@ function App({ initialConfiguration }) {
|
|
|
344
465
|
setRunStatus("thinking");
|
|
345
466
|
setChatScroll(0);
|
|
346
467
|
setStreamMetrics((current) => current.startedAt
|
|
347
|
-
? {
|
|
468
|
+
? {
|
|
469
|
+
...current,
|
|
470
|
+
generatedTokens: current.generatedTokens + estimateTokens(event.text),
|
|
471
|
+
}
|
|
348
472
|
: current);
|
|
349
473
|
setChat((current) => {
|
|
350
474
|
const last = current.at(-1);
|
|
351
475
|
if (!last || last.role !== "assistant")
|
|
352
476
|
return [...current, { role: "assistant", content: event.text }];
|
|
353
|
-
return [
|
|
477
|
+
return [
|
|
478
|
+
...current.slice(0, -1),
|
|
479
|
+
{ role: "assistant", content: last.content + event.text },
|
|
480
|
+
];
|
|
354
481
|
});
|
|
355
482
|
}
|
|
356
483
|
if (event.type === "tool_call_requested") {
|
|
@@ -446,14 +573,24 @@ function App({ initialConfiguration }) {
|
|
|
446
573
|
model: modelInput.trim(),
|
|
447
574
|
apiKey: initialConfiguration?.provider === providerKind
|
|
448
575
|
? initialConfiguration.apiKey
|
|
449
|
-
: process.env.TRUSS_HARNESS_API_KEY ??
|
|
576
|
+
: (process.env.TRUSS_HARNESS_API_KEY ??
|
|
577
|
+
(isCloudProviderId(providerKind)
|
|
578
|
+
? process.env[cloudProviderDefinition(providerKind)
|
|
579
|
+
.apiKeyEnvironmentVariable]
|
|
580
|
+
: undefined)),
|
|
450
581
|
systemPrompt: process.env.TRUSS_HARNESS_SYSTEM_PROMPT,
|
|
451
582
|
mode: agentMode,
|
|
452
583
|
internetAccess,
|
|
453
584
|
approval: {
|
|
454
585
|
approve: async (call) => {
|
|
455
|
-
const readOnly = [
|
|
456
|
-
|
|
586
|
+
const readOnly = [
|
|
587
|
+
"read_file",
|
|
588
|
+
"list_directory",
|
|
589
|
+
"search_files",
|
|
590
|
+
"grep",
|
|
591
|
+
].includes(call.name);
|
|
592
|
+
if (permissionMode === "auto-all" ||
|
|
593
|
+
(permissionMode === "auto-read" && readOnly))
|
|
457
594
|
return true;
|
|
458
595
|
return new Promise((resolve) => {
|
|
459
596
|
approvalResolvers.current.set(call.id, resolve);
|
|
@@ -461,8 +598,8 @@ function App({ initialConfiguration }) {
|
|
|
461
598
|
setRunStatus("waiting");
|
|
462
599
|
setScreen("approval");
|
|
463
600
|
});
|
|
464
|
-
}
|
|
465
|
-
}
|
|
601
|
+
},
|
|
602
|
+
},
|
|
466
603
|
};
|
|
467
604
|
setConfiguration(nextConfiguration);
|
|
468
605
|
setRunStatus("thinking");
|
|
@@ -498,11 +635,13 @@ function App({ initialConfiguration }) {
|
|
|
498
635
|
const maximumCharacters = Math.max(2_000, Math.min(20_000, contextWindow));
|
|
499
636
|
const content = await readFile(join(workspaceRoot, openFilePath), "utf8");
|
|
500
637
|
const clipped = content.slice(0, maximumCharacters);
|
|
501
|
-
return [
|
|
638
|
+
return [
|
|
639
|
+
{
|
|
502
640
|
source: `active-file:${openFilePath}`,
|
|
503
641
|
content: `This is the currently open workspace file and the primary context for this request. Tool results produced later in the run take precedence over this request-start snapshot.\n\n${clipped}`,
|
|
504
|
-
priority: 1_000
|
|
505
|
-
}
|
|
642
|
+
priority: 1_000,
|
|
643
|
+
},
|
|
644
|
+
];
|
|
506
645
|
}
|
|
507
646
|
catch {
|
|
508
647
|
return [];
|
|
@@ -512,10 +651,17 @@ function App({ initialConfiguration }) {
|
|
|
512
651
|
const prompt = chatInput.trim();
|
|
513
652
|
if (!prompt || busy)
|
|
514
653
|
return;
|
|
515
|
-
const command = await executeWorkspaceCommand({
|
|
654
|
+
const command = await executeWorkspaceCommand({
|
|
655
|
+
workspaceRoot,
|
|
656
|
+
input: prompt,
|
|
657
|
+
});
|
|
516
658
|
if (command.handled) {
|
|
517
659
|
setChatInput("");
|
|
518
|
-
setChat((current) => [
|
|
660
|
+
setChat((current) => [
|
|
661
|
+
...current,
|
|
662
|
+
{ role: "user", content: prompt },
|
|
663
|
+
{ role: "assistant", content: command.message },
|
|
664
|
+
]);
|
|
519
665
|
appendTerminal(`[workspace command] ${command.command ?? prompt}: ${command.ok ? "completed" : "failed"}`);
|
|
520
666
|
return;
|
|
521
667
|
}
|
|
@@ -529,11 +675,17 @@ function App({ initialConfiguration }) {
|
|
|
529
675
|
setBusy(true);
|
|
530
676
|
setRunStatus("thinking");
|
|
531
677
|
setStreamMetrics({ startedAt: Date.now(), generatedTokens: 0 });
|
|
532
|
-
setChat((current) => [
|
|
678
|
+
setChat((current) => [
|
|
679
|
+
...current,
|
|
680
|
+
{ role: "user", content: prompt },
|
|
681
|
+
{ role: "assistant", content: "" },
|
|
682
|
+
]);
|
|
533
683
|
const controller = new AbortController();
|
|
534
684
|
abortController.current = controller;
|
|
535
685
|
try {
|
|
536
|
-
const session = sessionId
|
|
686
|
+
const session = sessionId
|
|
687
|
+
? await client.runtime.getSession(sessionId)
|
|
688
|
+
: await client.runtime.createSession();
|
|
537
689
|
if (!session)
|
|
538
690
|
throw new Error("Conversation is unavailable.");
|
|
539
691
|
setSessionId(session.id);
|
|
@@ -575,7 +727,9 @@ function App({ initialConfiguration }) {
|
|
|
575
727
|
appendTerminal(command.message);
|
|
576
728
|
return;
|
|
577
729
|
}
|
|
578
|
-
await runTrackedCommand(input, workspaceRoot, appendTerminal, (process) => {
|
|
730
|
+
await runTrackedCommand(input, workspaceRoot, appendTerminal, (process) => {
|
|
731
|
+
terminalProcess.current = process;
|
|
732
|
+
});
|
|
579
733
|
};
|
|
580
734
|
const moveFocus = (direction) => {
|
|
581
735
|
setFocus((current) => {
|
|
@@ -641,13 +795,29 @@ function App({ initialConfiguration }) {
|
|
|
641
795
|
setScreen("workspace");
|
|
642
796
|
return;
|
|
643
797
|
}
|
|
798
|
+
if (screen === "agents") {
|
|
799
|
+
if (key.escape || input.toLowerCase() === "a")
|
|
800
|
+
setScreen("workspace");
|
|
801
|
+
if (input.toLowerCase() === "r")
|
|
802
|
+
void new FileAgentProfileStore(workspaceRoot)
|
|
803
|
+
.list()
|
|
804
|
+
.then(setAgentProfiles)
|
|
805
|
+
.catch((error) => appendTerminal(`[agents] ${error instanceof Error ? error.message : String(error)}`));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
644
808
|
if (screen === "settings") {
|
|
645
809
|
if (key.escape) {
|
|
646
810
|
setScreen("workspace");
|
|
647
811
|
return;
|
|
648
812
|
}
|
|
649
813
|
if (key.tab) {
|
|
650
|
-
const fields = [
|
|
814
|
+
const fields = [
|
|
815
|
+
"server",
|
|
816
|
+
"endpoint",
|
|
817
|
+
"model",
|
|
818
|
+
"internet",
|
|
819
|
+
"theme",
|
|
820
|
+
];
|
|
651
821
|
setSettingsField((current) => {
|
|
652
822
|
const index = fields.indexOf(current);
|
|
653
823
|
return fields[(index + (key.shift ? -1 : 1) + fields.length) % fields.length];
|
|
@@ -683,7 +853,8 @@ function App({ initialConfiguration }) {
|
|
|
683
853
|
}
|
|
684
854
|
if (settingsField === "theme") {
|
|
685
855
|
if (key.leftArrow || key.upArrow)
|
|
686
|
-
setThemeName((current) => tuiThemeNames[(tuiThemeNames.indexOf(current) - 1 + tuiThemeNames.length) %
|
|
856
|
+
setThemeName((current) => tuiThemeNames[(tuiThemeNames.indexOf(current) - 1 + tuiThemeNames.length) %
|
|
857
|
+
tuiThemeNames.length]);
|
|
687
858
|
if (key.rightArrow || key.downArrow || input === " ")
|
|
688
859
|
setThemeName((current) => tuiThemeNames[(tuiThemeNames.indexOf(current) + 1) % tuiThemeNames.length]);
|
|
689
860
|
if (key.return && endpointInput && modelInput)
|
|
@@ -705,6 +876,12 @@ function App({ initialConfiguration }) {
|
|
|
705
876
|
moveFocus(key.leftArrow ? -1 : 1);
|
|
706
877
|
return;
|
|
707
878
|
}
|
|
879
|
+
if (input.toLowerCase() === "a" &&
|
|
880
|
+
focus !== "chat" &&
|
|
881
|
+
focus !== "terminal") {
|
|
882
|
+
setScreen("agents");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
708
885
|
if (key.tab) {
|
|
709
886
|
moveFocus(key.shift ? -1 : 1);
|
|
710
887
|
return;
|
|
@@ -715,7 +892,10 @@ function App({ initialConfiguration }) {
|
|
|
715
892
|
appendTerminal("[agent] Cancellation requested.");
|
|
716
893
|
return;
|
|
717
894
|
}
|
|
718
|
-
if (input === "o" &&
|
|
895
|
+
if (input === "o" &&
|
|
896
|
+
focus !== "chat" &&
|
|
897
|
+
focus !== "terminal" &&
|
|
898
|
+
previewUrl) {
|
|
719
899
|
try {
|
|
720
900
|
openExternalPreview(previewUrl);
|
|
721
901
|
appendTerminal(`[preview] Opened ${previewUrl}`);
|
|
@@ -810,25 +990,75 @@ function App({ initialConfiguration }) {
|
|
|
810
990
|
else if (input)
|
|
811
991
|
setChatInput((current) => current + input);
|
|
812
992
|
});
|
|
813
|
-
const filesPanel = _jsxs(Panel, { title: "FILES [/] find", active: focus === "files", theme: theme, children: [visibleFileTree.map((entry, visibleIndex) => {
|
|
993
|
+
const filesPanel = (_jsxs(Panel, { title: "FILES [/] find", active: focus === "files", theme: theme, children: [visibleFileTree.map((entry, visibleIndex) => {
|
|
814
994
|
const selected = fileTreeStart + visibleIndex === fileIndex;
|
|
815
995
|
const marker = entry.kind === "directory" ? (entry.expanded ? "v " : "> ") : " ";
|
|
816
996
|
const indentation = " ".repeat(entry.depth * 2);
|
|
817
997
|
const width = Math.max(6, filesWidth - indentation.length - 7);
|
|
818
|
-
return _jsxs(Text, { wrap: "truncate-end", color: selected
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
998
|
+
return (_jsxs(Text, { wrap: "truncate-end", color: selected
|
|
999
|
+
? theme.focus
|
|
1000
|
+
: entry.kind === "directory"
|
|
1001
|
+
? theme.directory
|
|
1002
|
+
: entry.path === openFilePath
|
|
1003
|
+
? theme.success
|
|
1004
|
+
: theme.text, bold: selected || entry.kind === "directory", children: [selected ? "> " : " ", indentation, marker, truncate(entry.name, width)] }, `${entry.kind}:${entry.path}`));
|
|
1005
|
+
}), !fileTree.length && (_jsx(Text, { color: theme.muted, children: "No workspace files found." }))] }));
|
|
1006
|
+
const editorPanel = (_jsx(Panel, { title: `${editorTitle} [up/down] scroll [d] diff${previewUrl ? " [o] browser" : ""}`, active: focus === "editor", theme: theme, children: visibleEditorRows.map((row, index) => {
|
|
1007
|
+
const lineLabel = row.continuation
|
|
1008
|
+
? " |"
|
|
1009
|
+
: String(row.sourceLine).padStart(3);
|
|
1010
|
+
return (_jsxs(Text, { wrap: "truncate-end", color: theme.text, children: [_jsx(Text, { color: theme.muted, children: lineLabel }), " ", row.tokens.map((token, tokenIndex) => (_jsx(Text, { color: token.color ? theme.syntax[token.color] : theme.text, dimColor: token.dim, children: token.text }, `${tokenIndex}-${token.text}`)))] }, `${editorScroll + index}-${row.sourceLine}-${row.continuation}`));
|
|
1011
|
+
}) }));
|
|
1012
|
+
const agentPanel = (_jsxs(Panel, { title: chatScroll
|
|
1013
|
+
? `AGENT [${chatScroll} lines above]`
|
|
1014
|
+
: "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"
|
|
1015
|
+
? theme.success
|
|
1016
|
+
: step.status === "in_progress"
|
|
1017
|
+
? theme.warning
|
|
1018
|
+
: theme.muted, children: [step.status === "completed"
|
|
1019
|
+
? "[x]"
|
|
1020
|
+
: step.status === "in_progress"
|
|
1021
|
+
? "[..]"
|
|
1022
|
+
: "[ ]", " ", 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 ||
|
|
1023
|
+
(busy ? "Working... Escape cancels" : "Ask about this workspace"), Math.max(12, chatWidth - 5))] })] }));
|
|
1024
|
+
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
|
|
1025
|
+
? `${configuration.provider} / ${configuration.model}`
|
|
1026
|
+
: "No model selected" }), _jsx(Text, { color: runStatus === "waiting"
|
|
1027
|
+
? theme.warning
|
|
1028
|
+
: busy
|
|
1029
|
+
? theme.focus
|
|
1030
|
+
: theme.success, children: runStatus === "waiting"
|
|
1031
|
+
? "APPROVAL"
|
|
1032
|
+
: runStatus === "tool"
|
|
1033
|
+
? "TOOL"
|
|
1034
|
+
: busy
|
|
1035
|
+
? "WORKING"
|
|
1036
|
+
: "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
|
|
1037
|
+
? theme.error
|
|
1038
|
+
: contextTokens / contextWindow >= 0.7
|
|
1039
|
+
? theme.warning
|
|
1040
|
+
: 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
|
|
1041
|
+
? `${tokensPerSecond.toFixed(1)} tok/s`
|
|
1042
|
+
: "-- tok/s" }), previewUrl
|
|
1043
|
+
? ` PREVIEW ${truncate(previewUrl, Math.max(16, Math.floor(viewport.columns * 0.3)))}`
|
|
1044
|
+
: ""] })] }), 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]") ||
|
|
1045
|
+
line.startsWith("[terminal error]")
|
|
1046
|
+
? theme.error
|
|
1047
|
+
: line.startsWith("[tool")
|
|
1048
|
+
? theme.warning
|
|
1049
|
+
: 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 | 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) => {
|
|
1050
|
+
const selected = index ===
|
|
1051
|
+
Math.min(fileSearchIndex, Math.max(0, fileSearchResults.length - 1));
|
|
827
1052
|
const fileName = entry.path.split("/").at(-1) ?? entry.path;
|
|
828
|
-
return _jsxs(Text, { color: selected ? theme.focus : theme.text, bold: selected, children: [selected ? "> " : " ", fileName, _jsxs(Text, { color: theme.muted, children: ["
|
|
829
|
-
}), !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 === "
|
|
1053
|
+
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));
|
|
1054
|
+
}), !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 === "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()
|
|
1055
|
+
.split("\n")
|
|
1056
|
+
.slice(1)
|
|
1057
|
+
.map((line) => (_jsx(Text, { color: theme.text, children: line }, line))), _jsx(Text, { color: theme.muted, children: "Press any key to close this reference." })] }))] }));
|
|
830
1058
|
}
|
|
831
|
-
const initialConfiguration = await resolveConfiguration({
|
|
1059
|
+
const initialConfiguration = await resolveConfiguration({
|
|
1060
|
+
workspaceRoot: cwd(),
|
|
1061
|
+
}).catch((error) => {
|
|
832
1062
|
const message = error instanceof Error ? error.message : String(error);
|
|
833
1063
|
if (message.startsWith("Set a model"))
|
|
834
1064
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truss-harness/tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Interactive terminal client for Truss.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"ink": "^6.8.0",
|
|
44
44
|
"react": "^19.2.0",
|
|
45
45
|
"@truss-harness/branding": "0.1.0",
|
|
46
|
-
"@truss-harness/cli": "0.1.
|
|
47
|
-
"@truss-harness/provider-openai-compatible": "0.1.
|
|
48
|
-
"@truss-harness/runtime": "0.1.
|
|
46
|
+
"@truss-harness/cli": "0.1.9",
|
|
47
|
+
"@truss-harness/provider-openai-compatible": "0.1.6",
|
|
48
|
+
"@truss-harness/runtime": "0.1.6"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/react": "^19.2.0"
|