mindweave 2.4.7 → 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
@@ -25,7 +25,6 @@ It is built lean on purpose. Most of a coding agent's context budget goes on sca
25
25
  the model never needed. Mindweave keeps prompts thin and leaves the room for the model to
26
26
  reason about your code.
27
27
 
28
- Questions are welcome at zallinimann@gmail.com.
29
28
 
30
29
  ## Mindweave 1
31
30
 
@@ -71,8 +70,9 @@ else is configured inside a session.
71
70
 
72
71
  Short version, one line each. The depth is in the linked pages.
73
72
 
74
- - **14 providers, 52 models, one key** — DeepSeek, Anthropic, OpenAI, Gemini, xAI,
75
- 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
76
76
  use is loaded. Switch with `/provider` and `/model`; remembered per project.
77
77
  [PROVIDERS.md](src/drivers/PROVIDERS.md)
78
78
  - **Real tools** — read and edit files, multi-file edits, ripgrep search, a shell with
@@ -137,6 +137,8 @@ running it on real projects and fixing what breaks, and nearly everything in the
137
137
  changelog started as a failure someone watched happen. [What to include, and what is
138
138
  especially worth reporting.](CONTRIBUTING.md#reporting-a-bug)
139
139
 
140
+ Questions are welcome at zallinimann@gmail.com
141
+
140
142
  ## License
141
143
 
142
144
  [Apache License 2.0](LICENSE).
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";
@@ -79,7 +79,7 @@ import { applySelection, ctrlCShouldCopy, isEmpty, selectionText } from "./selec
79
79
  import { latestScreen, repaintOverlay, setFrameOverlay } from "./framebuffer/overlay.js";
80
80
  import { requestFullRepaint } from "./framebuffer/writer.js";
81
81
  import { copyToClipboard } from "./clipboard.js";
82
- import { chatLayout, reflowScroll } from "./chatAnchor.js";
82
+ import { chatLayout, reflowScroll, growScroll } from "./chatAnchor.js";
83
83
  import { growFill, INLINE_LIVE_RESERVE, NO_FILL } from "./startupFill.js";
84
84
  import { setRowsBelowCaret } from "./exitCursor.js";
85
85
  import { caretCell } from "./caretPark.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" },
@@ -254,6 +259,9 @@ export function App({ resumeSessionId, initialScreen }) {
254
259
  /** Reading position captured at a width change, pending the first measurement at the
255
260
  * new width. Null except across that one frame. See the width-change block below. */
256
261
  const reflowFrom = useRef(null);
262
+ /** `contentHeight` as of the last measurement, so growth between renders is a delta
263
+ * the scroll position can be compensated by. See the growth-compensation block below. */
264
+ const prevContentHeight = useRef(null);
257
265
  // Each block's real rendered height, so blocks off screen can be replaced by a
258
266
  // spacer of the exact same size instead of being laid out in full — see
259
267
  // `virtualWindow.ts` for why that is the whole performance story, and why exact
@@ -1169,6 +1177,15 @@ export function App({ resumeSessionId, initialScreen }) {
1169
1177
  const next = reflowScroll(from.scrolled, from.maxScroll, Math.max(0, height - chatRows));
1170
1178
  setScrollUp((s) => (s === next ? s : next));
1171
1179
  }
1180
+ else if (prevContentHeight.current !== null) {
1181
+ // Ordinary growth, not a resize — see growScroll for why this has to hold the
1182
+ // reader's absolute position rather than leave `scrollUp` where it was.
1183
+ const prev = prevContentHeight.current;
1184
+ const next = growScroll(scrollUp, prev, height);
1185
+ if (next !== scrollUp)
1186
+ setScrollUp(next);
1187
+ }
1188
+ prevContentHeight.current = height;
1172
1189
  });
1173
1190
  // Measure each block that was rendered without a known height yet, so the next
1174
1191
  // frame can replace it with an exact spacer when it scrolls out of view.
@@ -2001,7 +2018,10 @@ export function App({ resumeSessionId, initialScreen }) {
2001
2018
  // the provider's own sentence quoted inside it. Anything else — a malformed
2002
2019
  // request, a bug of ours — stays loud, which is the point of classifying
2003
2020
  // narrowly. See drivers/providerError.ts.
2004
- 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));
2005
2025
  if (refusal)
2006
2026
  enqueueReveal({ type: "notice", title: refusal.title, body: refusal.body });
2007
2027
  else
@@ -2136,15 +2156,28 @@ export function App({ resumeSessionId, initialScreen }) {
2136
2156
  }
2137
2157
  // Apply a /model pick: switch model (clamping reasoning to a valid level), persist
2138
2158
  // the choice for this project, and confirm.
2139
- async function applyModel(index) {
2159
+ async function applyModel(index, providerId) {
2140
2160
  const s = session.current;
2141
- // Index into the SAME list the picker rendered — the current provider's models in
2142
- // display order, not every model everywhere. Indexing a differently-ordered list
2143
- // here would silently select a different model than the one on screen, and would
2144
- // type-check perfectly.
2145
- 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];
2146
2167
  if (!s || !choice)
2147
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
+ }
2148
2181
  s.modelConfig = withModel(s.modelConfig, choice.id);
2149
2182
  await saveModelConfig(s.cwd, s.modelConfig);
2150
2183
  note(`model → ${modelLabel(s.modelConfig.model)} · ${thinkLabel(s.modelConfig)}`);
@@ -2190,6 +2223,10 @@ export function App({ resumeSessionId, initialScreen }) {
2190
2223
  s.modelConfig = withModel(s.modelConfig, model);
2191
2224
  await saveModelConfig(s.cwd, s.modelConfig);
2192
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);
2193
2230
  }
2194
2231
  // Apply a /think pick for the current model: set thinking + effort, persist, confirm.
2195
2232
  async function applyThink(index) {
@@ -2244,7 +2281,7 @@ export function App({ resumeSessionId, initialScreen }) {
2244
2281
  else if (o.kind === "provider")
2245
2282
  void applyProvider(index);
2246
2283
  else if (o.kind === "model")
2247
- void applyModel(index);
2284
+ void applyModel(index, o.providerId);
2248
2285
  else if (o.kind === "think")
2249
2286
  void applyThink(index);
2250
2287
  else if (o.kind === "screen") {
@@ -2608,12 +2645,12 @@ export function App({ resumeSessionId, initialScreen }) {
2608
2645
  setOverlay({ kind: "sessions", items: sessions });
2609
2646
  return;
2610
2647
  }
2611
- // Both pickers refresh discovered providers first, so a model pulled since the
2612
- // 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
2613
2650
  // picker renders from the list, and opening on a stale one that then changes
2614
2651
  // under the cursor is the exact "two-stage reveal" the UI work removed.
2615
2652
  if (name === "/provider") {
2616
- await refreshModels();
2653
+ await refreshModels({ maxAgeMs: DISCOVERY_TTL_MS });
2617
2654
  setOverlay({ kind: "provider" });
2618
2655
  return;
2619
2656
  }
@@ -2629,12 +2666,21 @@ export function App({ resumeSessionId, initialScreen }) {
2629
2666
  // getting the picker anyway — which is what happened before, because the argument
2630
2667
  // was dropped without a word — reads as the app not having heard you.
2631
2668
  if (name === "/model") {
2632
- await refreshModels();
2669
+ await refreshModels({ maxAgeMs: DISCOVERY_TTL_MS });
2633
2670
  if (arg) {
2634
- 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");
2635
2678
  if (picked.kind === "error")
2636
2679
  return say(picked.message);
2637
- 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);
2638
2684
  return;
2639
2685
  }
2640
2686
  setOverlay({ kind: "model" });
@@ -2663,7 +2709,7 @@ export function App({ resumeSessionId, initialScreen }) {
2663
2709
  if (name === "/think") {
2664
2710
  if (arg) {
2665
2711
  const picked = resolveChoice(arg, thinkLevels(s.modelConfig.model), "reasoning level");
2666
- if (picked.kind === "error")
2712
+ if (picked.kind !== "match")
2667
2713
  return say(picked.message);
2668
2714
  await applyThink(picked.index);
2669
2715
  return;
@@ -3090,9 +3136,10 @@ export function App({ resumeSessionId, initialScreen }) {
3090
3136
  }
3091
3137
  if (overlay.kind === "model") {
3092
3138
  const id = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
3093
- // Only the current provider's models, in display order (default first, rest A→Z).
3094
- // Switching provider is /provider's job.
3095
- 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;
3096
3143
  // The two facts that decide the choice and are nowhere else on the screen: how much
3097
3144
  // it can hold, and whether it can see an image you attach. They lead the description
3098
3145
  // because the row truncates from the RIGHT — put behind the prose they would be the
@@ -3106,7 +3153,7 @@ export function App({ resumeSessionId, initialScreen }) {
3106
3153
  description: m.description ? `${facts} · ${m.description}` : facts,
3107
3154
  };
3108
3155
  });
3109
- 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 }));
3110
3157
  }
3111
3158
  if (overlay.kind === "think") {
3112
3159
  const model = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;