feat: model lists and prices that follow each AI tool, a Settings tab per tool, live lane status (#40)
Pricing: the LiteLLM sync now adds every current Claude / GPT (>= 5) /
Gemini (>= 3) chat model before it is used and keeps its own rows
(model_pricing.source = 'sync', the Auto badge) at the published rate. A
user's row (any PUT) and a deleted pattern (model_pricing_dismissed) are
never touched; a deleted built-in row is not re-seeded at the next start; a
reset re-applies the list at once; retired Claude models are priced only
once used.
Models: GET /api/ai-tools/:id/models reads each tool's own list (Codex's
models_cache.json, Claude's aliases named after the newest version plus
priced versions, a catalog tool's vendor rows plus the models its sessions
used). A catalog `model: {flag, vendor?}` field passes `<flag> <id>`
(validated: never a flag or shell text); a per-tool default model starts
each run, and a resume keeps the session's own model.
Settings: every installed, enabled catalog tool without its own tab gets
one (AiToolConfig.tsx): path, homepage, Update, sessions/cost/lanes,
default model, vendor pricing, and the config files its release catalog
lists, edited over the opened sha, syntax-checked, backed up, written
atomically; credential-looking names refused. Antigravity keeps its hooks
card there.
Lanes: a just-started run no longer reads "dead" (age counts the run's
start while the old session is still bound); a second dashboard on the
same database re-broadcasts the lanes the other changed (PRAGMA
data_version, every 2 s); a lane start with a bad model answers 400.
DB: every migration runs under a cross-process lock (<db>-migrate.lock), so
two dashboards started at once on a fresh database both come up.
This commit is contained in:
@@ -10,6 +10,7 @@ import { MemoryRouter } from "react-router-dom";
|
||||
import { LaneConsolePane } from "../LaneConsolePane";
|
||||
import { api } from "../../../lib/api";
|
||||
import type { Lane } from "../../../lib/types";
|
||||
import { __resetAiTools } from "../../../hooks/useAiTools";
|
||||
|
||||
vi.mock("../TerminalView", () => ({
|
||||
// The exit button stands in for the server's `exit` frame (a double Ctrl+C
|
||||
@@ -28,6 +29,10 @@ vi.mock("../../../lib/api", () => ({
|
||||
action: vi.fn(),
|
||||
list: vi.fn(),
|
||||
},
|
||||
aiTools: {
|
||||
list: vi.fn().mockResolvedValue({ default: "claude", tools: [] }),
|
||||
models: vi.fn().mockResolvedValue({ flag: true, models: [], defaultModel: null }),
|
||||
},
|
||||
run: {
|
||||
list: vi.fn().mockResolvedValue({ items: [] }),
|
||||
history: vi.fn().mockResolvedValue({ items: [] }),
|
||||
@@ -36,7 +41,6 @@ vi.mock("../../../lib/api", () => ({
|
||||
},
|
||||
},
|
||||
RUN_MODEL_CHOICES: [],
|
||||
RUN_MODEL_CHOICES_CODEX: [],
|
||||
RUN_EFFORT_CHOICES: [],
|
||||
}));
|
||||
|
||||
@@ -136,6 +140,31 @@ describe("LaneConsolePane", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("starts a run on the tool's default model from AI Tools settings", async () => {
|
||||
__resetAiTools();
|
||||
(api.aiTools.list as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
default: "claude",
|
||||
tools: [{ id: "claude", label: "Claude Code", defaultModel: "sonnet", modelFlag: true }],
|
||||
});
|
||||
(api.lanes.action as ReturnType<typeof vi.fn>).mockResolvedValue({ lane: LANE });
|
||||
render(<LaneConsolePane {...baseProps()} />);
|
||||
fireEvent.change(screen.getByPlaceholderText(/type to search/i), {
|
||||
target: { value: "/workspace/a" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText(/ask claude/i), { target: { value: "go" } });
|
||||
await waitFor(() => expect(api.aiTools.list).toHaveBeenCalled());
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
fireEvent.click(screen.getByRole("button", { name: /^run$/i }));
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.action).toHaveBeenCalledWith(
|
||||
1,
|
||||
"start",
|
||||
expect.objectContaining({ model: "sonnet" })
|
||||
)
|
||||
);
|
||||
__resetAiTools();
|
||||
});
|
||||
|
||||
it("returns to the setup form when the pane exits, without a New run click", async () => {
|
||||
(api.lanes.action as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
lane: { ...LANE, run_id: "run-1" },
|
||||
|
||||
Reference in New Issue
Block a user