@truss-harness/tui 0.1.20 → 0.1.22
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 +26 -3
- 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 +2 -1
- 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
|
@@ -5,7 +5,8 @@ import { cwd } from "node:process";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { brand } from "@truss-harness/branding";
|
|
7
7
|
import { FileAgentProfileStore } from "@truss-harness/cli/agents";
|
|
8
|
-
import { resolveConfiguration, } from "@truss-harness/cli/config";
|
|
8
|
+
import { MissingModelError, resolveConfiguration, } from "@truss-harness/cli/config";
|
|
9
|
+
import { systemSecretStore } from "@truss-harness/cli/secrets";
|
|
9
10
|
import { detectLocalEndpoints, isLocalEndpointKind, listLocalModels, } from "@truss-harness/provider-openai-compatible";
|
|
10
11
|
import { render, useApp } from "ink";
|
|
11
12
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
|
@@ -14,6 +15,7 @@ import { useTuiInputController } from "./input-controller.js";
|
|
|
14
15
|
import { calculateLayout, focusReducer } from "./layout.js";
|
|
15
16
|
import { openExternalPreview } from "./processes.js";
|
|
16
17
|
import { useRuntimeSessionController } from "./runtime-session.js";
|
|
18
|
+
import { useSkillsController } from "./skills-controller.js";
|
|
17
19
|
import { useTerminalController } from "./terminal-controller.js";
|
|
18
20
|
import { tuiTheme } from "./theme.js";
|
|
19
21
|
import { useWorkspaceFiles } from "./workspace-files.js";
|
|
@@ -110,6 +112,7 @@ function App({ initialConfiguration, }) {
|
|
|
110
112
|
closeSearch: () => setScreen("workspace"),
|
|
111
113
|
});
|
|
112
114
|
const { fileIndex, setFileIndex, fileTree, selectedFileTreeEntry, fileSearchInput, setFileSearchInput, fileSearchIndex, setFileSearchIndex, fileSearchResults, openFilePath, editorTitle, setEditorScroll, editorRows, visibleEditorRows, fileTreeStart, visibleFileTree, loadFile, toggleDirectory, openSearchResult, toggleDiff, } = workspaceFiles;
|
|
115
|
+
const skillsController = useSkillsController(workspaceRoot, initialConfiguration?.skills);
|
|
113
116
|
const runtimeSession = useRuntimeSessionController({
|
|
114
117
|
initialConfiguration,
|
|
115
118
|
workspaceRoot,
|
|
@@ -123,6 +126,7 @@ function App({ initialConfiguration, }) {
|
|
|
123
126
|
contextWindow,
|
|
124
127
|
appendTerminal,
|
|
125
128
|
showScreen: setScreen,
|
|
129
|
+
skills: skillsController.skills,
|
|
126
130
|
});
|
|
127
131
|
const { configuration, mcpStatuses, sessionId, chatInput, setChatInput, chat, chatScroll, setChatScroll, busy, runStatus, activePlan, pendingTool, tokensPerSecond, configureRuntime, sendPrompt, resolveApproval, startNewConversation, cancelRun, testMcpConnections, } = runtimeSession;
|
|
128
132
|
const contextTokens = useMemo(() => chat.reduce((total, message) => total + estimateTokens(message.content), 400), [chat]);
|
|
@@ -226,6 +230,7 @@ function App({ initialConfiguration, }) {
|
|
|
226
230
|
editorLineCount,
|
|
227
231
|
chatTranscriptLength: chatTranscript.length,
|
|
228
232
|
chatLineCount,
|
|
233
|
+
chatInput,
|
|
229
234
|
},
|
|
230
235
|
setters: {
|
|
231
236
|
setScreen,
|
|
@@ -262,6 +267,15 @@ function App({ initialConfiguration, }) {
|
|
|
262
267
|
loadFile: (entry) => void loadFile(entry),
|
|
263
268
|
toggleDiff: () => void toggleDiff(),
|
|
264
269
|
sendPrompt: () => void sendPrompt(),
|
|
270
|
+
openSkills: () => {
|
|
271
|
+
skillsController.refreshSkills();
|
|
272
|
+
setScreen("skills");
|
|
273
|
+
},
|
|
274
|
+
moveSkill: skillsController.moveSkill,
|
|
275
|
+
toggleSkill: skillsController.toggleSkill,
|
|
276
|
+
cycleSkillMode: skillsController.cycleSkillMode,
|
|
277
|
+
toggleSuperpowers: skillsController.toggleSuperpowers,
|
|
278
|
+
toggleSkillScope: skillsController.toggleSkillScope,
|
|
265
279
|
},
|
|
266
280
|
});
|
|
267
281
|
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,13 +296,22 @@ function App({ initialConfiguration, }) {
|
|
|
282
296
|
pendingTool,
|
|
283
297
|
agentProfiles,
|
|
284
298
|
mcpStatuses,
|
|
299
|
+
skills: {
|
|
300
|
+
state: skillsController.skillState,
|
|
301
|
+
index: skillsController.skillIndex,
|
|
302
|
+
scope: skillsController.skillScope,
|
|
303
|
+
...(skillsController.skillError
|
|
304
|
+
? { error: skillsController.skillError }
|
|
305
|
+
: {}),
|
|
306
|
+
},
|
|
285
307
|
} }));
|
|
286
308
|
}
|
|
309
|
+
const secrets = systemSecretStore();
|
|
287
310
|
const initialConfiguration = await resolveConfiguration({
|
|
288
311
|
workspaceRoot: cwd(),
|
|
312
|
+
...(secrets ? { secrets } : {}),
|
|
289
313
|
}).catch((error) => {
|
|
290
|
-
|
|
291
|
-
if (message.startsWith("Set a model"))
|
|
314
|
+
if (error instanceof MissingModelError)
|
|
292
315
|
return undefined;
|
|
293
316
|
throw error;
|
|
294
317
|
});
|
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]) => ({
|
|
@@ -109,6 +109,7 @@ export function useRuntimeSessionController({ initialConfiguration, workspaceRoo
|
|
|
109
109
|
mode: agentMode,
|
|
110
110
|
internetAccess,
|
|
111
111
|
mcpServers: initialConfiguration?.mcpServers,
|
|
112
|
+
skills: await skills(),
|
|
112
113
|
approval: {
|
|
113
114
|
approve: async (call) => {
|
|
114
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.22",
|
|
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.25",
|
|
47
|
+
"@truss-harness/provider-openai-compatible": "^0.1.17",
|
|
48
|
+
"@truss-harness/runtime": "^0.1.15"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/react": "^19.2.0"
|