## Features
- Add Blackbox provider with `bb` alias (#1143)
- Add Xiaomi token plan provider
- Enhance model select modal UX + modal traffic lights (#1111)
- Default Usage dashboard period to Today (#1141)
## Fixes
- Fix Cowork model selection and Windows CLI packaging (#1129)
- Update provider name retrieval for compatibility provider (#1135)
- Update JWT_SECRET handling
Cherry-picked from upstream PR #1129 + local improvements:
- dedupe inline remove-model handler -> use handleRemoveModel
- add .next-cli-build/ and cli/.build-home/ to .gitignore
* feat(model-select-modal): highlight added models and support bulk selection
- Add addedModelValues prop to highlight already-added models with primary color
- Sort models alphabetically per provider, with added models floated to top
- Replace green highlight with primary brand color (orange #E56A4A)
- Use check icon (10px) inline with model name instead of check_circle
- Replace Done button with info bar explaining click-to-toggle behavior
- Add ProviderIcon to provider group headers replacing colored dot
- Import ProviderIcon, remove unused Button import
* feat(cli-tools): wire addedModelValues, onDeselect, and auto-save to model select modals
- Pass selectedModels as addedModelValues to ModelSelectModal in OpenCode and Copilot cards
- Add onDeselect handler to remove model from list on second click
- Set closeOnSelect=false to allow bulk model selection
- Remove manual setModalOpen(false) from onSelect callbacks
- Add saveModels() silent auto-save triggered on modal close (OpenCodeToolCard)
- Use useRef to track latest selectedModels in closure-safe way
* feat(modal): functional traffic light close button with hover icon and tooltip
- Make red dot a clickable button that closes the modal
- Show ✕ icon inside red dot on hover via group-hover opacity transition
- Gray out yellow and green dots (cursor-not-allowed, no tooltip)
- Increase dot size from w-3 h-3 to w-4 h-4
- Add Tooltip with brand-matched color #FF5F56 on red dot
- Remove X close button from modal header
* feat(tooltip): add color prop for themed tooltip backgrounds
* feat(i18n): add translations for model select info bar and close tooltip
- Add 'Click to add, click again to remove. Changes are saved automatically.' to all 32 locales
- Add 'Close' translation to all 32 locales
* fix(ui): address code review feedback on modal UX and auto-save
- Modal: remove showCloseButton prop, use showTrafficLights for header
condition, hide traffic lights on mobile (hidden md:flex), add mobile
X button (md:hidden) with aria-label, add aria-label and title on
traffic light close button
- OpenCodeToolCard: validate activeModel membership before saving —
fallback to models[0] or empty string; clear/reassign activeModel
on deselect when removed model was the active one
- CopilotToolCard: add useRef + selectedModelsRef, add saveModels()
using /api/cli-tools/copilot-settings, wire auto-save on modal close
- ModelSelectModal: fix JSX formatting — separate info bar closing div
from Search comment onto its own line
* feat(usage): add Today period option to Usage & Analytics
Bổ sung lựa chọn Today vào bộ lọc thời gian của trang Usage & Analytics
(trước đây chỉ có 24h, 7D, 30D, 60D).
Khác biệt với 24h:
- 24h: cuộn 24 giờ trước → hiện tại
- Today: cố định từ 00:00 hôm nay (giờ local) → hiện tại
Thay đổi:
- page.js, UsageStats.js: thêm option Today vào danh sách PERIODS,
đổi grid mobile từ 4 cột sang 5 cột để fit option mới.
- api/usage/stats, api/usage/chart: cho phép giá trị period today.
- usageRepo.js:
+ getUsageStats: dùng nhánh live history khi period = today,
cutoff lấy từ 00:00 hôm nay theo local time.
+ getChartData: thêm 24 bucket theo giờ từ 00:00 → 23:59 hôm nay.
* feat(usage): đặt mặc định period là Today khi mở dashboard/usage
Người dùng thường quan tâm usage trong ngày hôm nay nhiều hơn 7 ngày,
nên hiển thị Today ngay khi vừa mở trang để giảm 1 thao tác chuyển period.
Đồng bộ luôn fallback trong UsageStats để giữ nhất quán khi component
dùng standalone.
- Persistent raw mode across menus avoids per-prompt latency
- Suspend raw temporarily for line-buffered text input
- Update CHANGELOG v0.4.41
Co-authored-by: Cursor <cursoragent@cursor.com>
The visibility, copy, and delete buttons in the API Keys list used , which never reveals on touch devices because they don't fire :hover. Switch all three to — visible by default on mobile, hover-revealed on sm+ — matching the pattern already used in providers/ and media-providers/
Co-authored-by: Muhammad Ridwan Ramadhan <ridwanramadhan8888@gmail.com>
Bổ sung lựa chọn Today vào bộ lọc thời gian của trang Usage & Analytics
(trước đây chỉ có 24h, 7D, 30D, 60D).
Khác biệt với 24h:
- 24h: cuộn 24 giờ trước → hiện tại
- Today: cố định từ 00:00 hôm nay (giờ local) → hiện tại
Thay đổi:
- page.js, UsageStats.js: thêm option Today vào danh sách PERIODS,
đổi grid mobile từ 4 cột sang 5 cột để fit option mới.
- api/usage/stats, api/usage/chart: cho phép giá trị period today.
- usageRepo.js:
+ getUsageStats: dùng nhánh live history khi period = today,
cutoff lấy từ 00:00 hôm nay theo local time.
+ getChartData: thêm 24 bucket theo giờ từ 00:00 → 23:59 hôm nay.
* fix(autostart): resolve cli.js path locally, register with launchctl, verify state
The current `cli/src/cli/tray/autostart.js` is silently broken on every nvm
install (and likely Volta / asdf / Homebrew / user-prefix npm) and on every
npm version >= 9. Enabling auto-start from the tray menu writes a launchd
plist that references a non-existent script; the menu then reports
"✓ Auto-start Enabled" because the existence check only verifies the file is
on disk. On the next OS boot launchd fails with MODULE_NOT_FOUND. None of
this surfaces to the user.
This rewrite addresses the four problems reported in #1082:
1. `npm bin -g` was removed in npm 9. autostart.js called it as a primary
resolution mechanism and silently fell back to a hardcoded
`/usr/local/lib/node_modules/9router/cli.js` path on failure. Replaced
with a `getCliJsPath()` helper that tries (in order): the explicit
`cliPath` argument, `process.argv[1]` when it's our own cli.js, and a
path computed relative to autostart.js's own location (since this file
always lives at `<pkg>/src/cli/tray/autostart.js`, cli.js is three
levels up regardless of install layout). Returns null on no match.
2. The `/usr/local/...` fallback was outright wrong for nvm/Volta/asdf
installs. Dropped entirely. If no candidate resolves, return false
instead of writing a plist pointing at a missing script.
3. `enableMacOS()` never called `launchctl load -w`, only `unload`. The
plist was therefore inert until the next user login, with no signal to
the user that anything was wrong. Now it unloads (defensive, in case of
re-enable) and then loads, so the agent is active in the current session.
4. `isAutoStartEnabled()` on macOS only checked file existence — so the
tray menu reported "✓ Enabled" even when launchd had the agent in a
failed state or hadn't loaded it. Now also runs `launchctl list
${APP_LABEL}` and only returns true if launchd recognizes the label.
Additional changes for robustness:
- The macOS plist now invokes node + cli.js directly with absolute paths
instead of wrapping in `zsh -l -c "..."`. The shell-wrapper approach
depended on the user's login shell sourcing nvm/PATH correctly, which is
fragile (nvm.sh sourcing varies between users; some setups don't add
node to PATH from a non-interactive login shell).
- `EnvironmentVariables.PATH` in the plist now explicitly includes node's
bin directory plus the standard system paths, so child processes spawned
by cli.js (e.g. the runtime `npm install` calls) can still resolve `npm`
even under launchd's minimal default env.
- Windows and Linux paths were calling `npm bin -g` with the same fallback
problem; both now use `getCliJsPath()` consistently. The Windows VBS
branch is simplified (always run node+cli.js, drop the `9router.cmd`
lookup that depended on the npm prefix path).
- Removed the unused `getStartCommand()` helper that was never imported.
Fixes#1082
* fix(autostart): skip launchctl unload/load when current process is the agent
When the running 9router cli.js was itself spawned by the autostart launchd
agent (after a reboot when autostart was previously enabled), clicking the
tray menu's "Disable Auto-start" item would unload the agent — and that
unload sends SIGTERM to the running process, killing the click handler and
the tray icon before the menu could flip its label.
Add a small `isAgentSelfMacOS()` probe (parses `launchctl list ${label}` and
compares the PID against `process.pid`). When we're the agent itself,
disable skips the unload (the plist file removal is enough to prevent the
agent from starting on the next login) and enable skips the load (the plist
is already loaded under our own PID, the on-disk update is what matters for
next boot). External callers — e.g. enable from a manually-launched 9router
or from a script — still get the full unload/load behavior they need.
Without this, the tray UX after a reboot was: click Disable -> tray icon
silently disappears, no menu label change. Now: click Disable -> label
flips to "Enable Auto-start", tray stays, plist removed; click Enable again
-> label flips back, plist re-created.
Set outputFileTracingRoot back to projectRoot. In Docker, the parent
(monorepoRoot) was /, which caused Next.js to emit server.js at
.next/standalone/app/server.js and pull in /usr, /root, /proc paths,
breaking from /app.
Fixes#1064
Co-authored-by: Muhammad Ridwan Ramadhan <ridwanramadhan8888@gmail.com>
The legacy `systray@1.0.5` package (last published 2018) bundles a 2017
x86_64 Go binary whose Mach-O headers are rejected by modern dyld (macOS
14+ / Apple Silicon). The result on affected systems was that
`9router --tray` (and "Hide to Tray" from the interactive menu) printed
"Router is now running in system tray" but no menubar icon appeared —
the failure was silently swallowed by `catch (err) { return null }`.
This swaps the runtime tray library for `systray2@2.1.4`, which embeds
the maintained getlantern/systray-portable binaries that work on macOS
14+ under Rosetta. Changes:
- hooks/trayRuntime.js: install `systray2@2.1.4` (not `systray@1.0.5`)
into ~/.9router/runtime/node_modules. Always purge the legacy systray
package on every run — its binary is broken on macOS and an AV false
positive on Windows. chmod +x the bundled Go binary in case the npm
tarball drops the executable bit (observed on macOS).
- src/cli/tray/tray.js: resolveSystray() now prefers systray2 with a
fallback to legacy systray for safety. initUnixTray() uses the new
.ready() promise API, surfaces failures to stderr instead of silently
returning null, and sets isTemplateIcon:false so the full-color
icon.png renders correctly (template mode would show a solid white
square because only the alpha channel is used). killTray() passes
false to systray2's kill so it doesn't call process.exit(0) before
the rest of cleanup (server SIGKILL, MITM/tunnel) runs.
- package.json: update the `comment_systray` field to describe the new
package choice.
Fixes#1079
Fallback to default user directory (~/.9router) when configured
DATA_DIR is not writable (EACCES/EPERM). Other errors still throw.
Co-authored-by: Thiên Toán <toanalien@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Fixed variable declaration order in CLIToolsPageClient.js (functions before useEffect)
- Added lazy initialization for useState in BasicChatPageClient.js to read from localStorage
- Reduced ESLint errors by ~23%
Co-authored-by: yuangejiaozhu <leegajone@email.com>
- Replaced hardcoded LINUX_CERT_DIR with dynamic filesystem probing to support Debian, Arch, Fedora, and openSUSE system trust stores.
- Added updateNssDatabases helper to seamlessly inject root certificates directly into browser NSS databases (e.g., ~/.pki/nssdb, ~/.mozilla/firefox).
- Supported standard and snap-based Chrome/Chromium and Firefox installations.
- Made browser cert injection resilient, executing under the current user to prevent file ownership issues, and safely falling back if certutil is absent.
Deepseek API (and likely other providers) reject messages with
role: 'developer' — only accept system, user, assistant, tool.
filterToOpenAIFormat() normalizes content blocks but never touched
message roles, so developer passed through unmodified and caused
400 errors (issue #773).
Fix: add one-line developer → system mapping in filterToOpenAIFormat()
before role-specific logic. This is the common normalization point
called for all targetFormat=openai providers (Deepseek, Groq, Mistral,
Perplexity, Together, Fireworks, Cerebras, xAI, NVIDIA, etc.)
Closes#773