@rubixkube/rubix 0.0.1 → 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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +33 -0
- package/README.md +29 -12
- package/dist/cli.js +7 -0
- package/dist/commands/login.js +23 -2
- package/dist/commands/model.js +84 -0
- package/dist/core/device-auth.js +39 -1
- package/dist/core/rubix-api.js +175 -20
- package/dist/core/session-store.js +36 -0
- package/dist/core/settings.js +25 -0
- package/dist/core/update-check.js +51 -0
- package/dist/core/whats-new.js +56 -0
- package/dist/ui/App.js +305 -39
- package/dist/ui/components/BrandPanel.js +1 -1
- package/dist/ui/components/ChatTranscript.js +61 -8
- package/dist/ui/components/Composer.js +66 -7
- package/dist/ui/components/DashboardPanel.js +1 -1
- package/dist/ui/components/SplashScreen.js +2 -8
- package/dist/ui/hooks/useBracketedPaste.js +27 -0
- package/package.json +6 -4
- package/patches/ink-multiline-input+0.1.0.patch +246 -16
package/dist/ui/App.js
CHANGED
|
@@ -6,9 +6,14 @@ import { exec, execSync, spawn } from "child_process";
|
|
|
6
6
|
import { Box, Text, useApp, useInput } from "ink";
|
|
7
7
|
import { Select, Spinner, StatusMessage } from "@inkjs/ui";
|
|
8
8
|
import { clearAuthConfig, loadAuthConfig, saveAuthConfig } from "../core/auth-store.js";
|
|
9
|
-
import { authenticateWithDeviceFlow } from "../core/device-auth.js";
|
|
9
|
+
import { authenticateWithDeviceFlow, isTokenNearExpiry } from "../core/device-auth.js";
|
|
10
10
|
import { isFolderTrusted, trustFolder, untrustFolder } from "../core/trust-store.js";
|
|
11
|
-
import {
|
|
11
|
+
import { clearLocalSessions, loadLocalSessions, saveLocalSessions } from "../core/session-store.js";
|
|
12
|
+
import { loadSettings, saveSettings } from "../core/settings.js";
|
|
13
|
+
import { loadWhatsNew } from "../core/whats-new.js";
|
|
14
|
+
import { checkForUpdate } from "../core/update-check.js";
|
|
15
|
+
import { VERSION } from "../version.js";
|
|
16
|
+
import { createSession, fetchChatHistory, firstHealthyCluster, getOrCreateSession, listClusters, listModels, listSessions, listApps, refreshAndUpdateAuth, streamChat, StreamError, updateSessionState, } from "../core/rubix-api.js";
|
|
12
17
|
import { readFileContext, fetchUrlContext, formatContextBlock, } from "../core/file-context.js";
|
|
13
18
|
import { ChatTranscript } from "./components/ChatTranscript.js";
|
|
14
19
|
import { Composer } from "./components/Composer.js";
|
|
@@ -17,6 +22,7 @@ import { SplashScreen } from "./components/SplashScreen.js";
|
|
|
17
22
|
import { TrustDisclaimer } from "./components/TrustDisclaimer.js";
|
|
18
23
|
import { getConfig } from "../config/env.js";
|
|
19
24
|
import { RUBIX_THEME } from "./theme.js";
|
|
25
|
+
import { useBracketedPaste } from "./hooks/useBracketedPaste.js";
|
|
20
26
|
// Auth | Session | Input | Navigation | Help
|
|
21
27
|
const SLASH_COMMANDS = [
|
|
22
28
|
{ name: "/login", description: "Authenticate with device code flow" },
|
|
@@ -25,6 +31,8 @@ const SLASH_COMMANDS = [
|
|
|
25
31
|
{ name: "/resume", description: "Resume a previous conversation" },
|
|
26
32
|
{ name: "/new", description: "Start a fresh conversation" },
|
|
27
33
|
{ name: "/cluster", description: "Switch active cluster" },
|
|
34
|
+
{ name: "/models", description: "Switch AI model for this session" },
|
|
35
|
+
{ name: "/agents", description: "Switch agent (app) for new sessions" },
|
|
28
36
|
{ name: "/paste", description: "Insert clipboard content (avoids terminal paste truncation)" },
|
|
29
37
|
{ name: "/send-shell-output", description: "Send last shell command output to Rubix" },
|
|
30
38
|
{ name: "/clear", description: "Clear current conversation history" },
|
|
@@ -84,7 +92,7 @@ const IDLE_ACTIVITY_WORDS = [
|
|
|
84
92
|
];
|
|
85
93
|
function createMessage(role, content) {
|
|
86
94
|
return {
|
|
87
|
-
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
95
|
+
id: `${Date.now()} -${Math.random().toString(16).slice(2)} `,
|
|
88
96
|
role,
|
|
89
97
|
content,
|
|
90
98
|
ts: Date.now(),
|
|
@@ -125,7 +133,7 @@ function sessionLabel(sessionId) {
|
|
|
125
133
|
}
|
|
126
134
|
function setTerminalTitle(title) {
|
|
127
135
|
if (typeof process.stdout?.write === "function") {
|
|
128
|
-
process.stdout.write(`\x1b]0;${title}\x07`);
|
|
136
|
+
process.stdout.write(`\x1b]0;${title} \x07`);
|
|
129
137
|
}
|
|
130
138
|
}
|
|
131
139
|
function compactLine(value, max = 64) {
|
|
@@ -138,8 +146,8 @@ function compactLine(value, max = 64) {
|
|
|
138
146
|
}
|
|
139
147
|
function extractThoughtTitle(content, max = 58) {
|
|
140
148
|
const plain = (content ?? "")
|
|
141
|
-
.replace(/```[\s\S]
|
|
142
|
-
.replace(/`([
|
|
149
|
+
.replace(/```[\s\S]*? ```/g, "")
|
|
150
|
+
.replace(/`([^ `]*)` /g, "$1")
|
|
143
151
|
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
144
152
|
.replace(/\*([^*]+)\*/g, "$1")
|
|
145
153
|
.replace(/^#+\s+/gm, "")
|
|
@@ -211,6 +219,18 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
211
219
|
const [clusterPanelLoading, setClusterPanelLoading] = useState(false);
|
|
212
220
|
const [clusterPanelError, setClusterPanelError] = useState(null);
|
|
213
221
|
const [pendingClusterSwitch, setPendingClusterSwitch] = useState(null);
|
|
222
|
+
const [availableModels, setAvailableModels] = useState([]);
|
|
223
|
+
const [currentSessionModel, setCurrentSessionModel] = useState(null);
|
|
224
|
+
const [showModelPanel, setShowModelPanel] = useState(false);
|
|
225
|
+
const [modelPanelLoading, setModelPanelLoading] = useState(false);
|
|
226
|
+
const [modelPanelError, setModelPanelError] = useState(null);
|
|
227
|
+
const [modelSelectedIndex, setModelSelectedIndex] = useState(0);
|
|
228
|
+
const [availableAgents, setAvailableAgents] = useState([]);
|
|
229
|
+
const [currentAgent, setCurrentAgent] = useState("SRI Agent");
|
|
230
|
+
const [showAgentPanel, setShowAgentPanel] = useState(false);
|
|
231
|
+
const [agentPanelLoading, setAgentPanelLoading] = useState(false);
|
|
232
|
+
const [agentPanelError, setAgentPanelError] = useState(null);
|
|
233
|
+
const [agentSelectedIndex, setAgentSelectedIndex] = useState(0);
|
|
214
234
|
const [slashSelectedIndex, setSlashSelectedIndex] = useState(0);
|
|
215
235
|
const [sessionsSelectedIndex, setSessionsSelectedIndex] = useState(0);
|
|
216
236
|
const [atFilePanelDismissed, setAtFilePanelDismissed] = useState(false);
|
|
@@ -218,7 +238,9 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
218
238
|
const [atFileList, setAtFileList] = useState([]);
|
|
219
239
|
const [atFileSelectedIndex, setAtFileSelectedIndex] = useState(0);
|
|
220
240
|
const [shellMode, setShellMode] = useState(false);
|
|
221
|
-
const [workflowViewMode, setWorkflowViewMode] = useState("
|
|
241
|
+
const [workflowViewMode, setWorkflowViewMode] = useState("minimal");
|
|
242
|
+
const [whatsNew] = useState(() => loadWhatsNew(VERSION));
|
|
243
|
+
const [updateVersion, setUpdateVersion] = useState(null);
|
|
222
244
|
const streamController = useRef(null);
|
|
223
245
|
const streamAssistantRef = useRef(null);
|
|
224
246
|
const lastShellRef = useRef(null);
|
|
@@ -230,6 +252,42 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
230
252
|
const [workflowExpandedIds, setWorkflowExpandedIds] = useState(new Set());
|
|
231
253
|
const [messageQueue, setMessageQueue] = useState([]);
|
|
232
254
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
255
|
+
// --- Persistent User Settings ---
|
|
256
|
+
const currentSettingsRef = useRef({});
|
|
257
|
+
const updateSetting = useCallback(async (partial) => {
|
|
258
|
+
const next = { ...currentSettingsRef.current, ...partial };
|
|
259
|
+
currentSettingsRef.current = next;
|
|
260
|
+
try {
|
|
261
|
+
await saveSettings(next);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Non-critical — swallow errors silently so UI is never blocked.
|
|
265
|
+
}
|
|
266
|
+
}, []);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
loadSettings()
|
|
269
|
+
.then((cfg) => {
|
|
270
|
+
currentSettingsRef.current = cfg;
|
|
271
|
+
if (cfg.workflowViewMode)
|
|
272
|
+
setWorkflowViewMode(cfg.workflowViewMode);
|
|
273
|
+
if (cfg.agentId)
|
|
274
|
+
setCurrentAgent(cfg.agentId);
|
|
275
|
+
// model and cluster will be applied after their lists are fetched
|
|
276
|
+
// Check for updates
|
|
277
|
+
checkForUpdate(VERSION, cfg.lastUpdateCheck)
|
|
278
|
+
.then((newVer) => {
|
|
279
|
+
if (newVer) {
|
|
280
|
+
setUpdateVersion(newVer);
|
|
281
|
+
}
|
|
282
|
+
void updateSetting({ lastUpdateCheck: Date.now() });
|
|
283
|
+
})
|
|
284
|
+
.catch(() => { });
|
|
285
|
+
})
|
|
286
|
+
.catch(() => {
|
|
287
|
+
// No settings file yet — use defaults.
|
|
288
|
+
});
|
|
289
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
290
|
+
}, []);
|
|
233
291
|
const promptHistory = useMemo(() => {
|
|
234
292
|
const userContents = messages
|
|
235
293
|
.filter((m) => m.role === "user")
|
|
@@ -299,9 +357,15 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
299
357
|
setComposer(nextValue);
|
|
300
358
|
setComposerResetToken((prev) => prev + 1);
|
|
301
359
|
}, []);
|
|
360
|
+
// Bracketed paste mode: enable \x1b[?2004h so the terminal wraps paste
|
|
361
|
+
// in \x1b[200~...\x1b[201~. Composer.tsx's useFilteredInput handles
|
|
362
|
+
// the actual accumulation so there is only one stdin consumer.
|
|
363
|
+
useBracketedPaste();
|
|
302
364
|
const handleComposerChange = useCallback((newValue) => {
|
|
303
365
|
setHistoryIndex(-1);
|
|
304
|
-
if (!shellMode && newValue === "!") {
|
|
366
|
+
if (!shellMode && newValue === "! ") {
|
|
367
|
+
// Require "! " (exclamation + space) to avoid accidentally entering
|
|
368
|
+
// shell mode when pasting content that starts with "!" (e.g. "!kubectl ...").
|
|
305
369
|
setShellMode(true);
|
|
306
370
|
setComposer("");
|
|
307
371
|
setComposerResetToken((prev) => prev + 1);
|
|
@@ -366,17 +430,19 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
366
430
|
setSlashSelectedIndex(0);
|
|
367
431
|
}, [visibleSlashCandidates]);
|
|
368
432
|
useEffect(() => {
|
|
369
|
-
if (slashModeActive && (showSessionsPanel || showClusterPanel)) {
|
|
433
|
+
if (slashModeActive && (showSessionsPanel || showClusterPanel || showModelPanel)) {
|
|
370
434
|
setShowSessionsPanel(false);
|
|
371
435
|
setShowClusterPanel(false);
|
|
436
|
+
setShowModelPanel(false);
|
|
372
437
|
}
|
|
373
|
-
}, [showSessionsPanel, showClusterPanel, slashModeActive]);
|
|
438
|
+
}, [showSessionsPanel, showClusterPanel, showModelPanel, slashModeActive]);
|
|
374
439
|
useEffect(() => {
|
|
375
|
-
if (atModeActive && (showSessionsPanel || showClusterPanel)) {
|
|
440
|
+
if (atModeActive && (showSessionsPanel || showClusterPanel || showModelPanel)) {
|
|
376
441
|
setShowSessionsPanel(false);
|
|
377
442
|
setShowClusterPanel(false);
|
|
443
|
+
setShowModelPanel(false);
|
|
378
444
|
}
|
|
379
|
-
}, [atModeActive, showSessionsPanel, showClusterPanel]);
|
|
445
|
+
}, [atModeActive, showSessionsPanel, showClusterPanel, showModelPanel]);
|
|
380
446
|
const prevShowSetupSplash = useRef(showSetupSplash);
|
|
381
447
|
useEffect(() => {
|
|
382
448
|
if (showSetupSplash && !prevShowSetupSplash.current) {
|
|
@@ -426,10 +492,10 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
426
492
|
}, [addSystemMessage, updateAssistantMessage]);
|
|
427
493
|
const ensureSession = useCallback(async (config, preferredId, clusterIdOverride) => {
|
|
428
494
|
const clusterId = clusterIdOverride ?? selectedCluster?.cluster_id;
|
|
429
|
-
const resolved = await getOrCreateSession(config, preferredId, clusterId);
|
|
495
|
+
const resolved = await getOrCreateSession(config, preferredId, clusterId, currentAgent);
|
|
430
496
|
setSessionId(resolved);
|
|
431
497
|
return resolved;
|
|
432
|
-
}, [selectedCluster?.cluster_id]);
|
|
498
|
+
}, [selectedCluster?.cluster_id, currentAgent]);
|
|
433
499
|
const openSessionsPanel = useCallback(async () => {
|
|
434
500
|
if (!authConfig || !isAuthenticated) {
|
|
435
501
|
addSystemMessage("Not authenticated. Run /login first.");
|
|
@@ -460,6 +526,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
460
526
|
setSessionItems(loaded);
|
|
461
527
|
setSessionsHasMore(reachedLimit);
|
|
462
528
|
setRecentSessions(loaded.slice(0, 2));
|
|
529
|
+
void saveLocalSessions(loaded.slice(0, 20));
|
|
463
530
|
setLastError(null);
|
|
464
531
|
setStatus("ready");
|
|
465
532
|
}
|
|
@@ -501,9 +568,10 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
501
568
|
const cluster = pendingClusterSwitch;
|
|
502
569
|
setPendingClusterSwitch(null);
|
|
503
570
|
setSelectedCluster(cluster);
|
|
571
|
+
void updateSetting({ clusterId: cluster.cluster_id });
|
|
504
572
|
setStatus("creating session");
|
|
505
573
|
try {
|
|
506
|
-
const nextSession = await createSession(authConfig,
|
|
574
|
+
const nextSession = await createSession(authConfig, currentAgent, cluster.cluster_id, currentSessionModel?.modelId);
|
|
507
575
|
setSessionId(nextSession);
|
|
508
576
|
setSessionTitle(null);
|
|
509
577
|
const latestSessions = await listSessions(authConfig, 20, 0).catch(() => []);
|
|
@@ -516,7 +584,104 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
516
584
|
setStatus("ready");
|
|
517
585
|
addSystemMessage(`Failed to start session for cluster: ${error instanceof Error ? error.message : String(error)}`);
|
|
518
586
|
}
|
|
519
|
-
}, [pendingClusterSwitch, authConfig, addSystemMessage]);
|
|
587
|
+
}, [pendingClusterSwitch, authConfig, addSystemMessage, currentAgent, currentSessionModel]);
|
|
588
|
+
const openModelPanel = useCallback(async () => {
|
|
589
|
+
if (!authConfig || !isAuthenticated) {
|
|
590
|
+
addSystemMessage("Not authenticated. Run /login first.");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (!sessionId) {
|
|
594
|
+
addSystemMessage("No active session. Use /new or /resume to start a conversation first.");
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
setShowHelp(false);
|
|
598
|
+
setShowSessionsPanel(false);
|
|
599
|
+
setShowClusterPanel(false);
|
|
600
|
+
setShowModelPanel(true);
|
|
601
|
+
setModelPanelLoading(true);
|
|
602
|
+
setModelPanelError(null);
|
|
603
|
+
try {
|
|
604
|
+
const loaded = await listModels(authConfig);
|
|
605
|
+
setAvailableModels(loaded);
|
|
606
|
+
setModelPanelError(null);
|
|
607
|
+
setModelSelectedIndex(0);
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
setModelPanelError(error instanceof Error ? error.message : String(error));
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
setModelPanelLoading(false);
|
|
614
|
+
}
|
|
615
|
+
}, [authConfig, isAuthenticated, sessionId, addSystemMessage]);
|
|
616
|
+
const handleModelChange = useCallback((modelId) => {
|
|
617
|
+
if (!sessionId) {
|
|
618
|
+
addSystemMessage("No active session. Cannot switch model.");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const selected = availableModels.find((m) => m.id === modelId);
|
|
622
|
+
if (!selected) {
|
|
623
|
+
addSystemMessage(`Model not found: ${modelId}`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const sessionModel = {
|
|
627
|
+
modelId: selected.id,
|
|
628
|
+
model: selected.model,
|
|
629
|
+
displayName: selected.display_name,
|
|
630
|
+
thinkingSupported: selected.thinking_supported,
|
|
631
|
+
};
|
|
632
|
+
setCurrentSessionModel(sessionModel);
|
|
633
|
+
void updateSetting({ modelId: selected.id });
|
|
634
|
+
setShowModelPanel(false);
|
|
635
|
+
addSystemMessage(`Switched to: ${sessionModel.displayName}`);
|
|
636
|
+
}, [sessionId, availableModels, addSystemMessage]);
|
|
637
|
+
const openAgentPanel = useCallback(async () => {
|
|
638
|
+
if (!authConfig || !isAuthenticated) {
|
|
639
|
+
addSystemMessage("Not authenticated. Run /login first.");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
setShowHelp(false);
|
|
643
|
+
setShowSessionsPanel(false);
|
|
644
|
+
setShowClusterPanel(false);
|
|
645
|
+
setShowModelPanel(false);
|
|
646
|
+
setShowAgentPanel(true);
|
|
647
|
+
setAgentPanelLoading(true);
|
|
648
|
+
setAgentPanelError(null);
|
|
649
|
+
try {
|
|
650
|
+
const loaded = await listApps(authConfig);
|
|
651
|
+
setAvailableAgents(loaded);
|
|
652
|
+
setAgentPanelError(null);
|
|
653
|
+
setAgentSelectedIndex(0);
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
setAgentPanelError(error instanceof Error ? error.message : String(error));
|
|
657
|
+
}
|
|
658
|
+
finally {
|
|
659
|
+
setAgentPanelLoading(false);
|
|
660
|
+
}
|
|
661
|
+
}, [authConfig, isAuthenticated, addSystemMessage]);
|
|
662
|
+
const handleAgentChange = useCallback(async (agent) => {
|
|
663
|
+
setShowAgentPanel(false);
|
|
664
|
+
setCurrentAgent(agent);
|
|
665
|
+
void updateSetting({ agentId: agent });
|
|
666
|
+
addSystemMessage(`Switched agent to: ${agent}. Starting new session.`);
|
|
667
|
+
setSessionId(null);
|
|
668
|
+
setSessionTitle(null);
|
|
669
|
+
setMessages([]);
|
|
670
|
+
if (!authConfig)
|
|
671
|
+
return;
|
|
672
|
+
try {
|
|
673
|
+
setStatus("creating session");
|
|
674
|
+
const nextSession = await createSession(authConfig, agent, selectedCluster?.cluster_id, currentSessionModel?.modelId);
|
|
675
|
+
setSessionId(nextSession);
|
|
676
|
+
const latestSessions = await listSessions(authConfig, 20, 0).catch(() => []);
|
|
677
|
+
setRecentSessions(latestSessions.slice(0, 2));
|
|
678
|
+
setStatus("ready");
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
setStatus("ready");
|
|
682
|
+
addSystemMessage(`Failed to create session with agent ${agent}: ` + String(err));
|
|
683
|
+
}
|
|
684
|
+
}, [authConfig, selectedCluster, currentSessionModel, addSystemMessage]);
|
|
520
685
|
const activateSessionById = useCallback((selectedId) => {
|
|
521
686
|
const selected = sessionItems.find((item) => item.id === selectedId);
|
|
522
687
|
if (!selected)
|
|
@@ -550,18 +715,39 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
550
715
|
let cancelled = false;
|
|
551
716
|
const loadAuth = async () => {
|
|
552
717
|
try {
|
|
553
|
-
|
|
718
|
+
let cfg = await loadAuthConfig();
|
|
554
719
|
if (cancelled)
|
|
555
720
|
return;
|
|
556
|
-
|
|
721
|
+
let loggedIn = !!cfg?.isAuthenticated && !!(cfg?.idToken ?? cfg?.authToken);
|
|
722
|
+
// Proactive token refresh if near/at expiration
|
|
723
|
+
if (loggedIn && cfg && isTokenNearExpiry(cfg.idToken ?? cfg.authToken)) {
|
|
724
|
+
try {
|
|
725
|
+
cfg = await refreshAndUpdateAuth(cfg);
|
|
726
|
+
// Successfully refreshed, continue with fresh token
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
// Refresh failed - token is invalid/expired, force re-login
|
|
730
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
731
|
+
if (!cancelled) {
|
|
732
|
+
addSystemMessage(`Session expired (${errMsg}). Use /login to refresh.`);
|
|
733
|
+
}
|
|
734
|
+
loggedIn = false;
|
|
735
|
+
cfg = null;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
557
738
|
setAuthConfig(cfg);
|
|
558
739
|
setIsAuthenticated(loggedIn);
|
|
559
740
|
setActiveUser(cfg?.userName ?? cfg?.userEmail ?? null);
|
|
560
741
|
if (loggedIn && cfg) {
|
|
561
742
|
setStatus("loading clusters");
|
|
562
743
|
try {
|
|
744
|
+
const localSess = await loadLocalSessions().catch(() => []);
|
|
745
|
+
if (!cancelled && localSess.length > 0) {
|
|
746
|
+
setRecentSessions(localSess.slice(0, 2));
|
|
747
|
+
setSessionItems(localSess);
|
|
748
|
+
}
|
|
563
749
|
// Load clusters and recent sessions for dashboard display
|
|
564
|
-
const [clusterList, recentList] = await Promise.all([
|
|
750
|
+
const [clusterList, recentList, appList] = await Promise.all([
|
|
565
751
|
listClusters(cfg).catch((err) => {
|
|
566
752
|
if (!cancelled) {
|
|
567
753
|
addSystemMessage(`Could not load clusters: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -574,12 +760,31 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
574
760
|
}
|
|
575
761
|
return [];
|
|
576
762
|
}),
|
|
763
|
+
listApps(cfg).catch(() => []),
|
|
577
764
|
]);
|
|
578
765
|
const firstCluster = firstHealthyCluster(clusterList);
|
|
579
766
|
if (!cancelled) {
|
|
580
|
-
setClusters(clusterList);
|
|
581
|
-
setSelectedCluster(firstCluster);
|
|
582
767
|
setRecentSessions(recentList.slice(0, 2));
|
|
768
|
+
setSessionItems(recentList);
|
|
769
|
+
void saveLocalSessions(recentList);
|
|
770
|
+
if (appList && appList.length > 0) {
|
|
771
|
+
setAvailableAgents(appList);
|
|
772
|
+
// Prefer saved agentId from settings, then SRI Agent, then first available
|
|
773
|
+
const saved = currentSettingsRef.current;
|
|
774
|
+
const savedAgent = saved.agentId && appList.includes(saved.agentId) ? saved.agentId : null;
|
|
775
|
+
const preferApp = savedAgent ?? (appList.includes("SRI Agent") ? "SRI Agent" : appList[0]);
|
|
776
|
+
if (preferApp)
|
|
777
|
+
setCurrentAgent(preferApp);
|
|
778
|
+
}
|
|
779
|
+
// Apply saved cluster preference from settings
|
|
780
|
+
const savedClusterId = currentSettingsRef.current.clusterId;
|
|
781
|
+
const preferredCluster = savedClusterId
|
|
782
|
+
? clusterList.find((c) => c.cluster_id === savedClusterId) ?? firstCluster
|
|
783
|
+
: firstCluster;
|
|
784
|
+
if (!cancelled) {
|
|
785
|
+
setClusters(clusterList);
|
|
786
|
+
setSelectedCluster(preferredCluster);
|
|
787
|
+
}
|
|
583
788
|
setStatus("ready");
|
|
584
789
|
}
|
|
585
790
|
}
|
|
@@ -700,19 +905,34 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
700
905
|
case "/quit":
|
|
701
906
|
exit();
|
|
702
907
|
return;
|
|
703
|
-
case "/status":
|
|
908
|
+
case "/status": {
|
|
909
|
+
const workspace = cwd.replace(process.env.HOME ?? "", "~");
|
|
910
|
+
const clusterStr = selectedCluster
|
|
911
|
+
? `${selectedCluster.name} (${selectedCluster.status})`
|
|
912
|
+
: "no cluster selected";
|
|
913
|
+
const modelStr = currentSessionModel
|
|
914
|
+
? `${currentSessionModel.displayName}${currentSessionModel.thinkingSupported ? " (thinking supported)" : ""}`
|
|
915
|
+
: "default";
|
|
704
916
|
addSystemMessage([
|
|
917
|
+
`Version: ${VERSION}`,
|
|
918
|
+
`Workspace: ${workspace}`,
|
|
705
919
|
`Auth: ${isAuthenticated ? `logged in as ${activeUser ?? "unknown user"}` : "not logged in"}`,
|
|
706
|
-
`
|
|
920
|
+
`Agent: ${currentAgent}`,
|
|
921
|
+
`Cluster: ${clusterStr}`,
|
|
922
|
+
`Model: ${modelStr}`,
|
|
923
|
+
`Session: ${sessionId ?? "no session"}`,
|
|
707
924
|
`State: ${status}`,
|
|
708
925
|
].join("\n"));
|
|
709
926
|
return;
|
|
927
|
+
}
|
|
710
928
|
case "/logout":
|
|
711
929
|
streamController.current?.abort();
|
|
712
930
|
streamController.current = null;
|
|
713
931
|
await clearAuthConfig();
|
|
932
|
+
await clearLocalSessions();
|
|
714
933
|
setAuthConfig(null);
|
|
715
|
-
|
|
934
|
+
setRecentSessions([]);
|
|
935
|
+
setSessionItems([]);
|
|
716
936
|
setActiveUser(null);
|
|
717
937
|
setSessionId(null);
|
|
718
938
|
setSessionTitle(null);
|
|
@@ -722,7 +942,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
722
942
|
return;
|
|
723
943
|
case "/login": {
|
|
724
944
|
if (isAuthenticated && authConfig) {
|
|
725
|
-
addSystemMessage(`Already logged in as ${activeUser ?? "user"}.`);
|
|
945
|
+
addSystemMessage(`Already logged in as ${activeUser ?? "user"}. Type /logout first to switch accounts.`);
|
|
726
946
|
return;
|
|
727
947
|
}
|
|
728
948
|
setStatus("auth");
|
|
@@ -733,12 +953,18 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
733
953
|
setAuthConfig(nextAuth);
|
|
734
954
|
setIsAuthenticated(true);
|
|
735
955
|
setActiveUser(nextAuth.userName ?? nextAuth.userEmail ?? null);
|
|
736
|
-
const clusterList = await listClusters(nextAuth).catch(() =>
|
|
956
|
+
const clusterList = await listClusters(nextAuth).catch((err) => {
|
|
957
|
+
addSystemMessage(`Could not load clusters: ${err instanceof Error ? err.message : String(err)}`);
|
|
958
|
+
return [];
|
|
959
|
+
});
|
|
737
960
|
const firstCluster = firstHealthyCluster(clusterList);
|
|
738
961
|
setClusters(clusterList);
|
|
739
962
|
setSelectedCluster(firstCluster);
|
|
740
963
|
const resolved = await ensureSession(nextAuth, undefined, firstCluster?.cluster_id);
|
|
741
|
-
const latestSessions = await listSessions(nextAuth, 20, 0).catch(() =>
|
|
964
|
+
const latestSessions = await listSessions(nextAuth, 20, 0).catch((err) => {
|
|
965
|
+
addSystemMessage(`Could not load recent sessions: ${err instanceof Error ? err.message : String(err)}`);
|
|
966
|
+
return [];
|
|
967
|
+
});
|
|
742
968
|
setSessionId(resolved);
|
|
743
969
|
setRecentSessions(latestSessions.slice(0, 2));
|
|
744
970
|
const currentSession = latestSessions.find((s) => s.id === resolved);
|
|
@@ -765,7 +991,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
765
991
|
}
|
|
766
992
|
setStatus("creating session");
|
|
767
993
|
try {
|
|
768
|
-
const nextSession = await createSession(authConfig,
|
|
994
|
+
const nextSession = await createSession(authConfig, currentAgent, selectedCluster?.cluster_id, currentSessionModel?.modelId);
|
|
769
995
|
setSessionId(nextSession);
|
|
770
996
|
setSessionTitle(null);
|
|
771
997
|
setMessages([]);
|
|
@@ -773,7 +999,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
773
999
|
const now = new Date().toISOString();
|
|
774
1000
|
const next = {
|
|
775
1001
|
id: nextSession,
|
|
776
|
-
appName:
|
|
1002
|
+
appName: currentAgent,
|
|
777
1003
|
createdAt: now,
|
|
778
1004
|
updatedAt: now,
|
|
779
1005
|
};
|
|
@@ -792,6 +1018,15 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
792
1018
|
await openClusterPanel();
|
|
793
1019
|
return;
|
|
794
1020
|
}
|
|
1021
|
+
case "/models": {
|
|
1022
|
+
addSystemMessage("Opening model selector...");
|
|
1023
|
+
await openModelPanel();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
case "/agents": {
|
|
1027
|
+
await openAgentPanel();
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
795
1030
|
case "/rename": {
|
|
796
1031
|
if (!authConfig || !isAuthenticated) {
|
|
797
1032
|
addSystemMessage("Not authenticated. Run /login first.");
|
|
@@ -911,6 +1146,11 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
911
1146
|
isAuthenticated,
|
|
912
1147
|
openClusterPanel,
|
|
913
1148
|
openSessionsPanel,
|
|
1149
|
+
openAgentPanel,
|
|
1150
|
+
openModelPanel,
|
|
1151
|
+
currentAgent,
|
|
1152
|
+
currentSessionModel,
|
|
1153
|
+
selectedCluster,
|
|
914
1154
|
recordActivity,
|
|
915
1155
|
resetComposer,
|
|
916
1156
|
sessionId,
|
|
@@ -939,6 +1179,11 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
939
1179
|
try {
|
|
940
1180
|
resolvedSession = await ensureSession(authConfig);
|
|
941
1181
|
setSessionId(resolvedSession);
|
|
1182
|
+
// If we created a brand‑new session (not the most recent empty one), inform the user.
|
|
1183
|
+
const prevRecent = recentSessions[0];
|
|
1184
|
+
if (!prevRecent || prevRecent.id !== resolvedSession) {
|
|
1185
|
+
addSystemMessage(`New session ${sessionLabel(resolvedSession)} started.`);
|
|
1186
|
+
}
|
|
942
1187
|
}
|
|
943
1188
|
catch (error) {
|
|
944
1189
|
failAssistantMessage(assistantId, `Failed to initialize session: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -981,6 +1226,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
981
1226
|
message: prompt,
|
|
982
1227
|
messageParts: parts,
|
|
983
1228
|
signal: controller.signal,
|
|
1229
|
+
...(currentSessionModel ? { stateDelta: { model_id: currentSessionModel.modelId } } : {}),
|
|
984
1230
|
}, {
|
|
985
1231
|
onText: throttledOnText,
|
|
986
1232
|
onWorkflow: (event) => {
|
|
@@ -1103,6 +1349,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1103
1349
|
}, [
|
|
1104
1350
|
addSystemMessage,
|
|
1105
1351
|
authConfig,
|
|
1352
|
+
currentSessionModel,
|
|
1106
1353
|
ensureSession,
|
|
1107
1354
|
failAssistantMessage,
|
|
1108
1355
|
isAuthenticated,
|
|
@@ -1381,7 +1628,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1381
1628
|
}
|
|
1382
1629
|
// Arrow navigation for sessions panel
|
|
1383
1630
|
const isShowingSlashPanel = slashModeActive && !slashPanelDismissed;
|
|
1384
|
-
if (showSessionsPanel && !isShowingSlashPanel && !showClusterPanel) {
|
|
1631
|
+
if (showSessionsPanel && !isShowingSlashPanel && !showClusterPanel && !showModelPanel) {
|
|
1385
1632
|
const query = sessionsSearchQuery.toLowerCase();
|
|
1386
1633
|
const filtered = sessionItems.filter((item) => {
|
|
1387
1634
|
const title = (item.title ?? "").toLowerCase();
|
|
@@ -1415,7 +1662,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1415
1662
|
return;
|
|
1416
1663
|
}
|
|
1417
1664
|
// Up/Down: prompt history when composer empty, no interactive panels
|
|
1418
|
-
const hasInteractivePanelOpen = showHelp || (slashModeActive && !slashPanelDismissed) || showAtFilePanel || showSessionsPanel || showClusterPanel;
|
|
1665
|
+
const hasInteractivePanelOpen = showHelp || (slashModeActive && !slashPanelDismissed) || showAtFilePanel || showSessionsPanel || showClusterPanel || showModelPanel;
|
|
1419
1666
|
if (!hasInteractivePanelOpen &&
|
|
1420
1667
|
composer.trim().length === 0 &&
|
|
1421
1668
|
messageQueue.length === 0 &&
|
|
@@ -1490,7 +1737,11 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1490
1737
|
return;
|
|
1491
1738
|
}
|
|
1492
1739
|
if (key.ctrl && input === "o") {
|
|
1493
|
-
setWorkflowViewMode((prev) =>
|
|
1740
|
+
setWorkflowViewMode((prev) => {
|
|
1741
|
+
const next = prev === "detailed" ? "minimal" : "detailed";
|
|
1742
|
+
void updateSetting({ workflowViewMode: next });
|
|
1743
|
+
return next;
|
|
1744
|
+
});
|
|
1494
1745
|
return;
|
|
1495
1746
|
}
|
|
1496
1747
|
if (key.ctrl && input === "x") {
|
|
@@ -1509,7 +1760,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1509
1760
|
return;
|
|
1510
1761
|
}
|
|
1511
1762
|
const hasOverlayOpen = showHelp || (slashModeActive && !slashPanelDismissed) || showAtFilePanel;
|
|
1512
|
-
const hasInteractivePanelOpen = hasOverlayOpen || showSessionsPanel || showClusterPanel;
|
|
1763
|
+
const hasInteractivePanelOpen = hasOverlayOpen || showSessionsPanel || showClusterPanel || showModelPanel;
|
|
1513
1764
|
if (hasInteractivePanelOpen) {
|
|
1514
1765
|
setShowHelp(false);
|
|
1515
1766
|
setSlashPanelDismissed(true);
|
|
@@ -1607,7 +1858,8 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1607
1858
|
const showSlashPanel = slashModeActive && !slashPanelDismissed;
|
|
1608
1859
|
const showClusterInteractivePanel = showClusterPanel && !showSlashPanel;
|
|
1609
1860
|
const showSessionsInteractivePanel = showSessionsPanel && !showSlashPanel && !showClusterInteractivePanel;
|
|
1610
|
-
const
|
|
1861
|
+
const showModelInteractivePanel = showModelPanel && !showSlashPanel && !showSessionsInteractivePanel && !showClusterInteractivePanel;
|
|
1862
|
+
const showShortcutPanel = showHelp && !showSlashPanel && !showSessionsInteractivePanel && !showClusterInteractivePanel && !showModelInteractivePanel;
|
|
1611
1863
|
const latestAssistantMessage = useMemo(() => {
|
|
1612
1864
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1613
1865
|
const message = messages[index];
|
|
@@ -1645,7 +1897,10 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1645
1897
|
const toolName = typeof latestCall.details?.name === "string" && latestCall.details.name.trim().length > 0
|
|
1646
1898
|
? latestCall.details.name
|
|
1647
1899
|
: "tool";
|
|
1648
|
-
|
|
1900
|
+
// Only show thought as detail if it's the same or very close timestamp (part of the same logical step)
|
|
1901
|
+
const thoughtDetail = latestThought && latestThought.ts >= latestCall.ts - 1000
|
|
1902
|
+
? extractThoughtTitle(latestThought.content)
|
|
1903
|
+
: undefined;
|
|
1649
1904
|
return {
|
|
1650
1905
|
text: toolName,
|
|
1651
1906
|
detail: thoughtDetail,
|
|
@@ -1693,6 +1948,10 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1693
1948
|
label: `${c.name} ${c.status}${c.region ? ` · ${c.region}` : ""}${c.cluster_id !== c.name ? ` · ${c.cluster_id}` : ""}`,
|
|
1694
1949
|
value: c.cluster_id,
|
|
1695
1950
|
})), [clusters]);
|
|
1951
|
+
const modelSelectOptions = useMemo(() => availableModels.map((m) => ({
|
|
1952
|
+
label: `${m.default ? "●" : " "} ${m.display_name} ${m.thinking_supported ? "·thinking" : ""}${m.experimental ? " ·experimental" : ""}`,
|
|
1953
|
+
value: m.id,
|
|
1954
|
+
})), [availableModels]);
|
|
1696
1955
|
const sessionSelectOptions = useMemo(() => {
|
|
1697
1956
|
const query = sessionsSearchQuery.toLowerCase();
|
|
1698
1957
|
return sessionItems
|
|
@@ -1727,8 +1986,10 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1727
1986
|
? "running"
|
|
1728
1987
|
: status.includes("auth") || status.includes("session")
|
|
1729
1988
|
? status
|
|
1730
|
-
:
|
|
1731
|
-
|
|
1989
|
+
: currentSessionModel
|
|
1990
|
+
? `model: ${currentSessionModel.displayName}`
|
|
1991
|
+
: "";
|
|
1992
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showSetupSplash ? (_jsx(SplashScreen, { agentName: agentName, cwd: cwd, whatsNew: whatsNew, onActionSelect: handleSplashAction, selectDisabled: splashSelectionMade })) : null, showTrustDisclaimer ? (_jsx(TrustDisclaimer, { folderPath: cwd, onActionSelect: handleTrustDisclaimer })) : null, showDashboard && !showTrustDisclaimer ? (_jsxs(_Fragment, { children: [_jsx(DashboardPanel, { user: activeUser, agentName: agentName, cwd: cwd, recentSessions: recentSessions, selectedCluster: selectedCluster }), updateVersion && (_jsx(Box, { marginTop: 1, paddingX: 3, children: _jsxs(Box, { paddingX: 1, borderStyle: "round", borderColor: "yellow", children: [_jsx(Text, { color: "yellow", bold: true, children: "Update Available!" }), _jsxs(Text, { children: [" v", updateVersion, " (current: v", VERSION, ")"] }), _jsx(Text, { dimColor: true, children: " \u00B7 run " }), _jsx(Text, { color: "cyan", children: "npm i -g @rubixkube/rubix" })] }) }))] })) : null, showTranscript ? (_jsx(Box, { marginTop: 1, children: _jsx(ChatTranscript, { messages: messages, workflowViewMode: workflowViewMode }) })) : null, !showTrustDisclaimer ? (_jsx(Box, { marginTop: 1, paddingX: 1, gap: 1, minHeight: 1, children: liveActivity ? (_jsxs(_Fragment, { children: [liveActivity.spinning ? _jsx(Spinner, {}) : _jsx(Text, { color: liveActivity.color ?? "red", children: "!" }), _jsx(Text, { color: liveActivity.color ?? RUBIX_THEME.colors.assistantText, children: liveActivity.text }), liveActivity.detail ? _jsxs(Text, { dimColor: true, children: [" ", liveActivity.detail] }) : null] })) : (_jsx(Text, { dimColor: true, children: " " })) })) : null, lastError ? (_jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(StatusMessage, { variant: lastError === "Conversation paused." ? "info" : "error", children: lastError }) })) : null, !showSetupSplash && !showTrustDisclaimer ? (_jsx(Box, { marginTop: 1, children: _jsx(Composer, { value: composer, resetToken: composerResetToken, disabled: isCommandRunning || isAuthLoading || !!pendingClusterSwitch || showTrustDisclaimer, placeholder: isStreaming ? "Type to queue (Enter to add)" : "What would you like to do today?", shellMode: shellMode, captureArrowKeys: showAtFilePanel, onChange: handleComposerChange, onSubmit: submitComposer, suggestions: composerSuggestions, busy: composerStatusBusy, rightStatus: composerRightStatus, suggestion: slashModeActive
|
|
1732
1993
|
? selectedSlashCandidate
|
|
1733
1994
|
? `${selectedSlashCandidate.name} ${selectedSlashCandidate.description}`
|
|
1734
1995
|
: "No matching command"
|
|
@@ -1738,7 +1999,7 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1738
1999
|
? messageQueue.length > 0
|
|
1739
2000
|
? `Enter to queue · ${messageQueue.length} queued · ↑ to edit`
|
|
1740
2001
|
: "Enter to queue"
|
|
1741
|
-
: "Shift+Enter new line · / for commands, @ for files, ! for shell" }) })) : null, showSlashPanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [visibleSlashCandidates.length === 0 ? (_jsx(Text, { dimColor: true, children: "No matching command" })) : (visibleSlashCandidates.map((item, index) => (_jsxs(Text, { children: [_jsx(Text, { color: index === slashSelectedIndex ? RUBIX_THEME.colors.brand : undefined, children: index === slashSelectedIndex ? "› " : " " }), _jsx(Text, { bold: index === slashSelectedIndex, color: index === slashSelectedIndex ? RUBIX_THEME.colors.brand : undefined, children: item.name }), _jsxs(Text, { dimColor: true, children: [" ", item.description] })] }, item.name)))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u2022 Enter select \u2022 Tab autocomplete" })] })) : null, showAtFilePanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, children: ["Add file context", atFileCurrentDir !== cwd ? ` · ${path.relative(cwd, atFileCurrentDir)}/` : "", atFileQuery ? ` · filter: "${atFileQuery}"` : ""] }), atFileFiltered.length === 0 ? (_jsx(Text, { dimColor: true, children: atFileQuery ? "No matching files" : "No files in directory" })) : ((() => {
|
|
2002
|
+
: "Shift+Enter new line · / for commands, @ for files, '! ' for shell" }) })) : null, showSlashPanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [visibleSlashCandidates.length === 0 ? (_jsx(Text, { dimColor: true, children: "No matching command" })) : (visibleSlashCandidates.map((item, index) => (_jsxs(Text, { children: [_jsx(Text, { color: index === slashSelectedIndex ? RUBIX_THEME.colors.brand : undefined, children: index === slashSelectedIndex ? "› " : " " }), _jsx(Text, { bold: index === slashSelectedIndex, color: index === slashSelectedIndex ? RUBIX_THEME.colors.brand : undefined, children: item.name }), _jsxs(Text, { dimColor: true, children: [" ", item.description] })] }, item.name)))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u2022 Enter select \u2022 Tab autocomplete" })] })) : null, showAtFilePanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, children: ["Add file context", atFileCurrentDir !== cwd ? ` · ${path.relative(cwd, atFileCurrentDir)}/` : "", atFileQuery ? ` · filter: "${atFileQuery}"` : ""] }), atFileFiltered.length === 0 ? (_jsx(Text, { dimColor: true, children: atFileQuery ? "No matching files" : "No files in directory" })) : ((() => {
|
|
1742
2003
|
const visibleCount = 7;
|
|
1743
2004
|
const start = Math.max(0, Math.min(atFileSelectedIndex - 6, atFileFiltered.length - visibleCount));
|
|
1744
2005
|
return atFileFiltered.slice(start, start + visibleCount).map((name, i) => {
|
|
@@ -1762,5 +2023,10 @@ export function App({ initialSessionId, seedPrompt }) {
|
|
|
1762
2023
|
return;
|
|
1763
2024
|
setShowClusterPanel(false);
|
|
1764
2025
|
setPendingClusterSwitch(cluster);
|
|
1765
|
-
} })), _jsxs(Text, { dimColor: true, children: [clusters.length, " cluster", clusters.length === 1 ? "" : "s", " \u00B7 \u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc close"] })] })) : null,
|
|
2026
|
+
} })), _jsxs(Text, { dimColor: true, children: [clusters.length, " cluster", clusters.length === 1 ? "" : "s", " \u00B7 \u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc close"] })] })) : null, showModelInteractivePanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, children: ["Models", currentSessionModel ? ` · active: ${currentSessionModel.displayName}` : ""] }), modelPanelLoading ? (_jsx(Spinner, { label: "loading models..." })) : modelPanelError ? (_jsxs(Text, { color: "red", children: ["Failed to load models: ", modelPanelError] })) : availableModels.length === 0 ? (_jsx(Text, { dimColor: true, children: "No models available." })) : (_jsx(Select, { options: modelSelectOptions, visibleOptionCount: 7, onChange: (value) => {
|
|
2027
|
+
setShowModelPanel(false);
|
|
2028
|
+
handleModelChange(value);
|
|
2029
|
+
} })), _jsxs(Text, { dimColor: true, children: [availableModels.length, " model", availableModels.length === 1 ? "" : "s", " \u00B7 \u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc close"] })] })) : null, showAgentPanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, children: ["Agents", " · active: " + currentAgent] }), agentPanelLoading ? (_jsx(Spinner, { label: "loading agents..." })) : agentPanelError ? (_jsxs(Text, { color: "red", children: ["Failed to load agents: ", agentPanelError] })) : availableAgents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No agents available." })) : (_jsx(Select, { options: availableAgents.map((a) => ({ label: a, value: a })), visibleOptionCount: 7, onChange: (value) => {
|
|
2030
|
+
void handleAgentChange(value);
|
|
2031
|
+
} })), _jsxs(Text, { dimColor: true, children: [availableAgents.length, " agent", availableAgents.length === 1 ? "" : "s", " \u00B7 \u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc close"] })] })) : null, showShortcutPanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: "Shortcuts" }), SHORTCUT_ROWS.map((row) => (_jsxs(Text, { dimColor: true, children: [row.key.padEnd(24), " ", row.action] }, row.key))), _jsx(Text, { dimColor: true, children: "/login /logout /status /resume /new /cluster /models /paste /send-shell-output /clear /rename /console /docs /help /exit /quit" })] })) : null] }));
|
|
1766
2032
|
}
|