@rubixkube/rubix 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/App.js CHANGED
@@ -8,12 +8,12 @@ import { Select, Spinner, StatusMessage } from "@inkjs/ui";
8
8
  import { clearAuthConfig, loadAuthConfig, saveAuthConfig } from "../core/auth-store.js";
9
9
  import { authenticateWithDeviceFlow, isTokenNearExpiry } from "../core/device-auth.js";
10
10
  import { isFolderTrusted, trustFolder, untrustFolder } from "../core/trust-store.js";
11
- import { clearLocalSessions, loadLocalSessions, saveLocalSessions } from "../core/session-store.js";
11
+ import { clearLocalSessions, saveLocalSessions } from "../core/session-store.js";
12
12
  import { loadSettings, saveSettings } from "../core/settings.js";
13
13
  import { loadWhatsNew } from "../core/whats-new.js";
14
14
  import { checkForUpdate } from "../core/update-check.js";
15
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";
16
+ import { createSession, fetchChatHistory, fetchSystemStats, firstHealthyEnvironment, getOrCreateSession, listEnvironments, listModels, listSessions, listApps, refreshAndUpdateAuth, streamChat, StreamError, updateSessionState, } from "../core/rubix-api.js";
17
17
  import { readFileContext, fetchUrlContext, formatContextBlock, } from "../core/file-context.js";
18
18
  import { ChatTranscript } from "./components/ChatTranscript.js";
19
19
  import { Composer } from "./components/Composer.js";
@@ -21,7 +21,7 @@ import { DashboardPanel } from "./components/DashboardPanel.js";
21
21
  import { SplashScreen } from "./components/SplashScreen.js";
22
22
  import { TrustDisclaimer } from "./components/TrustDisclaimer.js";
23
23
  import { getConfig } from "../config/env.js";
24
- import { RUBIX_THEME } from "./theme.js";
24
+ import { compactSessionId, RUBIX_THEME } from "./theme.js";
25
25
  import { useBracketedPaste } from "./hooks/useBracketedPaste.js";
26
26
  // Auth | Session | Input | Navigation | Help
27
27
  const SLASH_COMMANDS = [
@@ -30,11 +30,11 @@ const SLASH_COMMANDS = [
30
30
  { name: "/status", description: "Show auth and session status" },
31
31
  { name: "/resume", description: "Resume a previous conversation" },
32
32
  { name: "/new", description: "Start a fresh conversation" },
33
- { name: "/cluster", description: "Switch active cluster" },
33
+ { name: "/environments", description: "Switch active environment" },
34
34
  { name: "/models", description: "Switch AI model for this session" },
35
35
  { name: "/agents", description: "Switch agent (app) for new sessions" },
36
36
  { name: "/paste", description: "Insert clipboard content (avoids terminal paste truncation)" },
37
- { name: "/send-shell-output", description: "Send last shell command output to Rubix" },
37
+ { name: "/send", description: "Send last shell output to Rubix" },
38
38
  { name: "/clear", description: "Clear current conversation history" },
39
39
  { name: "/rename", description: "Rename current session" },
40
40
  { name: "/console", description: "Open web dashboard in browser" },
@@ -61,6 +61,21 @@ const SHORTCUT_ROWS = [
61
61
  const SESSION_PAGE_SIZE = 100;
62
62
  const SESSION_MAX_PAGES = 10;
63
63
  const STREAM_THROTTLE_MS = 80;
64
+ const GOODBYE_MESSAGES = [
65
+ "◈ Watching.",
66
+ "◈ Rubix continues monitoring in the cloud.",
67
+ "◈ Bye.",
68
+ "◈ See you.",
69
+ "◈ Stay reliable.",
70
+ "◈ Catch you later.",
71
+ "◈ Laters.",
72
+ "◈ Ta.",
73
+ "◈ Take care.",
74
+ "◈ Until next time.",
75
+ "◈ Adios.",
76
+ "◈ Cheers.",
77
+ "◈ Out.",
78
+ ];
64
79
  const IDLE_ACTIVITY_WORDS = [
65
80
  "Boogieing",
66
81
  "Booping",
@@ -183,8 +198,15 @@ function formatTimeAgo(isoString) {
183
198
  }
184
199
  export function App({ initialSessionId, seedPrompt }) {
185
200
  const { exit } = useApp();
201
+ const gracefulExit = useCallback(() => {
202
+ if (typeof process.stdout?.write === "function") {
203
+ const msg = GOODBYE_MESSAGES[Math.floor(Math.random() * GOODBYE_MESSAGES.length)];
204
+ process.stdout.write(`\n \x1b[2m${msg}\x1b[0m\n\n`);
205
+ }
206
+ exit();
207
+ }, [exit]);
186
208
  const cwd = useMemo(() => process.cwd(), []);
187
- const agentName = useMemo(() => process.env.RUBIX_AGENT_NAME ?? "Rubix Agent", []);
209
+ const agentName = useMemo(() => process.env.RUBIX_AGENT_NAME ?? "Rubix", []);
188
210
  const [authConfig, setAuthConfig] = useState(null);
189
211
  const [sessionId, setSessionId] = useState(initialSessionId ?? null);
190
212
  const [status, setStatus] = useState("booting");
@@ -211,14 +233,15 @@ export function App({ initialSessionId, seedPrompt }) {
211
233
  const [composerResetToken, setComposerResetToken] = useState(0);
212
234
  const [lastError, setLastError] = useState(null);
213
235
  const [escClearArmed, setEscClearArmed] = useState(false);
236
+ const [ctrlCArmed, setCtrlCArmed] = useState(false);
214
237
  const [messages, setMessages] = useState([]);
215
238
  const [recentActivity, setRecentActivity] = useState([]);
216
- const [clusters, setClusters] = useState([]);
217
- const [selectedCluster, setSelectedCluster] = useState(null);
218
- const [showClusterPanel, setShowClusterPanel] = useState(false);
219
- const [clusterPanelLoading, setClusterPanelLoading] = useState(false);
220
- const [clusterPanelError, setClusterPanelError] = useState(null);
221
- const [pendingClusterSwitch, setPendingClusterSwitch] = useState(null);
239
+ const [environments, setEnvironments] = useState([]);
240
+ const [selectedEnvironment, setSelectedEnvironment] = useState(null);
241
+ const [showEnvironmentPanel, setShowEnvironmentPanel] = useState(false);
242
+ const [environmentPanelLoading, setEnvironmentPanelLoading] = useState(false);
243
+ const [environmentPanelError, setEnvironmentPanelError] = useState(null);
244
+ const [pendingEnvironmentSwitch, setPendingEnvironmentSwitch] = useState(null);
222
245
  const [availableModels, setAvailableModels] = useState([]);
223
246
  const [currentSessionModel, setCurrentSessionModel] = useState(null);
224
247
  const [showModelPanel, setShowModelPanel] = useState(false);
@@ -237,6 +260,7 @@ export function App({ initialSessionId, seedPrompt }) {
237
260
  const [atFileCurrentDir, setAtFileCurrentDir] = useState(cwd);
238
261
  const [atFileList, setAtFileList] = useState([]);
239
262
  const [atFileSelectedIndex, setAtFileSelectedIndex] = useState(0);
263
+ const [systemStats, setSystemStats] = useState(null);
240
264
  const [shellMode, setShellMode] = useState(false);
241
265
  const [workflowViewMode, setWorkflowViewMode] = useState("minimal");
242
266
  const [whatsNew] = useState(() => loadWhatsNew(VERSION));
@@ -272,7 +296,7 @@ export function App({ initialSessionId, seedPrompt }) {
272
296
  setWorkflowViewMode(cfg.workflowViewMode);
273
297
  if (cfg.agentId)
274
298
  setCurrentAgent(cfg.agentId);
275
- // model and cluster will be applied after their lists are fetched
299
+ // model and environment will be applied after their lists are fetched
276
300
  // Check for updates
277
301
  checkForUpdate(VERSION, cfg.lastUpdateCheck)
278
302
  .then((newVer) => {
@@ -430,19 +454,19 @@ export function App({ initialSessionId, seedPrompt }) {
430
454
  setSlashSelectedIndex(0);
431
455
  }, [visibleSlashCandidates]);
432
456
  useEffect(() => {
433
- if (slashModeActive && (showSessionsPanel || showClusterPanel || showModelPanel)) {
457
+ if (slashModeActive && (showSessionsPanel || showEnvironmentPanel || showModelPanel)) {
434
458
  setShowSessionsPanel(false);
435
- setShowClusterPanel(false);
459
+ setShowEnvironmentPanel(false);
436
460
  setShowModelPanel(false);
437
461
  }
438
- }, [showSessionsPanel, showClusterPanel, showModelPanel, slashModeActive]);
462
+ }, [showSessionsPanel, showEnvironmentPanel, showModelPanel, slashModeActive]);
439
463
  useEffect(() => {
440
- if (atModeActive && (showSessionsPanel || showClusterPanel || showModelPanel)) {
464
+ if (atModeActive && (showSessionsPanel || showEnvironmentPanel || showModelPanel)) {
441
465
  setShowSessionsPanel(false);
442
- setShowClusterPanel(false);
466
+ setShowEnvironmentPanel(false);
443
467
  setShowModelPanel(false);
444
468
  }
445
- }, [atModeActive, showSessionsPanel, showClusterPanel, showModelPanel]);
469
+ }, [atModeActive, showSessionsPanel, showEnvironmentPanel, showModelPanel]);
446
470
  const prevShowSetupSplash = useRef(showSetupSplash);
447
471
  useEffect(() => {
448
472
  if (showSetupSplash && !prevShowSetupSplash.current) {
@@ -490,15 +514,15 @@ export function App({ initialSessionId, seedPrompt }) {
490
514
  setLastError(message);
491
515
  addSystemMessage(message);
492
516
  }, [addSystemMessage, updateAssistantMessage]);
493
- const ensureSession = useCallback(async (config, preferredId, clusterIdOverride) => {
494
- const clusterId = clusterIdOverride ?? selectedCluster?.cluster_id;
495
- const resolved = await getOrCreateSession(config, preferredId, clusterId, currentAgent);
517
+ const ensureSession = useCallback(async (config, preferredId, environmentIdOverride) => {
518
+ const envId = environmentIdOverride ?? selectedEnvironment?.environment_id;
519
+ const resolved = await getOrCreateSession(config, preferredId, envId, currentAgent);
496
520
  setSessionId(resolved);
497
521
  return resolved;
498
- }, [selectedCluster?.cluster_id, currentAgent]);
522
+ }, [selectedEnvironment?.environment_id, currentAgent]);
499
523
  const openSessionsPanel = useCallback(async () => {
500
524
  if (!authConfig || !isAuthenticated) {
501
- addSystemMessage("Not authenticated. Run /login first.");
525
+ addSystemMessage("Not logged in. /login to connect.");
502
526
  return;
503
527
  }
504
528
  setShowHelp(false);
@@ -540,63 +564,63 @@ export function App({ initialSessionId, seedPrompt }) {
540
564
  setSessionsPanelLoading(false);
541
565
  }
542
566
  }, [addSystemMessage, authConfig, isAuthenticated]);
543
- const openClusterPanel = useCallback(async () => {
567
+ const openEnvironmentPanel = useCallback(async () => {
544
568
  if (!authConfig || !isAuthenticated) {
545
- addSystemMessage("Not authenticated. Run /login first.");
569
+ addSystemMessage("Not logged in. /login to connect.");
546
570
  return;
547
571
  }
548
572
  setShowHelp(false);
549
573
  setShowSessionsPanel(false);
550
- setShowClusterPanel(true);
551
- setClusterPanelLoading(true);
552
- setClusterPanelError(null);
574
+ setShowEnvironmentPanel(true);
575
+ setEnvironmentPanelLoading(true);
576
+ setEnvironmentPanelError(null);
553
577
  try {
554
- const loaded = await listClusters(authConfig);
555
- setClusters(loaded);
556
- setClusterPanelError(null);
578
+ const loaded = await listEnvironments(authConfig);
579
+ setEnvironments(loaded);
580
+ setEnvironmentPanelError(null);
557
581
  }
558
582
  catch (error) {
559
- setClusterPanelError(error instanceof Error ? error.message : String(error));
583
+ setEnvironmentPanelError(error instanceof Error ? error.message : String(error));
560
584
  }
561
585
  finally {
562
- setClusterPanelLoading(false);
586
+ setEnvironmentPanelLoading(false);
563
587
  }
564
588
  }, [authConfig, isAuthenticated, addSystemMessage]);
565
- const confirmClusterSwitch = useCallback(async () => {
566
- if (!pendingClusterSwitch || !authConfig)
589
+ const confirmEnvironmentSwitch = useCallback(async () => {
590
+ if (!pendingEnvironmentSwitch || !authConfig)
567
591
  return;
568
- const cluster = pendingClusterSwitch;
569
- setPendingClusterSwitch(null);
570
- setSelectedCluster(cluster);
571
- void updateSetting({ clusterId: cluster.cluster_id });
592
+ const env = pendingEnvironmentSwitch;
593
+ setPendingEnvironmentSwitch(null);
594
+ setSelectedEnvironment(env);
595
+ void updateSetting({ environmentId: env.environment_id });
572
596
  setStatus("creating session");
573
597
  try {
574
- const nextSession = await createSession(authConfig, currentAgent, cluster.cluster_id, currentSessionModel?.modelId);
598
+ const nextSession = await createSession(authConfig, currentAgent, env.environment_id, currentSessionModel?.modelId);
575
599
  setSessionId(nextSession);
576
600
  setSessionTitle(null);
577
601
  const latestSessions = await listSessions(authConfig, 20, 0).catch(() => []);
578
602
  setRecentSessions(latestSessions.slice(0, 2));
579
603
  setMessages([]);
580
604
  setStatus("ready");
581
- addSystemMessage(`Switched to cluster ${cluster.name}. Started new session.`);
605
+ addSystemMessage(`Now on ${env.name}. New session started.`);
582
606
  }
583
607
  catch (error) {
584
608
  setStatus("ready");
585
- addSystemMessage(`Failed to start session for cluster: ${error instanceof Error ? error.message : String(error)}`);
609
+ addSystemMessage(`Couldn't start session for environment: ${error instanceof Error ? error.message : String(error)}`);
586
610
  }
587
- }, [pendingClusterSwitch, authConfig, addSystemMessage, currentAgent, currentSessionModel]);
611
+ }, [pendingEnvironmentSwitch, authConfig, addSystemMessage, currentAgent, currentSessionModel]);
588
612
  const openModelPanel = useCallback(async () => {
589
613
  if (!authConfig || !isAuthenticated) {
590
- addSystemMessage("Not authenticated. Run /login first.");
614
+ addSystemMessage("Not logged in. /login to connect.");
591
615
  return;
592
616
  }
593
617
  if (!sessionId) {
594
- addSystemMessage("No active session. Use /new or /resume to start a conversation first.");
618
+ addSystemMessage("No active session. /new or /resume to start.");
595
619
  return;
596
620
  }
597
621
  setShowHelp(false);
598
622
  setShowSessionsPanel(false);
599
- setShowClusterPanel(false);
623
+ setShowEnvironmentPanel(false);
600
624
  setShowModelPanel(true);
601
625
  setModelPanelLoading(true);
602
626
  setModelPanelError(null);
@@ -615,7 +639,7 @@ export function App({ initialSessionId, seedPrompt }) {
615
639
  }, [authConfig, isAuthenticated, sessionId, addSystemMessage]);
616
640
  const handleModelChange = useCallback((modelId) => {
617
641
  if (!sessionId) {
618
- addSystemMessage("No active session. Cannot switch model.");
642
+ addSystemMessage("No active session. Can't switch model.");
619
643
  return;
620
644
  }
621
645
  const selected = availableModels.find((m) => m.id === modelId);
@@ -636,12 +660,12 @@ export function App({ initialSessionId, seedPrompt }) {
636
660
  }, [sessionId, availableModels, addSystemMessage]);
637
661
  const openAgentPanel = useCallback(async () => {
638
662
  if (!authConfig || !isAuthenticated) {
639
- addSystemMessage("Not authenticated. Run /login first.");
663
+ addSystemMessage("Not logged in. /login to connect.");
640
664
  return;
641
665
  }
642
666
  setShowHelp(false);
643
667
  setShowSessionsPanel(false);
644
- setShowClusterPanel(false);
668
+ setShowEnvironmentPanel(false);
645
669
  setShowModelPanel(false);
646
670
  setShowAgentPanel(true);
647
671
  setAgentPanelLoading(true);
@@ -663,7 +687,7 @@ export function App({ initialSessionId, seedPrompt }) {
663
687
  setShowAgentPanel(false);
664
688
  setCurrentAgent(agent);
665
689
  void updateSetting({ agentId: agent });
666
- addSystemMessage(`Switched agent to: ${agent}. Starting new session.`);
690
+ addSystemMessage(`Switched to ${agent}. New session starting.`);
667
691
  setSessionId(null);
668
692
  setSessionTitle(null);
669
693
  setMessages([]);
@@ -671,7 +695,7 @@ export function App({ initialSessionId, seedPrompt }) {
671
695
  return;
672
696
  try {
673
697
  setStatus("creating session");
674
- const nextSession = await createSession(authConfig, agent, selectedCluster?.cluster_id, currentSessionModel?.modelId);
698
+ const nextSession = await createSession(authConfig, agent, selectedEnvironment?.environment_id, currentSessionModel?.modelId);
675
699
  setSessionId(nextSession);
676
700
  const latestSessions = await listSessions(authConfig, 20, 0).catch(() => []);
677
701
  setRecentSessions(latestSessions.slice(0, 2));
@@ -681,7 +705,7 @@ export function App({ initialSessionId, seedPrompt }) {
681
705
  setStatus("ready");
682
706
  addSystemMessage(`Failed to create session with agent ${agent}: ` + String(err));
683
707
  }
684
- }, [authConfig, selectedCluster, currentSessionModel, addSystemMessage]);
708
+ }, [authConfig, selectedEnvironment, currentSessionModel, addSystemMessage]);
685
709
  const activateSessionById = useCallback((selectedId) => {
686
710
  const selected = sessionItems.find((item) => item.id === selectedId);
687
711
  if (!selected)
@@ -699,16 +723,14 @@ export function App({ initialSessionId, seedPrompt }) {
699
723
  .then((history) => {
700
724
  setMessages(history.length > 0 ? history : []);
701
725
  setStatus("ready");
702
- addSystemMessage(`Session ${sessionLabel(selected.id)}${selected.title ? ` — ${selected.title}` : ""}${history.length > 0 ? ` · ${history.length} messages loaded` : ""}`);
703
726
  })
704
727
  .catch((err) => {
705
728
  setStatus("ready");
706
- addSystemMessage(`Switched to session ${sessionLabel(selected.id)} (history unavailable: ${err instanceof Error ? err.message : String(err)})`);
729
+ addSystemMessage(`History unavailable: ${err instanceof Error ? err.message : String(err)}`);
707
730
  });
708
731
  }
709
732
  else {
710
733
  setStatus("ready");
711
- addSystemMessage(`Switched to session ${sessionLabel(selected.id)}${selected.title ? ` — ${selected.title}` : ""}`);
712
734
  }
713
735
  }, [addSystemMessage, authConfig, sessionItems]);
714
736
  useEffect(() => {
@@ -739,18 +761,13 @@ export function App({ initialSessionId, seedPrompt }) {
739
761
  setIsAuthenticated(loggedIn);
740
762
  setActiveUser(cfg?.userName ?? cfg?.userEmail ?? null);
741
763
  if (loggedIn && cfg) {
742
- setStatus("loading clusters");
764
+ setStatus("loading environments");
743
765
  try {
744
- const localSess = await loadLocalSessions().catch(() => []);
745
- if (!cancelled && localSess.length > 0) {
746
- setRecentSessions(localSess.slice(0, 2));
747
- setSessionItems(localSess);
748
- }
749
- // Load clusters and recent sessions for dashboard display
750
- const [clusterList, recentList, appList] = await Promise.all([
751
- listClusters(cfg).catch((err) => {
766
+ // Don't pre-populate from local cache — wait for API so Recent is never stale
767
+ const [envList, recentList, appList, stats] = await Promise.all([
768
+ listEnvironments(cfg).catch((err) => {
752
769
  if (!cancelled) {
753
- addSystemMessage(`Could not load clusters: ${err instanceof Error ? err.message : String(err)}`);
770
+ addSystemMessage(`Could not load environments: ${err instanceof Error ? err.message : String(err)}`);
754
771
  }
755
772
  return [];
756
773
  }),
@@ -761,29 +778,30 @@ export function App({ initialSessionId, seedPrompt }) {
761
778
  return [];
762
779
  }),
763
780
  listApps(cfg).catch(() => []),
781
+ fetchSystemStats(cfg),
764
782
  ]);
765
- const firstCluster = firstHealthyCluster(clusterList);
783
+ const firstEnv = firstHealthyEnvironment(envList);
766
784
  if (!cancelled) {
767
785
  setRecentSessions(recentList.slice(0, 2));
768
786
  setSessionItems(recentList);
769
787
  void saveLocalSessions(recentList);
770
788
  if (appList && appList.length > 0) {
771
789
  setAvailableAgents(appList);
772
- // Prefer saved agentId from settings, then SRI Agent, then first available
773
790
  const saved = currentSettingsRef.current;
774
791
  const savedAgent = saved.agentId && appList.includes(saved.agentId) ? saved.agentId : null;
775
792
  const preferApp = savedAgent ?? (appList.includes("SRI Agent") ? "SRI Agent" : appList[0]);
776
793
  if (preferApp)
777
794
  setCurrentAgent(preferApp);
778
795
  }
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;
796
+ const savedEnvId = currentSettingsRef.current.environmentId;
797
+ const preferredEnv = savedEnvId
798
+ ? envList.find((c) => c.environment_id === savedEnvId) ?? firstEnv
799
+ : firstEnv;
784
800
  if (!cancelled) {
785
- setClusters(clusterList);
786
- setSelectedCluster(preferredCluster);
801
+ setEnvironments(envList);
802
+ setSelectedEnvironment(preferredEnv);
803
+ if (stats)
804
+ setSystemStats(stats);
787
805
  }
788
806
  setStatus("ready");
789
807
  }
@@ -791,7 +809,7 @@ export function App({ initialSessionId, seedPrompt }) {
791
809
  catch (error) {
792
810
  if (!cancelled) {
793
811
  setStatus("ready");
794
- addSystemMessage(`Failed to load clusters: ${error instanceof Error ? error.message : String(error)}`);
812
+ addSystemMessage(`Failed to load environments: ${error instanceof Error ? error.message : String(error)}`);
795
813
  }
796
814
  }
797
815
  }
@@ -819,6 +837,25 @@ export function App({ initialSessionId, seedPrompt }) {
819
837
  cancelled = true;
820
838
  };
821
839
  }, [addSystemMessage, ensureSession, initialSessionId]);
840
+ // Fetch sessions when dashboard is first shown (ensures Recent has fresh API data)
841
+ const hasFetchedSessionsForDashboard = useRef(false);
842
+ useEffect(() => {
843
+ if (!authConfig || !isAuthenticated || !showDashboard || showTrustDisclaimer || messages.length > 0)
844
+ return;
845
+ if (hasFetchedSessionsForDashboard.current)
846
+ return;
847
+ let cancelled = false;
848
+ hasFetchedSessionsForDashboard.current = true;
849
+ listSessions(authConfig, 20, 0)
850
+ .then((list) => {
851
+ if (!cancelled) {
852
+ setRecentSessions(list.slice(0, 2));
853
+ void saveLocalSessions(list.slice(0, 20));
854
+ }
855
+ })
856
+ .catch(() => { });
857
+ return () => { cancelled = true; };
858
+ }, [authConfig, isAuthenticated, showDashboard, showTrustDisclaimer, messages.length]);
822
859
  // Check folder trust status after authentication
823
860
  useEffect(() => {
824
861
  if (!isAuthenticated || folderTrustCheckComplete)
@@ -903,13 +940,13 @@ export function App({ initialSessionId, seedPrompt }) {
903
940
  return;
904
941
  case "/exit":
905
942
  case "/quit":
906
- exit();
943
+ gracefulExit();
907
944
  return;
908
945
  case "/status": {
909
946
  const workspace = cwd.replace(process.env.HOME ?? "", "~");
910
- const clusterStr = selectedCluster
911
- ? `${selectedCluster.name} (${selectedCluster.status})`
912
- : "no cluster selected";
947
+ const envStr = selectedEnvironment
948
+ ? `${selectedEnvironment.name} (${selectedEnvironment.status})`
949
+ : "none selected";
913
950
  const modelStr = currentSessionModel
914
951
  ? `${currentSessionModel.displayName}${currentSessionModel.thinkingSupported ? " (thinking supported)" : ""}`
915
952
  : "default";
@@ -918,7 +955,7 @@ export function App({ initialSessionId, seedPrompt }) {
918
955
  `Workspace: ${workspace}`,
919
956
  `Auth: ${isAuthenticated ? `logged in as ${activeUser ?? "unknown user"}` : "not logged in"}`,
920
957
  `Agent: ${currentAgent}`,
921
- `Cluster: ${clusterStr}`,
958
+ `Environment: ${envStr}`,
922
959
  `Model: ${modelStr}`,
923
960
  `Session: ${sessionId ?? "no session"}`,
924
961
  `State: ${status}`,
@@ -938,29 +975,29 @@ export function App({ initialSessionId, seedPrompt }) {
938
975
  setSessionTitle(null);
939
976
  setStatus("setup");
940
977
  setMessages([]);
941
- addSystemMessage("Logged out. Type /login to set up again.");
978
+ addSystemMessage("Logged out. /login when you're ready.");
942
979
  return;
943
980
  case "/login": {
944
981
  if (isAuthenticated && authConfig) {
945
- addSystemMessage(`Already logged in as ${activeUser ?? "user"}. Type /logout first to switch accounts.`);
982
+ addSystemMessage(`Already in as ${activeUser ?? "user"}. /logout to switch.`);
946
983
  return;
947
984
  }
948
985
  setStatus("auth");
949
- addSystemMessage("Starting login flow...");
986
+ addSystemMessage("Starting login...");
950
987
  try {
951
988
  const nextAuth = await authenticateWithDeviceFlow((message) => addSystemMessage(message));
952
989
  await saveAuthConfig(nextAuth);
953
990
  setAuthConfig(nextAuth);
954
991
  setIsAuthenticated(true);
955
992
  setActiveUser(nextAuth.userName ?? nextAuth.userEmail ?? null);
956
- const clusterList = await listClusters(nextAuth).catch((err) => {
957
- addSystemMessage(`Could not load clusters: ${err instanceof Error ? err.message : String(err)}`);
993
+ const envList = await listEnvironments(nextAuth).catch((err) => {
994
+ addSystemMessage(`Could not load environments: ${err instanceof Error ? err.message : String(err)}`);
958
995
  return [];
959
996
  });
960
- const firstCluster = firstHealthyCluster(clusterList);
961
- setClusters(clusterList);
962
- setSelectedCluster(firstCluster);
963
- const resolved = await ensureSession(nextAuth, undefined, firstCluster?.cluster_id);
997
+ const firstEnv = firstHealthyEnvironment(envList);
998
+ setEnvironments(envList);
999
+ setSelectedEnvironment(firstEnv);
1000
+ const resolved = await ensureSession(nextAuth, undefined, firstEnv?.environment_id);
964
1001
  const latestSessions = await listSessions(nextAuth, 20, 0).catch((err) => {
965
1002
  addSystemMessage(`Could not load recent sessions: ${err instanceof Error ? err.message : String(err)}`);
966
1003
  return [];
@@ -972,7 +1009,6 @@ export function App({ initialSessionId, seedPrompt }) {
972
1009
  setSessionTitle(currentSession.title);
973
1010
  setStatus("ready");
974
1011
  setMessages([]);
975
- addSystemMessage(`Login successful${nextAuth.userName ? `, ${nextAuth.userName}` : ""}. Session ${sessionLabel(resolved)}.`);
976
1012
  }
977
1013
  catch (error) {
978
1014
  setStatus("setup");
@@ -986,12 +1022,12 @@ export function App({ initialSessionId, seedPrompt }) {
986
1022
  }
987
1023
  case "/new": {
988
1024
  if (!authConfig || !isAuthenticated) {
989
- addSystemMessage("Not authenticated. Run /login first.");
1025
+ addSystemMessage("Not logged in. /login to connect.");
990
1026
  return;
991
1027
  }
992
1028
  setStatus("creating session");
993
1029
  try {
994
- const nextSession = await createSession(authConfig, currentAgent, selectedCluster?.cluster_id, currentSessionModel?.modelId);
1030
+ const nextSession = await createSession(authConfig, currentAgent, selectedEnvironment?.environment_id, currentSessionModel?.modelId);
995
1031
  setSessionId(nextSession);
996
1032
  setSessionTitle(null);
997
1033
  setMessages([]);
@@ -1006,20 +1042,19 @@ export function App({ initialSessionId, seedPrompt }) {
1006
1042
  return [next, ...prev.filter((s) => s.id !== nextSession)].slice(0, 2);
1007
1043
  });
1008
1044
  setStatus("ready");
1009
- addSystemMessage(`Started new conversation ${sessionLabel(nextSession)}.`);
1010
1045
  }
1011
1046
  catch (error) {
1012
1047
  setStatus("ready");
1013
- addSystemMessage(`Failed to start session: ${error instanceof Error ? error.message : String(error)}`);
1048
+ addSystemMessage(`Couldn't start a session: ${error instanceof Error ? error.message : String(error)}`);
1014
1049
  }
1015
1050
  return;
1016
1051
  }
1017
- case "/cluster": {
1018
- await openClusterPanel();
1052
+ case "/environments": {
1053
+ await openEnvironmentPanel();
1019
1054
  return;
1020
1055
  }
1021
1056
  case "/models": {
1022
- addSystemMessage("Opening model selector...");
1057
+ addSystemMessage("Loading models...");
1023
1058
  await openModelPanel();
1024
1059
  return;
1025
1060
  }
@@ -1029,11 +1064,11 @@ export function App({ initialSessionId, seedPrompt }) {
1029
1064
  }
1030
1065
  case "/rename": {
1031
1066
  if (!authConfig || !isAuthenticated) {
1032
- addSystemMessage("Not authenticated. Run /login first.");
1067
+ addSystemMessage("Not logged in. /login to connect.");
1033
1068
  return;
1034
1069
  }
1035
1070
  if (!sessionId) {
1036
- addSystemMessage("No active session. Use /new or /resume.");
1071
+ addSystemMessage("No active session. /new or /resume to start.");
1037
1072
  return;
1038
1073
  }
1039
1074
  const rest = text.slice(rawCommand.length).trim();
@@ -1045,7 +1080,7 @@ export function App({ initialSessionId, seedPrompt }) {
1045
1080
  await updateSessionState(authConfig, sessionId, { session_title: rest });
1046
1081
  setSessionTitle(rest);
1047
1082
  setRecentSessions((prev) => prev.map((s) => (s.id === sessionId ? { ...s, title: rest } : s)));
1048
- addSystemMessage(`Session renamed to: ${rest}`);
1083
+ addSystemMessage(`Renamed: ${rest}`);
1049
1084
  }
1050
1085
  catch (error) {
1051
1086
  addSystemMessage(`Rename failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -1054,7 +1089,7 @@ export function App({ initialSessionId, seedPrompt }) {
1054
1089
  }
1055
1090
  case "/untrust": {
1056
1091
  if (!authConfig || !isAuthenticated) {
1057
- addSystemMessage("Not authenticated. Run /login first.");
1092
+ addSystemMessage("Not logged in. /login to connect.");
1058
1093
  return;
1059
1094
  }
1060
1095
  await untrustFolder(cwd);
@@ -1063,7 +1098,7 @@ export function App({ initialSessionId, seedPrompt }) {
1063
1098
  addSystemMessage("Folder untrusted. You'll be prompted to trust again next time.");
1064
1099
  return;
1065
1100
  }
1066
- case "/send-shell-output": {
1101
+ case "/send": {
1067
1102
  const last = lastShellRef.current;
1068
1103
  const streamFn = streamAssistantRef.current;
1069
1104
  if (last && authConfig && isAuthenticated && sessionId && streamFn) {
@@ -1134,7 +1169,7 @@ export function App({ initialSessionId, seedPrompt }) {
1134
1169
  return;
1135
1170
  }
1136
1171
  default:
1137
- addSystemMessage(`Unknown command: ${command}. Use /help.`);
1172
+ addSystemMessage(`Unknown: ${command} try /help`);
1138
1173
  }
1139
1174
  }, [
1140
1175
  activeUser,
@@ -1142,15 +1177,15 @@ export function App({ initialSessionId, seedPrompt }) {
1142
1177
  authConfig,
1143
1178
  cwd,
1144
1179
  ensureSession,
1145
- exit,
1180
+ gracefulExit,
1146
1181
  isAuthenticated,
1147
- openClusterPanel,
1182
+ openEnvironmentPanel,
1148
1183
  openSessionsPanel,
1149
1184
  openAgentPanel,
1150
1185
  openModelPanel,
1151
1186
  currentAgent,
1152
1187
  currentSessionModel,
1153
- selectedCluster,
1188
+ selectedEnvironment,
1154
1189
  recordActivity,
1155
1190
  resetComposer,
1156
1191
  sessionId,
@@ -1171,7 +1206,7 @@ export function App({ initialSessionId, seedPrompt }) {
1171
1206
  }, [exit, handleSlashCommand]);
1172
1207
  const streamAssistant = useCallback(async (prompt, assistantId, messageParts) => {
1173
1208
  if (!authConfig || !isAuthenticated) {
1174
- failAssistantMessage(assistantId, "Not authenticated. Run /login first.");
1209
+ failAssistantMessage(assistantId, "Not logged in. /login to connect.");
1175
1210
  return;
1176
1211
  }
1177
1212
  let resolvedSession = sessionId;
@@ -1179,11 +1214,6 @@ export function App({ initialSessionId, seedPrompt }) {
1179
1214
  try {
1180
1215
  resolvedSession = await ensureSession(authConfig);
1181
1216
  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
- }
1187
1217
  }
1188
1218
  catch (error) {
1189
1219
  failAssistantMessage(assistantId, `Failed to initialize session: ${error instanceof Error ? error.message : String(error)}`);
@@ -1271,11 +1301,6 @@ export function App({ initialSessionId, seedPrompt }) {
1271
1301
  }));
1272
1302
  },
1273
1303
  onSessionMetadata: (metadata) => {
1274
- if (metadata.title || metadata.description || metadata.category) {
1275
- const meta = [metadata.title, metadata.description, metadata.category].filter(Boolean).join(" · ");
1276
- if (meta)
1277
- addSystemMessage(`Session updated: ${meta}`);
1278
- }
1279
1304
  if (metadata.title && sessionId) {
1280
1305
  setSessionTitle(metadata.title);
1281
1306
  setRecentSessions((prev) => {
@@ -1306,15 +1331,15 @@ export function App({ initialSessionId, seedPrompt }) {
1306
1331
  if (!(result.text ?? "").trim()) {
1307
1332
  const fallback = sawWorkflowEvent
1308
1333
  ? "The agent processed your request but didn't produce a final response. Check the workflow above."
1309
- : "No response text was returned. Please try again.";
1334
+ : "Backend went quiet. Try again?";
1310
1335
  updateAssistantMessage(assistantId, (message) => ({
1311
1336
  ...message,
1312
1337
  content: message.content.trim().length > 0 ? message.content : fallback,
1313
1338
  isAccumulating: false,
1314
1339
  }));
1315
1340
  if (!sawWorkflowEvent) {
1316
- setLastError("No response text was returned by the backend.");
1317
- addSystemMessage("No response text was returned by the backend.");
1341
+ setLastError("Backend went quiet. Try again?");
1342
+ addSystemMessage("Backend went quiet. Try again?");
1318
1343
  }
1319
1344
  }
1320
1345
  }
@@ -1325,7 +1350,7 @@ export function App({ initialSessionId, seedPrompt }) {
1325
1350
  ...message,
1326
1351
  isAccumulating: false,
1327
1352
  }));
1328
- setLastError("Conversation paused.");
1353
+ setLastError("Paused.");
1329
1354
  }
1330
1355
  else {
1331
1356
  const reason = didTimeOut
@@ -1387,7 +1412,7 @@ export function App({ initialSessionId, seedPrompt }) {
1387
1412
  return;
1388
1413
  }
1389
1414
  if (!isAuthenticated) {
1390
- addSystemMessage("Setup required. Type /login.");
1415
+ addSystemMessage("Not logged in. /login to get started.");
1391
1416
  return;
1392
1417
  }
1393
1418
  if (trimmed.startsWith("@")) {
@@ -1480,7 +1505,7 @@ export function App({ initialSessionId, seedPrompt }) {
1480
1505
  const errOut = (stderr ?? "").trim();
1481
1506
  const output = err ? errOut || out || err.message : out || "(no output)";
1482
1507
  lastShellRef.current = { cmd, output };
1483
- const hint = "· /send-shell-output to send to Rubix";
1508
+ const hint = "· /send to send to Rubix";
1484
1509
  addSystemMessage(`$ ${cmd}\n${output}\n\n${hint}`);
1485
1510
  });
1486
1511
  return;
@@ -1576,6 +1601,9 @@ export function App({ initialSessionId, seedPrompt }) {
1576
1601
  if (!key.escape) {
1577
1602
  setEscClearArmed(false);
1578
1603
  }
1604
+ if (!(key.ctrl && input === "c")) {
1605
+ setCtrlCArmed(false);
1606
+ }
1579
1607
  // Arrow navigation for slash panel
1580
1608
  if (showSlashPanel && visibleSlashCandidates.length > 0) {
1581
1609
  if (key.downArrow) {
@@ -1628,7 +1656,7 @@ export function App({ initialSessionId, seedPrompt }) {
1628
1656
  }
1629
1657
  // Arrow navigation for sessions panel
1630
1658
  const isShowingSlashPanel = slashModeActive && !slashPanelDismissed;
1631
- if (showSessionsPanel && !isShowingSlashPanel && !showClusterPanel && !showModelPanel) {
1659
+ if (showSessionsPanel && !isShowingSlashPanel && !showEnvironmentPanel && !showModelPanel) {
1632
1660
  const query = sessionsSearchQuery.toLowerCase();
1633
1661
  const filtered = sessionItems.filter((item) => {
1634
1662
  const title = (item.title ?? "").toLowerCase();
@@ -1662,7 +1690,7 @@ export function App({ initialSessionId, seedPrompt }) {
1662
1690
  return;
1663
1691
  }
1664
1692
  // Up/Down: prompt history when composer empty, no interactive panels
1665
- const hasInteractivePanelOpen = showHelp || (slashModeActive && !slashPanelDismissed) || showAtFilePanel || showSessionsPanel || showClusterPanel || showModelPanel;
1693
+ const hasInteractivePanelOpen = showHelp || (slashModeActive && !slashPanelDismissed) || showAtFilePanel || showSessionsPanel || showEnvironmentPanel || showModelPanel;
1666
1694
  if (!hasInteractivePanelOpen &&
1667
1695
  composer.trim().length === 0 &&
1668
1696
  messageQueue.length === 0 &&
@@ -1686,17 +1714,17 @@ export function App({ initialSessionId, seedPrompt }) {
1686
1714
  return;
1687
1715
  }
1688
1716
  }
1689
- // Pending cluster switch: only Enter to confirm or Esc to cancel
1690
- if (pendingClusterSwitch) {
1717
+ // Pending environment switch: only Enter to confirm or Esc to cancel
1718
+ if (pendingEnvironmentSwitch) {
1691
1719
  if (key.return) {
1692
- confirmClusterSwitch().catch((error) => {
1693
- addSystemMessage(`Cluster switch failed: ${error instanceof Error ? error.message : String(error)}`);
1720
+ confirmEnvironmentSwitch().catch((error) => {
1721
+ addSystemMessage(`Environment switch failed: ${error instanceof Error ? error.message : String(error)}`);
1694
1722
  });
1695
1723
  return;
1696
1724
  }
1697
1725
  if (key.escape) {
1698
- setPendingClusterSwitch(null);
1699
- addSystemMessage("Cluster switch cancelled.");
1726
+ setPendingEnvironmentSwitch(null);
1727
+ addSystemMessage("Environment switch cancelled.");
1700
1728
  return;
1701
1729
  }
1702
1730
  return;
@@ -1712,7 +1740,7 @@ export function App({ initialSessionId, seedPrompt }) {
1712
1740
  return;
1713
1741
  }
1714
1742
  if (input === "q") {
1715
- exit();
1743
+ gracefulExit();
1716
1744
  return;
1717
1745
  }
1718
1746
  }
@@ -1720,10 +1748,16 @@ export function App({ initialSessionId, seedPrompt }) {
1720
1748
  if (isStreaming) {
1721
1749
  streamController.current?.abort();
1722
1750
  streamController.current = null;
1723
- addSystemMessage("Stream cancelled. Press Ctrl+C again to exit.");
1751
+ setCtrlCArmed(true);
1752
+ addSystemMessage("Interrupted. Ask something else, or Ctrl+C to leave.");
1724
1753
  return;
1725
1754
  }
1726
- exit();
1755
+ if (ctrlCArmed) {
1756
+ gracefulExit();
1757
+ return;
1758
+ }
1759
+ setCtrlCArmed(true);
1760
+ addSystemMessage("Press Ctrl+C again to exit.");
1727
1761
  return;
1728
1762
  }
1729
1763
  if (key.ctrl && input === "l") {
@@ -1733,7 +1767,7 @@ export function App({ initialSessionId, seedPrompt }) {
1733
1767
  return;
1734
1768
  }
1735
1769
  if (key.ctrl && input === "d" && composer.trim().length === 0) {
1736
- exit();
1770
+ gracefulExit();
1737
1771
  return;
1738
1772
  }
1739
1773
  if (key.ctrl && input === "o") {
@@ -1760,7 +1794,7 @@ export function App({ initialSessionId, seedPrompt }) {
1760
1794
  return;
1761
1795
  }
1762
1796
  const hasOverlayOpen = showHelp || (slashModeActive && !slashPanelDismissed) || showAtFilePanel;
1763
- const hasInteractivePanelOpen = hasOverlayOpen || showSessionsPanel || showClusterPanel || showModelPanel;
1797
+ const hasInteractivePanelOpen = hasOverlayOpen || showSessionsPanel || showEnvironmentPanel || showModelPanel;
1764
1798
  if (hasInteractivePanelOpen) {
1765
1799
  setShowHelp(false);
1766
1800
  setSlashPanelDismissed(true);
@@ -1768,7 +1802,7 @@ export function App({ initialSessionId, seedPrompt }) {
1768
1802
  setShowSessionsPanel(false);
1769
1803
  setSessionsSearchQuery("");
1770
1804
  setSessionsSelectedIndex(0);
1771
- setShowClusterPanel(false);
1805
+ setShowEnvironmentPanel(false);
1772
1806
  setEscClearArmed(true);
1773
1807
  return;
1774
1808
  }
@@ -1808,15 +1842,16 @@ export function App({ initialSessionId, seedPrompt }) {
1808
1842
  atFileFiltered,
1809
1843
  cwd,
1810
1844
  composer,
1811
- confirmClusterSwitch,
1845
+ confirmEnvironmentSwitch,
1846
+ ctrlCArmed,
1812
1847
  escClearArmed,
1813
- exit,
1848
+ gracefulExit,
1814
1849
  historyIndex,
1815
1850
  isStreaming,
1816
1851
  leaderMode,
1817
1852
  messageQueue,
1818
1853
  messages,
1819
- pendingClusterSwitch,
1854
+ pendingEnvironmentSwitch,
1820
1855
  promptHistory,
1821
1856
  resetComposer,
1822
1857
  selectedAtFile,
@@ -1831,17 +1866,17 @@ export function App({ initialSessionId, seedPrompt }) {
1831
1866
  setHistoryIndex,
1832
1867
  setLeaderMode,
1833
1868
  setMessages,
1834
- setPendingClusterSwitch,
1869
+ setPendingEnvironmentSwitch,
1835
1870
  setShowHelp,
1836
1871
  setSlashPanelDismissed,
1837
1872
  setSlashSelectedIndex,
1838
1873
  setSessionsSelectedIndex,
1839
- setShowClusterPanel,
1874
+ setShowEnvironmentPanel,
1840
1875
  setShowSessionsPanel,
1841
1876
  setWorkflowExpandedIds,
1842
1877
  showAtFilePanel,
1843
1878
  showHelp,
1844
- showClusterPanel,
1879
+ showEnvironmentPanel,
1845
1880
  showSessionsPanel,
1846
1881
  shellMode,
1847
1882
  slashModeActive,
@@ -1856,10 +1891,10 @@ export function App({ initialSessionId, seedPrompt }) {
1856
1891
  return !showDashboard || messages.length > 0;
1857
1892
  }, [messages.length, showDashboard, showSetupSplash]);
1858
1893
  const showSlashPanel = slashModeActive && !slashPanelDismissed;
1859
- const showClusterInteractivePanel = showClusterPanel && !showSlashPanel;
1860
- const showSessionsInteractivePanel = showSessionsPanel && !showSlashPanel && !showClusterInteractivePanel;
1861
- const showModelInteractivePanel = showModelPanel && !showSlashPanel && !showSessionsInteractivePanel && !showClusterInteractivePanel;
1862
- const showShortcutPanel = showHelp && !showSlashPanel && !showSessionsInteractivePanel && !showClusterInteractivePanel && !showModelInteractivePanel;
1894
+ const showEnvironmentInteractivePanel = showEnvironmentPanel && !showSlashPanel;
1895
+ const showSessionsInteractivePanel = showSessionsPanel && !showSlashPanel && !showEnvironmentInteractivePanel;
1896
+ const showModelInteractivePanel = showModelPanel && !showSlashPanel && !showSessionsInteractivePanel && !showEnvironmentInteractivePanel;
1897
+ const showShortcutPanel = showHelp && !showSlashPanel && !showSessionsInteractivePanel && !showEnvironmentInteractivePanel && !showModelInteractivePanel;
1863
1898
  const latestAssistantMessage = useMemo(() => {
1864
1899
  for (let index = messages.length - 1; index >= 0; index -= 1) {
1865
1900
  const message = messages[index];
@@ -1944,10 +1979,10 @@ export function App({ initialSessionId, seedPrompt }) {
1944
1979
  };
1945
1980
  }, [idleActivityTick, isCommandRunning, isStreaming, lastError, latestAssistantMessage, status]);
1946
1981
  const composerSuggestions = useMemo(() => (slashModeActive ? visibleSlashCandidates.map((item) => item.name) : []), [slashModeActive, visibleSlashCandidates]);
1947
- const clusterSelectOptions = useMemo(() => clusters.map((c) => ({
1948
- label: `${c.name} ${c.status}${c.region ? ` · ${c.region}` : ""}${c.cluster_id !== c.name ? ` · ${c.cluster_id}` : ""}`,
1949
- value: c.cluster_id,
1950
- })), [clusters]);
1982
+ const environmentSelectOptions = useMemo(() => environments.map((c) => ({
1983
+ label: `${c.name} ${c.status}${c.region ? ` · ${c.region}` : ""}${c.environment_id !== c.name ? ` · ${c.environment_id}` : ""}`,
1984
+ value: c.environment_id,
1985
+ })), [environments]);
1951
1986
  const modelSelectOptions = useMemo(() => availableModels.map((m) => ({
1952
1987
  label: `${m.default ? "●" : " "} ${m.display_name} ${m.thinking_supported ? "·thinking" : ""}${m.experimental ? " ·experimental" : ""}`,
1953
1988
  value: m.id,
@@ -1975,6 +2010,17 @@ export function App({ initialSessionId, seedPrompt }) {
1975
2010
  setSessionsSelectedIndex(0);
1976
2011
  }
1977
2012
  }, [sessionSelectOptions.length, sessionsSelectedIndex]);
2013
+ const dynamicPlaceholder = useMemo(() => {
2014
+ if (isStreaming)
2015
+ return "Type to queue (Enter to add)";
2016
+ if (!isAuthenticated)
2017
+ return "/login to get started";
2018
+ if (messages.length === 0)
2019
+ return "What's on your mind?";
2020
+ if (lastError && lastError !== "Paused.")
2021
+ return "Let's try that again.";
2022
+ return "Ask anything, / for commands, @ for files, ! for shell";
2023
+ }, [isStreaming, isAuthenticated, messages.length, lastError]);
1978
2024
  const composerStatusBusy = false;
1979
2025
  const composerRightStatus = liveActivity
1980
2026
  ? ""
@@ -1989,7 +2035,7 @@ export function App({ initialSessionId, seedPrompt }) {
1989
2035
  : currentSessionModel
1990
2036
  ? `model: ${currentSessionModel.displayName}`
1991
2037
  : "";
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
2038
+ 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, selectedEnvironment: selectedEnvironment, stats: systemStats }), 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 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [sessionId ? (_jsx(Box, { paddingX: 1, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Session ", compactSessionId(sessionId), sessionTitle ? ` — ${sessionTitle}` : "", " · ", messages.filter((m) => m.role === "user" || m.role === "assistant").length, " messages"] }) })) : null, _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 === "Paused." ? "info" : "error", children: lastError }) })) : null, !showSetupSplash && !showTrustDisclaimer ? (_jsx(Box, { marginTop: 1, children: _jsx(Composer, { value: composer, resetToken: composerResetToken, disabled: isCommandRunning || isAuthLoading || !!pendingEnvironmentSwitch || showTrustDisclaimer, placeholder: dynamicPlaceholder, shellMode: shellMode, captureArrowKeys: showAtFilePanel, onChange: handleComposerChange, onSubmit: submitComposer, suggestions: composerSuggestions, busy: composerStatusBusy, rightStatus: composerRightStatus, suggestion: slashModeActive
1993
2039
  ? selectedSlashCandidate
1994
2040
  ? `${selectedSlashCandidate.name} ${selectedSlashCandidate.description}`
1995
2041
  : "No matching command"
@@ -2017,16 +2063,16 @@ export function App({ initialSessionId, seedPrompt }) {
2017
2063
  const subtitle = `${formatTimeAgo(sessionItem?.updatedAt ?? new Date().toISOString())}${sessionItem?.clusterId ? ` · ${sessionItem.clusterId}` : ""}`;
2018
2064
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: isSelected ? RUBIX_THEME.colors.brand : undefined, children: isSelected ? "› " : " " }), _jsx(Text, { bold: isSelected, color: isSelected ? RUBIX_THEME.colors.brand : undefined, children: title })] }), _jsxs(Text, { dimColor: true, children: [" ", subtitle] })] }, option.value));
2019
2065
  });
2020
- })() })), _jsxs(Text, { dimColor: true, children: [sessionSelectOptions.length, " session", sessionSelectOptions.length === 1 ? "" : "s", " shown", sessionsHasMore ? " (more available)" : ""] }), _jsx(Text, { dimColor: true, children: "Type to search \u2022 \u2191\u2193 navigate \u2022 Enter switch \u2022 Esc close" })] })) : null, pendingClusterSwitch ? (_jsxs(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Switch to cluster: ", pendingClusterSwitch.name] }), _jsx(Text, { dimColor: true, children: "This will start a new session with cluster context." }), _jsx(Text, { dimColor: true, children: "Press Enter to confirm \u00B7 Esc to cancel" })] })) : null, showClusterInteractivePanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, children: ["Clusters", selectedCluster ? ` · active: ${selectedCluster.name}` : ""] }), clusterPanelLoading ? (_jsx(Spinner, { label: "loading clusters..." })) : clusterPanelError ? (_jsxs(Text, { color: "red", children: ["Failed to load clusters: ", clusterPanelError] })) : clusters.length === 0 ? (_jsx(Text, { dimColor: true, children: "No clusters found. Register a cluster via the console." })) : (_jsx(Select, { options: clusterSelectOptions, visibleOptionCount: 7, onChange: (value) => {
2021
- const cluster = clusters.find((c) => c.cluster_id === value);
2022
- if (!cluster)
2066
+ })() })), _jsxs(Text, { dimColor: true, children: [sessionSelectOptions.length, " session", sessionSelectOptions.length === 1 ? "" : "s", " shown", sessionsHasMore ? " (more available)" : ""] }), _jsx(Text, { dimColor: true, children: "Type to search \u2022 \u2191\u2193 navigate \u2022 Enter switch \u2022 Esc close" })] })) : null, pendingEnvironmentSwitch ? (_jsxs(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Switch to environment: ", pendingEnvironmentSwitch.name] }), _jsx(Text, { dimColor: true, children: "This will start a new session with this environment." }), _jsx(Text, { dimColor: true, children: "Press Enter to confirm \u00B7 Esc to cancel" })] })) : null, showEnvironmentInteractivePanel ? (_jsxs(Box, { borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, children: ["Environments", selectedEnvironment ? ` · active: ${selectedEnvironment.name}` : ""] }), environmentPanelLoading ? (_jsx(Spinner, { label: "loading environments..." })) : environmentPanelError ? (_jsxs(Text, { color: "red", children: ["Failed to load environments: ", environmentPanelError] })) : environments.length === 0 ? (_jsx(Text, { dimColor: true, children: "No environments found. Register one via the console." })) : (_jsx(Select, { options: environmentSelectOptions, visibleOptionCount: 7, onChange: (value) => {
2067
+ const env = environments.find((c) => c.environment_id === value);
2068
+ if (!env)
2023
2069
  return;
2024
- setShowClusterPanel(false);
2025
- setPendingClusterSwitch(cluster);
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) => {
2070
+ setShowEnvironmentPanel(false);
2071
+ setPendingEnvironmentSwitch(env);
2072
+ } })), _jsxs(Text, { dimColor: true, children: [environments.length, " environment", environments.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
2073
  setShowModelPanel(false);
2028
2074
  handleModelChange(value);
2029
2075
  } })), _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
2076
  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] }));
2077
+ } })), _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 /environments /models /paste /send /clear /rename /console /docs /help /exit /quit" })] })) : null] }));
2032
2078
  }