Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ Release tags use the form `vX.Y.Z` and match `package.json`. GitHub Releases car

## [Unreleased]

## [0.5.6] - 2026-07-20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New changelog version shows as broken/literal link with a stale comparison link

A new release section is added to the changelog (## [0.5.6] at CHANGELOG.md:12) without adding the matching reference-style link at the bottom, and the top comparison link still points at the previous version, so the new version renders as plain text and the comparison link is stale.
Impact: Readers of the changelog see a broken/non-clickable version entry and an out-of-date "unreleased" comparison link.

Keep a Changelog reference-link convention not followed

The file follows the Keep a Changelog format where every ## [x.y.z] heading has a corresponding reference definition at the bottom (see CHANGELOG.md:84-91). The PR adds the ## [0.5.6] - 2026-07-20 heading but does not add a [0.5.6]: https://github.com/gitcommit90/rerouted/releases/tag/v0.5.6 definition, so the shortcut reference link [0.5.6] resolves to nothing and renders literally. Additionally [Unreleased]: .../compare/v0.5.5...HEAD (CHANGELOG.md:84) should be updated to compare against v0.5.6.

Prompt for agents
In CHANGELOG.md, the newly added version heading `## [0.5.6] - 2026-07-20` needs a corresponding reference-style link definition at the bottom of the file to match the Keep a Changelog convention every other version follows. Add a line `[0.5.6]: https://github.com/gitcommit90/rerouted/releases/tag/v0.5.6` alongside the other version links (around CHANGELOG.md:84-91), and update the `[Unreleased]` comparison link from `compare/v0.5.5...HEAD` to `compare/v0.5.6...HEAD`.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


### Fixed

- Opening a route now replaces the routes list with a full editor instead of burying an edit panel below the grid with no scroll.
- Removed the laggy route-card lift animation so clicks feel immediate.

## [0.5.5] - 2026-07-20

### Added
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@gitcommit90/rerouted",
"productName": "ReRouted",
"version": "0.5.5",
"version": "0.5.6",
"description": "A local AI router for connected accounts, models, named routes, and automatic fallback.",
"author": "gitcommit90",
"license": "MIT",
Expand Down
71 changes: 55 additions & 16 deletions src/renderer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1746,7 +1746,18 @@ function beginComboEdit(combo) {
model: member.model || member.upstreamModel,
})),
};
renderCombos();
renderCombos({ focusEditor: true });
}

function focusRouteEditor() {
const editorEl = view.querySelector(".route-editor");
if (!editorEl) return;
editorEl.scrollIntoView({ block: "start", behavior: "auto" });
const name = $("#c-name");
if (name) {
name.focus({ preventScroll: true });
if (typeof name.select === "function") name.select();
}
}

function syncComboDraft() {
Expand All @@ -1767,7 +1778,7 @@ function routeAccountLabel(account) {
return account.name;
}

function renderCombos() {
function renderCombos(options = {}) {
if (blockSensitiveRenderIfLocked()) return;
const combos = state.combos || [];
const models = flatModels();
Expand All @@ -1787,15 +1798,14 @@ function renderCombos() {
editor.pickerModelId = null;
}

view.innerHTML = `
${pageHeader("Routing", "Routes", "Give clients one memorable model ID while ReRouted handles account and provider failover.", '<button type="button" class="btn btn-primary btn-sm" id="btn-new-route">New route</button>')}
const listMarkup = `
<div class="route-card-grid">
${
combos.length
? combos
.map(
(c, index) => `
<article class="route-card">
<article class="route-card${editor && (editor.id === (c.storageId || c.id) || (!editor.id && !c.storageId && !c.id)) ? " is-active" : ""}">
<button type="button" class="route-card-hit" data-edit-index="${index}" aria-label="Edit route ${esc(comboRouteId(c))}"></button>
<div class="route-card-top">
<span class="strategy-badge">${c.strategy === "round-robin" ? "Round robin" : "Fallback"}</span>
Expand All @@ -1804,17 +1814,21 @@ function renderCombos() {
<div class="route-name">${esc(comboRouteId(c))}</div>
<div class="route-summary">${c.strategy === "round-robin" ? "Rotates every request" : "Fills in order"} · ${(c.members || []).length} member${(c.members || []).length === 1 ? "" : "s"}</div>
<div class="route-nodes" aria-hidden="true">${(c.members || []).slice(0, 5).map((_, index) => `${index ? '<span class="route-node-line"></span>' : ""}<span class="route-node">${index + 1}</span>`).join("")}${(c.members || []).length > 5 ? '<span class="route-node-line"></span><span class="route-node">+</span>' : ""}</div>
<div class="route-card-action" aria-hidden="true">Edit route <span>→</span></div>
<div class="route-card-action" aria-hidden="true">${editor && (editor.id === (c.storageId || c.id)) ? "Editing" : "Open"} <span>→</span></div>
</article>`
)
.join("")
: `<div class="empty">No routes yet. Create a memorable model ID and choose where requests should go.</div>`
}
</div>
${
editor
? `<section class="action-panel route-editor">
<div class="action-panel-head"><div class="eyebrow">${editor.id ? "Edit route" : "New route"}</div><div class="action-panel-title">${editor.id ? esc(editor.name || "Route") : "Build a route"}</div><div class="action-panel-sub">The model ID below is what clients see in <span class="mono">/v1/models</span>.</div></div>
</div>`;

const editorMarkup = editor
? `<section class="action-panel route-editor" id="route-editor">
<div class="action-panel-head">
<div class="eyebrow">${editor.id ? "Edit route" : "New route"}</div>
<div class="action-panel-title">${editor.id ? esc(editor.name || "Route") : "Build a route"}</div>
<div class="action-panel-sub">The model ID below is what clients see in <span class="mono">/v1/models</span>.</div>
</div>
<div class="label">Model ID</div>
<input class="input" id="c-name" placeholder="coding-fast" value="${esc(editor.name)}" />
<div class="label">Routing behavior</div>
Expand Down Expand Up @@ -1845,17 +1859,39 @@ function renderCombos() {
<label class="route-picker-field" for="c-add-model"><span class="label">Model</span><select class="select" id="c-add-model" ${editor.pickerAccountId ? "" : "disabled"}><option value="">${editor.pickerAccountId ? "Choose a model…" : "Choose an account first…"}</option>${pickerModels.map((model) => `<option value="${esc(model.upstreamModel)}" ${model.upstreamModel === editor.pickerModelId ? "selected" : ""}>${esc(model.name)} · ${esc(model.upstreamModel)}</option>`).join("")}</select></label>
<button type="button" class="btn btn-secondary btn-sm route-picker-add" id="btn-add-member" ${editor.pickerAccountId && editor.pickerModelId ? "" : "disabled"}>Add to route</button>
</div>
<div class="btn-row"><button type="button" class="btn btn-secondary" id="btn-cancel-edit">Cancel</button><button type="button" class="btn btn-primary" id="btn-create">${editor.id ? "Save route" : "Create route"}</button></div>
<div class="btn-row"><button type="button" class="btn btn-secondary" id="btn-cancel-edit">Back to routes</button><button type="button" class="btn btn-primary" id="btn-create">${editor.id ? "Save route" : "Create route"}</button></div>
</section>`
: ""
}
: "";

// When editing, replace the list with the editor (full page take-over). No buried bottom panel.
view.innerHTML = `
${pageHeader(
"Routing",
editor ? (editor.id ? editor.name || "Edit route" : "New route") : "Routes",
editor
? "Change the model ID, order, and failover for this route."
: "Give clients one memorable model ID while ReRouted handles account and provider failover.",
editor
? '<button type="button" class="btn btn-secondary btn-sm" id="btn-back-routes">← Routes</button>'
: '<button type="button" class="btn btn-primary btn-sm" id="btn-new-route">New route</button>'
)}
${editor ? editorMarkup : listMarkup}
`;
$("#btn-new-route").onclick = () => beginComboEdit(null);

const backToList = () => {
comboDraft = null;
renderCombos();
};
const backBtn = $("#btn-back-routes");
if (backBtn) backBtn.onclick = backToList;
const newBtn = $("#btn-new-route");
if (newBtn) newBtn.onclick = () => beginComboEdit(null);
view.querySelectorAll("button[data-edit-index]").forEach((btn) => {
btn.onclick = () => beginComboEdit(combos[Number(btn.dataset.editIndex)]);
});
view.querySelectorAll("button[data-del-index]").forEach((btn) => {
btn.onclick = async () => {
btn.onclick = async (event) => {
event.stopPropagation();
const combo = combos[Number(btn.dataset.delIndex)];
const id = combo?.storageId || combo?.id;
if (!id) return;
Expand Down Expand Up @@ -1984,6 +2020,9 @@ function renderCombos() {
await refresh();
renderCombos();
};
if (options.focusEditor) {
requestAnimationFrame(() => focusRouteEditor());
}
}

let statsPeriod = "24h";
Expand Down
17 changes: 15 additions & 2 deletions src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1593,14 +1593,18 @@ summary:focus-visible,
background:
linear-gradient(145deg, rgba(239, 91, 42, 0.08), transparent 58%),
var(--paper-strong);
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
transition: border-color 80ms ease, box-shadow 80ms ease;
}

.route-card:hover,
.route-card:focus-within {
border-color: rgba(239, 91, 42, 0.55);
box-shadow: 0 8px 20px rgba(58, 49, 35, 0.08);
transform: translateY(-1px);
}

.route-card:active {
border-color: rgba(239, 91, 42, 0.75);
box-shadow: none;
}

.route-card-hit {
Expand All @@ -1613,6 +1617,15 @@ summary:focus-visible,
cursor: pointer;
}

.route-card-hit:active {
background: rgba(239, 91, 42, 0.04);
}

.route-editor {
margin: 0 0 0 7px;
animation: none;
}

.route-card-top {
display: flex;
position: relative;
Expand Down
Loading