cascade-ai 0.2.0 → 0.2.1

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/cli.js CHANGED
@@ -4,7 +4,7 @@ import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from '@google/genai';
4
4
  import axios2 from 'axios';
5
5
  import { render, useApp, useStdout, useInput, Box, Text } from 'ink';
6
6
  import { Command } from 'commander';
7
- import React5, { useState, useReducer, useRef, useCallback, useEffect } from 'react';
7
+ import React5, { useState, useReducer, useRef, useCallback, useEffect, useMemo } from 'react';
8
8
  import chalk8 from 'chalk';
9
9
  import dotenv from 'dotenv';
10
10
  import fs7 from 'fs/promises';
@@ -26,9 +26,9 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
26
26
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
27
27
  import { createContext, runInContext } from 'vm';
28
28
  import Spinner from 'ink-spinner';
29
- import SelectInput from 'ink-select-input';
30
29
  import wrapAnsi from 'wrap-ansi';
31
30
  import ora from 'ora';
31
+ import SelectInput from 'ink-select-input';
32
32
  import { createServer } from 'http';
33
33
  import { fileURLToPath } from 'url';
34
34
  import express from 'express';
@@ -82,7 +82,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
82
82
  var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
83
83
  var init_constants = __esm({
84
84
  "src/constants.ts"() {
85
- CASCADE_VERSION = "0.1.2";
85
+ CASCADE_VERSION = "0.2.0";
86
86
  CASCADE_CONFIG_FILE = ".cascade/config.json";
87
87
  CASCADE_DB_FILE = ".cascade/memory.db";
88
88
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2780,6 +2780,15 @@ var ModelSelector = class {
2780
2780
  markProviderUnavailable(provider) {
2781
2781
  this.availableProviders.delete(provider);
2782
2782
  }
2783
+ /**
2784
+ * Re-add a provider to the available set after it has recovered (e.g. after
2785
+ * a failover timeout expires or a successful call confirms recovery). Only
2786
+ * re-enables providers that were originally configured — callers should
2787
+ * guard against enabling providers that were never configured.
2788
+ */
2789
+ markProviderAvailable(provider) {
2790
+ this.availableProviders.add(provider);
2791
+ }
2783
2792
  resolveDynamicModel(overrideModelId) {
2784
2793
  let providerStr = null;
2785
2794
  let actualId = overrideModelId;
@@ -2849,10 +2858,23 @@ var FailoverManager = class {
2849
2858
  if (!failure) return true;
2850
2859
  if (Date.now() - failure.failedAt >= failure.retryAfterMs) {
2851
2860
  this.failures.delete(provider);
2861
+ this.selector.markProviderAvailable(provider);
2852
2862
  return true;
2853
2863
  }
2854
2864
  return false;
2855
2865
  }
2866
+ /**
2867
+ * Call after a successful generation to immediately re-enable a provider
2868
+ * that had previously been marked unavailable. This allows fast recovery
2869
+ * when a transient rate-limit clears before the backoff window expires,
2870
+ * preventing unnecessary routing to more expensive fallback models.
2871
+ */
2872
+ recordSuccess(provider) {
2873
+ if (this.failures.has(provider)) {
2874
+ this.failures.delete(provider);
2875
+ this.selector.markProviderAvailable(provider);
2876
+ }
2877
+ }
2856
2878
  getFallbackModel(currentModel, tier) {
2857
2879
  return this.selector.getNextFallback(currentModel.id, tier);
2858
2880
  }
@@ -2869,6 +2891,7 @@ var FailoverManager = class {
2869
2891
  }
2870
2892
  clearFailure(provider) {
2871
2893
  this.failures.delete(provider);
2894
+ this.selector.markProviderAvailable(provider);
2872
2895
  }
2873
2896
  };
2874
2897
 
@@ -3084,6 +3107,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
3084
3107
  throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
3085
3108
  }
3086
3109
  this.recordStats(tier, model, result.usage);
3110
+ this.failover.recordSuccess(model.provider);
3087
3111
  return result;
3088
3112
  } catch (err) {
3089
3113
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -7303,12 +7327,17 @@ var SlashCommandRegistry = class {
7303
7327
  });
7304
7328
  this.register({
7305
7329
  command: "/model",
7330
+ description: "Pick a provider and model for a tier (interactive)",
7331
+ handler: (_args, ctx) => ({ output: ctx.onModelPicker(), handled: true })
7332
+ });
7333
+ this.register({
7334
+ command: "/model-info",
7306
7335
  description: "Show active models per tier",
7307
7336
  handler: (_args, ctx) => ({ output: ctx.onModelInfo(), handled: true })
7308
7337
  });
7309
7338
  this.register({
7310
7339
  command: "/models",
7311
- description: "List available models by provider",
7340
+ description: "Browse available models by provider",
7312
7341
  handler: (_args, ctx) => ({ output: ctx.onModelsInfo(), handled: true })
7313
7342
  });
7314
7343
  this.register({
@@ -7611,55 +7640,140 @@ function ApprovalPrompt({ request, theme, onDecision }) {
7611
7640
  }
7612
7641
  );
7613
7642
  }
7614
- var ModelsDisplay = ({ providers, modelsByProvider, onClose }) => {
7615
- const [selectedProvider, setSelectedProvider] = useState(null);
7616
- useInput((_input, key) => {
7643
+ var TIERS = [
7644
+ { id: "T1", label: "T1 \u2014 Administrator", hint: "complex reasoning \xB7 runs once per task" },
7645
+ { id: "T2", label: "T2 \u2014 Manager", hint: "per-section planning \xB7 a few calls per task" },
7646
+ { id: "T3", label: "T3 \u2014 Worker", hint: "high volume \xB7 many parallel runs" }
7647
+ ];
7648
+ var ModelsDisplay = ({
7649
+ providers,
7650
+ modelsByProvider,
7651
+ onSelect,
7652
+ onClose
7653
+ }) => {
7654
+ const [step, setStep] = useState("PROVIDER");
7655
+ const [cursor, setCursor] = useState(0);
7656
+ const [picked, setPicked] = useState({});
7657
+ const providerItems = useMemo(() => {
7658
+ const base = [
7659
+ { label: "\u25C7 Auto", sublabel: "let Cascade pick per tier \u2014 recommended", value: "auto" }
7660
+ ];
7661
+ for (const p of providers) {
7662
+ const count = modelsByProvider.get(p)?.length ?? 0;
7663
+ base.push({ label: p, sublabel: `${count} model${count === 1 ? "" : "s"} discovered`, value: p });
7664
+ }
7665
+ return base;
7666
+ }, [providers, modelsByProvider]);
7667
+ const tierItems = useMemo(
7668
+ () => TIERS.map((t) => ({ label: t.label, sublabel: t.hint, value: t.id })),
7669
+ []
7670
+ );
7671
+ const modelItems = useMemo(() => {
7672
+ if (!picked.provider || picked.provider === "auto") return [];
7673
+ const list = (modelsByProvider.get(picked.provider) ?? []).slice().sort((a, b) => a.id.localeCompare(b.id));
7674
+ const items = [
7675
+ { label: "\u25C7 Auto", sublabel: "best available from this provider", value: "__auto__" }
7676
+ ];
7677
+ for (const m of list) {
7678
+ const ctx = m.contextWindow >= 1e6 ? `${(m.contextWindow / 1e6).toFixed(1)}M ctx` : m.contextWindow >= 1e3 ? `${Math.round(m.contextWindow / 1e3)}K ctx` : `${m.contextWindow} ctx`;
7679
+ items.push({ label: m.name, sublabel: `${m.id} \xB7 ${ctx}`, value: m.id });
7680
+ }
7681
+ return items;
7682
+ }, [picked.provider, modelsByProvider]);
7683
+ const currentItems = step === "PROVIDER" ? providerItems : step === "TIER" ? tierItems : modelItems;
7684
+ useInput((input, key) => {
7617
7685
  if (key.escape) {
7618
- if (selectedProvider) {
7619
- setSelectedProvider(null);
7620
- } else {
7621
- onClose();
7686
+ if (step === "MODEL") {
7687
+ setStep("TIER");
7688
+ setCursor(0);
7689
+ return;
7690
+ }
7691
+ if (step === "TIER") {
7692
+ setStep("PROVIDER");
7693
+ setCursor(0);
7694
+ return;
7622
7695
  }
7696
+ onClose();
7697
+ return;
7623
7698
  }
7624
- });
7625
- const providerItems = providers.map((p) => ({ label: p, value: p }));
7626
- const modelItems = selectedProvider ? (modelsByProvider.get(selectedProvider) ?? []).sort((a, b) => a.id.localeCompare(b.id)).map((m) => ({
7627
- label: `${m.id.padEnd(24)} \u2014 ${m.name}`,
7628
- value: m.id
7629
- })) : [];
7630
- if (!selectedProvider) {
7631
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
7632
- /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
7633
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u25C8 SELECT PROVIDER" }),
7634
- /* @__PURE__ */ jsx(Text, { color: "muted", children: " (ESC to Exit)" })
7635
- ] }),
7636
- /* @__PURE__ */ jsx(
7637
- SelectInput,
7638
- {
7639
- items: providerItems,
7640
- onSelect: (item) => setSelectedProvider(item.value)
7699
+ if (key.upArrow || input === "k") {
7700
+ setCursor((c) => currentItems.length === 0 ? 0 : (c - 1 + currentItems.length) % currentItems.length);
7701
+ return;
7702
+ }
7703
+ if (key.downArrow || key.tab || input === "j") {
7704
+ setCursor((c) => currentItems.length === 0 ? 0 : (c + 1) % currentItems.length);
7705
+ return;
7706
+ }
7707
+ if (/^[1-9]$/.test(input)) {
7708
+ const idx = parseInt(input, 10) - 1;
7709
+ if (idx < currentItems.length) setCursor(idx);
7710
+ return;
7711
+ }
7712
+ if (key.return) {
7713
+ const selected = currentItems[cursor];
7714
+ if (!selected) return;
7715
+ if (step === "PROVIDER") {
7716
+ if (selected.value === "auto") {
7717
+ setPicked({ provider: "auto" });
7718
+ setStep("TIER");
7719
+ setCursor(0);
7720
+ return;
7641
7721
  }
7642
- )
7643
- ] });
7644
- }
7645
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [
7646
- /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [
7647
- /* @__PURE__ */ jsxs(Text, { bold: true, color: "green", children: [
7648
- "\u25C8 ",
7649
- selectedProvider.toUpperCase(),
7650
- " \u2014 MODELS"
7651
- ] }),
7652
- /* @__PURE__ */ jsx(Text, { color: "muted", children: "[ESC to Back]" })
7653
- ] }),
7654
- modelItems.length === 0 ? /* @__PURE__ */ jsx(Text, { italic: true, color: "yellow", children: "No supported models discovered for this provider." }) : /* @__PURE__ */ jsx(
7655
- SelectInput,
7656
- {
7657
- items: modelItems,
7658
- limit: 10,
7659
- onSelect: () => {
7722
+ setPicked({ provider: selected.value });
7723
+ setStep("TIER");
7724
+ setCursor(0);
7725
+ return;
7726
+ }
7727
+ if (step === "TIER") {
7728
+ const tier = selected.value;
7729
+ if (picked.provider === "auto") {
7730
+ onSelect?.({ kind: "auto", tier });
7731
+ onClose();
7732
+ return;
7660
7733
  }
7734
+ setPicked((p) => ({ ...p, tier }));
7735
+ setStep("MODEL");
7736
+ setCursor(0);
7737
+ return;
7661
7738
  }
7662
- )
7739
+ if (!picked.tier || !picked.provider || picked.provider === "auto") {
7740
+ onClose();
7741
+ return;
7742
+ }
7743
+ if (selected.value === "__auto__") {
7744
+ onSelect?.({ kind: "auto", tier: picked.tier });
7745
+ } else {
7746
+ onSelect?.({
7747
+ kind: "pick",
7748
+ tier: picked.tier,
7749
+ provider: picked.provider,
7750
+ modelId: selected.value
7751
+ });
7752
+ }
7753
+ onClose();
7754
+ }
7755
+ });
7756
+ const title = step === "PROVIDER" ? "\u25C8 SELECT PROVIDER" : step === "TIER" ? `\u25C8 APPLY ${picked.provider === "auto" ? "AUTO" : String(picked.provider).toUpperCase()} TO WHICH TIER?` : `\u25C8 ${String(picked.provider).toUpperCase()} \u2192 SELECT MODEL FOR ${picked.tier}`;
7757
+ const breadcrumb = step === "PROVIDER" ? "Step 1 / 3" : step === "TIER" ? `Step 2 / 3 \xB7 provider: ${picked.provider}` : `Step 3 / 3 \xB7 ${picked.provider} \u2192 ${picked.tier}`;
7758
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
7759
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
7760
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: title }),
7761
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "[Esc back \xB7 Enter select]" })
7762
+ ] }),
7763
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
7764
+ breadcrumb,
7765
+ " \xB7 \u2191/\u2193 navigate \xB7 1\u20139 jump"
7766
+ ] }) }),
7767
+ currentItems.length === 0 ? /* @__PURE__ */ jsx(Text, { italic: true, color: "yellow", children: "No items to show." }) : /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: currentItems.map((item, i) => {
7768
+ const focused = i === cursor;
7769
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
7770
+ /* @__PURE__ */ jsx(Text, { color: focused ? "green" : "gray", children: focused ? "\u276F " : " " }),
7771
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
7772
+ /* @__PURE__ */ jsx(Text, { color: focused ? "white" : "gray", bold: focused, children: item.label }),
7773
+ item.sublabel && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: ` ${item.sublabel}` })
7774
+ ] })
7775
+ ] }, `${step}-${item.value}-${i}`);
7776
+ }) })
7663
7777
  ] });
7664
7778
  };
7665
7779
  function CostTracker({
@@ -7924,6 +8038,40 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName })
7924
8038
  const persistMessage = useCallback((role, content, timestamp) => {
7925
8039
  storeRef.current?.addMessage({ id: randomUUID(), sessionId: sessionIdRef.current, role, content, timestamp });
7926
8040
  }, []);
8041
+ const applyModelPick = useCallback(async (sel) => {
8042
+ const tierKey = sel.tier.toLowerCase();
8043
+ const tierLabel = sel.tier;
8044
+ if (sel.kind === "auto") {
8045
+ delete config.models[tierKey];
8046
+ } else {
8047
+ config.models[tierKey] = sel.modelId;
8048
+ }
8049
+ try {
8050
+ const router = cascadeRef.current?.getRouter();
8051
+ if (router && sel.kind === "pick") {
8052
+ const candidate = (cachedModels.get(sel.provider) ?? []).find((m) => m.id === sel.modelId);
8053
+ if (candidate) router.overrideTierModel(tierLabel, candidate);
8054
+ }
8055
+ } catch {
8056
+ }
8057
+ const configPath = path17.join(workspacePath, ".cascade", "config.json");
8058
+ try {
8059
+ await fs7.mkdir(path17.dirname(configPath), { recursive: true });
8060
+ await fs7.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
8061
+ } catch (err) {
8062
+ const msg = err instanceof Error ? err.message : String(err);
8063
+ dispatch({
8064
+ type: "ADD_MESSAGE",
8065
+ message: { id: randomUUID(), role: "error", content: `Failed to persist model selection: ${msg}`, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
8066
+ });
8067
+ return;
8068
+ }
8069
+ const summary = sel.kind === "auto" ? `${tierLabel} \u2192 Auto (Cascade will pick best available).` : `${tierLabel} \u2192 ${sel.modelId} (provider: ${sel.provider}).`;
8070
+ dispatch({
8071
+ type: "ADD_MESSAGE",
8072
+ message: { id: randomUUID(), role: "system", content: `\u2714 Model updated \u2014 ${summary}`, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
8073
+ });
8074
+ }, [config, workspacePath, cachedModels]);
7927
8075
  const globalStoreRef = useRef(null);
7928
8076
  const persistRuntimeSession = useCallback((status, latestPrompt) => {
7929
8077
  const runtimeSession = { sessionId: sessionIdRef.current, title: sessionTitle, workspacePath, status, startedAt: startedAtRef.current, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), latestPrompt, isGlobal: false };
@@ -8070,6 +8218,10 @@ ${msg.content}`).join("\n\n");
8070
8218
  return `${t}: ${m?.name ?? "none"} (${m?.provider ?? "\u2014"})${configured ? ` | configured: ${configured}` : ""}`;
8071
8219
  }).join("\n");
8072
8220
  },
8221
+ onModelPicker: async () => {
8222
+ setIsShowingModels(true);
8223
+ return "Opening model picker \u2014 choose provider \u2192 tier \u2192 model (ESC to exit).";
8224
+ },
8073
8225
  onModelsInfo: async () => {
8074
8226
  setIsShowingModels(true);
8075
8227
  return "Opening interactive models explorer... (ESC to exit)";
@@ -8352,6 +8504,16 @@ Use /identity <name|id> to switch.`;
8352
8504
  }
8353
8505
  }, [handleSlashCommand, persistMessage, state.messages, workspacePath, rebuildTree, recordNodeEvent, slashCompletions, slashIndex, state.isExecuting]);
8354
8506
  useInput((_input, key) => {
8507
+ if (isShowingModels) {
8508
+ if (key.ctrl && _input === "c") {
8509
+ if (quitAttempted) {
8510
+ exit();
8511
+ return;
8512
+ }
8513
+ setQuitAttempted(true);
8514
+ }
8515
+ return;
8516
+ }
8355
8517
  if (key.ctrl && _input === "c") {
8356
8518
  if (quitAttempted) {
8357
8519
  exit();
@@ -8520,7 +8682,17 @@ Use /identity <name|id> to switch.`;
8520
8682
  ] }) }),
8521
8683
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
8522
8684
  state.messages.length === 0 && !state.isStreaming && /* @__PURE__ */ jsx(WelcomeBanner, { theme, config, workspacePath, sessionId: sessionIdRef.current }),
8523
- isShowingModels && /* @__PURE__ */ jsx(ModelsDisplay, { providers: config.providers.map((p) => p.type), modelsByProvider: cachedModels, onClose: () => setIsShowingModels(false) })
8685
+ isShowingModels && /* @__PURE__ */ jsx(
8686
+ ModelsDisplay,
8687
+ {
8688
+ providers: config.providers.map((p) => p.type),
8689
+ modelsByProvider: cachedModels,
8690
+ onSelect: (sel) => {
8691
+ void applyModelPick(sel);
8692
+ },
8693
+ onClose: () => setIsShowingModels(false)
8694
+ }
8695
+ )
8524
8696
  ] }),
8525
8697
  /* @__PURE__ */ jsx(AgentTree, { root: state.agentTree, theme }),
8526
8698
  state.showDetails && /* @__PURE__ */ jsx(TimelinePanel, { nodes: [...treeNodesRef.current.values()], theme, currentIndex: timelineIndex, onChangeIndex: setTimelineIndex }),
@@ -8572,7 +8744,7 @@ Use /identity <name|id> to switch.`;
8572
8744
  /* @__PURE__ */ jsx(
8573
8745
  SafeTextInput,
8574
8746
  {
8575
- focus: !state.approvalRequest,
8747
+ focus: !state.approvalRequest && !isShowingModels,
8576
8748
  value: input,
8577
8749
  manageMouseReporting: false,
8578
8750
  onChange: (val) => {
@@ -8765,6 +8937,7 @@ var PROVIDER_LABELS = {
8765
8937
  ollama: "Ollama (local)",
8766
8938
  "openai-compatible": "OpenAI-Compatible"
8767
8939
  };
8940
+ var providerOrder = ["anthropic", "openai", "gemini", "azure", "ollama", "openai-compatible"];
8768
8941
  function buildInitialEntries(types) {
8769
8942
  return [...types].map((type) => ({
8770
8943
  id: randomUUID(),
@@ -8780,6 +8953,17 @@ function wizardReducer(state, action) {
8780
8953
  else next.add(action.provider);
8781
8954
  return { ...state, selectedTypes: next };
8782
8955
  }
8956
+ case "TOGGLE_ALL": {
8957
+ const allSelected = state.selectedTypes.size === providerOrder.length;
8958
+ return { ...state, selectedTypes: allSelected ? /* @__PURE__ */ new Set() : new Set(providerOrder) };
8959
+ }
8960
+ case "INVERT_SELECTION": {
8961
+ const next = /* @__PURE__ */ new Set();
8962
+ providerOrder.forEach((p) => {
8963
+ if (!state.selectedTypes.has(p)) next.add(p);
8964
+ });
8965
+ return { ...state, selectedTypes: next };
8966
+ }
8783
8967
  case "CONFIRM_PROVIDERS": {
8784
8968
  const entries = buildInitialEntries(state.selectedTypes);
8785
8969
  return { ...state, entries, currentEntryIdx: 0, step: "API_KEYS" };
@@ -8867,7 +9051,6 @@ function SetupWizard({ workspacePath, onComplete }) {
8867
9051
  tierSelectFocus: "T1",
8868
9052
  error: null
8869
9053
  });
8870
- const providerOrder = ["anthropic", "openai", "gemini", "azure", "ollama", "openai-compatible"];
8871
9054
  const [providerCursor, setProviderCursor] = useState(0);
8872
9055
  const [fieldBuffer, setFieldBuffer] = useState("");
8873
9056
  const [fieldStage, setFieldStage] = useState("apiKey");
@@ -8960,6 +9143,8 @@ function SetupWizard({ workspacePath, onComplete }) {
8960
9143
  if (key.upArrow) setProviderCursor((p) => Math.max(0, p - 1));
8961
9144
  if (key.downArrow) setProviderCursor((p) => Math.min(providerOrder.length - 1, p + 1));
8962
9145
  if (_input === " ") dispatch({ type: "TOGGLE_PROVIDER", provider: providerOrder[providerCursor] });
9146
+ if (_input === "a") dispatch({ type: "TOGGLE_ALL" });
9147
+ if (_input === "i") dispatch({ type: "INVERT_SELECTION" });
8963
9148
  if (key.return) {
8964
9149
  if (state.selectedTypes.size === 0) return;
8965
9150
  dispatch({ type: "CONFIRM_PROVIDERS" });
@@ -9021,21 +9206,23 @@ function SetupWizard({ workspacePath, onComplete }) {
9021
9206
  }, [currentEntry, fieldStage]);
9022
9207
  if (state.step === "PROVIDER_SELECT") {
9023
9208
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
9024
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "\u25C8 CASCADE AI \u2014 Setup" }),
9025
- /* @__PURE__ */ jsx(Box, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Which providers do you want to configure?" }) }),
9026
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Space to toggle \xB7 Enter to confirm" }),
9027
- /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: providerOrder.map((p, i) => {
9209
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
9210
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "? " }),
9211
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Which providers do you want to configure?" })
9212
+ ] }),
9213
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: providerOrder.map((p, i) => {
9028
9214
  const selected = state.selectedTypes.has(p);
9029
9215
  const focused = i === providerCursor;
9030
9216
  return /* @__PURE__ */ jsxs(Box, { children: [
9031
- /* @__PURE__ */ jsx(Text, { color: focused ? "green" : "white", children: focused ? "\u276F " : " " }),
9032
- /* @__PURE__ */ jsx(Text, { color: selected ? "green" : "white", children: selected ? "\u25C9 " : "\u25CB " }),
9033
- /* @__PURE__ */ jsx(Text, { color: focused ? "white" : "gray", children: PROVIDER_LABELS[p] }),
9217
+ /* @__PURE__ */ jsx(Text, { color: focused ? "magenta" : "white", children: focused ? "\u276F " : " " }),
9218
+ /* @__PURE__ */ jsx(Text, { color: selected ? "green" : "white", children: selected ? "\u25C9 " : "\u25EF " }),
9219
+ /* @__PURE__ */ jsx(Text, { color: focused ? "magenta" : selected ? "white" : "gray", children: PROVIDER_LABELS[p] }),
9034
9220
  p === "azure" && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 multiple deployments supported" }),
9035
9221
  p === "openai-compatible" && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 Groq, Together, custom" }),
9036
9222
  p === "ollama" && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 no API key needed" })
9037
9223
  ] }, p);
9038
9224
  }) }),
9225
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)" }) }),
9039
9226
  state.error && /* @__PURE__ */ jsx(Text, { color: "red", children: state.error })
9040
9227
  ] });
9041
9228
  }
@@ -9045,15 +9232,19 @@ function SetupWizard({ workspacePath, onComplete }) {
9045
9232
  const isOllama = currentEntry.type === "ollama";
9046
9233
  if (fieldStage === "askMore") {
9047
9234
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
9048
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "\u25C8 CASCADE AI \u2014 Setup" }),
9049
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: isAzure ? "Add another Azure deployment? (y/n)" : "Add another custom endpoint? (y/n)" }) }),
9050
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
9235
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
9236
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "? " }),
9237
+ /* @__PURE__ */ jsx(Text, { bold: true, children: isAzure ? "Add another Azure deployment? (y/n)" : "Add another custom endpoint? (y/n)" })
9238
+ ] }),
9239
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
9051
9240
  SelectInput,
9052
9241
  {
9053
9242
  items: [
9054
9243
  { label: "Yes \u2014 add another", value: "yes" },
9055
9244
  { label: "No \u2014 continue", value: "no" }
9056
9245
  ],
9246
+ indicatorComponent: ({ isSelected }) => /* @__PURE__ */ jsx(Text, { color: "magenta", children: isSelected ? "\u276F " : " " }),
9247
+ itemComponent: ({ isSelected, label }) => /* @__PURE__ */ jsx(Text, { color: isSelected ? "magenta" : "white", children: label }),
9057
9248
  onSelect: (item) => {
9058
9249
  if (item.value === "yes") {
9059
9250
  if (isAzure) dispatch({ type: "ADD_AZURE" });
@@ -9070,21 +9261,15 @@ function SetupWizard({ workspacePath, onComplete }) {
9070
9261
  ) })
9071
9262
  ] });
9072
9263
  }
9073
- const prompt = isAzure && fieldStage === "deploymentName" ? `Azure deployment name (${currentEntry.label}):` : isAzure && fieldStage === "baseUrl" ? `Azure endpoint URL:` : isCompat && fieldStage === "label" ? `Name for this endpoint (e.g. Groq):` : isCompat && fieldStage === "baseUrl" ? `Base URL (e.g. https://api.groq.com/openai/v1):` : isOllama ? `Ollama URL (Enter for http://localhost:11434):` : `${currentEntry.label} API Key:`;
9264
+ const prompt = isAzure && fieldStage === "deploymentName" ? `Azure deployment name (${currentEntry.label})` : isAzure && fieldStage === "baseUrl" ? `Azure endpoint URL` : isCompat && fieldStage === "label" ? `Name for this endpoint (e.g. Groq)` : isCompat && fieldStage === "baseUrl" ? `Base URL (e.g. https://api.groq.com/openai/v1)` : isOllama ? `Ollama URL (Enter for http://localhost:11434)` : `${currentEntry.label} API Key`;
9074
9265
  const isMasked = fieldStage === "apiKey";
9075
9266
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
9076
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "\u25C8 CASCADE AI \u2014 Setup" }),
9077
- /* @__PURE__ */ jsx(Box, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
9078
- "Provider ",
9079
- state.currentEntryIdx + 1,
9080
- " of ",
9081
- state.entries.length,
9082
- ": ",
9083
- currentEntry.label
9084
- ] }) }),
9085
- /* @__PURE__ */ jsx(Text, { children: prompt }),
9086
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
9087
- /* @__PURE__ */ jsx(Text, { color: "green", children: "> " }),
9267
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
9268
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "? " }),
9269
+ /* @__PURE__ */ jsx(Text, { bold: true, children: prompt })
9270
+ ] }),
9271
+ /* @__PURE__ */ jsxs(Box, { children: [
9272
+ /* @__PURE__ */ jsx(Text, { color: "magenta", children: "\u276F " }),
9088
9273
  /* @__PURE__ */ jsx(
9089
9274
  SafeTextInput,
9090
9275
  {
@@ -9102,13 +9287,13 @@ function SetupWizard({ workspacePath, onComplete }) {
9102
9287
  }
9103
9288
  if (state.step === "FETCH_MODELS") {
9104
9289
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
9105
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "\u25C8 CASCADE AI \u2014 Fetching Models" }),
9106
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
9290
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
9291
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "? " }),
9292
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Connecting to providers and fetching models..." })
9293
+ ] }),
9294
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
9107
9295
  state.fetchLog.map((line, i) => /* @__PURE__ */ jsx(Text, { children: line }, i)),
9108
- state.fetchedModels.length === 0 && /* @__PURE__ */ jsxs(Box, { children: [
9109
- /* @__PURE__ */ jsx(Spinner, { type: "dots" }),
9110
- /* @__PURE__ */ jsx(Text, { children: " Connecting to providers..." })
9111
- ] })
9296
+ state.fetchedModels.length === 0 && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) })
9112
9297
  ] })
9113
9298
  ] });
9114
9299
  }
@@ -9123,41 +9308,55 @@ function SetupWizard({ workspacePath, onComplete }) {
9123
9308
  const tierLabel = (tier, hint) => {
9124
9309
  const isFocused = state.tierSelectFocus === tier;
9125
9310
  const current = tier === "T1" ? state.tierT1 : tier === "T2" ? state.tierT2 : state.tierT3;
9311
+ if (!isFocused) {
9312
+ return /* @__PURE__ */ jsxs(Box, { children: [
9313
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "\u2714 " }),
9314
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
9315
+ tier,
9316
+ " ",
9317
+ hint,
9318
+ ": "
9319
+ ] }),
9320
+ /* @__PURE__ */ jsx(Text, { color: "magenta", children: current === "auto" ? "Auto \u2014 let Cascade choose best available" : state.fetchedModels.find((m) => m.id === current)?.name || current })
9321
+ ] }, tier);
9322
+ }
9126
9323
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
9127
- /* @__PURE__ */ jsxs(Text, { bold: true, color: isFocused ? "green" : "white", children: [
9128
- tier,
9129
- " ",
9130
- hint,
9131
- ":"
9132
- ] }),
9133
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
9134
- " currently: ",
9135
- current === "auto" ? "Auto" : current
9324
+ /* @__PURE__ */ jsxs(Box, { children: [
9325
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "? " }),
9326
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
9327
+ tier,
9328
+ " ",
9329
+ hint,
9330
+ ": "
9331
+ ] })
9136
9332
  ] }),
9137
- isFocused && /* @__PURE__ */ jsx(
9333
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
9138
9334
  SelectInput,
9139
9335
  {
9140
9336
  items: modelOptions,
9141
- onSelect: (item) => dispatch({ type: "SET_TIER", tier, value: item.value })
9337
+ onSelect: (item) => dispatch({ type: "SET_TIER", tier, value: item.value }),
9338
+ indicatorComponent: ({ isSelected }) => /* @__PURE__ */ jsx(Text, { color: "magenta", children: isSelected ? "\u276F " : " " }),
9339
+ itemComponent: ({ isSelected, label }) => /* @__PURE__ */ jsx(Text, { color: isSelected ? "magenta" : "white", children: label })
9142
9340
  }
9143
- )
9341
+ ) })
9144
9342
  ] }, tier);
9145
9343
  };
9146
9344
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
9147
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "\u25C8 CASCADE AI \u2014 Tier Assignment" }),
9148
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Tab to switch tier \xB7 Enter to confirm selection" }),
9149
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
9345
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
9150
9346
  tierLabel("T1", "(Administrator \u2014 complex reasoning, runs once per task)"),
9151
9347
  tierLabel("T2", "(Manager \u2014 runs per section)"),
9152
9348
  tierLabel("T3", "(Worker \u2014 high volume, many parallel runs)")
9153
9349
  ] }),
9154
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press Enter on the last tier to finish setup" }) }),
9350
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(Tab/Arrow Down to skip tier, Enter to select or save)" }) }),
9155
9351
  state.error && /* @__PURE__ */ jsx(Text, { color: "red", children: state.error })
9156
9352
  ] });
9157
9353
  }
9158
9354
  if (state.step === "SAVE") {
9159
9355
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
9160
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "\u25C8 CASCADE AI \u2014 Saving Configuration" }),
9356
+ /* @__PURE__ */ jsxs(Box, { children: [
9357
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "\u2714 " }),
9358
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Setup complete!" })
9359
+ ] }),
9161
9360
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
9162
9361
  /* @__PURE__ */ jsx(Spinner, { type: "dots" }),
9163
9362
  /* @__PURE__ */ jsx(Text, { children: " Writing .cascade/config.json..." })