@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.
@@ -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
- return (_jsx(Box, { flexDirection: "column", width: "100%", children: messages.map((message) => {
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 (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [mainMessage ? (_jsx(Box, { flexDirection: "column", children: mainMessage.split("\n").map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: i === 0 ? " " : " " }), _jsx(Text, { color: RUBIX_THEME.colors.userText, children: line })] }, `${message.id}-main-${i}`))) })) : null, hasContext
142
- ? contextParts.map((ctx, i) => {
143
- const labelMatch = ctx.match(/^\[CONTEXT\]\s*\*\*([^*]+)\*\*/);
144
- const label = labelMatch ? labelMatch[1] : `Context ${i + 1}`;
145
- const preview = previewContext(ctx);
146
- return (_jsxs(Text, { dimColor: true, children: [" ", _jsx(Text, { color: RUBIX_THEME.colors.brand, children: "\u25CB" }), " ", label, " \u2014 ", preview] }, `${message.id}-ctx-${i}`));
147
- })
148
- : null] }, message.id));
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
- if (shouldShowTimeline) {
166
- return (_jsxs(Box, { flexDirection: "column", children: [timelineRows.map((row) => {
167
- const lines = row.content.split("\n");
168
- 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] }));
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
- return (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: renderMixedView() }, message.id));
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
- // 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]);
35
- 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: {
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
- "check cluster health",
48
+ "what's the current state of my environment?",
49
+ "any critical issues right now?",
48
50
  "run a security audit",
49
- "inspect performance metrics",
50
- "debug active issues",
51
+ "debug active insights",
51
52
  ];
52
53
  const NAV_TIPS = [
53
- { cmd: "/sessions", desc: "reopen a thread" },
54
- { cmd: "/new", desc: "start fresh" },
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
- export const DashboardPanel = React.memo(function DashboardPanel({ user, agentName, cwd, recentSessions, selectedCluster }) {
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.slice(0, 2);
62
- 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: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Hello, ", greetingName] }), _jsx(Text, { dimColor: true, children: "Welcome to 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, width: "100%", alignItems: "flex-start", children: [_jsxs(Box, { flexDirection: "column", width: 42, children: [_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "\u25C8" }), _jsx(Text, { color: RUBIX_THEME.colors.assistantText, bold: true, children: agentName })] }), selectedCluster ? (_jsxs(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: selectedCluster.name }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: selectedCluster.status }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: "/cluster to switch" })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "no cluster \u00B7 /cluster to select" }) })), workspace ? (_jsx(Box, { marginTop: 1, 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(11) }), _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))))] })] })] })] }) }));
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] }), 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/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
- userText: "#a0aec0",
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.2",
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
- "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
+ }