@truss-harness/tui 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +21 -0
- package/dist/components.d.ts +9 -0
- package/dist/components.js +20 -2
- package/dist/file-browser.js +42 -6
- package/dist/input-controller.d.ts +7 -0
- package/dist/input-controller.js +28 -0
- package/dist/runtime-session.d.ts +3 -1
- package/dist/runtime-session.js +8 -2
- package/dist/skills-controller.d.ts +36 -0
- package/dist/skills-controller.js +102 -0
- package/dist/theme.js +29 -5
- package/dist/types.d.ts +1 -1
- package/package.json +5 -5
package/dist/bin.js
CHANGED
|
@@ -14,6 +14,7 @@ import { useTuiInputController } from "./input-controller.js";
|
|
|
14
14
|
import { calculateLayout, focusReducer } from "./layout.js";
|
|
15
15
|
import { openExternalPreview } from "./processes.js";
|
|
16
16
|
import { useRuntimeSessionController } from "./runtime-session.js";
|
|
17
|
+
import { useSkillsController } from "./skills-controller.js";
|
|
17
18
|
import { useTerminalController } from "./terminal-controller.js";
|
|
18
19
|
import { tuiTheme } from "./theme.js";
|
|
19
20
|
import { useWorkspaceFiles } from "./workspace-files.js";
|
|
@@ -110,6 +111,7 @@ function App({ initialConfiguration, }) {
|
|
|
110
111
|
closeSearch: () => setScreen("workspace"),
|
|
111
112
|
});
|
|
112
113
|
const { fileIndex, setFileIndex, fileTree, selectedFileTreeEntry, fileSearchInput, setFileSearchInput, fileSearchIndex, setFileSearchIndex, fileSearchResults, openFilePath, editorTitle, setEditorScroll, editorRows, visibleEditorRows, fileTreeStart, visibleFileTree, loadFile, toggleDirectory, openSearchResult, toggleDiff, } = workspaceFiles;
|
|
114
|
+
const skillsController = useSkillsController(workspaceRoot, initialConfiguration?.skills);
|
|
113
115
|
const runtimeSession = useRuntimeSessionController({
|
|
114
116
|
initialConfiguration,
|
|
115
117
|
workspaceRoot,
|
|
@@ -123,6 +125,7 @@ function App({ initialConfiguration, }) {
|
|
|
123
125
|
contextWindow,
|
|
124
126
|
appendTerminal,
|
|
125
127
|
showScreen: setScreen,
|
|
128
|
+
skills: skillsController.skills,
|
|
126
129
|
});
|
|
127
130
|
const { configuration, mcpStatuses, sessionId, chatInput, setChatInput, chat, chatScroll, setChatScroll, busy, runStatus, activePlan, pendingTool, tokensPerSecond, configureRuntime, sendPrompt, resolveApproval, startNewConversation, cancelRun, testMcpConnections, } = runtimeSession;
|
|
128
131
|
const contextTokens = useMemo(() => chat.reduce((total, message) => total + estimateTokens(message.content), 400), [chat]);
|
|
@@ -226,6 +229,7 @@ function App({ initialConfiguration, }) {
|
|
|
226
229
|
editorLineCount,
|
|
227
230
|
chatTranscriptLength: chatTranscript.length,
|
|
228
231
|
chatLineCount,
|
|
232
|
+
chatInput,
|
|
229
233
|
},
|
|
230
234
|
setters: {
|
|
231
235
|
setScreen,
|
|
@@ -262,6 +266,15 @@ function App({ initialConfiguration, }) {
|
|
|
262
266
|
loadFile: (entry) => void loadFile(entry),
|
|
263
267
|
toggleDiff: () => void toggleDiff(),
|
|
264
268
|
sendPrompt: () => void sendPrompt(),
|
|
269
|
+
openSkills: () => {
|
|
270
|
+
skillsController.refreshSkills();
|
|
271
|
+
setScreen("skills");
|
|
272
|
+
},
|
|
273
|
+
moveSkill: skillsController.moveSkill,
|
|
274
|
+
toggleSkill: skillsController.toggleSkill,
|
|
275
|
+
cycleSkillMode: skillsController.cycleSkillMode,
|
|
276
|
+
toggleSuperpowers: skillsController.toggleSuperpowers,
|
|
277
|
+
toggleSkillScope: skillsController.toggleSkillScope,
|
|
265
278
|
},
|
|
266
279
|
});
|
|
267
280
|
return (_jsx(TuiWorkspaceView, { viewport: viewport, layout: layout, theme: theme, configuration: configuration, runStatus: runStatus, busy: busy, contextTokens: contextTokens, contextWindow: contextWindow, tokensPerSecond: tokensPerSecond, previewUrl: previewUrl, focus: focus, visibleFileTree: visibleFileTree, fileTreeLength: fileTree.length, fileTreeStart: fileTreeStart, fileIndex: fileIndex, openFilePath: openFilePath, editorTitle: editorTitle, visibleEditorRows: visibleEditorRows, activePlan: activePlan, chatLines: chatLines, chatScroll: chatScroll, chatInput: chatInput, visibleTerminalLines: visibleTerminalLines, commandInput: commandInput, sessionId: sessionId, overlays: {
|
|
@@ -282,6 +295,14 @@ function App({ initialConfiguration, }) {
|
|
|
282
295
|
pendingTool,
|
|
283
296
|
agentProfiles,
|
|
284
297
|
mcpStatuses,
|
|
298
|
+
skills: {
|
|
299
|
+
state: skillsController.skillState,
|
|
300
|
+
index: skillsController.skillIndex,
|
|
301
|
+
scope: skillsController.skillScope,
|
|
302
|
+
...(skillsController.skillError
|
|
303
|
+
? { error: skillsController.skillError }
|
|
304
|
+
: {}),
|
|
305
|
+
},
|
|
285
306
|
} }));
|
|
286
307
|
}
|
|
287
308
|
const initialConfiguration = await resolveConfiguration({
|
package/dist/components.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { RuntimeServiceSkillState } from "@truss-harness/cli/protocol";
|
|
1
2
|
import type { LocalModelEndpoint } from "@truss-harness/provider-openai-compatible";
|
|
2
3
|
import type { AgentProfile, ToolCall } from "@truss-harness/runtime";
|
|
3
4
|
import type { FileEntry } from "./file-browser.js";
|
|
5
|
+
import { type SkillScope } from "./skills-controller.js";
|
|
4
6
|
import type { TuiTheme, TuiThemeName } from "./theme.js";
|
|
5
7
|
import type { Screen, SettingsField } from "./types.js";
|
|
6
8
|
export declare function Panel({ title, active, theme, children, }: {
|
|
@@ -39,6 +41,13 @@ export interface TuiOverlaysProps {
|
|
|
39
41
|
readonly pendingTool?: ToolCall;
|
|
40
42
|
readonly agentProfiles: readonly AgentProfile[];
|
|
41
43
|
readonly mcpStatuses: readonly McpServerView[];
|
|
44
|
+
readonly skills?: TuiSkillsView;
|
|
45
|
+
}
|
|
46
|
+
export interface TuiSkillsView {
|
|
47
|
+
readonly state?: RuntimeServiceSkillState;
|
|
48
|
+
readonly index: number;
|
|
49
|
+
readonly scope: SkillScope;
|
|
50
|
+
readonly error?: string;
|
|
42
51
|
}
|
|
43
52
|
export declare function TuiOverlays(props: TuiOverlaysProps): React.ReactElement;
|
|
44
53
|
export {};
|
package/dist/components.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { brand } from "@truss-harness/branding";
|
|
2
3
|
import { workspaceCommandHelp } from "@truss-harness/runtime";
|
|
3
4
|
import { Box, Text } from "ink";
|
|
4
5
|
import { truncate } from "./display.js";
|
|
6
|
+
import { skillPickerRows } from "./skills-controller.js";
|
|
5
7
|
export function Panel({ title, active, theme, children, }) {
|
|
6
8
|
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] }));
|
|
7
9
|
}
|
|
@@ -22,6 +24,10 @@ const tuiControlHelp = [
|
|
|
22
24
|
"AGENT",
|
|
23
25
|
"Enter sends; a opens saved multi-agent profiles; Up/Down scroll chat; n starts a new conversation outside the chat input.",
|
|
24
26
|
],
|
|
27
|
+
[
|
|
28
|
+
"SKILLS",
|
|
29
|
+
"s outside chat, or /skills in chat, opens the skill picker; Space selects, M mode, P Superpowers, W scope.",
|
|
30
|
+
],
|
|
25
31
|
[
|
|
26
32
|
"MCP",
|
|
27
33
|
"c opens credential-safe server and tool status; R tests configured servers.",
|
|
@@ -40,8 +46,11 @@ const tuiControlHelp = [
|
|
|
40
46
|
"Escape cancels an active agent run. Ctrl+C stops a process or run, and exits while idle.",
|
|
41
47
|
],
|
|
42
48
|
];
|
|
49
|
+
const visibleSkillRows = 12;
|
|
43
50
|
export function TuiOverlays(props) {
|
|
44
|
-
const { screen, theme, overlayWidth, fileSearchInput, fileSearchResults, fileSearchIndex, settingsField, selectedEndpoint, endpointInput, modelInput, models, modelIndex, internetAccess, themeName, providerKind, pendingTool, agentProfiles, mcpStatuses, } = props;
|
|
51
|
+
const { screen, theme, overlayWidth, fileSearchInput, fileSearchResults, fileSearchIndex, settingsField, selectedEndpoint, endpointInput, modelInput, models, modelIndex, internetAccess, themeName, providerKind, pendingTool, agentProfiles, mcpStatuses, skills, } = props;
|
|
52
|
+
const skillRows = skills?.state ? skillPickerRows(skills.state) : [];
|
|
53
|
+
const skillStart = Math.max(0, Math.min((skills?.index ?? 0) - Math.floor(visibleSkillRows / 2), skillRows.length - visibleSkillRows));
|
|
45
54
|
return (_jsxs(_Fragment, { children: [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) => {
|
|
46
55
|
const selected = index ===
|
|
47
56
|
Math.min(fileSearchIndex, Math.max(0, fileSearchResults.length - 1));
|
|
@@ -53,7 +62,16 @@ export function TuiOverlays(props) {
|
|
|
53
62
|
? theme.success
|
|
54
63
|
: theme.warning, children: [_jsx(Text, { bold: true, children: server.name }), " ", server.state, server.error
|
|
55
64
|
? ` — ${server.error}`
|
|
56
|
-
: ` — ${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 === "
|
|
65
|
+
: ` — ${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 === "skills" && (_jsxs(Box, { position: "absolute", flexDirection: "column", borderStyle: "double", borderColor: theme.focus, paddingX: 2, paddingY: 1, width: overlayWidth, marginLeft: 2, marginTop: 4, backgroundColor: theme.overlay, children: [_jsx(Text, { bold: true, color: theme.accent, children: "SKILLS" }), skills?.state ? (_jsxs(Text, { color: theme.text, children: ["Mode ", skills.state.selection.mode, " \u00B7 Superpowers", " ", skills.state.selection.superpowersEnabled ? "on" : "off", " \u00B7 Saving for", " ", skills.scope === "user" ? "all workspaces" : "this workspace"] })) : (_jsx(Text, { color: theme.muted, children: "Loading skills..." })), _jsx(Box, { flexDirection: "column", marginTop: 1, children: skillRows
|
|
66
|
+
.slice(skillStart, skillStart + visibleSkillRows)
|
|
67
|
+
.map((row, offset) => {
|
|
68
|
+
const active = skillStart + offset === skills?.index;
|
|
69
|
+
return (_jsxs(Text, { color: active
|
|
70
|
+
? theme.focus
|
|
71
|
+
: row.available
|
|
72
|
+
? theme.text
|
|
73
|
+
: theme.muted, wrap: "truncate-end", children: [active ? "> " : " ", "[", row.selected ? "x" : " ", "] ", row.id, row.note ? ` (${row.note})` : "", " ", row.description] }, row.id));
|
|
74
|
+
}) }), skills?.error && _jsx(Text, { color: theme.warning, children: skills.error }), _jsx(Text, { color: theme.muted, wrap: "wrap", children: `Space marks a skill always active. M cycles Automatic, Manual, Off. P toggles Superpowers. W switches all workspaces or this one. Changes apply to the next run. Import with \`${brand.cliCommand} skills add <folder>\`. S 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()
|
|
57
75
|
.split("\n")
|
|
58
76
|
.slice(1)
|
|
59
77
|
.map((line) => (_jsx(Text, { color: theme.text, children: line }, line))), _jsx(Text, { color: theme.muted, children: "Press any key to close this reference." })] }))] }));
|
package/dist/file-browser.js
CHANGED
|
@@ -32,7 +32,7 @@ export function buildFileTree(files, collapsed) {
|
|
|
32
32
|
kind: "file",
|
|
33
33
|
path: file.path,
|
|
34
34
|
name: file.path.split("/").at(-1) ?? file.path,
|
|
35
|
-
depth
|
|
35
|
+
depth,
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
};
|
|
@@ -68,12 +68,38 @@ export function fuzzyFiles(files, query, limit = 12) {
|
|
|
68
68
|
const score = fuzzyScore(file.path, query);
|
|
69
69
|
return score === undefined ? [] : [{ file, score }];
|
|
70
70
|
})
|
|
71
|
-
.sort((left, right) => left.score - right.score ||
|
|
71
|
+
.sort((left, right) => left.score - right.score ||
|
|
72
|
+
left.file.path.localeCompare(right.file.path))
|
|
72
73
|
.slice(0, limit)
|
|
73
74
|
.map(({ file }) => file);
|
|
74
75
|
}
|
|
75
|
-
const codeExtensions = new Set([
|
|
76
|
-
|
|
76
|
+
const codeExtensions = new Set([
|
|
77
|
+
".c",
|
|
78
|
+
".cc",
|
|
79
|
+
".cpp",
|
|
80
|
+
".cs",
|
|
81
|
+
".go",
|
|
82
|
+
".java",
|
|
83
|
+
".js",
|
|
84
|
+
".jsx",
|
|
85
|
+
".kt",
|
|
86
|
+
".mjs",
|
|
87
|
+
".php",
|
|
88
|
+
".py",
|
|
89
|
+
".rb",
|
|
90
|
+
".rs",
|
|
91
|
+
".sh",
|
|
92
|
+
".ts",
|
|
93
|
+
".tsx",
|
|
94
|
+
]);
|
|
95
|
+
const markupExtensions = new Set([
|
|
96
|
+
".html",
|
|
97
|
+
".htm",
|
|
98
|
+
".svg",
|
|
99
|
+
".vue",
|
|
100
|
+
".svelte",
|
|
101
|
+
".xml",
|
|
102
|
+
]);
|
|
77
103
|
const dataExtensions = new Set([".json", ".jsonc", ".yaml", ".yml", ".toml"]);
|
|
78
104
|
function tokenize(line, pattern, colors) {
|
|
79
105
|
const result = [];
|
|
@@ -83,7 +109,11 @@ function tokenize(line, pattern, colors) {
|
|
|
83
109
|
if (index > cursor)
|
|
84
110
|
result.push({ text: line.slice(cursor, index) });
|
|
85
111
|
const group = match.slice(1).findIndex((value) => value !== undefined);
|
|
86
|
-
result.push({
|
|
112
|
+
result.push({
|
|
113
|
+
text: match[0],
|
|
114
|
+
color: colors[Math.max(0, group)],
|
|
115
|
+
dim: group === 0,
|
|
116
|
+
});
|
|
87
117
|
cursor = index + match[0].length;
|
|
88
118
|
}
|
|
89
119
|
if (cursor < line.length)
|
|
@@ -105,7 +135,13 @@ export function syntaxTokens(line, path) {
|
|
|
105
135
|
const comment = extension === ".py" || extension === ".rb" || extension === ".sh"
|
|
106
136
|
? /(#.*$)|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b(?:as|async|await|break|case|catch|class|const|continue|def|default|delete|do|else|enum|export|extends|false|finally|fn|for|from|function|if|implements|import|in|instanceof|interface|let|match|new|null|package|private|protected|public|return|static|struct|super|switch|this|throw|true|try|type|typeof|undefined|use|var|void|while|yield)\b)|(\b(?:0x[\da-f]+|\d+(?:\.\d+)?)\b)|([{}()[\].,:;=+\-*/<>!?&|]+)/gi
|
|
107
137
|
: /(\/\/.*$)|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b(?:as|async|await|break|case|catch|class|const|continue|def|default|delete|do|else|enum|export|extends|false|finally|fn|for|from|function|if|implements|import|in|instanceof|interface|let|match|new|null|package|private|protected|public|return|static|struct|super|switch|this|throw|true|try|type|typeof|undefined|use|var|void|while|yield)\b)|(\b(?:0x[\da-f]+|\d+(?:\.\d+)?)\b)|([{}()[\].,:;=+\-*/<>!?&|]+)/gi;
|
|
108
|
-
return tokenize(line, comment, [
|
|
138
|
+
return tokenize(line, comment, [
|
|
139
|
+
"gray",
|
|
140
|
+
"green",
|
|
141
|
+
"magenta",
|
|
142
|
+
"yellow",
|
|
143
|
+
"cyan",
|
|
144
|
+
]);
|
|
109
145
|
}
|
|
110
146
|
return [{ text: line }];
|
|
111
147
|
}
|
|
@@ -28,6 +28,7 @@ interface InputState {
|
|
|
28
28
|
readonly editorLineCount: number;
|
|
29
29
|
readonly chatTranscriptLength: number;
|
|
30
30
|
readonly chatLineCount: number;
|
|
31
|
+
readonly chatInput: string;
|
|
31
32
|
}
|
|
32
33
|
interface InputSetters {
|
|
33
34
|
readonly setScreen: Setter<Screen>;
|
|
@@ -64,6 +65,12 @@ interface InputActions {
|
|
|
64
65
|
readonly loadFile: (entry: FileEntry) => void;
|
|
65
66
|
readonly toggleDiff: () => void;
|
|
66
67
|
readonly sendPrompt: () => void;
|
|
68
|
+
readonly openSkills: () => void;
|
|
69
|
+
readonly moveSkill: (delta: number) => void;
|
|
70
|
+
readonly toggleSkill: () => void;
|
|
71
|
+
readonly cycleSkillMode: () => void;
|
|
72
|
+
readonly toggleSuperpowers: () => void;
|
|
73
|
+
readonly toggleSkillScope: () => void;
|
|
67
74
|
}
|
|
68
75
|
export interface TuiInputControllerOptions {
|
|
69
76
|
readonly state: InputState;
|
package/dist/input-controller.js
CHANGED
|
@@ -56,6 +56,25 @@ export function useTuiInputController({ state, setters, actions, }) {
|
|
|
56
56
|
actions.refreshAgentProfiles();
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
+
if (state.screen === "skills") {
|
|
60
|
+
if (key.escape || input === "s") {
|
|
61
|
+
setters.setScreen("workspace");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (key.upArrow)
|
|
65
|
+
actions.moveSkill(-1);
|
|
66
|
+
else if (key.downArrow)
|
|
67
|
+
actions.moveSkill(1);
|
|
68
|
+
else if (input === " " || key.return)
|
|
69
|
+
actions.toggleSkill();
|
|
70
|
+
else if (input.toLowerCase() === "m")
|
|
71
|
+
actions.cycleSkillMode();
|
|
72
|
+
else if (input.toLowerCase() === "p")
|
|
73
|
+
actions.toggleSuperpowers();
|
|
74
|
+
else if (input.toLowerCase() === "w")
|
|
75
|
+
actions.toggleSkillScope();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
59
78
|
if (state.screen === "mcp") {
|
|
60
79
|
if (key.escape || input.toLowerCase() === "c") {
|
|
61
80
|
setters.setScreen("workspace");
|
|
@@ -193,6 +212,10 @@ export function useTuiInputController({ state, setters, actions, }) {
|
|
|
193
212
|
setters.setScreen("mcp");
|
|
194
213
|
return;
|
|
195
214
|
}
|
|
215
|
+
if (input === "s" && state.focus !== "chat") {
|
|
216
|
+
actions.openSkills();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
196
219
|
if (input === "n" && state.focus !== "chat") {
|
|
197
220
|
actions.startNewConversation();
|
|
198
221
|
return;
|
|
@@ -240,6 +263,11 @@ export function useTuiInputController({ state, setters, actions, }) {
|
|
|
240
263
|
return;
|
|
241
264
|
}
|
|
242
265
|
if (key.return) {
|
|
266
|
+
if (state.chatInput.trim() === "/skills") {
|
|
267
|
+
setters.setChatInput("");
|
|
268
|
+
actions.openSkills();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
243
271
|
actions.sendPrompt();
|
|
244
272
|
return;
|
|
245
273
|
}
|
|
@@ -16,8 +16,10 @@ export interface RuntimeSessionOptions {
|
|
|
16
16
|
readonly contextWindow: number;
|
|
17
17
|
readonly appendTerminal: (output: string) => void;
|
|
18
18
|
readonly showScreen: (screen: Screen) => void;
|
|
19
|
+
/** Shared skill wiring for every runtime this session creates. */
|
|
20
|
+
readonly skills: () => Promise<ResolvedConfiguration["skills"]>;
|
|
19
21
|
}
|
|
20
|
-
export declare function useRuntimeSessionController({ initialConfiguration, workspaceRoot, providerKind, endpointInput, modelInput, agentMode, permissionMode, internetAccess, openFilePath, contextWindow, appendTerminal, showScreen, }: RuntimeSessionOptions): {
|
|
22
|
+
export declare function useRuntimeSessionController({ initialConfiguration, workspaceRoot, providerKind, endpointInput, modelInput, agentMode, permissionMode, internetAccess, openFilePath, contextWindow, appendTerminal, showScreen, skills, }: RuntimeSessionOptions): {
|
|
21
23
|
configuration: ClientConfiguration | undefined;
|
|
22
24
|
mcpStatuses: readonly import("@truss-harness/mcp").McpServerStatus[];
|
|
23
25
|
sessionId: string | undefined;
|
package/dist/runtime-session.js
CHANGED
|
@@ -5,7 +5,7 @@ import { cloudProviderDefinition, isCloudProviderId, } from "@truss-harness/prov
|
|
|
5
5
|
import { executeWorkspaceCommand, FileWorkspacePlanStore, } from "@truss-harness/runtime";
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { estimateTokens, truncate } from "./display.js";
|
|
8
|
-
export function useRuntimeSessionController({ initialConfiguration, workspaceRoot, providerKind, endpointInput, modelInput, agentMode, permissionMode, internetAccess, openFilePath, contextWindow, appendTerminal, showScreen, }) {
|
|
8
|
+
export function useRuntimeSessionController({ initialConfiguration, workspaceRoot, providerKind, endpointInput, modelInput, agentMode, permissionMode, internetAccess, openFilePath, contextWindow, appendTerminal, showScreen, skills, }) {
|
|
9
9
|
const [configuration, setConfiguration] = useState(initialConfiguration);
|
|
10
10
|
const [client, setClient] = useState();
|
|
11
11
|
const [mcpStatuses, setMcpStatuses] = useState(() => Object.entries(initialConfiguration?.mcpServers ?? {}).map(([name, server]) => ({
|
|
@@ -100,10 +100,16 @@ export function useRuntimeSessionController({ initialConfiguration, workspaceRoo
|
|
|
100
100
|
? process.env[cloudProviderDefinition(providerKind)
|
|
101
101
|
.apiKeyEnvironmentVariable]
|
|
102
102
|
: undefined)),
|
|
103
|
-
systemPrompt: process.env.
|
|
103
|
+
systemPrompt: process.env.TRUSS_HARNESS_MASTER_PROMPT
|
|
104
|
+
? undefined
|
|
105
|
+
: process.env.TRUSS_HARNESS_SYSTEM_PROMPT,
|
|
106
|
+
masterPrompt: process.env.TRUSS_HARNESS_MASTER_PROMPT
|
|
107
|
+
? { enabled: true, template: process.env.TRUSS_HARNESS_MASTER_PROMPT }
|
|
108
|
+
: undefined,
|
|
104
109
|
mode: agentMode,
|
|
105
110
|
internetAccess,
|
|
106
111
|
mcpServers: initialConfiguration?.mcpServers,
|
|
112
|
+
skills: await skills(),
|
|
107
113
|
approval: {
|
|
108
114
|
approve: async (call) => {
|
|
109
115
|
const readOnly = [
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ResolvedConfiguration } from "@truss-harness/cli/config";
|
|
2
|
+
import type { RuntimeServiceSkillState } from "@truss-harness/cli/protocol";
|
|
3
|
+
import type { SkillMode, SkillSelection } from "@truss-harness/runtime";
|
|
4
|
+
export type SkillScope = "user" | "workspace";
|
|
5
|
+
type DefaultSkills = ResolvedConfiguration["skills"];
|
|
6
|
+
export interface SkillPickerRow {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
readonly selected: boolean;
|
|
10
|
+
readonly available: boolean;
|
|
11
|
+
readonly note?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function skillPickerRows(state: RuntimeServiceSkillState): SkillPickerRow[];
|
|
14
|
+
export declare function nextSkillMode(mode: SkillMode): SkillMode;
|
|
15
|
+
/** Space on a row: unavailable skills can be cleared but never added. */
|
|
16
|
+
export declare function toggledSkillSelection(state: RuntimeServiceSkillState, row: SkillPickerRow): Partial<SkillSelection> | undefined;
|
|
17
|
+
/** Turning the pack off drops selected pack skills so runs stay valid. */
|
|
18
|
+
export declare function superpowersSelection(selection: SkillSelection): Partial<SkillSelection>;
|
|
19
|
+
/**
|
|
20
|
+
* Keyboard skill picker state. Skills come from the same user store and
|
|
21
|
+
* settings file as the CLI, so changes apply to every client's next run.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useSkillsController(workspaceRoot: string, configured?: DefaultSkills): {
|
|
24
|
+
refreshSkills: () => void;
|
|
25
|
+
moveSkill: (delta: number) => void;
|
|
26
|
+
toggleSkill: () => void;
|
|
27
|
+
cycleSkillMode: () => void;
|
|
28
|
+
toggleSuperpowers: () => void;
|
|
29
|
+
toggleSkillScope: () => void;
|
|
30
|
+
skills: () => Promise<DefaultSkills>;
|
|
31
|
+
skillState: RuntimeServiceSkillState | undefined;
|
|
32
|
+
skillIndex: number;
|
|
33
|
+
skillScope: SkillScope;
|
|
34
|
+
skillError: string | undefined;
|
|
35
|
+
};
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { resolveSkills, } from "@truss-harness/cli/config";
|
|
2
|
+
import { skillServiceHost } from "@truss-harness/cli/skills";
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
4
|
+
const superpowersPrefix = "superpowers:";
|
|
5
|
+
const modes = ["automatic", "manual", "off"];
|
|
6
|
+
export function skillPickerRows(state) {
|
|
7
|
+
const selected = new Set(state.selection.skillIds);
|
|
8
|
+
return state.skills.map((skill) => {
|
|
9
|
+
const packOff = skill.id.startsWith(superpowersPrefix) &&
|
|
10
|
+
!state.selection.superpowersEnabled;
|
|
11
|
+
const note = !skill.trusted
|
|
12
|
+
? "untrusted: import to use"
|
|
13
|
+
: packOff
|
|
14
|
+
? "pack off"
|
|
15
|
+
: undefined;
|
|
16
|
+
return {
|
|
17
|
+
id: skill.id,
|
|
18
|
+
description: skill.description,
|
|
19
|
+
selected: selected.has(skill.id),
|
|
20
|
+
available: skill.trusted && !packOff,
|
|
21
|
+
...(note ? { note } : {}),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function nextSkillMode(mode) {
|
|
26
|
+
return modes[(modes.indexOf(mode) + 1) % modes.length] ?? "automatic";
|
|
27
|
+
}
|
|
28
|
+
/** Space on a row: unavailable skills can be cleared but never added. */
|
|
29
|
+
export function toggledSkillSelection(state, row) {
|
|
30
|
+
if (!row.selected && !row.available)
|
|
31
|
+
return undefined;
|
|
32
|
+
const rest = state.selection.skillIds.filter((id) => id !== row.id);
|
|
33
|
+
return { skillIds: row.selected ? rest : [...rest, row.id] };
|
|
34
|
+
}
|
|
35
|
+
/** Turning the pack off drops selected pack skills so runs stay valid. */
|
|
36
|
+
export function superpowersSelection(selection) {
|
|
37
|
+
return selection.superpowersEnabled
|
|
38
|
+
? {
|
|
39
|
+
superpowersEnabled: false,
|
|
40
|
+
skillIds: selection.skillIds.filter((id) => !id.startsWith(superpowersPrefix)),
|
|
41
|
+
}
|
|
42
|
+
: { superpowersEnabled: true };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Keyboard skill picker state. Skills come from the same user store and
|
|
46
|
+
* settings file as the CLI, so changes apply to every client's next run.
|
|
47
|
+
*/
|
|
48
|
+
export function useSkillsController(workspaceRoot, configured) {
|
|
49
|
+
const resolved = useRef(undefined);
|
|
50
|
+
const skills = useCallback(() => {
|
|
51
|
+
if (configured)
|
|
52
|
+
return Promise.resolve(configured);
|
|
53
|
+
resolved.current ??= resolveSkills({ workspaceRoot });
|
|
54
|
+
return resolved.current;
|
|
55
|
+
}, [configured, workspaceRoot]);
|
|
56
|
+
const [skillState, setSkillState] = useState();
|
|
57
|
+
const [skillIndex, setSkillIndex] = useState(0);
|
|
58
|
+
const [skillScope, setSkillScope] = useState("user");
|
|
59
|
+
const [skillError, setSkillError] = useState();
|
|
60
|
+
const run = useCallback((action) => {
|
|
61
|
+
void skills()
|
|
62
|
+
.then((value) => action(skillServiceHost(value, workspaceRoot)))
|
|
63
|
+
.then((next) => {
|
|
64
|
+
setSkillState(next);
|
|
65
|
+
setSkillError(undefined);
|
|
66
|
+
setSkillIndex((index) => Math.min(index, Math.max(0, next.skills.length - 1)));
|
|
67
|
+
})
|
|
68
|
+
.catch((error) => setSkillError(error instanceof Error ? error.message : String(error)));
|
|
69
|
+
}, [skills, workspaceRoot]);
|
|
70
|
+
const actions = useMemo(() => {
|
|
71
|
+
const save = (selection) => {
|
|
72
|
+
if (selection)
|
|
73
|
+
run((host) => host.setSettings(skillScope, undefined, selection));
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
refreshSkills: () => run((host) => host.list()),
|
|
77
|
+
moveSkill: (delta) => setSkillIndex((index) => Math.min(Math.max(0, index + delta), Math.max(0, (skillState?.skills.length ?? 1) - 1))),
|
|
78
|
+
toggleSkill: () => {
|
|
79
|
+
const row = skillState && skillPickerRows(skillState)[skillIndex];
|
|
80
|
+
if (skillState && row)
|
|
81
|
+
save(toggledSkillSelection(skillState, row));
|
|
82
|
+
},
|
|
83
|
+
cycleSkillMode: () => {
|
|
84
|
+
if (skillState)
|
|
85
|
+
save({ mode: nextSkillMode(skillState.selection.mode) });
|
|
86
|
+
},
|
|
87
|
+
toggleSuperpowers: () => {
|
|
88
|
+
if (skillState)
|
|
89
|
+
save(superpowersSelection(skillState.selection));
|
|
90
|
+
},
|
|
91
|
+
toggleSkillScope: () => setSkillScope((scope) => (scope === "user" ? "workspace" : "user")),
|
|
92
|
+
};
|
|
93
|
+
}, [run, skillIndex, skillScope, skillState]);
|
|
94
|
+
return {
|
|
95
|
+
skills,
|
|
96
|
+
skillState,
|
|
97
|
+
skillIndex,
|
|
98
|
+
skillScope,
|
|
99
|
+
skillError,
|
|
100
|
+
...actions,
|
|
101
|
+
};
|
|
102
|
+
}
|
package/dist/theme.js
CHANGED
|
@@ -14,7 +14,15 @@ export const tuiThemes = {
|
|
|
14
14
|
warning: "#d3b978",
|
|
15
15
|
error: "#d79a8e",
|
|
16
16
|
overlay: "#13231d",
|
|
17
|
-
syntax: {
|
|
17
|
+
syntax: {
|
|
18
|
+
blue: "#9eb4cf",
|
|
19
|
+
cyan: "#8cc8bd",
|
|
20
|
+
gray: "#71867d",
|
|
21
|
+
green: "#a9c994",
|
|
22
|
+
magenta: "#c4a8c8",
|
|
23
|
+
red: "#d4958a",
|
|
24
|
+
yellow: "#d6c184",
|
|
25
|
+
},
|
|
18
26
|
},
|
|
19
27
|
sage: {
|
|
20
28
|
name: "sage",
|
|
@@ -30,7 +38,15 @@ export const tuiThemes = {
|
|
|
30
38
|
warning: "#d6ba83",
|
|
31
39
|
error: "#d69b91",
|
|
32
40
|
overlay: "#1a241d",
|
|
33
|
-
syntax: {
|
|
41
|
+
syntax: {
|
|
42
|
+
blue: "#9eafcf",
|
|
43
|
+
cyan: "#89c0b4",
|
|
44
|
+
gray: "#7d8d80",
|
|
45
|
+
green: "#afcc9a",
|
|
46
|
+
magenta: "#c5aac4",
|
|
47
|
+
red: "#d5958d",
|
|
48
|
+
yellow: "#d7c183",
|
|
49
|
+
},
|
|
34
50
|
},
|
|
35
51
|
dusk: {
|
|
36
52
|
name: "dusk",
|
|
@@ -46,11 +62,19 @@ export const tuiThemes = {
|
|
|
46
62
|
warning: "#ceb776",
|
|
47
63
|
error: "#d29289",
|
|
48
64
|
overlay: "#101d1c",
|
|
49
|
-
syntax: {
|
|
50
|
-
|
|
65
|
+
syntax: {
|
|
66
|
+
blue: "#93aaca",
|
|
67
|
+
cyan: "#7eb9ae",
|
|
68
|
+
gray: "#687d76",
|
|
69
|
+
green: "#9dbf8c",
|
|
70
|
+
magenta: "#bda2c3",
|
|
71
|
+
red: "#cf9188",
|
|
72
|
+
yellow: "#d0ba80",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
51
75
|
};
|
|
52
76
|
export function isTuiThemeName(value) {
|
|
53
|
-
return typeof value === "string" && tuiThemeNames.includes(value);
|
|
77
|
+
return (typeof value === "string" && tuiThemeNames.includes(value));
|
|
54
78
|
}
|
|
55
79
|
export function tuiTheme(value) {
|
|
56
80
|
return tuiThemes[value ?? "forest"];
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SyntaxToken } from "./file-browser.js";
|
|
2
2
|
export declare const focusOrder: readonly ["files", "editor", "chat", "terminal"];
|
|
3
3
|
export type Focus = (typeof focusOrder)[number];
|
|
4
|
-
export type Screen = "workspace" | "settings" | "approval" | "help" | "file-search" | "agents" | "mcp";
|
|
4
|
+
export type Screen = "workspace" | "settings" | "approval" | "help" | "file-search" | "agents" | "mcp" | "skills";
|
|
5
5
|
export type SettingsField = "server" | "endpoint" | "model" | "internet" | "theme";
|
|
6
6
|
export type RunStatus = "ready" | "thinking" | "tool" | "waiting";
|
|
7
7
|
export interface ChatMessage {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truss-harness/tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Interactive terminal client for Truss.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"logo.png"
|
|
18
18
|
],
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": ">=22.15.1"
|
|
21
21
|
},
|
|
22
22
|
"license": "SEE LICENSE IN LICENSE",
|
|
23
23
|
"repository": {
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"ink": "^6.8.0",
|
|
44
44
|
"react": "^19.2.0",
|
|
45
45
|
"@truss-harness/branding": "0.1.1",
|
|
46
|
-
"@truss-harness/cli": "^0.1.
|
|
47
|
-
"@truss-harness/provider-openai-compatible": "^0.1.
|
|
48
|
-
"@truss-harness/runtime": "^0.1.
|
|
46
|
+
"@truss-harness/cli": "^0.1.24",
|
|
47
|
+
"@truss-harness/provider-openai-compatible": "^0.1.16",
|
|
48
|
+
"@truss-harness/runtime": "^0.1.15"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/react": "^19.2.0"
|