mindweave 2.4.8 → 2.4.9

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/README.md CHANGED
@@ -70,8 +70,9 @@ else is configured inside a session.
70
70
 
71
71
  Short version, one line each. The depth is in the linked pages.
72
72
 
73
- - **14 providers, 52 models, one key** — DeepSeek, Anthropic, OpenAI, Gemini, xAI,
74
- Mistral, Groq, Cerebras, Qwen, Kimi, GLM, Meta, MiniMax, Tencent. Only the driver you
73
+ - **15 providers, 53 models, one key** — DeepSeek, Anthropic, OpenAI, Gemini, xAI,
74
+ Mistral, Groq, Cerebras, Qwen, Kimi, GLM, Meta, MiniMax, Tencent, plus every
75
+ tool-capable model on OpenRouter. Only the driver you
75
76
  use is loaded. Switch with `/provider` and `/model`; remembered per project.
76
77
  [PROVIDERS.md](src/drivers/PROVIDERS.md)
77
78
  - **Real tools** — read and edit files, multi-file edits, ripgrep search, a shell with
package/dist/cli/App.js CHANGED
@@ -46,10 +46,10 @@ import { TrustGate } from "./components/TrustGate.js";
46
46
  import { rootBreadth, breadthWarning, trustPersists, isTrusted, rememberTrust } from "./trust.js";
47
47
  import { projectDir } from "../memory/store.js";
48
48
  import { parseUndoArg, undoNotice } from "../tools/checkpoints.js";
49
- import { DEFAULT_MODEL_CONFIG, thinkLevels, thinkLabel, modelLabel, modelsOfProvider, providerOf, usableFallback, needsKeySetup, withModel, saveModelConfig, refreshModels } from "../dynamo/model.js";
49
+ import { DEFAULT_MODEL_CONFIG, thinkLevels, thinkLabel, modelLabel, modelsOfProvider, providerOf, usableFallback, needsKeySetup, withModel, saveModelConfig, refreshModels, DISCOVERY_TTL_MS } from "../dynamo/model.js";
50
50
  import { allProviders, manifestForModel, modelsOf } from "../drivers/registry.js";
51
51
  import { orderProviders, orderModels } from "./pickerOrder.js";
52
- import { accessRefusal } from "../drivers/providerError.js";
52
+ import { accessRefusal, providerOutage } from "../drivers/providerError.js";
53
53
  import { resolveAttachments, stripAttachments } from "./attachments.js";
54
54
  import { collapsePastes, wrapPastedText } from "../memory/pastedText.js";
55
55
  import { createDropHandles, expandHandles } from "./dropHandles.js";
@@ -89,7 +89,7 @@ import { perf, perfEnabled } from "./perfLog.js";
89
89
  import { isGroupMember, groupSettled, planGroupReveal, planStandaloneReveal, resultQueued, STANDALONE_HOLD_MS } from "./groupReveal.js";
90
90
  import { drain as drainQueue, popAll as popAllQueued, queueMessage, takeSteerable, visibleQueue } from "./messageQueue.js";
91
91
  import { routeCommand, parseCommandLine, unknownCommandMessage } from "./commandRoute.js";
92
- import { resolveChoice } from "./commandArgs.js";
92
+ import { resolveChoice, splitModelArg } from "./commandArgs.js";
93
93
  import { carryAcrossFreshSession } from "./sessionCarry.js";
94
94
  import { toolDisplay, isGroupable, KIND_COLOR } from "./toolDisplay.js";
95
95
  import { workingVerb } from "./workingVerb.js";
@@ -141,6 +141,11 @@ function orderedProviderList() {
141
141
  function orderedModelList(model) {
142
142
  return orderModels(modelsOfProvider(model));
143
143
  }
144
+ /** The same order, for a provider named by id rather than by one of its models. */
145
+ function orderedModelsOf(providerId) {
146
+ const provider = allProviders().find((p) => p.id === providerId);
147
+ return provider ? orderModels(modelsOf(provider)) : [];
148
+ }
144
149
  // After you pick a session in /continue, the three ways to resume it.
145
150
  const RESUME_MODES = [
146
151
  { label: "Compact & continue", description: "summarize the old chat first so it won't eat your context, then pick up where you left off" },
@@ -2013,7 +2018,10 @@ export function App({ resumeSessionId, initialScreen }) {
2013
2018
  // the provider's own sentence quoted inside it. Anything else — a malformed
2014
2019
  // request, a bug of ours — stays loud, which is the point of classifying
2015
2020
  // narrowly. See drivers/providerError.ts.
2016
- const refusal = accessRefusal(error, providerOf(s.modelConfig.model).label, otherProviderHasKey(s.modelConfig.model));
2021
+ // A provider falling over after the retries ran out gets the same calm treatment:
2022
+ // it is not our crash, and the useful news is "wait, or switch model".
2023
+ const refusal = accessRefusal(error, providerOf(s.modelConfig.model).label, otherProviderHasKey(s.modelConfig.model)) ??
2024
+ providerOutage(error, providerOf(s.modelConfig.model).label, modelLabel(s.modelConfig.model));
2017
2025
  if (refusal)
2018
2026
  enqueueReveal({ type: "notice", title: refusal.title, body: refusal.body });
2019
2027
  else
@@ -2148,15 +2156,28 @@ export function App({ resumeSessionId, initialScreen }) {
2148
2156
  }
2149
2157
  // Apply a /model pick: switch model (clamping reasoning to a valid level), persist
2150
2158
  // the choice for this project, and confirm.
2151
- async function applyModel(index) {
2159
+ async function applyModel(index, providerId) {
2152
2160
  const s = session.current;
2153
- // Index into the SAME list the picker rendered — the current provider's models in
2154
- // display order, not every model everywhere. Indexing a differently-ordered list
2155
- // here would silently select a different model than the one on screen, and would
2156
- // type-check perfectly.
2157
- const choice = s ? orderedModelList(s.modelConfig.model)[index] : undefined;
2161
+ // Index into the SAME list the picker rendered — that provider's models in display
2162
+ // order, not every model everywhere. Indexing a differently-ordered list here would
2163
+ // silently select a different model than the one on screen, and would type-check
2164
+ // perfectly.
2165
+ const list = !s ? [] : providerId ? orderedModelsOf(providerId) : orderedModelList(s.modelConfig.model);
2166
+ const choice = list[index];
2158
2167
  if (!s || !choice)
2159
2168
  return;
2169
+ // A model on another provider is a provider switch, and gets the same key check.
2170
+ const target = manifestForModel(choice.id);
2171
+ if (target.id !== providerOf(s.modelConfig.model).id) {
2172
+ if (missingKeyFor(choice.id)) {
2173
+ pendingSwitch.current = { model: choice.id, apiKeyEnv: target.apiKeyEnv, label: target.label };
2174
+ note(`${target.label} has no key yet — add one and the switch will finish.`);
2175
+ setKeysOpen(true);
2176
+ return;
2177
+ }
2178
+ await switchTo(choice.id, target.label);
2179
+ return;
2180
+ }
2160
2181
  s.modelConfig = withModel(s.modelConfig, choice.id);
2161
2182
  await saveModelConfig(s.cwd, s.modelConfig);
2162
2183
  note(`model → ${modelLabel(s.modelConfig.model)} · ${thinkLabel(s.modelConfig)}`);
@@ -2202,6 +2223,10 @@ export function App({ resumeSessionId, initialScreen }) {
2202
2223
  s.modelConfig = withModel(s.modelConfig, model);
2203
2224
  await saveModelConfig(s.cwd, s.modelConfig);
2204
2225
  note(`provider → ${providerLabel} · model → ${modelLabel(s.modelConfig.model)} · ${thinkLabel(s.modelConfig)}`);
2226
+ // Where prompts go is worth a sentence at the moment someone moves their work there.
2227
+ const notice = manifestForModel(model).notice;
2228
+ if (notice)
2229
+ note(notice);
2205
2230
  }
2206
2231
  // Apply a /think pick for the current model: set thinking + effort, persist, confirm.
2207
2232
  async function applyThink(index) {
@@ -2256,7 +2281,7 @@ export function App({ resumeSessionId, initialScreen }) {
2256
2281
  else if (o.kind === "provider")
2257
2282
  void applyProvider(index);
2258
2283
  else if (o.kind === "model")
2259
- void applyModel(index);
2284
+ void applyModel(index, o.providerId);
2260
2285
  else if (o.kind === "think")
2261
2286
  void applyThink(index);
2262
2287
  else if (o.kind === "screen") {
@@ -2620,12 +2645,12 @@ export function App({ resumeSessionId, initialScreen }) {
2620
2645
  setOverlay({ kind: "sessions", items: sessions });
2621
2646
  return;
2622
2647
  }
2623
- // Both pickers refresh discovered providers first, so a model pulled since the
2624
- // session started appears without a restart. Awaited rather than fired off: the
2648
+ // Both pickers refresh discovered providers whose list has gone stale, so a newly
2649
+ // served model appears without a restart. Awaited rather than fired off: the
2625
2650
  // picker renders from the list, and opening on a stale one that then changes
2626
2651
  // under the cursor is the exact "two-stage reveal" the UI work removed.
2627
2652
  if (name === "/provider") {
2628
- await refreshModels();
2653
+ await refreshModels({ maxAgeMs: DISCOVERY_TTL_MS });
2629
2654
  setOverlay({ kind: "provider" });
2630
2655
  return;
2631
2656
  }
@@ -2641,12 +2666,21 @@ export function App({ resumeSessionId, initialScreen }) {
2641
2666
  // getting the picker anyway — which is what happened before, because the argument
2642
2667
  // was dropped without a word — reads as the app not having heard you.
2643
2668
  if (name === "/model") {
2644
- await refreshModels();
2669
+ await refreshModels({ maxAgeMs: DISCOVERY_TTL_MS });
2645
2670
  if (arg) {
2646
- const picked = resolveChoice(arg, orderedModelList(s.modelConfig.model), "model");
2671
+ const current = providerOf(s.modelConfig.model).id;
2672
+ const here = resolveChoice(arg, orderedModelList(s.modelConfig.model), "model");
2673
+ const { providerId, words } = splitModelArg(arg, allProviders(), current, here.kind !== "error");
2674
+ // `/model openrouter` alone: that provider's picker.
2675
+ if (!words)
2676
+ return setOverlay({ kind: "model", providerId });
2677
+ const picked = providerId === current ? here : resolveChoice(words, orderedModelsOf(providerId), "model");
2647
2678
  if (picked.kind === "error")
2648
2679
  return say(picked.message);
2649
- await applyModel(picked.index);
2680
+ // Several fit: show exactly those, in the picker, rather than a list to retype from.
2681
+ if (picked.kind === "several")
2682
+ return setOverlay({ kind: "model", providerId, filter: words });
2683
+ await applyModel(picked.index, providerId);
2650
2684
  return;
2651
2685
  }
2652
2686
  setOverlay({ kind: "model" });
@@ -2675,7 +2709,7 @@ export function App({ resumeSessionId, initialScreen }) {
2675
2709
  if (name === "/think") {
2676
2710
  if (arg) {
2677
2711
  const picked = resolveChoice(arg, thinkLevels(s.modelConfig.model), "reasoning level");
2678
- if (picked.kind === "error")
2712
+ if (picked.kind !== "match")
2679
2713
  return say(picked.message);
2680
2714
  await applyThink(picked.index);
2681
2715
  return;
@@ -3102,9 +3136,10 @@ export function App({ resumeSessionId, initialScreen }) {
3102
3136
  }
3103
3137
  if (overlay.kind === "model") {
3104
3138
  const id = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
3105
- // Only the current provider's models, in display order (default first, rest A→Z).
3106
- // Switching provider is /provider's job.
3107
- const models = orderedModelList(id);
3139
+ // One provider's models, in display order (default first, rest A→Z): the one in use,
3140
+ // unless `/model <provider> …` named another.
3141
+ const models = overlay.providerId ? orderedModelsOf(overlay.providerId) : orderedModelList(id);
3142
+ const pickerProvider = overlay.providerId ? allProviders().find((p) => p.id === overlay.providerId)?.label ?? "" : providerOf(id).label;
3108
3143
  // The two facts that decide the choice and are nowhere else on the screen: how much
3109
3144
  // it can hold, and whether it can see an image you attach. They lead the description
3110
3145
  // because the row truncates from the RIGHT — put behind the prose they would be the
@@ -3118,7 +3153,7 @@ export function App({ resumeSessionId, initialScreen }) {
3118
3153
  description: m.description ? `${facts} · ${m.description}` : facts,
3119
3154
  };
3120
3155
  });
3121
- return (_jsx(Picker, { title: `Choose a ${providerOf(id).label} model`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, models.findIndex((m) => m.id === id)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
3156
+ return (_jsx(Picker, { title: `Choose a ${pickerProvider} model`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, models.findIndex((m) => m.id === id)), initialFilter: overlay.filter, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
3122
3157
  }
3123
3158
  if (overlay.kind === "think") {
3124
3159
  const model = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;