release: v6.9.1 — GLM 5.3 Flash fix, resume from any directory, UI and install fixes - #58
Conversation
The plan-aware default replaced any selection equal to DEFAULT_MODEL_ID (zai/glm-5.3-flash), so explicitly picking that model was treated as "never chosen" and swapped for the catalog's first model. Persist a modelExplicit flag (set by user picks, settings.json and MATTERAI_MODEL; cleared by automatic defaults and plan fallbacks) and only re-resolve a model that was never explicitly chosen, in the TUI and headless mode.
|
✅ Reviewed the changes: Release v6.9.1 adds a |
/resume only listed sessions whose cwd matched the current directory, so a conversation started elsewhere was hard to find. The picker now has a Tab toggle between this directory and all directories (showing where each other-directory session lives), and opens on all directories when there is nothing to resume here. Resuming a session from another directory switches the working directory to it and rebuilds the directory-bound state (project settings, MCP servers, project hook trust), so tools and AGENTS.md match the conversation.
Summary By MatterAI <a href="https://app.matterai.so" target=""_blank">
🔄 What Changed
🔍 Impact of the Change
📁 Total Files ChangedClick to Expand
🧪 Test Added/RecommendedAdded
🔒Security Vulnerabilities
⏳ Estimated code review effortLOW (~7 minutes) Tip Quality Recommendations
♫ Tanka Poem
Sequence DiagramsequenceDiagram
participant User
participant TUI as App / Headless
participant Config as config.json
participant Catalog as Model Catalog
User->>TUI: Select model (e.g., GLM 5.3 Flash)
TUI->>Config: Save model & set modelExplicit = true
Note over TUI,Config: Subsequent Launch
TUI->>Config: Read model & modelExplicit
alt modelExplicit is true
TUI->>Config: Keep user's chosen model
else modelExplicit is false
TUI->>Catalog: Fallback to default plan model
Catalog-->>TUI: Default model
end
|
The Axon Auto, Eido 3.2 and Lumen 4 entries were removed from the model catalog, so the tests that looked them up in BUILTIN_AXON_MODELS (and asserted the old default) always failed. The plan-gate and id-helper tests for those families still pass and are kept.
There was a problem hiding this comment.
🧪 PR Review is completed: Solid implementation of cross-directory session resume with proper MCP/settings/hook reloading on chdir. Main concerns: an unhandled promise rejection on MCP restart, a doubled full-session-directory read in /resume, and Windows path handling in shortDir. Reviewed test/session-picker.test.tsx, test/sessions.test.ts, src/core/sessions.ts, and package.json: no issues found.
Skipped files
CHANGELOG.md: Skipped file pattern
⬇️ Low Priority Suggestions (3)
src/ui/App.tsx (2 suggestions)
Location:
src/ui/App.tsx(Lines 913-916)🟠 Error Handling
Issue: The new MCP manager restart after
process.chdirfiresmanager.start().then(...)with no.catch(). Ifstart()rejects (e.g. malformed MCP config in the resumed directory, connection setup failure), this becomes an unhandled promise rejection, which crashes the Node process by default — killing the whole TUI right after a resume. The siblingoldManager?.stop()call correctly has.catch(() => {}), so this looks like an omission.Fix: Attach a
.catch(() => {})to the start chain, mirroring the stop handling.Impact: A failing MCP setup in the resumed directory degrades gracefully instead of crashing the app mid-resume.
- void manager.start().then(() => { - const pendingMcp = manager.getPendingApproval(); - if (pendingMcp.length > 0) setPendingMcpApproval(pendingMcp); - }); + void manager.start().then(() => { + const pendingMcp = manager.getPendingApproval(); + if (pendingMcp.length > 0) setPendingMcpApproval(pendingMcp); + }).catch(() => {});Location:
src/ui/App.tsx(Lines 1203-1205)🟡 Performance
Issue:
/resumenow callslistSessions(process.cwd())andlistSessions()back to back. Each call re-reads and JSON-parses every session file in the sessions directory — and session files contain full message transcripts, so they can be large. This doubles the disk I/O and parse work on every/resumeinvocation.Fix: Read the directory once and derive the
herelist by filtering oncwd. Note the small tradeoff:herebecomes the local sessions within the global top-25 cap rather than an independently capped top-25; given the picker shows 8 rows, this is negligible.Impact: Halves the I/O and JSON parsing cost of opening the resume picker for users with many/large sessions.
- const notCurrent = (s: SessionData) => s.id !== agentRef.current?.taskId; - const here = listSessions(process.cwd()).filter(notCurrent); - const everywhere = listSessions().filter(notCurrent); + const notCurrent = (s: SessionData) => s.id !== agentRef.current?.taskId; + const everywhere = listSessions().filter(notCurrent); + const here = everywhere.filter((s) => s.cwd === process.cwd());
src/ui/components/SessionPicker.tsx (1 suggestion)
Location:
src/ui/components/SessionPicker.tsx(Lines 27-30)🔵 Portability
Issue:
shortDirhardcodes/as the path separator (dir.startsWith(home + "/"),withHome.split("/")). On Windows, where paths use\, the home-directory~substitution never matches andsplit("/")yields a single segment, so users see full unshortenedC:\Users\...paths in the picker.Fix: Detect the separator from the input path and use it consistently for the home prefix check, splitting, and the ellipsis join.
Impact: Directory labels shorten correctly on Windows while remaining unchanged on POSIX.
- const home = os.homedir() - const withHome = dir === home || dir.startsWith(home + "/") ? "~" + dir.slice(home.length) : dir - const parts = withHome.split("/") - return parts.length > 4 ? "…/" + parts.slice(-3).join("/") : withHome + const home = os.homedir() + const sep = dir.includes("\\") ? "\\" : "/" + const withHome = dir === home || dir.startsWith(home + sep) ? "~" + dir.slice(home.length) : dir + const parts = withHome.split(sep) + return parts.length > 4 ? "…" + sep + parts.slice(-3).join(sep) : withHome
|
✅ Reviewed the changes: Deletion-only cleanup of stale Axon registry tests (DEFAULT_MODEL_ID pin, BUILTIN_AXON_MODELS metadata lookups) that no longer match the live registry; the remaining predicate/plan-gate tests are registry-independent and all retained imports are still used — no issues found in the new code. Note: the changed files contain only these two test deletions; the modelExplicit fix, the v6.9.1 version bump, and the new test/settings-model.test.ts described in the PR body are not among the changed files — worth confirming nothing was left out of the PR before tagging the release. |
There was a problem hiding this comment.
🧪 PR Review is completed: Trivial one-line UI tweak (hint text color dim → info) in SessionPicker; the only concern is that COLORS.info does not appear to be defined in the branding module.
⬇️ Low Priority Suggestions (1)
src/ui/components/SessionPicker.tsx (1 suggestion)
Location:
src/ui/components/SessionPicker.tsx(Lines 121-121)🟡 Type Safety / Runtime Bug Risk
Issue: The new line references
COLORS.info, but a search ofsrc/branding.tsshows noinfokey defined in theCOLORSobject (onlyprimary,dim,accent, etc. are used elsewhere in this file). IfCOLORSis loosely typed (e.g.Record<string, string>),COLORS.inforesolves toundefinedat runtime and Ink silently falls back to the default color — making this change a visual no-op. IfCOLORSis strictly typed, this is a compile error.Fix: Use a nullish fallback to an existing color (or add an
infoentry toCOLORSinsrc/branding.tsif a distinct info color is intended).Impact: Guarantees the hint line actually renders with the intended color instead of silently reverting to the default.
- <Text color={COLORS.info}> + <Text color={COLORS.info ?? COLORS.accent}>
|
Note PR Review SkippedPR review skipped as no relevant changes found due to large diff hunk OR part of a non-reviewable file. 📄Files skipped in review
💡Tips to use MatterAICommand List
|
There was a problem hiding this comment.
🧪 PR Review is completed: Whitespace-filtering changes in agent.ts/App.tsx/rows.tsx are consistent and well-tested, but the assistant message construction can now emit a content: null assistant message with no tool_calls, which some OpenAI-compatible APIs reject. Reviewed src/ui/App.tsx, src/ui/components/rows.tsx, test/agent-context.test.ts, test/session-picker.test.tsx: no other issues found. Note: the modelExplicit flag described in the PR body does not appear in the provided changed files (grep finds no modelExplicit; App.tsx:998 still gates on settings.model !== DEFAULT_MODEL_ID) — please confirm those changes are actually part of this PR.
Skipped files
CHANGELOG.md: Skipped file pattern
⬇️ Low Priority Suggestions (1)
src/core/agent.ts (1 suggestion)
Location:
src/core/agent.ts(Lines 1341-1341)🟡 API Compatibility / Regression Risk
Issue: The old code (
content: assistantText || null) kept whitespace-only text as a string. The new code nulls it unconditionally, so when a model streams whitespace-only content with no tool calls (e.g. an empty/aborted completion), the assistant message is pushed as{ role: "assistant", content: null }with notool_calls. Several OpenAI-compatible providers reject assistant messages that have neither content nor tool_calls ("Invalid value for 'content': expected a string, received null"), breaking the next turn with a 400. The new test only covers the whitespace + tool-call path, wherecontent: nullis valid.Fix: Only collapse to
nullwhen tool calls are present; otherwise keep the original (whitespace) string, matching the previous API-safe behavior.Impact: Prevents a potential hard API failure on whitespace-only, tool-call-free completions while preserving the intended
nullcontent for tool-call turns.- content: assistantText.trim() ? assistantText : null, + content: assistantText.trim() ? assistantText : toolCalls.length > 0 ? null : assistantText,
openai@4 depended on node-fetch@2 (whatwg-url -> tr46 loads the deprecated built-in punycode, DEP0040) and formdata-node@4 (deprecated node-domexception, warned on every install/update). openai@7 has no dependencies and uses native fetch. Only the gateway client uses the SDK at runtime; no code changes were needed.
There was a problem hiding this comment.
🧪 PR Review is completed: The only change visible in this diff is the addition of the openai dependency to package.json, which is not mentioned anywhere in the PR description (the described modelExplicit fix, version bump, and CHANGELOG changes are not present in the provided diff). Flagging the undocumented dependency for confirmation.
Skipped files
CHANGELOG.md: Skipped file patternpackage-lock.json: Skipped file pattern
⬇️ Low Priority Suggestions (1)
package.json (1 suggestion)
Location:
package.json(Lines 55-55)🟠 Undocumented / Possibly Unused Dependency
Issue: This PR adds
"openai": "^7.25.0"as a dependency, but the PR description — which meticulously documents the model-selection fix, version bump, CHANGELOG move, and test additions — makes no mention of an OpenAI SDK integration. The described fix (a persistedmodelExplicitflag) requires no OpenAI SDK, and the repo's OpenAI-compatible gateway support goes through the AI SDK layer (usesAiSdk), which would normally pull@ai-sdk/openai, not the officialopenaipackage.Fix: Confirm this dependency is actually imported somewhere in
src/and belongs in this release. If it was added accidentally (or left over from a local experiment), remove it — an unused direct dependency ships unnecessary install weight and an extra supply-chain surface in a tagged release.Impact: Keeps the v6.9.1 release artifact clean and the PR description accurate; avoids shipping an unexplained third-party SDK to every user.
- "openai": "^7.25.0", +
Summary
/resumecan browse and continue sessions from any directory.openai4 → 7.1. GLM 5.3 Flash reverting
Cause:
zai/glm-5.3-flashis the staticDEFAULT_MODEL_ID. The plan-aware default added in 6.8.6 replaced any selection equal toDEFAULT_MODEL_IDwith the catalog's first model,deepseek/deepseek-v4.1-flash. So explicitly picking GLM 5.3 Flash was indistinguishable from never picking a model. Other models have different ids and were unaffected. This isn't plan- or OS-specific: the catalog flags nofreePlanmodel, so every plan resolves to catalog index 0.Fix: a persisted
modelExplicitflag inconfig.json./modeland picker selections, amodelinsettings.json, andMATTERAI_MODEL.App.tsx) and headless mode.Known limitation: configs saved by earlier versions have no flag. A stored GLM 5.3 Flash is swapped one more time on the first launch after updating, and sticks once the user picks it again.
2. Resume from any directory
/resume(andorbcode --resume) only listed sessions whosecwdmatched the current directory, so a conversation started in another project was hard to find.The picker lists this directory's sessions first. Tab switches to sessions from all directories, each labelled with its (shortened) directory. When there's nothing to resume here, it opens on all directories.
Resuming a session from another directory
chdirs into that session's directory, then reloads:Tools,
AGENTS.mdand relative paths then match the conversation. The header and status bar show the new directory.If the session's directory no longer exists, an error is shown and nothing changes.
listSessions(cwd?)lists every directory whencwdis omitted.Other pickers are unchanged:
/taskdoesn't passallSessions.3. Empty "●" rows
Models often stream whitespace-only content (e.g.
"\n\n") right before a tool call, which rendered as a bare●between "Thought for …" and the tool row. Whitespace-only content is now dropped from:App.tsx),agent.ts,content: null).Blank rows in already-saved sessions render as nothing and take no height (
rows.tsx,rowHeight).4. Deprecation warnings on install/update
DEP0040 punycode:openai@4→node-fetch@2→whatwg-url@5→tr46requires Node's built-inpunycode.npm warn deprecated node-domexception:openai@4→formdata-node@4→node-domexception.openai@7.25.0has no dependencies and uses nativefetch. Onlysrc/api/client.tsuses the SDK at runtime; the other imports are type-only, and the typecheck passed with no code changes.Verified:
npm pack+npm install -ginto a temp prefix gives 180 packages and no npm warnings. 6.9.0 installs 203 and prints thenode-domexceptionwarning.punycodeusers left: no file in the new install loads the built-inpunycode. 6.9.0 has two,tr46andwhatwg-url.orbcode updatefrom a temp-prefix install of 6.8.7 reproduces thenode-domexceptionwarning.The
punycodewarning didn't reproduce on Node 24.15. Node only reports it for some callers and versions, so this is confirmed by the absence of anypunycodeuser in the new install. Updating from ≤6.9.0 can still show the warnings once, because the old version runs the update.npm audit --omit=devreports 11 vulnerabilities (5 low, 3 moderate, 3 high) both before and after this change, inhono,qs,@xmldom/xmldom,fast-uri,ip-addressand@ai-sdk/provider-utils. They are unrelated and not addressed here.Testing
npx tsc -p tsconfig.json --noEmitpasses.test/settings-model.test.ts(6),test/sessions.test.ts(1),test/session-picker.test.tsx(3, rendered with OpenTUI under Bun),rows.tsxfix reverted),axon-*catalog entries (and asserted the old default model) were removed; they failed identically onmain. The plan-gate and id-helper tests for those model families are kept.npm run test:ui(Bun): 16/16. The UI tests only fail undernode --testbecause OpenTUI's native renderer needs Bun;test:uinow includes the picker test.zai/glm-5.3-flash:main, the session ran ondeepseek/deepseek-v4.1-flash;zai/glm-5.3-flash.Release
Bumps
package.json/package-lock.jsonto 6.9.1 and moves the CHANGELOG entry to[6.9.1] - 2026-09-30. After merge:git checkout main && git pull && git tag v6.9.1 && git push origin v6.9.1.