mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
Merge branch 'feat/local-model-harness-choice' into feat/local-model-skill-agents
This commit is contained in:
commit
d04fb63837
5 files changed
+117
-12
No files matched your search
@@ -1796,7 +1796,7 @@ A probe that fails is an answer, not an error: a server that is not on macOS, or
|
||||
- `folder`: the absolute folder everything of this feature is kept in.
|
||||
- `library[]`: the models on this machine, the model in use first. Empty with `idle` (no runtime is there to ask). Each row: `id` (the name the runtime lists, `name:tag`), `label`, `context?` (the context it was checked with), `sizeBytes?` and `sizeGb?` (its size on disk as the runtime reports it, else as recorded when it was checked), `active` (it is the model in use), `verified` (Codeman checked it), `status` and `actions[]`. `status` is `ready`, `changed` (checked, and the runtime now reports another digest under the name), `missing` (checked, and the runtime no longer lists it) or `unverified` (the runtime lists it and Codeman never checked it: a model an earlier Remove kept, the model of a job that has not passed its check, or one another program pulled through the runtime's port). `actions` is what the server accepts for THAT row right now, out of `use` and `delete`; `delete` needs the running runtime, and a name that is not a plain `name:tag` is listed with no action.
|
||||
- `store?`: `{ bytes, gb }`, what the model store takes on disk (the files of its blob folder, so shared layers count once and an interrupted download is included). Present in every state once it was measured; cached for about thirty seconds.
|
||||
- `harnesses[]`: the agent CLIs a session on the model in use can be started with, the default one first, then by the catalogue's rank. The agent is chosen per session: a session is started with `mode` set to a row's `cli` and `customModel: { endpointId, modelId }`. Empty in every state but `installed`. Each row: `cli`, `cliLabel`, `default` (true on the row `install.cli` names), `status`, `installCommand?` (with `installable` and `installing`: the literal command `POST .../agent` runs), `slowFirstTurn?` (the first answer takes minutes), `readyMark?` (a literal without white space that the agent's composer draws once it takes input: what a script waits for on `GET /api/sessions/:id/wait-output` before it sends the first prompt; absent for an agent with no measured mark), `error?` (`{ message, detail? }`: the last install of this agent through `POST .../agent` failed; kept in memory only) and `actions[]` (`start-session`, `install`). `status` is `ready` (it can be started, the answer the Run menu gets), `installable` (not on this machine, and `POST .../agent` installs it), `installing` (that install is running; the model stays as it is) or `setup-needed` (not startable, and one command does not make it so: DeepSeek Harness without a terminal profile, or a command that needs an administrator; set it up from the Run menu or a terminal).
|
||||
- `harnesses[]`: the agent CLIs a session on the model in use can be started with, the default one first, then by the catalogue's rank. The agent is chosen per session: a session is started with `mode` set to a row's `cli` and `customModel: { endpointId, modelId }`. Empty in every state but `installed`. Each row: `cli`, `cliLabel`, `default` (true on the row `install.cli` names), `status`, `installCommand?` (with `installable` and `installing`: the literal command `POST .../agent` runs), `slowFirstTurn?` (the first answer takes minutes), `readyMark?` (a literal without white space that the agent's composer draws once it takes input: what a script waits for on `GET /api/sessions/:id/wait-output` before it sends the first prompt; absent for an agent with no measured mark. A mark says the composer is there, not that nothing else is: DeepSeek Harness with dsh-TUI 0.19 opens a setup guide whose cursor is its `❯` too until a person has finished it once, so a script reads the pane after the mark), `error?` (`{ message, detail? }`: the last install of this agent through `POST .../agent` failed; kept in memory only) and `actions[]` (`start-session`, `install`). `status` is `ready` (it can be started, the answer the Run menu gets), `installable` (not on this machine, and `POST .../agent` installs it), `installing` (that install is running; the model stays as it is) or `setup-needed` (not startable, and one command does not make it so: DeepSeek Harness without a terminal profile, or a command that needs an administrator; set it up from the Run menu or a terminal).
|
||||
- `actions[]`: exactly what the server accepts right now, out of `install` (with `installed`: add another model), `cancel`, `resume`, `start`, `stop`, `remove` (with `idle` only while `store` holds something), `use` and `delete-model` (only while a row of `library` offers them), `install-agent` (only while a row of `harnesses` offers `install`) and `start-session` (not a route here: start a session on `endpointId`; offered only while the runtime runs, nothing blocks a session and, when `harnesses` is known, at least one agent CLI can be started).
|
||||
|
||||
Error codes (`install.error.code`), each with one next step:
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**Opened outside Codeman** (`foreign-sessions.js`, both home screens): the tmux sessions a human started, listed from `GET /api/mux/foreign` (root CLAUDE.md, Adopted tmux sessions). ONE renderer, `renderForeignSessions(container)`, fills the welcome block and the phone overview's, and every row goes through `textContent`, since a session name and a command line come from someone else's process. ⚠️ It polls only while a home screen is up (`startForeignPolling`/`stopForeignPolling` from `showWelcome()`/`hideWelcome()`) and the tab is visible (`_foreignPollTick()` is the one gate, and `visibilitychange` catches up; the cache TTL sits under the poll interval, so one tab's every tick is a full socket sweep), and the poll is local only: the docker and ssh scan costs one connection per target, so it runs once on the toggle and again on Rescan, and its rows and notes are kept apart (`_foreignWideSessions`, `_foreignWideNotes`) or the next local poll would wipe them. ⚠️ A 403 is a non-admin in multi-user mode, an answer and not a failed poll: `_foreignRefused` stops the polling and shows no block. ⚠️ `closeSession()` re-reads the list after an adopted tab's DELETE, since the home screen's own load races it and the row would say `Go to tab` until the next poll. ⚠️ An adopted tab has no kill outcome to offer, so the close dialog hides the red button and always tears the wrapper down. ⚠️ `session.remote` and `session.docker` are undefined on an adopted session, so a check that hides a row for those two usually needs `session.adopt` as well. Tests: `test/foreign-sessions-ui.test.ts`, `test/foreign-sessions-i18n.test.ts` (harvests every string from the real module).
|
||||
|
||||
**Run a model on this Mac** (`local-model-ui.js`; welcome button `#welcomeLocalModelBtn`, chooser window `#localModelModal`, App Settings > Models row `#localModelSettings`; server rules in the root CLAUDE.md, Local model): ONE window does everything (choose, confirm, progress, paused, failed, installed, no recommendation); the button and the Settings row only open it. ⚠️ The models on this Mac (`install.library`) are ONE list builder, `_localModelLibraryEl()`: on the installed screen always (the model in use first, in the chooser's selected look, each row with the context it was checked with, a state tag and its size on disk), on a paused or failed screen only when a row offers something (the way back to an earlier model), never on the screen that ends an install. A row has "Use" and "Delete" only for what the server lists for THAT row (`row.actions`); Use is a switch with no confirm screen (nothing is downloaded), Delete asks once in place under its row (`.lm-lib-ask`, the one red button, the focus on Cancel), and the asked row keeps its buttons unseen (`.lm-lib-actions-away`) so the sizes do not jump sideways. Under the list: what all downloads take (`install.store`) and "Add a model", which opens the chooser from an installed model; there a row with `candidate.library` says "In use" or "On this Mac" and the main button becomes a disabled "In use" or "Use this model". With nothing installed and a store an earlier Remove kept, the chooser and the nothing-fits screen say what it takes and offer "Delete them" (`_localModelStoreEl()`). ⚠️ A screen that carries the list sets `data-long` on the body, which switches it from the centred short-screen layout (bottom-aligned on a phone) to top-aligned: a centred flex column that overflows cuts off its first lines. ⚠️ The row buttons live in `.lm-lib-actions`, not `.lm-buttons` (the screen's own buttons in the foot), and are a size smaller (`.lm-btn-row`). ⚠️ A job whose steps hold no `model` step is a switch (`localModelIsSwitch()`) and is named one ("Switching to", no download bar). Which screen shows follows `install.state` of `GET /api/local-model`; the page itself holds only the selection and whether the person is on the chooser or the confirm screen. ⚠️ The window is ONE frame for the whole flow: a fixed height on desktop (`.modal-content.local-model-window`), App Settings' boxed close button, the body scrolling between the header and a foot of its own (`#localModelWindowFoot`): every screen's last part (`.lm-foot`, or the `.lm-remove` question) is moved there by `_renderLocalModelWindow()`, so a button never moves away from under the pointer and is never clipped; a test that looks for a button looks in the frame, not the body. The short screens are centred in that frame on desktop and sit above the buttons on a phone (`data-screen` on the body). ⚠️ The memory bar (`.lm-bar`, `role="meter"`) is the one strong element: filled to the selected row's `budgetShare`, the model at its PEAK (the legend says so, and the installed line uses the same words beside what the model takes now, so the two figures are never read as a contradiction), the memory map (`Candidate.memory`: the model, its context and the brief peaks side by side, each named in the legend, on a track of `--text-muted` at about a third) in the feature's own colours (`--lm-*`: white for the feature, turned over to dark ink on the light skins, blue the context, green good, amber tight, orange slow, rose the brief peaks, deeper on the light skins), never `--accent`, which the owner rejected as too much blue (about 3:1 against the window, so the free part can be seen; a border token is 1.5:1 and the inputs' `--bg-input` is the window's own surface on the dark skins), 10px, no gradient or glow, and its width transition is the only motion (off under `prefers-reduced-motion`); a repaint starts each bar at the last width of its kind, so keep `_settleLocalModelBars()` after every body write. The figure beside it is an estimate and says "about" in whole GB (`localModelWholeGb()` on the server's text); only the download figure keeps tabular figures. ⚠️ The rows are the server's pick first, then the other models by download size, largest first (`localModelCandidates()`): the server's alternatives are its ranking, which no column shows. ⚠️ The download bar means ONE thing, the model download: at the start while the runtime is fetched, gone once the model is in (the step list carries verify, agent and connect); the first paint after an install finished shows "Start a session" alone (`justInstalled`), an error on an installed model is the headline with a grey dot, and Remove is quiet everywhere but red in its own question, never the accent. ⚠️ The window polls the GET once a second WHILE OPEN and never otherwise (no SSE event): `closeLocalModelWindow()` clears the timer, a background tab asks nothing, an unchanged answer repaints nothing, and a tick that moved only the download reLine truncated
|
||||
**Run a model on this Mac** (`local-model-ui.js`; welcome button `#welcomeLocalModelBtn`, chooser window `#localModelModal`, App Settings > Models row `#localModelSettings`; server rules in the root CLAUDE.md, Local model): ONE window does everything (choose, confirm, progress, paused, failed, installed, no recommendation); the button and the Settings row only open it. ⚠️ The models on this Mac (`install.library`) are ONE list builder, `_localModelLibraryEl()`: on the installed screen always (the model in use first, in the chooser's selected look, each row with the context it was checked with, a state tag and its size on disk), on a paused or failed screen only when a row offers something (the way back to an earlier model), never on the screen that ends an install. A row has "Use" and "Delete" only for what the server lists for THAT row (`row.actions`); Use is a switch with no confirm screen (nothing is downloaded), Delete asks once in place under its row (`.lm-lib-ask`, the one red button, the focus on Cancel), and the asked row keeps its buttons unseen (`.lm-lib-actions-away`) so the sizes do not jump sideways. Under the list: what all downloads take (`install.store`) and "Add a model", which opens the chooser from an installed model; there a row with `candidate.library` says "In use" or "On this Mac" and the main button becomes a disabled "In use" or "Use this model". With nothing installed and a store an earlier Remove kept, the chooser and the nothing-fits screen say what it takes and offer "Delete them" (`_localModelStoreEl()`). ⚠️ A screen that carries the list sets `data-long` on the body, which switches it from the centred short-screen layout (bottom-aligned on a phone) to top-aligned: a centred flex column that overflows cuts off its first lines. ⚠️ The row buttons live in `.lm-lib-actions`, not `.lm-buttons` (the screen's own buttons in the foot), and are a size smaller (`.lm-btn-row`). ⚠️ A job whose steps hold no `model` step is a switch (`localModelIsSwitch()`) and is named one ("Switching to", no download bar). Which screen shows follows `install.state` of `GET /api/local-model`; the page itself holds only the selection and whether the person is on the chooser or the confirm screen. ⚠️ The window is ONE frame for the whole flow: a fixed height on desktop (`.modal-content.local-model-window`), App Settings' boxed close button, the body scrolling between the header and a foot of its own (`#localModelWindowFoot`): every screen's last part (`.lm-foot`, or the `.lm-remove` question) is moved there by `_renderLocalModelWindow()`, so a button never moves away from under the pointer and is never clipped; a test that looks for a button looks in the frame, not the body. The short screens are centred in that frame on desktop and sit above the buttons on a phone (`data-screen` on the body). ⚠️ The memory bar (`.lm-bar`, `role="meter"`) is the one strong element: filled to the selected row's `budgetShare`, the model at its PEAK (the legend says so, and the installed line uses the same words beside what the model takes now, so the two figures are never read as a contradiction), the memory map (`Candidate.memory`: the model, its context and the brief peaks side by side, each named in the legend, on a track of `--text-muted` at about a third) in the feature's own colours (`--lm-*`: white for the feature, turned over to dark ink on the light skins, blue the context, green good, amber tight, orange slow, rose the brief peaks, deeper on the light skins), never `--accent`, which the owner rejected as too much blue (about 3:1 against the window, so the free part can be seen; a border token is 1.5:1 and the inputs' `--bg-input` is the window's own surface on the dark skins), 10px, no gradient or glow, and its width transition is the only motion (off under `prefers-reduced-motion`); a repaint starts each bar at the last width of its kind, so keep `_settleLocalModelBars()` after every body write. The figure beside it is an estimate and says "about" in whole GB (`localModelWholeGb()` on the server's text); only the download figure keeps tabular figures. ⚠️ The rows are the server's pick first, then the other models by download size, largest first (`localModelCandidates()`): the server's alternatives are its ranking, which no column shows. ⚠️ The download bar means ONE thing, the model download: at the start while the runtime is fetched, gone once the model is in (the step list carries verify, agent and connect); the first paint after an install finished shows "Start a session" alone (`justInstalled`), an error on an installed model is the headline with a grey dot, and Remove is quiet everywhere but red in its own question, never the accent. ⚠️ The window polls the GET once a second WHILE OPEN and never otherwise (no SSE event): `closeLocalModelWindow()` clears the timer, a background tab asks nothing, an unchanged answer repaints nothing, and a tick that moved only the download reLine truncated
|
||||
|
||||
**Home-screen session order** (`CodemanSessionOrder` in constants.js, pure): BOTH home screens (phone overview, desktop rail) must order rows through this ONE comparator. Rank `needs` → `error` → `waiting` → `working` → `idle` → `done`. ⚠️ The tiebreak flips: states a session is still IN sort oldest-first, states it has STOPPED sort newest-first. ⚠️ The running group keys off `lastSubmitAt`, never `lastActivityAt` (a working pane repaints constantly). ⚠️ A 0 stamp means unknown and sorts last within its state. Final tiebreak is `orderIndex`, so the list never shuffles. The tab strip itself is NOT sorted by this. Test: `test/session-overview-order.test.ts`. → [architecture-invariants#home-screen-session-order](../../docs/architecture-invariants.md#home-screen-session-order)
|
||||
|
||||
|
||||
@@ -70,7 +70,9 @@
|
||||
* "Install <agent>" for one that is not on this Mac (its literal install command is on screen
|
||||
* above the button, so the click is the consent), and nothing to press for one that needs a
|
||||
* setup of its own. An install of an agent runs beside the model: the state stays `installed`,
|
||||
* and another agent can be started meanwhile. The choice is remembered on this device.
|
||||
* and another agent can be started meanwhile. The device remembers the agent a session was
|
||||
* last STARTED with, and each opening of the window selects it while it can be started (a
|
||||
* chip that was only looked at is not a choice, or the window would reopen on "Install").
|
||||
*
|
||||
* ## Polling
|
||||
*
|
||||
@@ -633,15 +635,22 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
/**
|
||||
* The coding agents of the installed model and the one a session would start with: the one
|
||||
* chosen in this window, else the one this device chose last, else the server's default
|
||||
* (its first row). `row` is null when the server lists none.
|
||||
* The coding agents of the installed model and the one a session would start with: the chip
|
||||
* selected in this opening of the window, else the agent this device last STARTED a session
|
||||
* with while it can still be started, else the server's default (its first row). A
|
||||
* remembered agent that is not startable now never wins: the window would open on "Install"
|
||||
* or on nothing to press, with "Start a session" out of sight. `row` is null when the server
|
||||
* lists none.
|
||||
*/
|
||||
_localModelAgent() {
|
||||
const ui = this._localModelWindowState();
|
||||
const rows = localModelAgents(localModelInstall(this._localModelStatus));
|
||||
const wanted = ui.agent || localModelStoredAgent();
|
||||
const row = rows.find((entry) => entry.cli === wanted) || rows.find((entry) => entry.default === true) || rows[0];
|
||||
const stored = ui.agent ? '' : localModelStoredAgent();
|
||||
const row =
|
||||
rows.find((entry) => entry.cli === ui.agent) ||
|
||||
rows.find((entry) => entry.cli === stored && entry.status === 'ready') ||
|
||||
rows.find((entry) => entry.default === true) ||
|
||||
rows[0];
|
||||
return { rows, row: row || null };
|
||||
},
|
||||
|
||||
@@ -680,6 +689,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
busy: false,
|
||||
notice: null,
|
||||
expected: null,
|
||||
// A chip that was only looked at in an earlier opening is not a choice: each opening
|
||||
// starts from the agent last STARTED on this device, else the server's default.
|
||||
agent: null,
|
||||
confirmRemove: false,
|
||||
confirmDelete: null,
|
||||
justInstalled: false,
|
||||
@@ -910,13 +922,16 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (asked) this._focusLocalModelWindow(asked === LOCAL_MODEL_STORE_KEY ? 'store-delete' : `delete:${asked}`);
|
||||
},
|
||||
|
||||
/** Select a coding agent for the next session. Remembered on this device; nothing is sent. */
|
||||
/**
|
||||
* Select a coding agent for the next session. Nothing is sent, and nothing is remembered
|
||||
* yet: a chip can be selected just to see what installing it would run. The device
|
||||
* remembers an agent once a session was started with it.
|
||||
*/
|
||||
selectLocalModelAgent(cli) {
|
||||
const ui = this._localModelWindowState();
|
||||
if (ui.busy || typeof cli !== 'string' || !cli) return;
|
||||
ui.agent = cli;
|
||||
ui.notice = null;
|
||||
localModelStoreAgent(cli);
|
||||
this.renderLocalModel();
|
||||
},
|
||||
|
||||
@@ -967,6 +982,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
const install = localModelInstall(this._localModelStatus);
|
||||
const agent = typeof cli === 'string' && cli ? cli : install.cli;
|
||||
if (typeof agent !== 'string' || typeof install.endpointId !== 'string') return;
|
||||
// The agent a session was started with is the one the next opening selects.
|
||||
if (typeof cli === 'string' && cli) localModelStoreAgent(cli);
|
||||
const settingsOpen = document.getElementById('appSettingsModal')?.classList.contains('active');
|
||||
this.closeLocalModelWindow();
|
||||
if (settingsOpen) this.closeAppSettings?.();
|
||||
|
||||
@@ -510,6 +510,71 @@ describe('the local model window (real Chromium)', () => {
|
||||
expect(overflow).toBeLessThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('the coding agents: chips that wrap inside the window at every width, the install command included', async () => {
|
||||
const agents = [
|
||||
{ cli: 'opencode', cliLabel: 'OpenCode', default: true, status: 'ready', actions: ['start-session'] },
|
||||
{
|
||||
cli: 'claude',
|
||||
cliLabel: 'Claude Code',
|
||||
default: false,
|
||||
status: 'ready',
|
||||
slowFirstTurn: true,
|
||||
actions: ['start-session'],
|
||||
},
|
||||
{
|
||||
cli: 'pi',
|
||||
cliLabel: 'Pi',
|
||||
default: false,
|
||||
status: 'installable',
|
||||
installCommand: 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent',
|
||||
actions: ['install'],
|
||||
},
|
||||
{ cli: 'deepseek', cliLabel: 'DeepSeek', default: false, status: 'setup-needed', actions: [] },
|
||||
];
|
||||
const answer = {
|
||||
...INSTALLED,
|
||||
install: {
|
||||
...INSTALLED.install,
|
||||
harnesses: agents,
|
||||
actions: [...INSTALLED.install.actions, 'install-agent'],
|
||||
},
|
||||
};
|
||||
for (const [width, height] of [
|
||||
[1440, 900],
|
||||
[390, 844],
|
||||
[320, 640],
|
||||
]) {
|
||||
await load(width, height, answer);
|
||||
await open();
|
||||
// The agent that is not on this Mac: its command is the longest line of the block.
|
||||
await p.locator('.lm-agent', { hasText: 'Pi' }).click();
|
||||
const seen = await p.evaluate(() => {
|
||||
const frame = document.querySelector('.local-model-window')!.getBoundingClientRect();
|
||||
const body = document.getElementById('localModelWindowBody')!;
|
||||
const outside = [...document.querySelectorAll('.lm-agent, .lm-agents .lm-command, .lm-buttons button')]
|
||||
.map((el) => el.getBoundingClientRect())
|
||||
.filter((box) => box.width > 0 && (box.left < frame.left - 0.5 || box.right > frame.right + 0.5)).length;
|
||||
const chips = [...document.querySelectorAll<HTMLElement>('.lm-agent')];
|
||||
return {
|
||||
chips: chips.length,
|
||||
outside,
|
||||
sideways: body.scrollWidth - body.clientWidth,
|
||||
checked: document.querySelector('.lm-agent[aria-checked="true"] .lm-agent-name')!.textContent,
|
||||
tabStops: chips.filter((chip) => chip.tabIndex === 0).length,
|
||||
main: document.querySelector('.lm-buttons .wt-btn-primary')!.textContent,
|
||||
};
|
||||
});
|
||||
expect(seen, `${width}px`).toEqual({
|
||||
chips: 4,
|
||||
outside: 0,
|
||||
sideways: 0,
|
||||
checked: 'Pi',
|
||||
tabStops: 1,
|
||||
main: 'Install Pi',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('paints above App Settings when it is opened from there', async () => {
|
||||
await load(1440);
|
||||
await p.evaluate(() => document.getElementById('appSettingsModal')!.classList.add('active'));
|
||||
|
||||
@@ -2898,7 +2898,7 @@ describe('the coding agents of an installed model', () => {
|
||||
expect(frameOf(page.body).querySelectorAll('.lm-buttons .wt-btn-primary')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('starts the session with the selected agent, and this device remembers the choice', async () => {
|
||||
it('starts the session with the selected agent, and this device remembers the agent it started', async () => {
|
||||
const page = mount();
|
||||
const { app } = await opened(page, WITH_AGENTS);
|
||||
const launch = launcher(app);
|
||||
@@ -2907,11 +2907,13 @@ describe('the coding agents of an installed model', () => {
|
||||
// What the catalogue says about its first answer.
|
||||
expect(texts(page.body, '.lm-agent-about .lm-aside')).toEqual(['The first answer can take minutes.']);
|
||||
expect(verbs(app)).toEqual([]);
|
||||
// Looking at a chip is not a choice yet.
|
||||
expect(page.dom.window.localStorage.getItem('codeman-local-model-agent')).toBeNull();
|
||||
press(page.body, 'Start a session');
|
||||
await settle();
|
||||
expect(launch).toHaveBeenCalledWith('claude', 'local-ollama');
|
||||
|
||||
// The window opened again later: the last choice stands, not the default.
|
||||
// The window opened again later: the agent last started stands, not the default.
|
||||
expect(page.dom.window.localStorage.getItem('codeman-local-model-agent')).toBe('claude');
|
||||
const again = page.newApp(ok(WITH_AGENTS));
|
||||
await again.loadLocalModel();
|
||||
@@ -2920,6 +2922,27 @@ describe('the coding agents of an installed model', () => {
|
||||
expect(chosen(page)).toBe('Claude Code');
|
||||
});
|
||||
|
||||
it('a chip that was only looked at never hides Start a session when the window is opened again', async () => {
|
||||
const page = mount();
|
||||
const { app } = await opened(page, WITH_AGENTS);
|
||||
pick(page, 'DeepSeek');
|
||||
expect(buttons(page.body)).toEqual(['Remove', 'Stop']);
|
||||
app.closeLocalModelWindow();
|
||||
app.openLocalModelWindow();
|
||||
await settle();
|
||||
// Back on the default agent, with the main action in reach.
|
||||
expect(chosen(page)).toBe('OpenCode');
|
||||
expect(buttons(page.body)).toEqual(['Remove', 'Stop', 'Start a session']);
|
||||
|
||||
// And an agent that was started once and has since been uninstalled does not win either.
|
||||
page.dom.window.localStorage.setItem('codeman-local-model-agent', 'pi');
|
||||
app.closeLocalModelWindow();
|
||||
app.openLocalModelWindow();
|
||||
await settle();
|
||||
expect(chosen(page)).toBe('OpenCode');
|
||||
expect(buttons(page.body)).toEqual(['Remove', 'Stop', 'Start a session']);
|
||||
});
|
||||
|
||||
it('a remembered agent the server no longer lists falls back to the default', async () => {
|
||||
const page = mount();
|
||||
page.dom.window.localStorage.setItem('codeman-local-model-agent', 'gone');
|
||||
|
||||
Reference in new issue
Block a user