@truss-harness/tui 0.1.8 → 0.1.13

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 (4) hide show
  1. package/LICENSE +233 -21
  2. package/README.md +15 -1
  3. package/dist/bin.js +357 -84
  4. package/package.json +5 -5
package/LICENSE CHANGED
@@ -1,21 +1,233 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Truss contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Truss Collaborative Source License, Version 1.0
2
+
3
+ Copyright (c) 2026 Jared Hooker and Truss contributors
4
+
5
+ Required Notice: This software is based on Truss.
6
+ Required Notice: Truss original project: https://github.com/truss-agent/truss-harness
7
+
8
+ This is a source-available license. It is not an Open Source Initiative
9
+ approved open-source license.
10
+
11
+ 1. Acceptance
12
+
13
+ To receive any license under these terms, you must accept them as strict
14
+ obligations and as conditions of every license granted below. If you do not
15
+ accept these terms, you receive no license.
16
+
17
+ 2. Definitions
18
+
19
+ "Software" means the Truss source code, object code, documentation, artwork,
20
+ and other material distributed with these terms, excluding Third-Party
21
+ Components identified below.
22
+
23
+ "Licensor" means each person or entity that owns copyright or patent rights in
24
+ the Software and has authority to license those rights.
25
+
26
+ "Official Project" means the Truss project maintained at
27
+ https://github.com/truss-agent/truss-harness and its official releases.
28
+
29
+ "Project Steward" means Jared Hooker or a successor publicly identified as the
30
+ project owner in the Official Project repository.
31
+
32
+ "You" means the individual or legal entity exercising rights under these
33
+ terms. For a legal entity, You includes entities that control, are controlled
34
+ by, or are under common control with that entity.
35
+
36
+ "Commercial Purpose" means any purpose intended to provide, or that does
37
+ provide, direct or indirect commercial advantage or monetary compensation to
38
+ You or another person. It includes internal business use by a for-profit
39
+ entity; use in developing, operating, or supporting a revenue-generating
40
+ product or service; sale, licensing, subscription, hosting, paid support,
41
+ consulting, advertising, or other monetization; and use intended to reduce the
42
+ cost of a commercial product or service.
43
+
44
+ "Competing Product" means any product or service, whether paid or free, that
45
+ provides the same or substantially similar functionality as the Software or an
46
+ official Truss product, or that is marketed or made available as a substitute
47
+ for any of them. Products and services may compete even when they use a
48
+ different interface, platform, programming language, architecture, or delivery
49
+ model. Coding-agent runtimes, harnesses, clients, development tools, model
50
+ routing systems, agent orchestration systems, and hosted versions of any of
51
+ those may be Competing Products.
52
+
53
+ "Contribution Activity" means reproducing, modifying, running, and sharing the
54
+ Software only as reasonably necessary to prepare, test, review, or submit a
55
+ good-faith contribution to the Official Project.
56
+
57
+ "Distribute" means to provide any copy of the Software or a work based on the
58
+ Software to another person or entity, including by publishing, transferring,
59
+ sublicensing, offering as a network service, or making it available for
60
+ download.
61
+
62
+ "Corresponding Source" means the preferred form for making modifications,
63
+ including build scripts and other material reasonably necessary to build and
64
+ run the distributed version, but excluding generally available unmodified
65
+ tools and dependencies.
66
+
67
+ "Third-Party Components" means material that is identified as being governed
68
+ by separate license terms. Those separate terms continue to control that
69
+ material.
70
+
71
+ 3. Copyright License
72
+
73
+ Subject to these terms, each Licensor grants You a worldwide, royalty-free,
74
+ non-exclusive, non-transferable, non-sublicensable copyright license to use,
75
+ reproduce, modify, and create works based on the Software solely for:
76
+
77
+ a. a purpose that is both noncommercial and does not provide or support a
78
+ Competing Product; or
79
+
80
+ b. Contribution Activity.
81
+
82
+ No permission is granted for a Commercial Purpose or a Competing Product
83
+ except under Section 8.
84
+
85
+ 4. Distribution License and Conditions
86
+
87
+ Subject to these terms, each Licensor grants You a worldwide, royalty-free,
88
+ non-exclusive, non-transferable, non-sublicensable license to Distribute the
89
+ Software, with or without modifications, only for a purpose permitted by
90
+ Section 3 and when You satisfy every condition below. You may not market,
91
+ direct, or knowingly provide a distribution for a Commercial Purpose or a
92
+ Competing Product. A public fork maintained in good faith for Contribution
93
+ Activity is permitted.
94
+
95
+ a. You must provide the complete Corresponding Source at no charge with the
96
+ distribution or through a durable, publicly accessible location clearly
97
+ identified in the distribution.
98
+
99
+ b. You must license the Software and every work based on the Software under
100
+ this exact license. You may not offer additional or conflicting terms
101
+ that restrict or expand the rights granted by this license.
102
+
103
+ c. You must include an unmodified copy of this license and preserve every
104
+ copyright, attribution, and Required Notice supplied with the Software.
105
+
106
+ d. The primary README or equivalent documentation must prominently state:
107
+ "Based on Truss (https://github.com/truss-agent/truss-harness)."
108
+
109
+ e. A program with a graphical or command-line user interface must display
110
+ the statement in Section 4(d) in a reasonably accessible About, Credits,
111
+ Help, or version view. A library or service without such an interface
112
+ must display it in its primary documentation and package metadata where
113
+ that metadata format supports attribution.
114
+
115
+ f. You must identify significant modifications and must not imply that a
116
+ modified version is an official Truss release or is endorsed by the
117
+ Official Project.
118
+
119
+ g. You may not remove, obscure, minimize, or bypass the notices required by
120
+ this section.
121
+
122
+ Merely creating a private clone does not require publication, but every notice
123
+ already present in that clone must remain intact. Distribution through a fork,
124
+ mirror, package, archive, installer, container, application, or network service
125
+ is subject to this section.
126
+
127
+ 5. No Commercial Use
128
+
129
+ You may not use, reproduce, modify, create works based on, or Distribute the
130
+ Software for a Commercial Purpose without separate written permission under
131
+ Section 8. A commercial activity does not become noncommercial because the
132
+ Software is provided without a separate charge.
133
+
134
+ 6. No Competitive Use
135
+
136
+ You may not use, reproduce, modify, create works based on, or Distribute the
137
+ Software to build, provide, maintain, market, fund, or support a Competing
138
+ Product without separate written permission under Section 8. This restriction
139
+ applies even if the Competing Product is free, source-available, internal,
140
+ nonprofit, or not yet publicly released.
141
+
142
+ 7. Contributions Are Welcome
143
+
144
+ You may perform Contribution Activity even when You work for a commercial
145
+ organization, provided the activity is genuinely directed toward improving
146
+ the Official Project and is not used to develop or support a separate
147
+ Commercial Purpose or Competing Product.
148
+
149
+ If You intentionally submit a contribution for inclusion in the Official
150
+ Project, You retain ownership of Your contribution and grant the Project
151
+ Steward a perpetual, worldwide, irrevocable, non-exclusive, royalty-free
152
+ copyright license to use, reproduce, modify, prepare derivative works of,
153
+ publicly display, publicly perform, Distribute, sublicense, and relicense that
154
+ contribution as part of or in connection with Truss. Recipients receive rights
155
+ to the contribution only under the license that accompanies the Official
156
+ Project version they receive. You also grant the Project Steward a patent
157
+ license covering patent claims You can license that would necessarily be
158
+ infringed by the contribution alone or in combination with the Official
159
+ Project, with the right to sublicense that patent license to recipients under
160
+ the terms accompanying the Official Project.
161
+
162
+ You represent that You have the right to grant these permissions and that You
163
+ will identify any contribution governed by separate third-party terms.
164
+
165
+ 8. Separate Commercial or Competitive License
166
+
167
+ The Project Steward or applicable Licensor may offer separate written terms
168
+ for a Commercial Purpose, a Competing Product, reduced attribution, or another
169
+ use not permitted here. Permission is effective only when stated in a separate
170
+ written agreement signed or expressly issued by the party authorized to grant
171
+ it. Payment, sponsorship, contribution, or prior communication alone does not
172
+ create permission.
173
+
174
+ 9. Patents
175
+
176
+ Subject to these terms, each Licensor grants You a worldwide, royalty-free,
177
+ non-exclusive patent license under patent claims the Licensor can license that
178
+ You would necessarily infringe by exercising the copyright licenses granted
179
+ here. If You or Your organization makes a written claim that the Software
180
+ infringes or contributes to infringement of a patent, every patent license
181
+ granted to You under these terms ends immediately.
182
+
183
+ 10. Trademarks
184
+
185
+ These terms do not grant permission to use the Truss name, logos, trade dress,
186
+ or other marks except as strictly necessary to provide the attribution
187
+ required by Section 4. You may not use any mark in a way that suggests
188
+ affiliation with or endorsement by the Official Project.
189
+
190
+ 11. Third-Party Components
191
+
192
+ Third-Party Components remain governed by their own license terms. Nothing in
193
+ this license limits rights You receive directly under those terms, and nothing
194
+ in those terms expands Your rights in the Software.
195
+
196
+ 12. Termination and Cure
197
+
198
+ Any license granted under these terms terminates automatically when You breach
199
+ them. If this is Your first breach and You cure it fully within 30 days after
200
+ receiving written notice from a Licensor, Your license is reinstated
201
+ retroactively. A later breach terminates Your license immediately and
202
+ permanently unless the applicable Licensor agrees otherwise in writing.
203
+
204
+ Sections that by their nature should survive termination, including Sections
205
+ 7, 10, 13, 14, and 15, survive.
206
+
207
+ 13. Disclaimer of Warranty
208
+
209
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS
210
+ OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
211
+ PURPOSE, TITLE, AND NON-INFRINGEMENT. YOU BEAR ALL RISK ARISING FROM USE OR
212
+ DISTRIBUTION OF THE SOFTWARE.
213
+
214
+ 14. Limitation of Liability
215
+
216
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, NO LICENSOR, PROJECT STEWARD, OR
217
+ CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
218
+ EXEMPLARY, CONSEQUENTIAL, OR OTHER DAMAGES, OR FOR LOST PROFITS, DATA,
219
+ GOODWILL, OR BUSINESS INTERRUPTION, ARISING FROM OR RELATED TO THE SOFTWARE OR
220
+ THESE TERMS, UNDER ANY LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY.
221
+
222
+ 15. General Terms
223
+
224
+ If a provision of these terms is unenforceable, it will be modified only to
225
+ the minimum extent necessary to make it enforceable, and the remaining terms
226
+ will continue in effect. Failure to enforce a provision is not a waiver. These
227
+ terms do not limit rights You may have under applicable law without a license,
228
+ such as fair use.
229
+
230
+ This license applies only to Software versions distributed with it. Software
231
+ versions previously received under another license remain governed by the
232
+ license that accompanied those versions. No future license version applies
233
+ unless the Software is distributed with that version.
package/README.md CHANGED
@@ -29,7 +29,12 @@ The default `forest` theme uses restrained green, sage, and teal accents with so
29
29
 
30
30
  Inside the TUI, open settings with `m`, Tab to **Theme**, and use Left or Right to preview a preset for the current session.
31
31
 
32
- Configured MCP stdio servers are connected when an Agent session starts, and their connection status appears in the Terminal pane. Plan mode loads only servers explicitly marked `readOnly`. Run `truss-cli config path` to locate the shared user and workspace configuration files.
32
+ Configured MCP stdio servers are connected when an Agent session starts, and
33
+ their connection status appears in the Terminal pane. Press `c` outside the
34
+ chat/terminal panes to inspect safe server and tool status, then `R` to test
35
+ the configured servers. Plan mode loads only servers explicitly marked
36
+ `readOnly`. Run `truss-cli config path` to locate the shared user and workspace
37
+ configuration files.
33
38
 
34
39
  ## Controls
35
40
 
@@ -41,6 +46,7 @@ Configured MCP stdio servers are connected when an Agent session starts, and the
41
46
  | `Enter` | Send the chat input or open the selected file. |
42
47
  | `Left` / `Right` | Collapse or expand the selected directory. |
43
48
  | `m` | Open local model settings outside the chat pane. |
49
+ | `c` | Open credential-safe MCP server and tool status. |
44
50
  | `d` | Toggle the selected file's Git diff. |
45
51
  | Type directly | Enter a command while the terminal pane has focus. |
46
52
  | `?` | Open workspace command help. |
@@ -48,3 +54,11 @@ Configured MCP stdio servers are connected when an Agent session starts, and the
48
54
  | `Ctrl+C` | Stop generation, or exit while idle. |
49
55
 
50
56
  Type `/init`, `/update [note]`, `/status`, `/clear-memory`, or `/help` into the Agent pane. They run locally without a model call. Approvals for model-initiated tools still respect the selected permission policy.
57
+
58
+ ## License and contributions
59
+
60
+ This client is source-available under the [Truss Collaborative Source
61
+ License](LICENSE). Contributions to the official project are welcome. Every
62
+ copy, fork, or derivative must preserve the license and prominently state:
63
+ **Based on Truss (https://github.com/truss-agent/truss-harness).** Commercial
64
+ use and competing products or services require separate written permission.
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,43 @@ 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
+ "MCP",
50
+ "c opens credential-safe server and tool status; R tests configured servers.",
51
+ ],
52
+ [
53
+ "TERMINAL",
54
+ "Type directly and press Enter; Up/Down scroll output; slash commands run through Truss.",
55
+ ],
56
+ [
57
+ "SETTINGS",
58
+ "m opens settings outside chat; Tab changes fields; Theme previews forest, sage, and dusk.",
59
+ ],
37
60
  ["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."]
61
+ [
62
+ "CANCEL / EXIT",
63
+ "Escape cancels an active agent run. Ctrl+C stops a process or run, and exits while idle.",
64
+ ],
39
65
  ];
40
- if (process.argv.slice(2).some((argument) => argument === "--help" || argument === "-h")) {
66
+ if (process.argv
67
+ .slice(2)
68
+ .some((argument) => argument === "--help" || argument === "-h")) {
41
69
  process.stdout.write(tuiHelp);
42
70
  process.exit(0);
43
71
  }
@@ -46,23 +74,43 @@ function ensureInteractiveTerminal() {
46
74
  return;
47
75
  // Git Bash exposes a pipe to Node for some console programs. winpty creates
48
76
  // 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)], {
77
+ if (process.platform === "win32" &&
78
+ process.env.MSYSTEM &&
79
+ process.env.TRUSS_TUI_WINPTY !== "1") {
80
+ const result = spawnSync("winpty", [
81
+ process.execPath,
82
+ fileURLToPath(import.meta.url),
83
+ ...process.argv.slice(2),
84
+ ], {
51
85
  stdio: "inherit",
52
- env: { ...process.env, TRUSS_TUI_WINPTY: "1" }
86
+ env: { ...process.env, TRUSS_TUI_WINPTY: "1" },
53
87
  });
54
88
  if (!result.error)
55
89
  process.exit(result.status ?? 1);
56
90
  }
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");
91
+ process.stderr.write(brand.productName +
92
+ " TUI needs an interactive terminal with raw keyboard input. Open PowerShell or Windows Terminal and run " +
93
+ brand.tuiCommand +
94
+ ", or run winpty " +
95
+ brand.tuiCommand +
96
+ " from Git Bash.\n");
58
97
  process.exit(1);
59
98
  }
60
99
  ensureInteractiveTerminal();
61
100
  const execFile = promisify(execFileCallback);
62
- const ignoredDirectories = new Set([".git", ".next", ".truss-harness", "node_modules", "dist", "coverage"]);
101
+ const ignoredDirectories = new Set([
102
+ ".git",
103
+ ".next",
104
+ ".truss-harness",
105
+ "node_modules",
106
+ "dist",
107
+ "coverage",
108
+ ]);
63
109
  const focusOrder = ["files", "editor", "chat", "terminal"];
64
110
  function truncate(value, length) {
65
- return value.length <= length ? value : `${value.slice(0, Math.max(0, length - 3))}...`;
111
+ return value.length <= length
112
+ ? value
113
+ : `${value.slice(0, Math.max(0, length - 3))}...`;
66
114
  }
67
115
  function clamp(value, minimum, maximum) {
68
116
  return Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
@@ -95,7 +143,9 @@ function wrapText(value, width) {
95
143
  }
96
144
  function plainChatText(value) {
97
145
  let inCodeBlock = false;
98
- return value.split(/\r?\n/).flatMap((sourceLine) => {
146
+ return value
147
+ .split(/\r?\n/)
148
+ .flatMap((sourceLine) => {
99
149
  if (/^\s*```/.test(sourceLine)) {
100
150
  inCodeBlock = !inCodeBlock;
101
151
  return [];
@@ -111,23 +161,38 @@ function plainChatText(value) {
111
161
  .replace(/`([^`]+)`/g, "$1")
112
162
  .replace(/(\*\*|__|\*|_|~~)/g, "");
113
163
  return /^\s*[-:|]+\s*$/.test(line) ? [] : [line];
114
- }).join("\n");
164
+ })
165
+ .join("\n");
115
166
  }
116
167
  function chatDisplayLines(messages, busy, width) {
117
168
  const transcript = messages.flatMap((message) => {
118
169
  const content = message.content
119
- ? message.role === "assistant" ? plainChatText(message.content) : message.content
120
- : busy && message.role === "assistant" ? "Thinking..." : "";
170
+ ? message.role === "assistant"
171
+ ? plainChatText(message.content)
172
+ : message.content
173
+ : busy && message.role === "assistant"
174
+ ? "Thinking..."
175
+ : "";
121
176
  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 }))
177
+ {
178
+ role: message.role,
179
+ text: message.role === "user" ? "YOU" : "AGENT",
180
+ header: true,
181
+ },
182
+ ...wrapText(content, width).map((text) => ({
183
+ role: message.role,
184
+ text,
185
+ header: false,
186
+ })),
124
187
  ];
125
188
  });
126
189
  return transcript;
127
190
  }
128
191
  function configuredContextWindow() {
129
192
  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;
193
+ return Number.isFinite(configured)
194
+ ? Math.max(512, Math.min(1_000_000, configured))
195
+ : 8_192;
131
196
  }
132
197
  async function collectFiles(root, current = root, result = []) {
133
198
  if (result.length >= 2_000)
@@ -144,7 +209,11 @@ async function collectFiles(root, current = root, result = []) {
144
209
  }
145
210
  async function runTrackedCommand(command, workspaceRoot, onOutput, onProcess) {
146
211
  await new Promise((resolve, reject) => {
147
- const child = spawn(command, { cwd: workspaceRoot, shell: true, windowsHide: true });
212
+ const child = spawn(command, {
213
+ cwd: workspaceRoot,
214
+ shell: true,
215
+ windowsHide: true,
216
+ });
148
217
  onProcess?.(child);
149
218
  child.stdout.on("data", (data) => onOutput(data.toString()));
150
219
  child.stderr.on("data", (data) => onOutput(data.toString()));
@@ -161,7 +230,9 @@ function detectedPreviewUrl(output) {
161
230
  return match?.[0].replace(/[),.;]+$/, "").replace("0.0.0.0", "127.0.0.1");
162
231
  }
163
232
  function normalizedPreviewUrl(value) {
164
- const normalized = /^[a-z][a-z\d+.-]*:\/\//i.test(value.trim()) ? value.trim() : `http://${value.trim()}`;
233
+ const normalized = /^[a-z][a-z\d+.-]*:\/\//i.test(value.trim())
234
+ ? value.trim()
235
+ : `http://${value.trim()}`;
165
236
  const url = new URL(normalized);
166
237
  if (url.protocol !== "http:" && url.protocol !== "https:")
167
238
  throw new Error("Preview URLs must use HTTP or HTTPS.");
@@ -174,7 +245,11 @@ function openExternalPreview(value) {
174
245
  : process.platform === "darwin"
175
246
  ? ["open", [url]]
176
247
  : ["xdg-open", [url]];
177
- const child = spawn(command, arguments_, { detached: true, stdio: "ignore", windowsHide: true });
248
+ const child = spawn(command, arguments_, {
249
+ detached: true,
250
+ stdio: "ignore",
251
+ windowsHide: true,
252
+ });
178
253
  child.once("error", () => undefined);
179
254
  child.unref();
180
255
  }
@@ -190,15 +265,23 @@ async function stopProcessTree(child) {
190
265
  }
191
266
  child.kill();
192
267
  }
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] });
268
+ function Panel({ title, active, theme, children, }) {
269
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: active ? theme.focus : theme.panel, paddingX: 1, width: "100%", height: "100%", overflow: "hidden", children: [_jsx(Text, { color: active ? theme.focus : theme.muted, bold: true, children: title }), children] }));
195
270
  }
196
- function App({ initialConfiguration }) {
271
+ function App({ initialConfiguration, }) {
197
272
  const { exit } = useApp();
198
- const [viewport, setViewport] = useState(() => ({ columns: process.stdout.columns || 120, rows: process.stdout.rows || 36 }));
273
+ const [viewport, setViewport] = useState(() => ({
274
+ columns: process.stdout.columns || 120,
275
+ rows: process.stdout.rows || 36,
276
+ }));
199
277
  const workspaceRoot = useMemo(() => cwd(), []);
200
278
  const [configuration, setConfiguration] = useState(initialConfiguration);
201
279
  const [client, setClient] = useState();
280
+ const [mcpStatuses, setMcpStatuses] = useState(() => Object.entries(initialConfiguration?.mcpServers ?? {}).map(([name, server]) => ({
281
+ name,
282
+ state: server.enabled === false ? "disabled" : "idle",
283
+ toolCount: 0,
284
+ })));
202
285
  const [sessionId, setSessionId] = useState();
203
286
  const [files, setFiles] = useState([]);
204
287
  const [fileIndex, setFileIndex] = useState(0);
@@ -214,8 +297,13 @@ function App({ initialConfiguration }) {
214
297
  const [chatInput, setChatInput] = useState("");
215
298
  const [chat, setChat] = useState([]);
216
299
  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."]);
300
+ const [streamMetrics, setStreamMetrics] = useState({
301
+ startedAt: 0,
302
+ generatedTokens: 0,
303
+ });
304
+ const [terminalLines, setTerminalLines] = useState([
305
+ "Terminal ready. Shell commands run in the workspace root; slash commands run locally.",
306
+ ]);
219
307
  const [terminalScroll, setTerminalScroll] = useState(0);
220
308
  const [previewUrl, setPreviewUrl] = useState();
221
309
  const [editorScroll, setEditorScroll] = useState(0);
@@ -227,6 +315,7 @@ function App({ initialConfiguration }) {
227
315
  const [endpoints, setEndpoints] = useState([]);
228
316
  const [serverIndex, setServerIndex] = useState(0);
229
317
  const [models, setModels] = useState([]);
318
+ const [agentProfiles, setAgentProfiles] = useState([]);
230
319
  const [modelIndex, setModelIndex] = useState(0);
231
320
  const [settingsField, setSettingsField] = useState("server");
232
321
  const [themeName, setThemeName] = useState(initialConfiguration?.tuiTheme ?? "forest");
@@ -242,7 +331,8 @@ function App({ initialConfiguration }) {
242
331
  const contextWindow = useMemo(configuredContextWindow, []);
243
332
  const contextTokens = useMemo(() => chat.reduce((total, message) => total + estimateTokens(message.content), 400), [chat]);
244
333
  const tokensPerSecond = streamMetrics.startedAt && streamMetrics.generatedTokens
245
- ? streamMetrics.generatedTokens / Math.max((Date.now() - streamMetrics.startedAt) / 1_000, 0.1)
334
+ ? streamMetrics.generatedTokens /
335
+ Math.max((Date.now() - streamMetrics.startedAt) / 1_000, 0.1)
246
336
  : undefined;
247
337
  const theme = tuiTheme(themeName);
248
338
  const appendTerminal = (output) => {
@@ -257,7 +347,12 @@ function App({ initialConfiguration }) {
257
347
  };
258
348
  const candidates = useMemo(() => [
259
349
  ...endpoints,
260
- { id: "custom", label: "Custom compatible endpoint", kind: "openai-compatible", baseUrl: "" }
350
+ {
351
+ id: "custom",
352
+ label: "Custom compatible endpoint",
353
+ kind: "openai-compatible",
354
+ baseUrl: "",
355
+ },
261
356
  ], [endpoints]);
262
357
  const selectedEndpoint = candidates[Math.min(serverIndex, Math.max(candidates.length - 1, 0))];
263
358
  const fileTree = useMemo(() => buildFileTree(files, collapsedDirectories), [files, collapsedDirectories]);
@@ -267,10 +362,18 @@ function App({ initialConfiguration }) {
267
362
  const terminalHeight = clamp(Math.floor(viewport.rows * 0.24), 7, 11);
268
363
  const workspaceHeight = Math.max(compactLayout ? 14 : 9, viewport.rows - terminalHeight - 6);
269
364
  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;
365
+ const chatWidth = compactLayout
366
+ ? Math.max(40, viewport.columns - 4)
367
+ : Math.max(28, Math.min(48, Math.floor(viewport.columns * 0.31)));
368
+ const editorWidth = compactLayout
369
+ ? Math.max(30, viewport.columns - filesWidth - 4)
370
+ : Math.max(30, viewport.columns - filesWidth - chatWidth - 4);
371
+ const compactChatHeight = compactLayout
372
+ ? clamp(Math.floor(workspaceHeight * 0.4), 6, 10)
373
+ : workspaceHeight;
374
+ const editorHeight = compactLayout
375
+ ? Math.max(6, workspaceHeight - compactChatHeight - 1)
376
+ : workspaceHeight;
274
377
  const editorLineCount = Math.max(2, editorHeight - 3);
275
378
  const chatLineCount = Math.max(2, compactChatHeight - (activePlan ? 7 : 0) - 4);
276
379
  const chatTranscript = chatDisplayLines(chat, busy, Math.max(12, chatWidth - 5));
@@ -285,11 +388,13 @@ function App({ initialConfiguration }) {
285
388
  ? "red"
286
389
  : undefined
287
390
  : undefined;
288
- const tokens = isDiff ? [{ text: line, color }] : syntaxTokens(line, openFilePath ?? "");
391
+ const tokens = isDiff
392
+ ? [{ text: line, color }]
393
+ : syntaxTokens(line, openFilePath ?? "");
289
394
  return wrapSyntaxTokens(tokens, maximumCharacters).map((row, rowIndex) => ({
290
395
  sourceLine: sourceIndex + 1,
291
396
  continuation: rowIndex > 0,
292
- tokens: row
397
+ tokens: row,
293
398
  }));
294
399
  });
295
400
  }, [editor, editorWidth, isDiff, openFilePath]);
@@ -299,11 +404,23 @@ function App({ initialConfiguration }) {
299
404
  const visibleFileTree = fileTree.slice(fileTreeStart, fileTreeStart + editorLineCount);
300
405
  const overlayWidth = clamp(viewport.columns - 4, 42, 90);
301
406
  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);
407
+ void collectFiles(workspaceRoot)
408
+ .then(setFiles)
409
+ .catch((error) => setEditor(`Unable to list workspace files: ${String(error)}`));
410
+ void new FileAgentProfileStore(workspaceRoot)
411
+ .list()
412
+ .then(setAgentProfiles)
413
+ .catch((error) => appendTerminal(`[agents] ${error instanceof Error ? error.message : String(error)}`));
414
+ void new FileWorkspacePlanStore(workspaceRoot)
415
+ .load()
416
+ .then(setActivePlan)
417
+ .catch(() => undefined);
304
418
  void detectLocalEndpoints().then((detected) => {
305
419
  setEndpoints(detected);
306
- const preferred = configuration ? detected.findIndex((endpoint) => endpoint.kind === configuration.provider && endpoint.baseUrl === configuration.baseUrl) : -1;
420
+ const preferred = configuration
421
+ ? detected.findIndex((endpoint) => endpoint.kind === configuration.provider &&
422
+ endpoint.baseUrl === configuration.baseUrl)
423
+ : -1;
307
424
  if (preferred >= 0)
308
425
  setServerIndex(preferred);
309
426
  });
@@ -322,35 +439,60 @@ function App({ initialConfiguration }) {
322
439
  setFileIndex(index);
323
440
  }, [fileTree, openFilePath]);
324
441
  useEffect(() => {
325
- const refreshViewport = () => setViewport({ columns: process.stdout.columns || 120, rows: process.stdout.rows || 36 });
442
+ const refreshViewport = () => setViewport({
443
+ columns: process.stdout.columns || 120,
444
+ rows: process.stdout.rows || 36,
445
+ });
326
446
  process.stdout.on("resize", refreshViewport);
327
- return () => { process.stdout.off("resize", refreshViewport); };
447
+ return () => {
448
+ process.stdout.off("resize", refreshViewport);
449
+ };
328
450
  }, []);
329
451
  useEffect(() => {
330
- if (!selectedEndpoint || !endpointInput || !isLocalEndpointKind(providerKind))
452
+ if (!selectedEndpoint ||
453
+ !endpointInput ||
454
+ !isLocalEndpointKind(providerKind))
331
455
  return;
332
- const endpoint = { ...selectedEndpoint, kind: providerKind, baseUrl: endpointInput };
333
- void listLocalModels(endpoint).then((available) => {
456
+ const endpoint = {
457
+ ...selectedEndpoint,
458
+ kind: providerKind,
459
+ baseUrl: endpointInput,
460
+ };
461
+ void listLocalModels(endpoint)
462
+ .then((available) => {
334
463
  setModels(available.map((model) => model.name));
335
464
  const selected = available.findIndex((model) => model.name === modelInput);
336
465
  setModelIndex(Math.max(0, selected));
337
- }).catch(() => setModels([]));
466
+ })
467
+ .catch(() => setModels([]));
338
468
  }, [selectedEndpoint?.id, endpointInput, providerKind]);
339
469
  useEffect(() => {
340
470
  if (!client)
341
471
  return;
472
+ const unsubscribeMcp = client.mcp.subscribe((statuses) => setMcpStatuses((current) => {
473
+ const merged = new Map(current.map((status) => [status.name, status]));
474
+ for (const status of statuses)
475
+ merged.set(status.name, status);
476
+ return [...merged.values()];
477
+ }));
342
478
  const unsubscribe = client.events.subscribe((event) => {
343
479
  if (event.type === "text_delta") {
344
480
  setRunStatus("thinking");
345
481
  setChatScroll(0);
346
482
  setStreamMetrics((current) => current.startedAt
347
- ? { ...current, generatedTokens: current.generatedTokens + estimateTokens(event.text) }
483
+ ? {
484
+ ...current,
485
+ generatedTokens: current.generatedTokens + estimateTokens(event.text),
486
+ }
348
487
  : current);
349
488
  setChat((current) => {
350
489
  const last = current.at(-1);
351
490
  if (!last || last.role !== "assistant")
352
491
  return [...current, { role: "assistant", content: event.text }];
353
- return [...current.slice(0, -1), { role: "assistant", content: last.content + event.text }];
492
+ return [
493
+ ...current.slice(0, -1),
494
+ { role: "assistant", content: last.content + event.text },
495
+ ];
354
496
  });
355
497
  }
356
498
  if (event.type === "tool_call_requested") {
@@ -370,6 +512,7 @@ function App({ initialConfiguration }) {
370
512
  });
371
513
  return () => {
372
514
  unsubscribe();
515
+ unsubscribeMcp();
373
516
  void client.dispose();
374
517
  };
375
518
  }, [client]);
@@ -446,14 +589,25 @@ function App({ initialConfiguration }) {
446
589
  model: modelInput.trim(),
447
590
  apiKey: initialConfiguration?.provider === providerKind
448
591
  ? initialConfiguration.apiKey
449
- : process.env.TRUSS_HARNESS_API_KEY ?? (isCloudProviderId(providerKind) ? process.env[cloudProviderDefinition(providerKind).apiKeyEnvironmentVariable] : undefined),
592
+ : (process.env.TRUSS_HARNESS_API_KEY ??
593
+ (isCloudProviderId(providerKind)
594
+ ? process.env[cloudProviderDefinition(providerKind)
595
+ .apiKeyEnvironmentVariable]
596
+ : undefined)),
450
597
  systemPrompt: process.env.TRUSS_HARNESS_SYSTEM_PROMPT,
451
598
  mode: agentMode,
452
599
  internetAccess,
600
+ mcpServers: initialConfiguration?.mcpServers,
453
601
  approval: {
454
602
  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))
603
+ const readOnly = [
604
+ "read_file",
605
+ "list_directory",
606
+ "search_files",
607
+ "grep",
608
+ ].includes(call.name);
609
+ if (permissionMode === "auto-all" ||
610
+ (permissionMode === "auto-read" && readOnly))
457
611
  return true;
458
612
  return new Promise((resolve) => {
459
613
  approvalResolvers.current.set(call.id, resolve);
@@ -461,8 +615,8 @@ function App({ initialConfiguration }) {
461
615
  setRunStatus("waiting");
462
616
  setScreen("approval");
463
617
  });
464
- }
465
- }
618
+ },
619
+ },
466
620
  };
467
621
  setConfiguration(nextConfiguration);
468
622
  setRunStatus("thinking");
@@ -498,11 +652,13 @@ function App({ initialConfiguration }) {
498
652
  const maximumCharacters = Math.max(2_000, Math.min(20_000, contextWindow));
499
653
  const content = await readFile(join(workspaceRoot, openFilePath), "utf8");
500
654
  const clipped = content.slice(0, maximumCharacters);
501
- return [{
655
+ return [
656
+ {
502
657
  source: `active-file:${openFilePath}`,
503
658
  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
- }];
659
+ priority: 1_000,
660
+ },
661
+ ];
506
662
  }
507
663
  catch {
508
664
  return [];
@@ -512,10 +668,17 @@ function App({ initialConfiguration }) {
512
668
  const prompt = chatInput.trim();
513
669
  if (!prompt || busy)
514
670
  return;
515
- const command = await executeWorkspaceCommand({ workspaceRoot, input: prompt });
671
+ const command = await executeWorkspaceCommand({
672
+ workspaceRoot,
673
+ input: prompt,
674
+ });
516
675
  if (command.handled) {
517
676
  setChatInput("");
518
- setChat((current) => [...current, { role: "user", content: prompt }, { role: "assistant", content: command.message }]);
677
+ setChat((current) => [
678
+ ...current,
679
+ { role: "user", content: prompt },
680
+ { role: "assistant", content: command.message },
681
+ ]);
519
682
  appendTerminal(`[workspace command] ${command.command ?? prompt}: ${command.ok ? "completed" : "failed"}`);
520
683
  return;
521
684
  }
@@ -529,11 +692,17 @@ function App({ initialConfiguration }) {
529
692
  setBusy(true);
530
693
  setRunStatus("thinking");
531
694
  setStreamMetrics({ startedAt: Date.now(), generatedTokens: 0 });
532
- setChat((current) => [...current, { role: "user", content: prompt }, { role: "assistant", content: "" }]);
695
+ setChat((current) => [
696
+ ...current,
697
+ { role: "user", content: prompt },
698
+ { role: "assistant", content: "" },
699
+ ]);
533
700
  const controller = new AbortController();
534
701
  abortController.current = controller;
535
702
  try {
536
- const session = sessionId ? await client.runtime.getSession(sessionId) : await client.runtime.createSession();
703
+ const session = sessionId
704
+ ? await client.runtime.getSession(sessionId)
705
+ : await client.runtime.createSession();
537
706
  if (!session)
538
707
  throw new Error("Conversation is unavailable.");
539
708
  setSessionId(session.id);
@@ -575,7 +744,9 @@ function App({ initialConfiguration }) {
575
744
  appendTerminal(command.message);
576
745
  return;
577
746
  }
578
- await runTrackedCommand(input, workspaceRoot, appendTerminal, (process) => { terminalProcess.current = process; });
747
+ await runTrackedCommand(input, workspaceRoot, appendTerminal, (process) => {
748
+ terminalProcess.current = process;
749
+ });
579
750
  };
580
751
  const moveFocus = (direction) => {
581
752
  setFocus((current) => {
@@ -641,13 +812,45 @@ function App({ initialConfiguration }) {
641
812
  setScreen("workspace");
642
813
  return;
643
814
  }
815
+ if (screen === "agents") {
816
+ if (key.escape || input.toLowerCase() === "a")
817
+ setScreen("workspace");
818
+ if (input.toLowerCase() === "r")
819
+ void new FileAgentProfileStore(workspaceRoot)
820
+ .list()
821
+ .then(setAgentProfiles)
822
+ .catch((error) => appendTerminal(`[agents] ${error instanceof Error ? error.message : String(error)}`));
823
+ return;
824
+ }
825
+ if (screen === "mcp") {
826
+ if (key.escape || input.toLowerCase() === "c") {
827
+ setScreen("workspace");
828
+ return;
829
+ }
830
+ if (input.toLowerCase() === "r" && configuration) {
831
+ void createClientRuntime({ ...configuration, mode: "edit" })
832
+ .then(async (inspection) => {
833
+ setMcpStatuses(inspection.mcpServers);
834
+ appendTerminal("[mcp] Tested configured servers.");
835
+ await inspection.dispose();
836
+ })
837
+ .catch((error) => appendTerminal(`[mcp] ${error instanceof Error ? error.message : String(error)}`));
838
+ }
839
+ return;
840
+ }
644
841
  if (screen === "settings") {
645
842
  if (key.escape) {
646
843
  setScreen("workspace");
647
844
  return;
648
845
  }
649
846
  if (key.tab) {
650
- const fields = ["server", "endpoint", "model", "internet", "theme"];
847
+ const fields = [
848
+ "server",
849
+ "endpoint",
850
+ "model",
851
+ "internet",
852
+ "theme",
853
+ ];
651
854
  setSettingsField((current) => {
652
855
  const index = fields.indexOf(current);
653
856
  return fields[(index + (key.shift ? -1 : 1) + fields.length) % fields.length];
@@ -683,7 +886,8 @@ function App({ initialConfiguration }) {
683
886
  }
684
887
  if (settingsField === "theme") {
685
888
  if (key.leftArrow || key.upArrow)
686
- setThemeName((current) => tuiThemeNames[(tuiThemeNames.indexOf(current) - 1 + tuiThemeNames.length) % tuiThemeNames.length]);
889
+ setThemeName((current) => tuiThemeNames[(tuiThemeNames.indexOf(current) - 1 + tuiThemeNames.length) %
890
+ tuiThemeNames.length]);
687
891
  if (key.rightArrow || key.downArrow || input === " ")
688
892
  setThemeName((current) => tuiThemeNames[(tuiThemeNames.indexOf(current) + 1) % tuiThemeNames.length]);
689
893
  if (key.return && endpointInput && modelInput)
@@ -705,6 +909,12 @@ function App({ initialConfiguration }) {
705
909
  moveFocus(key.leftArrow ? -1 : 1);
706
910
  return;
707
911
  }
912
+ if (input.toLowerCase() === "a" &&
913
+ focus !== "chat" &&
914
+ focus !== "terminal") {
915
+ setScreen("agents");
916
+ return;
917
+ }
708
918
  if (key.tab) {
709
919
  moveFocus(key.shift ? -1 : 1);
710
920
  return;
@@ -715,7 +925,10 @@ function App({ initialConfiguration }) {
715
925
  appendTerminal("[agent] Cancellation requested.");
716
926
  return;
717
927
  }
718
- if (input === "o" && focus !== "chat" && focus !== "terminal" && previewUrl) {
928
+ if (input === "o" &&
929
+ focus !== "chat" &&
930
+ focus !== "terminal" &&
931
+ previewUrl) {
719
932
  try {
720
933
  openExternalPreview(previewUrl);
721
934
  appendTerminal(`[preview] Opened ${previewUrl}`);
@@ -755,6 +968,10 @@ function App({ initialConfiguration }) {
755
968
  setScreen("settings");
756
969
  return;
757
970
  }
971
+ if (input.toLowerCase() === "c" && focus !== "chat") {
972
+ setScreen("mcp");
973
+ return;
974
+ }
758
975
  if (input === "n" && focus !== "chat") {
759
976
  startNewConversation();
760
977
  return;
@@ -810,25 +1027,81 @@ function App({ initialConfiguration }) {
810
1027
  else if (input)
811
1028
  setChatInput((current) => current + input);
812
1029
  });
813
- const filesPanel = _jsxs(Panel, { title: "FILES [/] find", active: focus === "files", theme: theme, children: [visibleFileTree.map((entry, visibleIndex) => {
1030
+ const filesPanel = (_jsxs(Panel, { title: "FILES [/] find", active: focus === "files", theme: theme, children: [visibleFileTree.map((entry, visibleIndex) => {
814
1031
  const selected = fileTreeStart + visibleIndex === fileIndex;
815
1032
  const marker = entry.kind === "directory" ? (entry.expanded ? "v " : "> ") : " ";
816
1033
  const indentation = " ".repeat(entry.depth * 2);
817
1034
  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));
1035
+ return (_jsxs(Text, { wrap: "truncate-end", color: selected
1036
+ ? theme.focus
1037
+ : entry.kind === "directory"
1038
+ ? theme.directory
1039
+ : entry.path === openFilePath
1040
+ ? theme.success
1041
+ : theme.text, bold: selected || entry.kind === "directory", children: [selected ? "> " : " ", indentation, marker, truncate(entry.name, width)] }, `${entry.kind}:${entry.path}`));
1042
+ }), !fileTree.length && (_jsx(Text, { color: theme.muted, children: "No workspace files found." }))] }));
1043
+ const editorPanel = (_jsx(Panel, { title: `${editorTitle} [up/down] scroll [d] diff${previewUrl ? " [o] browser" : ""}`, active: focus === "editor", theme: theme, children: visibleEditorRows.map((row, index) => {
1044
+ const lineLabel = row.continuation
1045
+ ? " |"
1046
+ : String(row.sourceLine).padStart(3);
1047
+ 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}`));
1048
+ }) }));
1049
+ const agentPanel = (_jsxs(Panel, { title: chatScroll
1050
+ ? `AGENT [${chatScroll} lines above]`
1051
+ : "AGENT [Enter] send", active: focus === "chat", theme: theme, children: [activePlan && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: theme.accent, wrap: "truncate-end", children: ["PLAN: ", truncate(activePlan.title, Math.max(8, chatWidth - 9))] }), activePlan.steps.slice(0, 3).map((step) => (_jsxs(Text, { wrap: "truncate-end", color: step.status === "completed"
1052
+ ? theme.success
1053
+ : step.status === "in_progress"
1054
+ ? theme.warning
1055
+ : theme.muted, children: [step.status === "completed"
1056
+ ? "[x]"
1057
+ : step.status === "in_progress"
1058
+ ? "[..]"
1059
+ : "[ ]", " ", truncate(step.content, Math.max(8, chatWidth - 7))] }, step.id)))] })), chatLines.map((line, index) => (_jsx(Text, { color: line.role === "user" ? theme.user : theme.agent, bold: line.header, children: line.text }, `${line.role}-${line.header}-${index}-${line.text}`))), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { wrap: "truncate-end", color: focus === "chat" ? theme.focus : theme.muted, children: [focus === "chat" ? "> " : " ", truncate(chatInput ||
1060
+ (busy ? "Working... Escape cancels" : "Ask about this workspace"), Math.max(12, chatWidth - 5))] })] }));
1061
+ return (_jsxs(Box, { flexDirection: "column", height: viewport.rows, paddingX: 1, overflow: "hidden", children: [_jsxs(Box, { height: 1, justifyContent: "space-between", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.accent, children: brand.productName.toUpperCase() }), _jsx(Text, { color: theme.muted, children: configuration
1062
+ ? `${configuration.provider} / ${configuration.model}`
1063
+ : "No model selected" }), _jsx(Text, { color: runStatus === "waiting"
1064
+ ? theme.warning
1065
+ : busy
1066
+ ? theme.focus
1067
+ : theme.success, children: runStatus === "waiting"
1068
+ ? "APPROVAL"
1069
+ : runStatus === "tool"
1070
+ ? "TOOL"
1071
+ : busy
1072
+ ? "WORKING"
1073
+ : "READY" })] }), _jsxs(Box, { height: 1, justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Text, { color: theme.muted, children: ["CONTEXT", " ", _jsxs(Text, { color: contextTokens / contextWindow >= 0.9
1074
+ ? theme.error
1075
+ : contextTokens / contextWindow >= 0.7
1076
+ ? theme.warning
1077
+ : theme.accent, children: [formatTokenCount(contextTokens), " /", " ", formatTokenCount(contextWindow)] }), " ", "estimated"] }), _jsxs(Text, { color: theme.muted, children: ["SPEED", " ", _jsx(Text, { color: busy ? theme.success : theme.muted, children: tokensPerSecond
1078
+ ? `${tokensPerSecond.toFixed(1)} tok/s`
1079
+ : "-- tok/s" }), previewUrl
1080
+ ? ` PREVIEW ${truncate(previewUrl, Math.max(16, Math.floor(viewport.columns * 0.3)))}`
1081
+ : ""] })] }), compactLayout ? (_jsxs(Box, { height: workspaceHeight, flexDirection: "column", gap: 1, overflow: "hidden", children: [_jsxs(Box, { height: editorHeight, flexDirection: "row", gap: 1, overflow: "hidden", children: [_jsx(Box, { width: filesWidth, height: "100%", children: filesPanel }), _jsx(Box, { width: editorWidth, height: "100%", children: editorPanel })] }), _jsx(Box, { height: compactChatHeight, width: "100%", children: agentPanel })] })) : (_jsxs(Box, { height: workspaceHeight, flexDirection: "row", gap: 1, overflow: "hidden", children: [_jsx(Box, { width: filesWidth, height: "100%", children: filesPanel }), _jsx(Box, { width: editorWidth, height: "100%", children: editorPanel }), _jsx(Box, { width: chatWidth, height: "100%", children: agentPanel })] })), _jsx(Box, { height: terminalHeight, marginTop: 1, children: _jsxs(Panel, { title: "TERMINAL [Enter] run", active: focus === "terminal", theme: theme, children: [visibleTerminalLines.map((line, index) => (_jsx(Text, { wrap: "truncate-end", color: line.startsWith("[agent error]") ||
1082
+ line.startsWith("[terminal error]")
1083
+ ? theme.error
1084
+ : line.startsWith("[tool")
1085
+ ? theme.warning
1086
+ : theme.muted, children: truncate(line, Math.max(20, viewport.columns - 8)) }, `${index}-${line}`))), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { wrap: "truncate-end", color: focus === "terminal" ? theme.focus : theme.muted, children: [focus === "terminal" ? "> " : " ", truncate(commandInput || "Type a workspace command", Math.max(16, viewport.columns - 10))] })] }) }), _jsxs(Box, { height: 1, marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: theme.muted, wrap: "truncate-end", children: "Tab focus | Ctrl+Left/Right pane | / find | a agents | c MCP | m settings | Esc cancel run | Ctrl+C exit idle" }), _jsx(Text, { color: theme.muted, children: sessionId ? `session ${sessionId.slice(0, 8)}` : "new session" })] }), screen === "file-search" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 3, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "FIND FILE" }), _jsxs(Text, { color: theme.text, children: [_jsx(Text, { color: theme.focus, children: "> " }), fileSearchInput || (_jsx(Text, { color: theme.muted, children: "Type part of a file name or path" }))] }), _jsx(Text, { color: theme.muted, children: "Up/Down select Enter open Escape close" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [fileSearchResults.map((entry, index) => {
1087
+ const selected = index ===
1088
+ Math.min(fileSearchIndex, Math.max(0, fileSearchResults.length - 1));
827
1089
  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." })] })] });
1090
+ return (_jsxs(Text, { color: selected ? theme.focus : theme.text, bold: selected, children: [selected ? "> " : " ", fileName, _jsxs(Text, { color: theme.muted, children: [" ", truncate(entry.path, Math.max(12, overlayWidth - fileName.length - 10))] })] }, entry.path));
1091
+ }), !fileSearchResults.length && (_jsx(Text, { color: theme.warning, children: "No matching files." }))] })] })), screen === "settings" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 3, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "LOCAL MODEL CONFIGURATION" }), _jsx(Text, { color: theme.muted, children: "Tab changes fields. Enter selects an item or saves when endpoint and model are set." }), _jsxs(Text, { color: settingsField === "server" ? theme.focus : theme.text, children: ["SERVER: ", selectedEndpoint?.label ?? "Scanning local servers..."] }), _jsxs(Text, { color: settingsField === "endpoint" ? theme.focus : theme.text, children: ["ENDPOINT: ", endpointInput] }), _jsxs(Text, { color: settingsField === "model" ? theme.focus : theme.text, children: ["MODEL:", " ", modelInput || models[modelIndex] || "Choose or type a model"] }), _jsxs(Text, { color: settingsField === "internet" ? theme.focus : theme.text, children: ["INTERNET RESEARCH: ", internetAccess ? "enabled" : "disabled", " ", settingsField === "internet" ? "[Space toggles]" : ""] }), _jsxs(Text, { color: settingsField === "theme" ? theme.focus : theme.text, children: ["THEME: ", themeName, " ", settingsField === "theme" ? "[Left/Right changes]" : ""] }), _jsxs(Text, { color: theme.muted, children: ["Detected models:", " ", models.slice(0, 5).join(", ") || "none; type one manually"] }), _jsxs(Text, { color: theme.warning, children: ["Provider: ", providerKind, ". Local endpoints use the server selector. Configure cloud BYOK profiles with truss-cli setup, then relaunch the TUI."] })] })), screen === "approval" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.warning, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 8, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.warning, children: "TOOL APPROVAL REQUIRED" }), _jsxs(Text, { color: theme.text, children: [pendingTool?.name, " ", JSON.stringify(pendingTool?.input)] }), _jsx(Text, { color: theme.muted, children: "Press Y or Enter to allow. Press N or Escape to deny." })] })), screen === "agents" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 6, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "MULTI-AGENT PROFILES" }), _jsx(Text, { color: theme.muted, children: "Profiles are shared with truss-cli in .truss-harness/agents.json. Use `truss agents run <id> <task>` for independent concurrent runs." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [agentProfiles.map((profile) => (_jsxs(Text, { color: theme.text, children: [_jsx(Text, { color: theme.focus, children: profile.displayName }), _jsxs(Text, { color: theme.muted, children: [" ", profile.id.slice(0, 8), " ", profile.provider.providerId, "/", profile.provider.modelId, " ", profile.mode, " ", profile.approvalPolicy] })] }, profile.id))), !agentProfiles.length && (_jsx(Text, { color: theme.warning, children: "No profiles yet. Add one with `truss agents add <name>`." }))] }), _jsx(Text, { color: theme.muted, children: "R refreshes. A or Escape closes." })] })), screen === "mcp" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 6, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "MCP CONNECTIONS" }), _jsx(Text, { color: theme.muted, children: "Status and discovered tools only. Commands, environment values, and credentials remain hidden." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [mcpStatuses.map((server) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: server.state === "failed"
1092
+ ? theme.error
1093
+ : server.state === "connected"
1094
+ ? theme.success
1095
+ : theme.warning, children: [_jsx(Text, { bold: true, children: server.name }), " ", server.state, server.error
1096
+ ? ` — ${server.error}`
1097
+ : ` — ${server.toolCount} tool${server.toolCount === 1 ? "" : "s"}`] }), (server.tools ?? []).map((tool) => (_jsxs(Text, { color: theme.muted, children: [" ", tool.name, " [", tool.readOnly ? "read-only" : "approval", "]"] }, `${server.name}:${tool.name}`)))] }, server.name))), !mcpStatuses.length && (_jsx(Text, { color: theme.warning, children: "No MCP servers configured." }))] }), _jsx(Text, { color: theme.muted, children: "R tests configured servers. C or Escape closes." })] })), screen === "help" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 7, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "TUI CONTROLS" }), tuiControlHelp.map(([label, detail]) => (_jsxs(Text, { color: theme.text, wrap: "wrap", children: [_jsxs(Text, { bold: true, color: theme.focus, children: [label, ":", " "] }), detail] }, label))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: theme.accent, children: "WORKSPACE COMMANDS" }) }), workspaceCommandHelp()
1098
+ .split("\n")
1099
+ .slice(1)
1100
+ .map((line) => (_jsx(Text, { color: theme.text, children: line }, line))), _jsx(Text, { color: theme.muted, children: "Press any key to close this reference." })] }))] }));
830
1101
  }
831
- const initialConfiguration = await resolveConfiguration({ workspaceRoot: cwd() }).catch((error) => {
1102
+ const initialConfiguration = await resolveConfiguration({
1103
+ workspaceRoot: cwd(),
1104
+ }).catch((error) => {
832
1105
  const message = error instanceof Error ? error.message : String(error);
833
1106
  if (message.startsWith("Set a model"))
834
1107
  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.13",
4
4
  "description": "Interactive terminal client for Truss.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "engines": {
20
20
  "node": ">=20"
21
21
  },
22
- "license": "MIT",
22
+ "license": "SEE LICENSE IN LICENSE",
23
23
  "repository": {
24
24
  "type": "git",
25
25
  "url": "git+https://github.com/truss-agent/truss-harness.git"
@@ -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.17",
47
+ "@truss-harness/provider-openai-compatible": "0.1.10",
48
+ "@truss-harness/runtime": "0.1.9"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/react": "^19.2.0"