@truss-harness/tui 0.1.8 → 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.
Files changed (2) hide show
  1. package/dist/bin.js +314 -84
  2. package/package.json +4 -4
package/dist/bin.js CHANGED
@@ -9,13 +9,14 @@ 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 { executeWorkspaceCommand, FileWorkspacePlanStore, workspaceCommandHelp } from "@truss-harness/runtime";
17
- import { buildFileTree, fuzzyFiles, syntaxTokens, wrapSyntaxTokens } from "./file-browser.js";
18
- import { tuiTheme, tuiThemeNames } from "./theme.js";
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";
19
20
  const tuiHelp = `${brand.productName} TUI
20
21
 
21
22
  Full-screen terminal workspace for local coding models.
@@ -28,16 +29,39 @@ profiles as ${brand.cliCommand}. Press ? inside the TUI for its complete
28
29
  keyboard-control reference.
29
30
  `;
30
31
  const tuiControlHelp = [
31
- ["NAVIGATION", "Tab / Shift+Tab move focus; Ctrl+Left / Ctrl+Right move to an adjacent pane."],
32
- ["FILES", "/ opens fuzzy file search; Up/Down select; Left/Right collapse or expand; Enter opens."],
33
- ["EDITOR", "Up/Down scroll; d toggles Git diff; o opens the detected preview URL externally."],
34
- ["AGENT", "Enter sends; Up/Down scroll chat; n starts a new conversation outside the chat input."],
35
- ["TERMINAL", "Type directly and press Enter; Up/Down scroll output; slash commands run through Truss."],
36
- ["SETTINGS", "m opens settings outside chat; Tab changes fields; Theme previews forest, sage, and dusk."],
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
- ["CANCEL / EXIT", "Escape cancels an active agent run. Ctrl+C stops a process or run, and exits while idle."]
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.slice(2).some((argument) => argument === "--help" || argument === "-h")) {
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" && process.env.MSYSTEM && process.env.TRUSS_TUI_WINPTY !== "1") {
50
- const result = spawnSync("winpty", [process.execPath, fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
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 + " TUI needs an interactive terminal with raw keyboard input. Open PowerShell or Windows Terminal and run " + brand.tuiCommand + ", or run winpty " + brand.tuiCommand + " from Git Bash.\n");
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([".git", ".next", ".truss-harness", "node_modules", "dist", "coverage"]);
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 ? value : `${value.slice(0, Math.max(0, length - 3))}...`;
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.split(/\r?\n/).flatMap((sourceLine) => {
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
- }).join("\n");
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" ? plainChatText(message.content) : message.content
120
- : busy && message.role === "assistant" ? "Thinking..." : "";
166
+ ? message.role === "assistant"
167
+ ? plainChatText(message.content)
168
+ : message.content
169
+ : busy && message.role === "assistant"
170
+ ? "Thinking..."
171
+ : "";
121
172
  return [
122
- { role: message.role, text: message.role === "user" ? "YOU" : "AGENT", header: true },
123
- ...wrapText(content, width).map((text) => ({ role: message.role, text, header: false }))
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) ? Math.max(512, Math.min(1_000_000, configured)) : 8_192;
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, { cwd: workspaceRoot, shell: true, windowsHide: true });
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()) ? value.trim() : `http://${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_, { detached: true, stdio: "ignore", windowsHide: true });
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(() => ({ columns: process.stdout.columns || 120, rows: process.stdout.rows || 36 }));
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({ startedAt: 0, generatedTokens: 0 });
218
- const [terminalLines, setTerminalLines] = useState(["Terminal ready. Shell commands run in the workspace root; slash commands run locally."]);
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 / Math.max((Date.now() - streamMetrics.startedAt) / 1_000, 0.1)
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
- { id: "custom", label: "Custom compatible endpoint", kind: "openai-compatible", baseUrl: "" }
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 ? Math.max(40, viewport.columns - 4) : Math.max(28, Math.min(48, Math.floor(viewport.columns * 0.31)));
271
- const editorWidth = compactLayout ? Math.max(30, viewport.columns - filesWidth - 4) : Math.max(30, viewport.columns - filesWidth - chatWidth - 4);
272
- const compactChatHeight = compactLayout ? clamp(Math.floor(workspaceHeight * 0.4), 6, 10) : workspaceHeight;
273
- const editorHeight = compactLayout ? Math.max(6, workspaceHeight - compactChatHeight - 1) : workspaceHeight;
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 ? [{ text: line, color }] : syntaxTokens(line, openFilePath ?? "");
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).then(setFiles).catch((error) => setEditor(`Unable to list workspace files: ${String(error)}`));
303
- void new FileWorkspacePlanStore(workspaceRoot).load().then(setActivePlan).catch(() => undefined);
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 ? detected.findIndex((endpoint) => endpoint.kind === configuration.provider && endpoint.baseUrl === configuration.baseUrl) : -1;
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({ columns: process.stdout.columns || 120, rows: process.stdout.rows || 36 });
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 () => { process.stdout.off("resize", refreshViewport); };
438
+ return () => {
439
+ process.stdout.off("resize", refreshViewport);
440
+ };
328
441
  }, []);
329
442
  useEffect(() => {
330
- if (!selectedEndpoint || !endpointInput || !isLocalEndpointKind(providerKind))
443
+ if (!selectedEndpoint ||
444
+ !endpointInput ||
445
+ !isLocalEndpointKind(providerKind))
331
446
  return;
332
- const endpoint = { ...selectedEndpoint, kind: providerKind, baseUrl: endpointInput };
333
- void listLocalModels(endpoint).then((available) => {
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
- }).catch(() => setModels([]));
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
- ? { ...current, generatedTokens: current.generatedTokens + estimateTokens(event.text) }
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 [...current.slice(0, -1), { role: "assistant", content: last.content + event.text }];
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 ?? (isCloudProviderId(providerKind) ? process.env[cloudProviderDefinition(providerKind).apiKeyEnvironmentVariable] : undefined),
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 = ["read_file", "list_directory", "search_files", "grep"].includes(call.name);
456
- if (permissionMode === "auto-all" || (permissionMode === "auto-read" && readOnly))
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({ workspaceRoot, input: prompt });
654
+ const command = await executeWorkspaceCommand({
655
+ workspaceRoot,
656
+ input: prompt,
657
+ });
516
658
  if (command.handled) {
517
659
  setChatInput("");
518
- setChat((current) => [...current, { role: "user", content: prompt }, { role: "assistant", content: command.message }]);
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) => [...current, { role: "user", content: prompt }, { role: "assistant", content: "" }]);
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 ? await client.runtime.getSession(sessionId) : await client.runtime.createSession();
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) => { terminalProcess.current = 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 = ["server", "endpoint", "model", "internet", "theme"];
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) % 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" && focus !== "chat" && focus !== "terminal" && previewUrl) {
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 ? theme.focus : entry.kind === "directory" ? theme.directory : entry.path === openFilePath ? theme.success : theme.text, bold: selected || entry.kind === "directory", children: [selected ? "> " : " ", indentation, marker, truncate(entry.name, width)] }, `${entry.kind}:${entry.path}`);
819
- }), !fileTree.length && _jsx(Text, { color: theme.muted, children: "No workspace files found." })] });
820
- const editorPanel = _jsx(Panel, { title: `${editorTitle} [up/down] scroll [d] diff${previewUrl ? " [o] browser" : ""}`, active: focus === "editor", theme: theme, children: visibleEditorRows.map((row, index) => {
821
- const lineLabel = row.continuation ? " |" : String(row.sourceLine).padStart(3);
822
- 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}`);
823
- }) });
824
- const agentPanel = _jsxs(Panel, { title: chatScroll ? `AGENT [${chatScroll} lines above]` : "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" ? theme.success : step.status === "in_progress" ? theme.warning : theme.muted, children: [step.status === "completed" ? "[x]" : step.status === "in_progress" ? "[..]" : "[ ]", " ", 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 || (busy ? "Working... Escape cancels" : "Ask about this workspace"), Math.max(12, chatWidth - 5))] })] });
825
- 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 ? `${configuration.provider} / ${configuration.model}` : "No model selected" }), _jsx(Text, { color: runStatus === "waiting" ? theme.warning : busy ? theme.focus : theme.success, children: runStatus === "waiting" ? "APPROVAL" : runStatus === "tool" ? "TOOL" : busy ? "WORKING" : "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 ? theme.error : contextTokens / contextWindow >= 0.7 ? theme.warning : 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 ? `${tokensPerSecond.toFixed(1)} tok/s` : "-- tok/s" }), previewUrl ? ` PREVIEW ${truncate(previewUrl, Math.max(16, Math.floor(viewport.columns * 0.3)))}` : ""] })] }), 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]") || line.startsWith("[terminal error]") ? theme.error : line.startsWith("[tool") ? theme.warning : 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 | 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) => {
826
- const selected = index === Math.min(fileSearchIndex, Math.max(0, fileSearchResults.length - 1));
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: [" ", truncate(entry.path, Math.max(12, overlayWidth - fileName.length - 10))] })] }, entry.path);
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 === "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().split("\n").slice(1).map((line) => _jsx(Text, { color: theme.text, children: line }, line)), _jsx(Text, { color: theme.muted, children: "Press any key to close this reference." })] })] });
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({ workspaceRoot: cwd() }).catch((error) => {
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.8",
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.8",
47
- "@truss-harness/provider-openai-compatible": "0.1.5",
48
- "@truss-harness/runtime": "0.1.5"
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"