@rubixkube/rubix 0.0.2 → 0.0.3

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.
@@ -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,12 +153,13 @@ 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 };
132
163
  }
133
164
  export const ChatTranscript = React.memo(function ChatTranscript({ messages, workflowViewMode = "detailed", }) {
134
165
  if (messages.length === 0) {
@@ -160,16 +191,38 @@ export const ChatTranscript = React.memo(function ChatTranscript({ messages, wor
160
191
  const hasWorkflow = workflow.length > 0;
161
192
  // Build mixed timeline: interleave workflow events with text response
162
193
  const renderMixedView = () => {
194
+ const uiComponents = timelineRows.filter(r => r.uiData);
195
+ const standardRows = timelineRows.filter(r => !r.uiData);
163
196
  // Show timeline during streaming if workflow exists, or after streaming in detailed mode
164
197
  const shouldShowTimeline = (isStreaming && hasWorkflow) || (!isStreaming && isDetailed && hasWorkflow);
165
- if (shouldShowTimeline) {
166
- return (_jsxs(Box, { flexDirection: "column", children: [timelineRows.map((row) => {
198
+ const hasCollapsibleItems = stats.toolCalls > 0 || stats.thoughtCount > 0;
199
+ const renderSummaryFooter = () => {
200
+ if (isStreaming || !hasCollapsibleItems)
201
+ return null;
202
+ const parts = [];
203
+ if (stats.toolCalls > 0)
204
+ parts.push(`${stats.toolCalls} tool call${stats.toolCalls !== 1 ? "s" : ""}`);
205
+ if (stats.thoughtCount > 0)
206
+ parts.push(`${stats.thoughtCount} thought${stats.thoughtCount !== 1 ? "s" : ""}`);
207
+ const summaryText = parts.join(", ");
208
+ const timeText = stats.durationSec != null ? ` · ${stats.durationSec}s` : "";
209
+ return (_jsxs(Text, { dimColor: true, children: [" ··· ", summaryText, timeText, " ", isDetailed ? "(Ctrl+O to collapse)" : "(Ctrl+O for timeline)"] }));
210
+ };
211
+ return (_jsxs(Box, { flexDirection: "column", children: [standardRows.length > 0 && (_jsx(Box, { flexDirection: "column", children: standardRows
212
+ .filter((row) => shouldShowTimeline || row.label === "thought" || row.label === "tool call" || row.label === "tool error")
213
+ .map((row) => {
167
214
  const lines = row.content.split("\n");
215
+ const firstLine = lines[0] || "";
216
+ if (!shouldShowTimeline) {
217
+ const displayTitle = row.label === "tool call"
218
+ ? `${row.label}: ${firstLine}()`
219
+ : row.label === "thought"
220
+ ? `${row.label}: ${firstLine} ...`
221
+ : `${row.label}`;
222
+ return (_jsxs(Text, { dimColor: true, children: [" ", _jsxs(Text, { color: row.color, children: ["\u25CF ", displayTitle] })] }, `condensed-${row.key}`));
223
+ }
168
224
  return (_jsx(Box, { flexDirection: "column", 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));
169
- }), hasNonEmptyContent ? (_jsx(Box, { flexDirection: "column", children: contentLines.map((line, index) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: index === 0 ? " ● " : " " }), line.length > 0 ? line : " "] }, `${message.id}-text-${index}`))) })) : null, !isStreaming && (_jsxs(Text, { dimColor: true, children: [" \u00B7\u00B7\u00B7 ", stats.toolCalls, " tool call", stats.toolCalls !== 1 ? "s" : "", stats.durationSec != null ? ` · ${stats.durationSec}s` : "", " (Ctrl+O to collapse)"] }))] }));
170
- }
171
- // Minimal view: text + summary stat
172
- return (_jsxs(Box, { flexDirection: "column", children: [hasNonEmptyContent ? (_jsx(_Fragment, { children: contentLines.map((line, index) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: index === 0 ? "● " : " " }), line.length > 0 ? line : " "] }, `${message.id}-${index}`))) })) : null, hasWorkflow ? (_jsxs(Text, { dimColor: true, children: [" ", stats.toolCalls > 0 && `${stats.toolCalls} tool call${stats.toolCalls !== 1 ? "s" : ""}`, stats.toolCalls > 0 && stats.durationSec != null && " · ", stats.durationSec != null && `${stats.durationSec}s`, hasWorkflow && " · Ctrl+O for timeline"] })) : null] }));
225
+ }) })), uiComponents.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: shouldShowTimeline && standardRows.length > 0 ? 1 : 0, children: uiComponents.map((row) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, 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}`))) })), hasNonEmptyContent && (_jsx(Box, { flexDirection: "column", children: contentLines.map((line, index) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: (index === 0 && (!shouldShowTimeline || standardRows.length === 0)) ? "" : " " }), line.length > 0 ? line : " "] }, `${message.id}-text-${index}`))) })), renderSummaryFooter()] }));
173
226
  };
174
227
  return (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: renderMixedView() }, message.id));
175
228
  }
@@ -6,32 +6,91 @@ 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
- // Buffer value locally so MultilineInput always receives the latest on fast typing.
13
- // Parent state updates can lag; without this, MultilineInput's closure uses stale value
14
- // and drops characters (e.g. "different" -> "diffrnt").
15
- const [bufferedValue, setBufferedValue] = useState(value);
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 effectiveValue = resetToken !== lastResetTokenRef.current ? value : bufferedValue;
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 }), [captureArrowKeys]);
91
+ }, { isActive }),
92
+ // eslint-disable-next-line react-hooks/exhaustive-deps
93
+ [captureArrowKeys, onChange]);
35
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.assistantText, 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),
@@ -55,7 +55,7 @@ const NAV_TIPS = [
55
55
  { cmd: "/cluster", desc: "switch cluster" },
56
56
  { cmd: "?", desc: "show shortcuts" },
57
57
  ];
58
- export const DashboardPanel = React.memo(function DashboardPanel({ user, agentName, cwd, recentSessions, selectedCluster }) {
58
+ export const DashboardPanel = React.memo(function DashboardPanel({ user, agentName, cwd, recentSessions, selectedCluster, }) {
59
59
  const greetingName = displayName(user);
60
60
  const workspace = compactWorkspace(cwd);
61
61
  const latest = recentSessions.slice(0, 2);
@@ -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] }), WHATS_NEW.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 }) })] })] }) }));
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubixkube/rubix",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
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
- "README.md"
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": "MIT",
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
+ }
@@ -1,20 +1,211 @@
1
1
  diff --git a/node_modules/ink-multiline-input/dist/index.js b/node_modules/ink-multiline-input/dist/index.js
2
- index b1262e7..e6e7fca 100644
2
+ index b1262e7..53a5c68 100644
3
3
  --- a/node_modules/ink-multiline-input/dist/index.js
4
4
  +++ b/node_modules/ink-multiline-input/dist/index.js
5
- @@ -360,6 +360,73 @@ var MultilineInput = ({
6
- setCursorIndex(Math.min(value.length, cursorIndex + 1));
5
+ @@ -1,5 +1,5 @@
6
+ // src/MultilineInput.tsx
7
+ -import { useState as useState2, useEffect as useEffect3, useMemo as useMemo2 } from "react";
8
+ +import { useState as useState2, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2 } from "react";
9
+ import { useInput } from "ink";
10
+
11
+ // src/ControlledMultilineInput.tsx
12
+ @@ -212,7 +212,8 @@ var ControlledMultilineInput = ({
13
+ flexDirection: "column",
14
+ flexGrow: 0,
15
+ flexShrink: 0,
16
+ - children: /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
17
+ + children: /* @__PURE__ */ jsxs(Box2, {
18
+ + flexDirection: "column", children: [
19
+ /* @__PURE__ */ jsxs(
20
+ Box2,
21
+ {
22
+ @@ -221,16 +222,23 @@ var ControlledMultilineInput = ({
23
+ flexShrink: 0,
24
+ flexDirection: "column",
25
+ children: [
26
+ - /* @__PURE__ */ jsx2(Box2, { marginTop: -scrollOffset, flexDirection: "column", children: /* @__PURE__ */ jsx2(MeasureBox, { onHeightChange: setContentHeight, children: /* @__PURE__ */ jsxs(Text, { children: [
27
+ - preCursor?.map((segment, idx) => /* @__PURE__ */ jsx2(Text, { ...getStyle(segment.type), children: segment.value }, idx)),
28
+ - postCursor?.map((segment, idx) => /* @__PURE__ */ jsx2(Text, { ...getStyle(segment.type), children: segment.value }, idx))
29
+ - ] }) }) }),
30
+ + /* @__PURE__ */ jsx2(Box2, {
31
+ + marginTop: -scrollOffset, flexDirection: "column", children: /* @__PURE__ */ jsx2(MeasureBox, {
32
+ + onHeightChange: setContentHeight, children: /* @__PURE__ */ jsxs(Text, {
33
+ + children: [
34
+ + preCursor?.map((segment, idx) => /* @__PURE__ */ jsx2(Text, { ...getStyle(segment.type), children: segment.value }, idx)),
35
+ + postCursor?.map((segment, idx) => /* @__PURE__ */ jsx2(Text, { ...getStyle(segment.type), children: segment.value }, idx))
36
+ + ]
37
+ + })
38
+ + })
39
+ + }),
40
+ /* @__PURE__ */ jsx2(Spacer, {})
41
+ ]
42
+ }
43
+ ),
44
+ /* @__PURE__ */ jsx2(MeasureBox, { onHeightChange: setMarkerHeight, children: /* @__PURE__ */ jsx2(Text, { children: preCursor?.map((segment, idx) => /* @__PURE__ */ jsx2(Text, { ...getStyle(segment.type), children: segment.value }, idx)) }) })
45
+ - ] })
46
+ + ]
47
+ + })
48
+ }
49
+ );
50
+ };
51
+ @@ -250,21 +258,53 @@ var MultilineInput = ({
52
+ }) => {
53
+ const [cursorIndex, setCursorIndex] = useState2(value.length);
54
+ const [pasteLength, setPasteLength] = useState2(0);
55
+ + // ── Fast-typing fix ─────────────────────────────────────────────────────
56
+ + // Both `value` (prop) and `cursorIndex` (state) are captured by the
57
+ + // useCustomInput closure at render time. When keystrokes arrive faster
58
+ + // than React can re-render, the closure sees stale values and characters
59
+ + // are inserted into an outdated string at wrong positions.
60
+ + //
61
+ + // Fix: mirror both in refs that we update **synchronously inside the
62
+ + // handler** right after computing new values. The next keystroke reads
63
+ + // the refs (always current) instead of the stale closure variables.
64
+ + const valueRef = useRef2(value);
65
+ + const cursorRef = useRef2(cursorIndex);
66
+ + // Track the last value we sent to the parent via onChange so we can
67
+ + // distinguish external resets (parent changed value independently)
68
+ + // from our own in-flight updates that React hasn't applied yet.
69
+ + const lastNotifiedRef = useRef2(value);
70
+ + // Only sync from props when the parent has caught up or an external
71
+ + // reset occurred. Without this guard, a render with a stale prop
72
+ + // (from a previous keystroke's onChange that hasn't propagated yet)
73
+ + // would overwrite the ref and undo the handler's synchronous updates.
74
+ + if (value !== lastNotifiedRef.current) {
75
+ + // External reset (e.g. submit cleared the value, or parent changed it)
76
+ + valueRef.current = value;
77
+ + cursorRef.current = cursorIndex;
78
+ + lastNotifiedRef.current = value;
79
+ + }
80
+ useEffect3(() => {
81
+ if (cursorIndex > value.length) {
82
+ setCursorIndex(value.length);
83
+ + cursorRef.current = value.length;
84
+ }
85
+ }, [value, cursorIndex]);
86
+ useCustomInput((input, key) => {
87
+ + // Read latest value & cursor from refs (never stale).
88
+ + const v = valueRef.current;
89
+ + const ci = cursorRef.current;
90
+ const submitKey = keyBindings?.submit ?? ((key2) => key2.return && key2.ctrl);
91
+ const newlineKey = keyBindings?.newline ?? ((key2) => key2.return);
92
+ if (submitKey(key)) {
93
+ - onSubmit?.(value);
94
+ + onSubmit?.(v);
95
+ return;
96
+ } else if (newlineKey(key)) {
97
+ - const newValue = value.slice(0, cursorIndex) + "\n" + value.slice(cursorIndex);
98
+ + const newValue = v.slice(0, ci) + "\n" + v.slice(ci);
99
+ + valueRef.current = newValue;
100
+ + cursorRef.current = ci + 1;
101
+ onChange(newValue);
102
+ - setCursorIndex(cursorIndex + 1);
103
+ + lastNotifiedRef.current = newValue;
104
+ + setCursorIndex(ci + 1);
105
+ setPasteLength(0);
106
+ return;
107
+ }
108
+ @@ -272,9 +312,12 @@ var MultilineInput = ({
109
+ return;
110
+ }
111
+ if (keyBindings?.newline?.(key)) {
112
+ - const newValue = value.slice(0, cursorIndex) + "\n" + value.slice(cursorIndex);
113
+ + const newValue = v.slice(0, ci) + "\n" + v.slice(ci);
114
+ + valueRef.current = newValue;
115
+ + cursorRef.current = ci + 1;
116
+ onChange(newValue);
117
+ - setCursorIndex(cursorIndex + 1);
118
+ + lastNotifiedRef.current = newValue;
119
+ + setCursorIndex(ci + 1);
120
+ setPasteLength(0);
121
+ return;
122
+ }
123
+ @@ -284,7 +327,7 @@ var MultilineInput = ({
124
+ }
125
+ if (key.upArrow) {
126
+ if (showCursor) {
127
+ - const lines = normalizeLineEndings(value).split("\n");
128
+ + const lines = normalizeLineEndings(v).split("\n");
129
+ let currentLineIndex = 0;
130
+ let currentPos = 0;
131
+ let col = 0;
132
+ @@ -293,9 +336,9 @@ var MultilineInput = ({
133
+ if (line === void 0) continue;
134
+ const lineLen = line.length;
135
+ const lineEnd = currentPos + lineLen;
136
+ - if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
137
+ + if (ci >= currentPos && ci <= lineEnd) {
138
+ currentLineIndex = i;
139
+ - col = cursorIndex - currentPos;
140
+ + col = ci - currentPos;
141
+ break;
142
+ }
143
+ currentPos = lineEnd + 1;
144
+ @@ -311,6 +354,7 @@ var MultilineInput = ({
145
+ newIndex += lines[i].length + 1;
146
+ }
147
+ newIndex += newCol;
148
+ + cursorRef.current = newIndex;
149
+ setCursorIndex(newIndex);
150
+ setPasteLength(0);
151
+ }
152
+ @@ -318,7 +362,7 @@ var MultilineInput = ({
153
+ }
154
+ } else if (key.downArrow) {
155
+ if (showCursor) {
156
+ - const lines = normalizeLineEndings(value).split("\n");
157
+ + const lines = normalizeLineEndings(v).split("\n");
158
+ let currentLineIndex = 0;
159
+ let currentPos = 0;
160
+ let col = 0;
161
+ @@ -327,9 +371,9 @@ var MultilineInput = ({
162
+ if (line === void 0) continue;
163
+ const lineLen = line.length;
164
+ const lineEnd = currentPos + lineLen;
165
+ - if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
166
+ + if (ci >= currentPos && ci <= lineEnd) {
167
+ currentLineIndex = i;
168
+ - col = cursorIndex - currentPos;
169
+ + col = ci - currentPos;
170
+ break;
171
+ }
172
+ currentPos = lineEnd + 1;
173
+ @@ -345,6 +389,7 @@ var MultilineInput = ({
174
+ newIndex += lines[i].length + 1;
175
+ }
176
+ newIndex += newCol;
177
+ + cursorRef.current = newIndex;
178
+ setCursorIndex(newIndex);
179
+ setPasteLength(0);
180
+ }
181
+ @@ -352,31 +397,118 @@ var MultilineInput = ({
182
+ }
183
+ } else if (key.leftArrow) {
184
+ if (showCursor) {
185
+ - setCursorIndex(Math.max(0, cursorIndex - 1));
186
+ + const nc = Math.max(0, ci - 1);
187
+ + cursorRef.current = nc;
188
+ + setCursorIndex(nc);
189
+ setPasteLength(0);
190
+ }
191
+ } else if (key.rightArrow) {
192
+ if (showCursor) {
193
+ - setCursorIndex(Math.min(value.length, cursorIndex + 1));
194
+ + const nc = Math.min(v.length, ci + 1);
195
+ + cursorRef.current = nc;
196
+ + setCursorIndex(nc);
7
197
  setPasteLength(0);
8
198
  }
9
199
  + } else if (key.ctrl && (input === "a" || input === "\u0001")) {
10
200
  + if (showCursor) {
11
- + const lines = normalizeLineEndings(value).split("\n");
201
+ + const lines = normalizeLineEndings(v).split("\n");
12
202
  + let currentPos = 0;
13
203
  + for (let i = 0; i < lines.length; i++) {
14
204
  + const line = lines[i];
15
205
  + if (line === void 0) continue;
16
206
  + const lineEnd = currentPos + line.length;
17
- + if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
207
+ + if (ci >= currentPos && ci <= lineEnd) {
208
+ + cursorRef.current = currentPos;
18
209
  + setCursorIndex(currentPos);
19
210
  + setPasteLength(0);
20
211
  + break;
@@ -24,13 +215,14 @@ index b1262e7..e6e7fca 100644
24
215
  + }
25
216
  + } else if (key.ctrl && (input === "e" || input === "\u0005")) {
26
217
  + if (showCursor) {
27
- + const lines = normalizeLineEndings(value).split("\n");
218
+ + const lines = normalizeLineEndings(v).split("\n");
28
219
  + let currentPos = 0;
29
220
  + for (let i = 0; i < lines.length; i++) {
30
221
  + const line = lines[i];
31
222
  + if (line === void 0) continue;
32
223
  + const lineEnd = currentPos + line.length;
33
- + if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
224
+ + if (ci >= currentPos && ci <= lineEnd) {
225
+ + cursorRef.current = lineEnd;
34
226
  + setCursorIndex(lineEnd);
35
227
  + setPasteLength(0);
36
228
  + break;
@@ -39,16 +231,18 @@ index b1262e7..e6e7fca 100644
39
231
  + }
40
232
  + }
41
233
  + } else if (key.ctrl && (input === "k" || input === "\u000b")) {
42
- + if (showCursor && cursorIndex < value.length) {
43
- + const lines = normalizeLineEndings(value).split("\n");
234
+ + if (showCursor && ci < v.length) {
235
+ + const lines = normalizeLineEndings(v).split("\n");
44
236
  + let currentPos = 0;
45
237
  + for (let i = 0; i < lines.length; i++) {
46
238
  + const line = lines[i];
47
239
  + if (line === void 0) continue;
48
240
  + const lineEnd = currentPos + line.length;
49
- + if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
50
- + const newValue = value.slice(0, cursorIndex) + value.slice(lineEnd);
241
+ + if (ci >= currentPos && ci <= lineEnd) {
242
+ + const newValue = v.slice(0, ci) + v.slice(lineEnd);
243
+ + valueRef.current = newValue;
51
244
  + onChange(newValue);
245
+ + lastNotifiedRef.current = newValue;
52
246
  + setPasteLength(0);
53
247
  + break;
54
248
  + }
@@ -56,16 +250,19 @@ index b1262e7..e6e7fca 100644
56
250
  + }
57
251
  + }
58
252
  + } else if (key.ctrl && (input === "u" || input === "\u0015")) {
59
- + if (showCursor && cursorIndex > 0) {
60
- + const lines = normalizeLineEndings(value).split("\n");
253
+ + if (showCursor && ci > 0) {
254
+ + const lines = normalizeLineEndings(v).split("\n");
61
255
  + let currentPos = 0;
62
256
  + for (let i = 0; i < lines.length; i++) {
63
257
  + const line = lines[i];
64
258
  + if (line === void 0) continue;
65
259
  + const lineEnd = currentPos + line.length;
66
- + if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
67
- + const newValue = value.slice(0, currentPos) + value.slice(cursorIndex);
260
+ + if (ci >= currentPos && ci <= lineEnd) {
261
+ + const newValue = v.slice(0, currentPos) + v.slice(ci);
262
+ + valueRef.current = newValue;
263
+ + cursorRef.current = currentPos;
68
264
  + onChange(newValue);
265
+ + lastNotifiedRef.current = newValue;
69
266
  + setCursorIndex(currentPos);
70
267
  + setPasteLength(0);
71
268
  + break;
@@ -74,5 +271,38 @@ index b1262e7..e6e7fca 100644
74
271
  + }
75
272
  + }
76
273
  } else if (key.return) {
77
- const newValue = value.slice(0, cursorIndex) + "\n" + value.slice(cursorIndex);
274
+ - const newValue = value.slice(0, cursorIndex) + "\n" + value.slice(cursorIndex);
275
+ + const newValue = v.slice(0, ci) + "\n" + v.slice(ci);
276
+ + valueRef.current = newValue;
277
+ + cursorRef.current = ci + 1;
78
278
  onChange(newValue);
279
+ - setCursorIndex(cursorIndex + 1);
280
+ + lastNotifiedRef.current = newValue;
281
+ + setCursorIndex(ci + 1);
282
+ setPasteLength(0);
283
+ } else if (key.backspace || key.delete) {
284
+ - if (cursorIndex > 0) {
285
+ - const newValue = value.slice(0, cursorIndex - 1) + value.slice(cursorIndex);
286
+ + if (ci > 0) {
287
+ + const newValue = v.slice(0, ci - 1) + v.slice(ci);
288
+ + valueRef.current = newValue;
289
+ + cursorRef.current = ci - 1;
290
+ onChange(newValue);
291
+ - setCursorIndex(cursorIndex - 1);
292
+ + lastNotifiedRef.current = newValue;
293
+ + setCursorIndex(ci - 1);
294
+ setPasteLength(0);
295
+ }
296
+ } else {
297
+ if (input) {
298
+ - const newValue = value.slice(0, cursorIndex) + input + value.slice(cursorIndex);
299
+ + const newValue = v.slice(0, ci) + input + v.slice(ci);
300
+ + valueRef.current = newValue;
301
+ + cursorRef.current = ci + input.length;
302
+ onChange(newValue);
303
+ - setCursorIndex(cursorIndex + input.length);
304
+ + lastNotifiedRef.current = newValue;
305
+ + setCursorIndex(ci + input.length);
306
+ setPasteLength(nextPasteLength);
307
+ }
308
+ }