@rubixkube/rubix 0.0.2 → 0.0.4
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/CHANGELOG.md +36 -0
- package/LICENSE +33 -0
- package/README.md +29 -12
- package/dist/cli.js +7 -0
- package/dist/commands/chat.js +2 -1
- package/dist/commands/login.js +23 -2
- package/dist/commands/model.js +84 -0
- package/dist/config/env.js +2 -0
- package/dist/core/device-auth.js +39 -1
- package/dist/core/rubix-api.js +211 -27
- package/dist/core/session-store.js +36 -0
- package/dist/core/settings.js +30 -0
- package/dist/core/update-check.js +51 -0
- package/dist/core/whats-new.js +56 -0
- package/dist/ui/App.js +453 -141
- package/dist/ui/components/BrandPanel.js +1 -1
- package/dist/ui/components/ChatTranscript.js +108 -22
- package/dist/ui/components/Composer.js +67 -8
- package/dist/ui/components/DashboardPanel.js +32 -9
- package/dist/ui/components/SplashScreen.js +2 -8
- package/dist/ui/hooks/useBracketedPaste.js +27 -0
- package/dist/ui/theme.js +3 -1
- package/package.json +6 -4
- package/patches/ink-multiline-input+0.1.0.patch +246 -16
|
@@ -9,5 +9,5 @@ const LOGO_LINES = [
|
|
|
9
9
|
"\\_| \\_\\__,_|_.__/|_/_/\\_\\_| |_|\\__,_.__/ \\___|",
|
|
10
10
|
];
|
|
11
11
|
export function BrandPanel({ cwd, model, user }) {
|
|
12
|
-
return (_jsx(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 0, marginBottom: 1, children: _jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsxs(Box, { flexDirection: "column", width: 58, children: [LOGO_LINES.map((line, index) => (_jsx(Text, { color: index % 2 === 0 ? "cyan" : "magenta", children: line }, line))), _jsx(Text, { bold: true, color: "yellow", children: "RubixKube Agent CLI" }), _jsxs(Text, { dimColor: true, children: [model, " • ", user ? `logged in as ${user}` : "not logged in"] }), _jsx(Text, { dimColor: true, children: cwd })] }), _jsx(Box, { marginX: 1, children: _jsx(Text, { dimColor: true, children: "\u2502" }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "Tips for getting started" }), _jsx(Text, { dimColor: true, children: "1. Use /login to authenticate first." }), _jsx(Text, { dimColor: true, children: "2. Press / to open command search." }), _jsx(Text, { dimColor: true, children: "3. Use @ to mention files and ! for shell mode." }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { color: "yellow", bold: true, children: "Recent activity" }), _jsx(Text, { dimColor: true, children: "No recent activity" })] })] }) }));
|
|
12
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 0, marginBottom: 1, children: _jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsxs(Box, { flexDirection: "column", width: 58, children: [LOGO_LINES.map((line, index) => (_jsx(Text, { color: index % 2 === 0 ? "cyan" : "magenta", children: line }, line))), _jsx(Text, { bold: true, color: "yellow", children: "RubixKube Agent CLI" }), _jsxs(Text, { dimColor: true, children: [model, " • ", user ? `logged in as ${user}` : "not logged in"] }), _jsx(Text, { dimColor: true, children: cwd })] }), _jsx(Box, { marginX: 1, children: _jsx(Text, { dimColor: true, children: "\u2502" }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "Tips for getting started" }), _jsx(Text, { dimColor: true, children: "1. Use /login to authenticate first." }), _jsx(Text, { dimColor: true, children: "2. Press / to open command search." }), _jsx(Text, { dimColor: true, children: "3. Use @ to mention files and '! ' (exclamation + space) for shell mode." }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { color: "yellow", bold: true, children: "Recent activity" }), _jsx(Text, { dimColor: true, children: "No recent activity" })] })] }) }));
|
|
13
13
|
}
|
|
@@ -107,6 +107,36 @@ function buildTimelineRows(workflow, fullContent) {
|
|
|
107
107
|
}
|
|
108
108
|
if (event.type === "function_response") {
|
|
109
109
|
const raw = event.content || `[${name || eventId || "tool"}]`;
|
|
110
|
+
// Intercept Web UI component structures specifically mapped from Web Console
|
|
111
|
+
if (name === "create_ui_component") {
|
|
112
|
+
try {
|
|
113
|
+
// Remove the outer "[create_ui_component]" wrapper if present from stringification
|
|
114
|
+
const cleaned = raw.startsWith("[create_ui_component]\n")
|
|
115
|
+
? raw.replace("[create_ui_component]\n", "")
|
|
116
|
+
: raw;
|
|
117
|
+
const parsed = JSON.parse(cleaned);
|
|
118
|
+
if (parsed.status !== "failed" && !parsed.error && (parsed.title || parsed.body)) {
|
|
119
|
+
rows.push({
|
|
120
|
+
key: `${event.id}-${index}`,
|
|
121
|
+
label: "ui component",
|
|
122
|
+
content: "",
|
|
123
|
+
color: RUBIX_THEME.colors.thought,
|
|
124
|
+
uiData: {
|
|
125
|
+
title: parsed.title,
|
|
126
|
+
description: parsed.description,
|
|
127
|
+
body: parsed.body,
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// Drop the preceding function_call for create_ui_component so the terminal won't get messy
|
|
131
|
+
if (rows.length > 1 && rows[rows.length - 2].label === "tool call" && rows[rows.length - 2].content.includes("create_ui_component")) {
|
|
132
|
+
rows.splice(rows.length - 2, 1);
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// eslint-disable-next-line no-empty
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
}
|
|
110
140
|
const responseContent = fullContent ? tryPrettyJson(raw) || raw : compact(raw, 88);
|
|
111
141
|
const isError = /error|failed|exception|denied|invalid/i.test(responseContent);
|
|
112
142
|
if (!fullContent && previous?.label === "tool result" && previous.content === responseContent)
|
|
@@ -123,33 +153,59 @@ function buildTimelineRows(workflow, fullContent) {
|
|
|
123
153
|
return rows;
|
|
124
154
|
}
|
|
125
155
|
function buildWorkflowStats(workflow) {
|
|
126
|
-
const toolCalls = workflow.filter((e) => e.type === "function_call").length;
|
|
156
|
+
const toolCalls = workflow.filter((e) => e.type === "function_call" && !e.content.includes("create_ui_component")).length;
|
|
157
|
+
const thoughtCount = workflow.filter((e) => e.type === "thought").length;
|
|
127
158
|
const withTs = workflow.filter((e) => e.ts != null && e.ts > 0);
|
|
128
159
|
const durationSec = withTs.length >= 2
|
|
129
160
|
? Math.round((Math.max(...withTs.map((e) => e.ts)) - Math.min(...withTs.map((e) => e.ts))) / 1000)
|
|
130
161
|
: null;
|
|
131
|
-
return { toolCalls, durationSec };
|
|
162
|
+
return { toolCalls, thoughtCount, durationSec };
|
|
163
|
+
}
|
|
164
|
+
/** System messages that are session metadata; shown in SessionBar instead. */
|
|
165
|
+
function isSessionMetadata(content) {
|
|
166
|
+
const c = (content || "").trim();
|
|
167
|
+
return (/^Session\s+.+·.+messages?/i.test(c) ||
|
|
168
|
+
/^Switched to session\s+/i.test(c) ||
|
|
169
|
+
/^You're in\.\s+Session\s+/i.test(c) ||
|
|
170
|
+
/^Fresh thread\.\s+/i.test(c));
|
|
132
171
|
}
|
|
133
172
|
export const ChatTranscript = React.memo(function ChatTranscript({ messages, workflowViewMode = "detailed", }) {
|
|
134
173
|
if (messages.length === 0) {
|
|
135
174
|
return _jsx(Box, {});
|
|
136
175
|
}
|
|
137
|
-
|
|
176
|
+
const cumulativeStatsByIndex = React.useMemo(() => {
|
|
177
|
+
const result = new Map();
|
|
178
|
+
let cumTools = 0;
|
|
179
|
+
let cumThoughts = 0;
|
|
180
|
+
let cumDuration = 0;
|
|
181
|
+
messages.forEach((m, idx) => {
|
|
182
|
+
if (m.role === "assistant") {
|
|
183
|
+
const s = buildWorkflowStats(m.workflow ?? []);
|
|
184
|
+
cumTools += s.toolCalls;
|
|
185
|
+
cumThoughts += s.thoughtCount;
|
|
186
|
+
cumDuration += s.durationSec ?? 0;
|
|
187
|
+
result.set(idx, { toolCalls: cumTools, thoughtCount: cumThoughts, durationSec: cumDuration });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return result;
|
|
191
|
+
}, [messages]);
|
|
192
|
+
return (_jsx(Box, { flexDirection: "column", width: "100%", children: messages.map((message, msgIndex) => {
|
|
138
193
|
if (message.role === "user") {
|
|
139
194
|
const { mainMessage, contextParts } = parseContextParts(message.content || "");
|
|
140
195
|
const hasContext = contextParts.length > 0;
|
|
141
|
-
return (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
196
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Box, { flexDirection: "column", backgroundColor: RUBIX_THEME.colors.userBlockBg, paddingX: 1, paddingY: 1, children: [mainMessage ? (_jsx(Box, { flexDirection: "column", children: mainMessage.split("\n").map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.userPrompt, children: i === 0 ? "❯ " : " " }), _jsx(Text, { color: RUBIX_THEME.colors.userText, children: line })] }, `${message.id}-main-${i}`))) })) : null, hasContext
|
|
197
|
+
? contextParts.map((ctx, i) => {
|
|
198
|
+
const labelMatch = ctx.match(/^\[CONTEXT\]\s*\*\*([^*]+)\*\*/);
|
|
199
|
+
const label = labelMatch ? labelMatch[1] : `Context ${i + 1}`;
|
|
200
|
+
const preview = previewContext(ctx);
|
|
201
|
+
return (_jsxs(Text, { dimColor: true, children: [" ", _jsx(Text, { color: RUBIX_THEME.colors.userPrompt, children: "\u25CB" }), " ", label, " \u2014 ", preview] }, `${message.id}-ctx-${i}`));
|
|
202
|
+
})
|
|
203
|
+
: null] }) }, message.id));
|
|
149
204
|
}
|
|
150
205
|
if (message.role === "assistant") {
|
|
151
206
|
const workflow = message.workflow ?? [];
|
|
152
207
|
const stats = buildWorkflowStats(workflow);
|
|
208
|
+
const cumStats = cumulativeStatsByIndex.get(msgIndex);
|
|
153
209
|
const isDetailed = workflowViewMode === "detailed";
|
|
154
210
|
const timelineRows = buildTimelineRows(workflow, isDetailed);
|
|
155
211
|
const rawContent = message.content || "";
|
|
@@ -158,21 +214,51 @@ export const ChatTranscript = React.memo(function ChatTranscript({ messages, wor
|
|
|
158
214
|
const hasNonEmptyContent = rawContent.trim().length > 0;
|
|
159
215
|
const isStreaming = message.isAccumulating === true;
|
|
160
216
|
const hasWorkflow = workflow.length > 0;
|
|
161
|
-
// Build mixed timeline: interleave workflow events with text response
|
|
162
217
|
const renderMixedView = () => {
|
|
163
|
-
// Show timeline during streaming if workflow exists, or after streaming in detailed mode
|
|
164
218
|
const shouldShowTimeline = (isStreaming && hasWorkflow) || (!isStreaming && isDetailed && hasWorkflow);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
219
|
+
const hasCollapsibleItems = stats.toolCalls > 0 || stats.thoughtCount > 0;
|
|
220
|
+
const renderSummaryFooter = () => {
|
|
221
|
+
if (isStreaming || !hasCollapsibleItems)
|
|
222
|
+
return null;
|
|
223
|
+
const c = cumStats ?? { toolCalls: stats.toolCalls, thoughtCount: stats.thoughtCount, durationSec: stats.durationSec ?? 0 };
|
|
224
|
+
const parts = [];
|
|
225
|
+
if (c.toolCalls > 0)
|
|
226
|
+
parts.push(`${c.toolCalls} tool call${c.toolCalls !== 1 ? "s" : ""}`);
|
|
227
|
+
if (c.thoughtCount > 0)
|
|
228
|
+
parts.push(`${c.thoughtCount} thought${c.thoughtCount !== 1 ? "s" : ""}`);
|
|
229
|
+
const summaryText = parts.join(", ");
|
|
230
|
+
const timeText = c.durationSec > 0 ? ` · ${c.durationSec}s` : "";
|
|
231
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [" ··· ", summaryText, timeText, " ", isDetailed ? "(Ctrl+O to collapse)" : "(Ctrl+O for timeline)"] }) }));
|
|
232
|
+
};
|
|
233
|
+
const renderRow = (row, isFirstInBlock) => {
|
|
234
|
+
const rowMargin = isFirstInBlock ? 0 : 1;
|
|
235
|
+
if (row.uiData) {
|
|
236
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: rowMargin, marginBottom: 1, marginLeft: 4, paddingX: 2, paddingY: 1, borderStyle: "round", borderColor: row.uiData.color || "gray", children: [_jsx(Text, { bold: true, children: row.uiData.title }), row.uiData.description ? _jsx(Text, { dimColor: true, children: row.uiData.description }) : null, row.uiData.body ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: row.uiData.body }) })) : null] }, `ui-${row.key}`));
|
|
237
|
+
}
|
|
238
|
+
if (!shouldShowTimeline && row.label !== "thought" && row.label !== "tool call" && row.label !== "tool error") {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const lines = row.content.split("\n");
|
|
242
|
+
const firstLine = lines[0] || "";
|
|
243
|
+
if (!shouldShowTimeline) {
|
|
244
|
+
const displayTitle = row.label === "tool call"
|
|
245
|
+
? `${row.label}: ${firstLine}()`
|
|
246
|
+
: row.label === "thought"
|
|
247
|
+
? `${row.label}: ${firstLine} ...`
|
|
248
|
+
: `${row.label}`;
|
|
249
|
+
return (_jsx(Box, { marginTop: rowMargin, children: _jsx(Text, { dimColor: true, children: _jsxs(Text, { color: row.color, children: ["\u25CF ", displayTitle] }) }) }, `condensed-${row.key}`));
|
|
250
|
+
}
|
|
251
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: rowMargin, children: lines.map((line, i) => (_jsx(Text, { children: i === 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: row.color, children: ["\u25CF ", row.label, ": "] }), _jsx(Text, { italic: true, dimColor: true, children: line })] })) : row.label === "thought" ? (_jsxs(Text, { dimColor: true, children: [" ", line] })) : (_jsxs(Text, { dimColor: true, children: [" ", line] })) }, `${row.key}-${i}`))) }, row.key));
|
|
252
|
+
};
|
|
253
|
+
const hasVisibleRows = timelineRows.length > 0;
|
|
254
|
+
return (_jsxs(Box, { flexDirection: "column", children: [hasVisibleRows && (_jsx(Box, { flexDirection: "column", children: timelineRows.map((row, idx) => renderRow(row, idx === 0)) })), hasNonEmptyContent && (_jsx(Box, { flexDirection: "column", marginTop: hasVisibleRows ? 1 : 0, children: contentLines.map((line, index) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: (index === 0 && (!shouldShowTimeline || !hasVisibleRows)) ? "● " : " " }), line.length > 0 ? line : " "] }, `${message.id}-text-${index}`))) })), renderSummaryFooter()] }));
|
|
173
255
|
};
|
|
174
|
-
|
|
256
|
+
// Consistent gap between user and agent turn for both streaming and history
|
|
257
|
+
const gapAfterUser = 1;
|
|
258
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: gapAfterUser, marginBottom: 0, children: renderMixedView() }, message.id));
|
|
175
259
|
}
|
|
260
|
+
if (isSessionMetadata(message.content || ""))
|
|
261
|
+
return null;
|
|
176
262
|
const isError = /error|failed|timed out|unable/i.test(message.content);
|
|
177
263
|
return (_jsxs(Text, { color: isError ? "red" : undefined, dimColor: !isError, children: [isError ? "! " : "", message.content || ""] }, message.id));
|
|
178
264
|
}) }));
|
|
@@ -6,33 +6,92 @@ import { MultilineInput } from "ink-multiline-input";
|
|
|
6
6
|
import { RUBIX_THEME } from "../theme.js";
|
|
7
7
|
/** Ctrl+letter shortcuts used by App — don't insert into composer. */
|
|
8
8
|
const GLOBAL_CTRL_KEYS = ["c", "d", "l", "o", "x"];
|
|
9
|
-
export const Composer = React.memo(function Composer({ value, resetToken, disabled, shellMode = false, placeholder = "Ask anything, / for commands, @ for files, ! for shell", rightStatus = "", busy = false, suggestion = "", suggestions = [], captureArrowKeys = false, onChange, onSubmit, }) {
|
|
9
|
+
export const Composer = React.memo(function Composer({ value, resetToken, disabled, shellMode = false, placeholder = "Ask anything, / for commands, @ for files, '! ' for shell", rightStatus = "", busy = false, suggestion = "", suggestions = [], captureArrowKeys = false, onChange, onSubmit, }) {
|
|
10
10
|
const effectivePlaceholder = disabled ? "busy..." : placeholder;
|
|
11
11
|
const bashMode = shellMode;
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
12
|
+
// ── Fast-typing fix ──────────────────────────────────────────────────────
|
|
13
|
+
// `MultilineInput` reads `value` *inside* its input handler closure.
|
|
14
|
+
// When typing quickly, React hasn't re-rendered yet, so the next character
|
|
15
|
+
// is inserted into the previous (stale) value — dropping characters.
|
|
16
|
+
//
|
|
17
|
+
// We keep `latestValueRef` synchronously up-to-date on every onChange so
|
|
18
|
+
// the effective value fed to `MultilineInput` on re-render is always fresh.
|
|
19
|
+
const latestValueRef = React.useRef(value);
|
|
16
20
|
const lastResetTokenRef = React.useRef(resetToken);
|
|
17
|
-
const
|
|
21
|
+
const [bufferedValue, setBufferedValue] = useState(value);
|
|
22
|
+
// On external reset (e.g. submit) propagate down.
|
|
18
23
|
useEffect(() => {
|
|
19
24
|
if (resetToken !== lastResetTokenRef.current) {
|
|
20
25
|
lastResetTokenRef.current = resetToken;
|
|
26
|
+
latestValueRef.current = value;
|
|
21
27
|
setBufferedValue(value);
|
|
22
28
|
}
|
|
23
29
|
}, [value, resetToken]);
|
|
30
|
+
const effectiveValue = resetToken !== lastResetTokenRef.current ? value : bufferedValue;
|
|
24
31
|
const handleChange = (newValue) => {
|
|
32
|
+
latestValueRef.current = newValue;
|
|
25
33
|
setBufferedValue(newValue);
|
|
26
34
|
onChange(newValue);
|
|
27
35
|
};
|
|
36
|
+
// Ref that always holds the latest buffered value so the paste handler below
|
|
37
|
+
// can append to it without a stale closure.
|
|
38
|
+
const bufferedValueRef = React.useRef(bufferedValue);
|
|
39
|
+
bufferedValueRef.current = bufferedValue;
|
|
40
|
+
// Accumulates paste content while between [200~ and [201~ markers.
|
|
41
|
+
// null = not currently in a paste; string = accumulating paste.
|
|
42
|
+
const pasteBufferRef = React.useRef(null);
|
|
28
43
|
const useFilteredInput = useCallback((handler, isActive) => useInput((input, key) => {
|
|
29
44
|
if (key.ctrl && GLOBAL_CTRL_KEYS.includes((input ?? "").toLowerCase()))
|
|
30
45
|
return;
|
|
31
46
|
if (captureArrowKeys && (key.upArrow || key.downArrow))
|
|
32
47
|
return;
|
|
48
|
+
// ── Bracketed Paste Mode (primary path) ─────────────────────────────
|
|
49
|
+
// Terminal BPM wraps pastes in \x1b[200~...\x1b[201~.
|
|
50
|
+
// Ink's key parser consumes the leading \x1b and delivers the rest as
|
|
51
|
+
// bare input strings: "[200~" and "[201~".
|
|
52
|
+
if (input === "[200~") {
|
|
53
|
+
pasteBufferRef.current = ""; // start accumulating
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (input === "[201~") {
|
|
57
|
+
const pasted = pasteBufferRef.current;
|
|
58
|
+
pasteBufferRef.current = null;
|
|
59
|
+
if (pasted !== null && pasted.length > 0) {
|
|
60
|
+
const normalised = pasted.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
61
|
+
const next = bufferedValueRef.current + normalised;
|
|
62
|
+
setBufferedValue(next);
|
|
63
|
+
onChange(next);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// While accumulating a bracketed paste, absorb all input here so
|
|
68
|
+
// nothing reaches MultilineInput (which fires onSubmit on every \n).
|
|
69
|
+
if (pasteBufferRef.current !== null) {
|
|
70
|
+
if (key.return) {
|
|
71
|
+
pasteBufferRef.current += "\n";
|
|
72
|
+
}
|
|
73
|
+
else if (typeof input === "string" && input.length > 0) {
|
|
74
|
+
pasteBufferRef.current += input;
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// ────────────────────────────────────────────────────────────────────
|
|
79
|
+
// ── Fallback: heuristic for terminals without BPM ───────────────────
|
|
80
|
+
// A real Enter key arrives with key.return===true and input="" / "\r".
|
|
81
|
+
// A pasted chunk is a longer string that contains "\n".
|
|
82
|
+
if (typeof input === "string" && input.includes("\n") && input.length > 1) {
|
|
83
|
+
const normalised = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
84
|
+
const next = bufferedValueRef.current + normalised;
|
|
85
|
+
setBufferedValue(next);
|
|
86
|
+
onChange(next);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// ────────────────────────────────────────────────────────────────────
|
|
33
90
|
handler(input, key);
|
|
34
|
-
}, { isActive }),
|
|
35
|
-
|
|
91
|
+
}, { isActive }),
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
[captureArrowKeys, onChange]);
|
|
94
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { borderStyle: "single", borderColor: bashMode ? RUBIX_THEME.colors.bash : RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Text, { color: bashMode ? RUBIX_THEME.colors.bashPrompt : RUBIX_THEME.colors.userPrompt, children: bashMode ? "! " : "❯ " }), _jsx(MultilineInput, { value: effectiveValue, onChange: handleChange, onSubmit: onSubmit, placeholder: effectivePlaceholder, focus: !disabled, rows: 1, maxRows: 10, textStyle: { color: bashMode ? RUBIX_THEME.colors.bash : RUBIX_THEME.colors.assistantText }, keyBindings: {
|
|
36
95
|
submit: (key) => !!key.return && !key.shift && !key.meta && !key.alt,
|
|
37
96
|
newline: (key) => !!key.return && (!!key.shift || !!key.meta || !!key.alt),
|
|
38
97
|
}, useCustomInput: useFilteredInput }, `composer-${resetToken}`)] }), _jsxs(Box, { justifyContent: "space-between", paddingX: 1, flexDirection: "column", children: [bashMode ? (_jsx(Text, { color: RUBIX_THEME.colors.bash, children: "Shell mode \u00B7 Esc to switch back" })) : null, _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: suggestion || "? for shortcuts" }), busy ? _jsx(Spinner, { label: rightStatus }) : _jsx(Text, { dimColor: true, children: rightStatus })] })] })] }));
|
|
@@ -2,6 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { compactSessionId, RUBIX_THEME } from "../theme.js";
|
|
5
|
+
import { VERSION } from "../../version.js";
|
|
5
6
|
function displayName(user) {
|
|
6
7
|
const normalized = (user ?? "").trim();
|
|
7
8
|
if (!normalized)
|
|
@@ -44,20 +45,42 @@ function oneLine(input, max = 40) {
|
|
|
44
45
|
return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
|
|
45
46
|
}
|
|
46
47
|
const SUGGESTIONS = [
|
|
47
|
-
"
|
|
48
|
+
"what's the current state of my environment?",
|
|
49
|
+
"any critical issues right now?",
|
|
48
50
|
"run a security audit",
|
|
49
|
-
"
|
|
50
|
-
"debug active issues",
|
|
51
|
+
"debug active insights",
|
|
51
52
|
];
|
|
52
53
|
const NAV_TIPS = [
|
|
53
|
-
{ cmd: "/
|
|
54
|
-
{ cmd: "/
|
|
55
|
-
{ cmd: "/cluster", desc: "switch cluster" },
|
|
54
|
+
{ cmd: "/resume", desc: "resume a thread" },
|
|
55
|
+
{ cmd: "/environments", desc: "switch environment" },
|
|
56
56
|
{ cmd: "?", desc: "show shortcuts" },
|
|
57
57
|
];
|
|
58
|
-
|
|
58
|
+
function statsLine(stats) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (stats.activeInsights > 0) {
|
|
61
|
+
parts.push(`${stats.activeInsights} active insight${stats.activeInsights !== 1 ? "s" : ""}`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
parts.push("0 active insights");
|
|
65
|
+
}
|
|
66
|
+
if (stats.rcaReports > 0) {
|
|
67
|
+
parts.push(`${stats.rcaReports} RCA${stats.rcaReports !== 1 ? "s" : ""} completed`);
|
|
68
|
+
}
|
|
69
|
+
if (stats.resolvedInsights > 0) {
|
|
70
|
+
parts.push(`${stats.resolvedInsights} resolved`);
|
|
71
|
+
}
|
|
72
|
+
if (stats.lastUpdated) {
|
|
73
|
+
parts.push(`updated ${relativeTime(stats.lastUpdated)}`);
|
|
74
|
+
}
|
|
75
|
+
return parts.join(" · ");
|
|
76
|
+
}
|
|
77
|
+
export const DashboardPanel = React.memo(function DashboardPanel({ user, agentName, cwd, recentSessions, selectedEnvironment, stats, }) {
|
|
59
78
|
const greetingName = displayName(user);
|
|
60
79
|
const workspace = compactWorkspace(cwd);
|
|
61
|
-
const latest = recentSessions
|
|
62
|
-
|
|
80
|
+
const latest = [...recentSessions]
|
|
81
|
+
.sort((a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime())
|
|
82
|
+
.slice(0, 2);
|
|
83
|
+
const envName = selectedEnvironment?.name;
|
|
84
|
+
const envStatus = selectedEnvironment?.status;
|
|
85
|
+
return (_jsx(Box, { flexDirection: "column", width: "100%", marginTop: 1, paddingX: 2, paddingY: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "row", marginBottom: 1, children: _jsxs(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: ["\u25C8 ", agentName, " v", VERSION] }) }), _jsxs(Box, { flexDirection: "column", children: [envName ? (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: envName }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { color: RUBIX_THEME.colors.brand, children: envStatus }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: "Run /environments to switch" })] })) : null, stats ? (_jsx(Text, { dimColor: true, children: statsLine(stats) })) : (_jsxs(Text, { dimColor: true, children: ["Hello, ", greetingName, ". What's happening in your infrastructure?"] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: RUBIX_THEME.colors.border, dimColor: true, children: "─".repeat(64) }) }), _jsxs(Box, { flexDirection: "row", marginTop: 1, width: "100%", alignItems: "flex-start", children: [_jsxs(Box, { flexDirection: "column", width: 42, children: [!envName ? (_jsx(Box, { marginTop: 0, children: _jsx(Text, { dimColor: true, children: "no environment \u00B7 /environments to select" }) })) : null, workspace ? (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: workspace }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Try asking" }), SUGGESTIONS.map((item) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: "\u203A" }), _jsxs(Text, { dimColor: true, children: [" ", item] })] }, item)))] })] }), _jsx(Box, { paddingX: 2, flexDirection: "column", children: Array.from({ length: 10 }).map((_, i) => (_jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u2502" }, i))) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", minWidth: 26, children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Quick nav" }), NAV_TIPS.map(({ cmd, desc }) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: cmd.padEnd(16) }), _jsx(Text, { dimColor: true, children: desc })] }, cmd))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Recent" }), latest.length === 0 ? (_jsx(Text, { dimColor: true, children: "no sessions yet \u00B7 /new to start" })) : (latest.map((session, i) => (_jsxs(Box, { flexDirection: "column", marginTop: i > 0 ? 1 : 0, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u25CB " }), _jsx(Text, { children: oneLine(session.title) ?? "Untitled session" })] }), _jsxs(Text, { dimColor: true, children: [" ", relativeTime(session.updatedAt) || compactSessionId(session.id)] })] }, session.id))))] })] })] })] }) }));
|
|
63
86
|
});
|
|
@@ -7,17 +7,11 @@ import { Select } from "@inkjs/ui";
|
|
|
7
7
|
import { RUBIX_THEME } from "../theme.js";
|
|
8
8
|
import { VERSION } from "../../version.js";
|
|
9
9
|
const GRAD_BLUE = ["#4ea8ff", "#7f88ff"];
|
|
10
|
-
const WHATS_NEW = [
|
|
11
|
-
"Multi-cluster support with /cluster",
|
|
12
|
-
"Session history with auto-resume",
|
|
13
|
-
"Streaming workflow event tracing",
|
|
14
|
-
"/console opens the web dashboard",
|
|
15
|
-
];
|
|
16
10
|
const SPLASH_OPTIONS = [
|
|
17
11
|
{ label: "Login to RubixKube", value: "login" },
|
|
18
12
|
{ label: "Exit", value: "exit" },
|
|
19
13
|
];
|
|
20
|
-
export const SplashScreen = React.memo(function SplashScreen({ agentName, onActionSelect, selectDisabled = false, }) {
|
|
14
|
+
export const SplashScreen = React.memo(function SplashScreen({ agentName, whatsNew, onActionSelect, selectDisabled = false, }) {
|
|
21
15
|
const handleChange = useCallback((value) => onActionSelect(value), [onActionSelect]);
|
|
22
|
-
return (_jsx(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", children: _jsx(Gradient, { colors: GRAD_BLUE, children: _jsx(BigText, { text: "RUBIXKUBE", font: "block" }) }) }), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { dimColor: true, children: agentName }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: "Site Reliability Intelligence" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: RUBIX_THEME.colors.border, dimColor: true, children: "─".repeat(64) }) }), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 38, children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: ["What's New \u00B7 v", VERSION] }),
|
|
16
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", children: _jsx(Gradient, { colors: GRAD_BLUE, children: _jsx(BigText, { text: "RUBIXKUBE", font: "block" }) }) }), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { dimColor: true, children: agentName }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: "Site Reliability Intelligence" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: RUBIX_THEME.colors.border, dimColor: true, children: "─".repeat(64) }) }), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [whatsNew.length > 0 && (_jsxs(Box, { flexDirection: "column", width: 38, children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: ["What's New \u00B7 v", VERSION] }), whatsNew.map((item) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: "\u203A" }), _jsxs(Text, { dimColor: true, children: [" ", item] })] }, item)))] })), _jsx(Box, { paddingX: 2, flexDirection: "column", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u2502" }, i))) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Setup" }), _jsx(Text, { dimColor: true, children: "1. Choose Login below or type /login." }), _jsx(Text, { dimColor: true, children: "2. Open the verification URL." }), _jsx(Text, { dimColor: true, children: "3. Enter the shown device code." }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Prompt" }), _jsx(Text, { dimColor: true, children: "/ commands \u00B7 @ files \u00B7 ! shell" })] })] }), _jsxs(Box, { marginTop: 2, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "What would you like to do?" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: SPLASH_OPTIONS, visibleOptionCount: 2, isDisabled: selectDisabled, onChange: handleChange }) })] })] }) }));
|
|
23
17
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* useBracketedPaste
|
|
4
|
+
*
|
|
5
|
+
* Enables / disables terminal Bracketed Paste Mode (BPM).
|
|
6
|
+
* When BPM is active the terminal wraps every CMD+V paste in
|
|
7
|
+
* the escape sequences \x1b[200~ ... \x1b[201~, which Composer
|
|
8
|
+
* detects inside `useFilteredInput` to accumulate the full paste
|
|
9
|
+
* block before delivering it to the input value.
|
|
10
|
+
*
|
|
11
|
+
* This hook only handles the terminal toggle — all accumulation
|
|
12
|
+
* logic lives in Composer.tsx so there is a single stdin consumer.
|
|
13
|
+
*/
|
|
14
|
+
export function useBracketedPaste() {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!process.stdout.isTTY)
|
|
17
|
+
return;
|
|
18
|
+
// Enable bracketed paste mode.
|
|
19
|
+
process.stdout.write("\x1b[?2004h");
|
|
20
|
+
return () => {
|
|
21
|
+
if (process.stdout.isTTY) {
|
|
22
|
+
// Disable bracketed paste mode on cleanup.
|
|
23
|
+
process.stdout.write("\x1b[?2004l");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
}
|
package/dist/ui/theme.js
CHANGED
|
@@ -2,7 +2,9 @@ export const RUBIX_THEME = {
|
|
|
2
2
|
colors: {
|
|
3
3
|
brand: "#5b8def",
|
|
4
4
|
border: "#4a5568",
|
|
5
|
-
|
|
5
|
+
userPrompt: "#5b8def", // Blue for user > prefix
|
|
6
|
+
userText: "#e2e8f0", // Light text for user body
|
|
7
|
+
userBlockBg: "#1a2942", // Subtle blue-tinted bg for user turn (Claude-style)
|
|
6
8
|
assistantText: "#e2e8f0",
|
|
7
9
|
thought: "#ff9f6b",
|
|
8
10
|
tool: "#63b3ed",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubixkube/rubix",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Chat with your infrastructure from the terminal. RubixKube CLI for Site Reliability Intelligence—predict, prevent, and fix failures with AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"patches",
|
|
12
|
-
"
|
|
12
|
+
"CHANGELOG.md",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
13
15
|
],
|
|
14
16
|
"scripts": {
|
|
15
17
|
"postinstall": "patch-package",
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
"mttr",
|
|
34
36
|
"ai-agent"
|
|
35
37
|
],
|
|
36
|
-
"license": "
|
|
38
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
37
39
|
"repository": {
|
|
38
40
|
"type": "git",
|
|
39
41
|
"url": "https://github.com/rubixkube-io/rubix-cli"
|
|
@@ -65,4 +67,4 @@
|
|
|
65
67
|
"url": "https://github.com/rubixkube-io/rubix-cli/issues"
|
|
66
68
|
},
|
|
67
69
|
"author": "RubixKube <connect@rubixkube.ai>"
|
|
68
|
-
}
|
|
70
|
+
}
|