feat(desktop): find the Codeman servers running on this Mac and offer to connect

When a Codeman server of this user ran at an address the app could not open
as its default one, the This Mac tab only named host and port and sent the
user to Another machine to type the address again. A second instance beside
the one that answers (a beta, a `codeman web --port 4000` in a terminal) was
not shown at all.

The look at this Mac now works out an address for every listening socket of
this user's Codeman servers (`found_addresses`, pure): 127.0.0.1 for a bind
to loopback or to every interface, `[::1]` when that is the only loopback
socket, the address itself for a bind to one; `https` and the base path come
from `--https` and `--base-url` on the command line. A server at the default
address with no `--base-url` of its own keeps the default URL, since
install.sh's launchd job has the base path in its environment, where `ps`
does not show it. The default address comes first, then the others by port.

Each address other than the default one is asked once, all at the same time,
with no login. The default probe's answer is reused for its own row. When
nothing answers at the default address, the first found server that answers
is still what Open is for, so each gets the probe's usual 5 s; next to a
Codeman that answers they are only listed and get 2 s. `LocalStatus.found`
replaces `elsewhere`.

The tab lists them: "Codeman is already running on this Mac." with a row
each when none answers at the default address, and "Also running on this
Mac" under Open otherwise. A row has the address, the version and Connect,
or the reason the app cannot open it. Connect fills the address in and runs
the page's one connect(), so the app checks it, uses its saved login and
remembers it like an address typed under Another machine. Nothing connects
by itself, and a refused connect from a row never arms the reopen that Open
has for a starting server.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-07 04:27:16 +01:00
1 parent 84d5c8de52
commit 59af305e84
6 files changed
+1104 -118

No files matched your search

+270 -5
View File
@@ -1,11 +1,13 @@
/**
* @fileoverview The graphical install's screens in the Codeman Desktop connect page.
* @fileoverview The graphical install's screens in the Codeman Desktop connect page, and the This
* Mac tab's rows for the Codeman servers the app found on this Mac.
*
* Runs the real packages/desktop/assets/connect.html in jsdom with a fake native side:
* `window.ipc` records what the page asks the app for, and `onNative` is what the app
* calls. The Rust side of the same messages is pinned in packages/desktop/src/policy.rs
* (`ipc_graphical_install_commands`) and src/gui_install.rs, which do not build on Linux
* CI, so the spelling of every message the page sends is asserted here.
* (`ipc_graphical_install_commands`), src/gui_install.rs and src/setup.rs (the `found` list:
* `a_found_server_row_says_what_answered`), which do not build on Linux CI, so the spelling of
* every message the page sends, and of what it reads, is asserted here.
*
* Port: N/A.
*/
@@ -45,7 +47,7 @@ const NOT_INSTALLED = {
installComplete: false,
installRunning: false,
installInApp: false,
elsewhere: [],
found: [],
outcome: { kind: 'refused' },
};
@@ -482,7 +484,11 @@ describe('adding a coding agent to a Mac that has Codeman', () => {
RUNNING,
STOPPED,
{ ...RUNNING, state: 'needsLogin' },
{ ...RUNNING, state: 'runningElsewhere', elsewhere: ['127.0.0.1:4000'] },
{
...RUNNING,
state: 'runningElsewhere',
found: [{ url: 'https://127.0.0.1:4000/', version: null, state: 'certificate', isDefault: false }],
},
]) {
t.native({ type: 'localStatus', status });
expect(t.visible('local-agents'), String(status.state)).toBe(true);
@@ -735,3 +741,262 @@ describe('after Skip for now', () => {
expect(t.sentOf('connect')).toHaveLength(1);
});
});
describe('Codeman servers found on this Mac', () => {
type Found = { url: string; version: string | null; state: string; isDefault: boolean };
const found = (url: string, state = 'running', version: string | null = '1.49.0'): Found => ({
url,
version: state === 'running' ? version : null,
state,
isDefault: false,
});
const ELSEWHERE = {
...NOT_INSTALLED,
state: 'runningElsewhere',
installed: true,
installComplete: true,
found: [found('http://127.0.0.1:4000/'), found('http://192.168.1.5:5000/cm/', 'needsLogin')],
};
const RUNNING = {
...NOT_INSTALLED,
state: 'running',
version: '1.49.0',
installed: true,
installComplete: true,
outcome: { kind: 'ok', version: '1.49.0' },
found: [{ ...found('http://127.0.0.1:3000/'), isDefault: true }],
};
function at(status: Record<string, unknown>, first?: Record<string, unknown>) {
const t = open(first ?? { servers: [] });
t.native({ type: 'localStatus', status });
const rows = (list: string) =>
Array.from(t.$(list).querySelectorAll('.found')).map((row) => ({
address: row.querySelector('b')?.textContent,
note: row.querySelector('small')?.textContent ?? '',
button: row.querySelector('button'),
}));
return { ...t, rows };
}
it('recommends the server that runs at another address, a row each, with no trip to the other tab', () => {
const t = at(ELSEWHERE);
expect(t.$('local-title').textContent).toBe('Codeman is already running on this Mac.');
expect(t.$('local-detail').textContent).toBe('');
const rows = t.rows('local-found');
expect(rows.map((r) => [r.address, r.note])).toEqual([
['http://127.0.0.1:4000/', 'version 1.49.0'],
['http://192.168.1.5:5000/cm/', 'Asks for a login.'],
]);
expect(rows.map((r) => r.button?.textContent)).toEqual(['Connect', 'Connect']);
// The first row the app can open is the tab's main action.
expect(rows.map((r) => r.button?.className)).toEqual(['primary', 'secondary']);
expect(rows[0].button?.getAttribute('aria-label')).toBe('Connect to http://127.0.0.1:4000/');
expect(t.visible('local-found-note')).toBe(true);
expect(t.$('local-found-note').textContent).toBe('Starting another server here would take over its sessions.');
expect(t.$('pane-local').textContent).not.toContain('Another machine');
expect(t.visible('local-check')).toBe(true);
for (const id of ['local-open', 'local-start', 'local-install', 'local-also']) {
expect(t.visible(id), id).toBe(false);
}
expect(t.$('local-dot').className).toBe('dot ok');
// Finding a server connects to nothing by itself.
expect(t.sentOf('connect')).toHaveLength(0);
});
it('Connect is the connect of an address typed under Another machine', () => {
const t = at(ELSEWHERE);
t.rows('local-found')[1].button!.click();
expect(t.sentOf('connect')).toEqual([{ cmd: 'connect', url: 'http://192.168.1.5:5000/cm/', remember: true }]);
expect(t.input('url').value).toBe('http://192.168.1.5:5000/cm/');
expect(t.$('status-text').textContent).toContain('Connecting to http://192.168.1.5:5000/cm/');
// One request at a time, as everywhere on the page.
expect(t.rows('local-found').every((r) => r.button!.disabled)).toBe(true);
t.rows('local-found')[0].button!.click();
expect(t.sentOf('connect')).toHaveLength(1);
});
it('uses the login saved for that server, and never the one of the server shown before', () => {
const servers = [
{ url: 'https://box.ts.net/', username: 'other', hasSavedPassword: true },
{ url: 'http://127.0.0.1:4000/', username: 'kay', hasSavedPassword: true },
];
const t = at(ELSEWHERE, { servers });
t.click('tab-local');
t.rows('local-found')[0].button!.click();
expect(t.sentOf('connect')).toEqual([
{ cmd: 'connect', url: 'http://127.0.0.1:4000/', remember: true, username: 'kay' },
]);
const u = at(ELSEWHERE, { servers: servers.slice(0, 1) });
u.click('tab-local');
u.rows('local-found')[0].button!.click();
expect(u.sentOf('connect')).toEqual([{ cmd: 'connect', url: 'http://127.0.0.1:4000/', remember: true }]);
});
it('a server that asks for a login gets the login fields, as any connect does', () => {
const t = at(ELSEWHERE);
t.rows('local-found')[1].button!.click();
t.native({
type: 'probeResult',
url: 'http://192.168.1.5:5000/cm/',
outcome: { kind: 'needsLogin' },
canStart: false,
});
expect(t.$('tab-remote').getAttribute('aria-selected')).toBe('true');
expect(t.visible('login')).toBe(true);
expect(t.input('url').value).toBe('http://192.168.1.5:5000/cm/');
});
it('says why instead of offering Connect for a server the app cannot open', () => {
const t = at({
...ELSEWHERE,
found: [
found('https://127.0.0.1:4000/', 'certificate'),
found('https://192.168.1.5:4100/', 'certificate'),
found('http://[::1]:5000/', 'unreachable'),
found('http://127.0.0.1:6000/'),
],
});
const rows = t.rows('local-found');
expect(rows.map((r) => !!r.button)).toEqual([false, false, false, true]);
// The wording of a stopped Codeman that is set up with HTTPS.
const https =
'It is set up with HTTPS (codeman web --https), whose self-signed certificate this app refuses. Run it without --https to open it here.';
expect(rows.slice(0, 2).map((r) => r.note)).toEqual([https, https]);
expect(rows[2].note).toBe('It does not answer at this address.');
const stopped = at({ ...NOT_INSTALLED, state: 'stopped', installed: true, url: 'https://127.0.0.1:3000/' });
expect(stopped.$('local-detail').textContent).toBe(https);
// The first one it can open is still the main action.
expect(rows[3].button?.className).toBe('primary');
expect(t.$('local-dot').className).toBe('dot ok');
});
it('with no server it can open, offers no Connect and keeps Check again', () => {
const t = at({ ...ELSEWHERE, found: [found('https://127.0.0.1:4000/', 'certificate')] });
expect(t.$('pane-local').querySelectorAll('.found button')).toHaveLength(0);
expect(t.$('local-dot').className).toBe('dot warn');
expect(t.visible('local-found-note')).toBe(true);
expect(t.visible('local-check')).toBe(true);
t.click('local-check');
expect(t.sentOf('localStatus').at(-1)).toEqual({ cmd: 'localStatus', quick: false, deep: true });
});
it('a server process with no address to try still says what to do', () => {
const t = at({ ...ELSEWHERE, found: [] });
expect(t.$('local-title').textContent).toBe('Codeman is already running on this Mac.');
expect(t.$('local-detail').textContent).toContain('could not find the address it listens at');
expect(t.$('local-detail').textContent).toContain('take over its sessions');
expect(t.visible('local-found')).toBe(false);
expect(t.visible('local-found-note')).toBe(false);
// A status from before the list existed reads the same.
const old = at({ ...ELSEWHERE, found: undefined });
expect(old.$('local-detail').textContent).toContain('could not find the address it listens at');
});
it('keeps Open for the Codeman that answers, and lists the others under it', () => {
const t = at({
...RUNNING,
found: [
...RUNNING.found,
found('http://127.0.0.1:5000/', 'running', '1.50.0-beta'),
found('https://127.0.0.1:6000/', 'certificate'),
],
});
expect(t.$('local-title').textContent).toBe('Codeman is running on this Mac.');
expect(t.$('local-detail').textContent).toBe('http://127.0.0.1:3000/ · version 1.49.0');
expect(t.visible('local-open')).toBe(true);
expect(t.visible('local-also')).toBe(true);
expect(t.$('local-also-head').textContent).toBe('Also running on this Mac');
const rows = t.rows('local-also-list');
// Not the one the first line is about.
expect(rows.map((r) => [r.address, r.note.slice(0, 19)])).toEqual([
['http://127.0.0.1:5000/', 'version 1.50.0-beta'],
['https://127.0.0.1:6000/', 'It is set up with H'],
]);
// Open stays the main action: a row's Connect is never the primary one here.
expect(rows.map((r) => r.button?.className)).toEqual(['secondary', undefined]);
expect(t.visible('local-found')).toBe(false);
expect(t.visible('local-found-note')).toBe(false);
rows[0].button!.click();
expect(t.sentOf('connect')).toEqual([{ cmd: 'connect', url: 'http://127.0.0.1:5000/', remember: true }]);
});
it('lists them beside a Codeman that asks for a login, and beside something else on its port', () => {
const other = found('http://127.0.0.1:5000/');
const login = at({ ...RUNNING, state: 'needsLogin', outcome: { kind: 'needsLogin' }, found: [other] });
expect(login.visible('local-open')).toBe(true);
expect(login.rows('local-also-list').map((r) => r.address)).toEqual(['http://127.0.0.1:5000/']);
const taken = { ...RUNNING, state: 'other', version: null, outcome: { kind: 'notCodeman', status: 404 } };
const t = at({ ...taken, found: [other] });
expect(t.$('local-title').textContent).toBe('Something else answers at http://127.0.0.1:3000/');
expect(t.$('local-detail').textContent).toContain('or connect to the Codeman listed below.');
expect(t.rows('local-also-list').map((r) => r.button?.textContent)).toEqual(['Connect']);
// With nothing to connect to, the old pointer stays.
const none = at({ ...taken, found: [] });
expect(none.$('local-detail').textContent).toContain('under Another machine');
expect(none.visible('local-also')).toBe(false);
});
it('shows no list when this Mac runs only the Codeman the tab is about', () => {
const t = at(RUNNING);
expect(t.visible('local-also')).toBe(false);
expect(t.$('pane-local').querySelectorAll('.found')).toHaveLength(0);
// The rows of an earlier look go when the next one finds none.
t.native({ type: 'localStatus', status: ELSEWHERE });
expect(t.rows('local-found')).toHaveLength(2);
t.native({ type: 'localStatus', status: { ...NOT_INSTALLED, state: 'stopped', installed: true } });
expect(t.$('pane-local').querySelectorAll('.found')).toHaveLength(0);
expect(t.visible('local-found')).toBe(false);
expect(t.visible('local-found-note')).toBe(false);
});
it('shows what the app found as text, never as markup', () => {
const hostile = '"><img src=x onerror=alert(1)><script>alert(2)</script>';
const t = at({
...ELSEWHERE,
found: [{ url: hostile, version: '<b onclick=alert(3)>9</b>', state: 'running', isDefault: false }],
});
const rows = t.rows('local-found');
expect(t.$('pane-local').querySelector('img, script, small b')).toBeNull();
expect(rows[0].address).toBe(hostile);
expect(rows[0].note).toBe('version <b onclick=alert(3)>9</b>');
expect(rows[0].button?.title).toBe('Connect to ' + hostile);
// The app gets it as it is, and refuses what is not an address (policy::normalize_server_url).
rows[0].button!.click();
expect(t.sentOf('connect')).toEqual([{ cmd: 'connect', url: hostile, remember: true }]);
});
it('holds a found address to the length the app accepts (MAX_URL_LEN in policy.rs)', () => {
const long = 'http://127.0.0.1:4000/' + 'a'.repeat(4096);
const t = at({ ...ELSEWHERE, found: [found(long)] });
t.rows('local-found')[0].button!.click();
expect(t.sentOf('connect')).toHaveLength(0);
expect(t.$('status-text').textContent).toContain('too long');
});
it('looks again when the server it connected to refuses, and opens nothing by itself', () => {
const t = at(ELSEWHERE);
const looks = t.sentOf('localStatus').length;
t.rows('local-found')[0].button!.click();
t.native({ type: 'probeResult', url: 'http://127.0.0.1:4000/', outcome: { kind: 'refused' }, canStart: true });
expect(t.$('tab-local').getAttribute('aria-selected')).toBe('true');
expect(t.$('status-text').textContent).toContain('Nothing is answering at this address.');
expect(t.sentOf('localStatus')).toHaveLength(looks + 1);
// A server that then turns out to be starting is shown, not opened: the
// one that comes up may not be the one this row was about.
t.native({ type: 'localStatus', status: { ...ELSEWHERE, state: 'starting', found: [] } });
t.native({ type: 'localStatus', status: RUNNING });
expect(t.sentOf('connect')).toHaveLength(1);
expect(t.visible('local-open')).toBe(true);
});
it('still opens the Codeman of the first line once it is up, after Open was refused', () => {
const t = at(RUNNING);
t.click('local-open');
t.native({ type: 'probeResult', url: RUNNING.url, outcome: { kind: 'refused' }, canStart: true });
t.native({ type: 'localStatus', status: { ...RUNNING, state: 'starting', found: [] } });
t.native({ type: 'localStatus', status: RUNNING });
expect(t.sentOf('connect').map((m) => m.url)).toEqual([RUNNING.url, RUNNING.url]);
});
});
+93 -5
View File
@@ -874,15 +874,85 @@ async function everyState(browser: Browser) {
p.length === 1 && p[0].cmd === 'localStatus' && p[0].deep === true && p[0].quick === false,
p
);
await native({ type: 'localStatus', status: st16('runningElsewhere', { elsewhere: ['192.168.1.5:3000'] }) });
const elsewhere = [
{ url: 'https://127.0.0.1:4000/', version: null, state: 'certificate', isDefault: false },
{ url: 'http://192.168.1.5:3000/', version: '1.49.0', state: 'running', isDefault: false },
];
await native({ type: 'localStatus', status: st16('runningElsewhere', { found: elsewhere }) });
check(
'a server running elsewhere: no Start, no Install, says where',
'a server running elsewhere: no Start, no Install, a row for each address',
!(await visible('#local-start')) &&
!(await visible('#local-install')) &&
!(await visible('#local-open')) &&
(await page.textContent('#local-detail')).includes('192.168.1.5:3000') &&
(await page.textContent('#local-found')).includes('http://192.168.1.5:3000/') &&
(await page.locator('#local-found .found').count()) === 2 &&
(await page.textContent('#local-title')).includes('already running')
);
const foundBox = await page.evaluate(() => {
const r = (sel) => document.querySelector(sel).getBoundingClientRect();
const card = r('.card');
const rows = [...document.querySelectorAll('#local-found .found')].map((row) => row.getBoundingClientRect());
return {
order: [r('.local').bottom, rows[0].top, rows[1].bottom, r('#local-found-note').top, r('#local-check').top],
inside: rows.every((row) => row.left >= card.left && row.right <= card.right),
buttons: [...document.querySelectorAll('#local-found .found')].map((row) => {
const b = row.querySelector('button');
return b ? b.className : '';
}),
};
});
check(
'the rows sit under the title, then the sentence about a second server, then Check again',
foundBox.order.every((y, i) => i === 0 || y >= foundBox.order[i - 1]) && foundBox.inside,
foundBox
);
check(
'a server the app cannot open has no Connect; the first it can open is the main action',
foundBox.buttons[0] === '' && foundBox.buttons[1] === 'primary',
foundBox.buttons
);
n0 = (await all()).length;
await page.locator('#local-found .found button').dblclick();
p = (await all()).slice(n0);
check(
'Connect on a found server connects once, to its address',
p.length === 1 && p[0].cmd === 'connect' && p[0].url === 'http://192.168.1.5:3000/' && !('password' in p[0]),
p
);
await native({
type: 'probeResult',
url: 'http://192.168.1.5:3000/',
outcome: { kind: 'refused' },
canStart: false,
});
await native({
type: 'localStatus',
status: st16('running', {
version: '1.49.0',
outcome: { kind: 'ok', version: '1.49.0' },
found: [{ url: 'http://127.0.0.1:3000/', version: '1.49.0', state: 'running', isDefault: true }, elsewhere[1]],
}),
});
const alsoBox = await page.evaluate(() => {
const r = (sel) => document.querySelector(sel).getBoundingClientRect();
return {
open: r('#local-open').bottom,
also: r('#local-also').top,
rows: document.querySelectorAll('#local-also .found').length,
head: document.querySelector('#local-also-head').textContent,
button: document.querySelector('#local-also .found button').className,
};
});
check(
'running with another server on this Mac: it is listed under Open, as a secondary action',
(await visible('#local-open')) &&
alsoBox.also >= alsoBox.open &&
alsoBox.rows === 1 &&
alsoBox.head === 'Also running on this Mac' &&
alsoBox.button === 'secondary' &&
!(await visible('#local-found')),
alsoBox
);
await native({ type: 'localStatus', status: st16('stopped', { url: 'https://127.0.0.1:3000/', service: true }) });
check(
'stopped with HTTPS: no Start (the app refuses its certificate), explained',
@@ -1058,7 +1128,7 @@ async function layoutAndLimits(browser: Browser) {
installComplete: true,
installRunning: false,
installPort: null,
elsewhere: [],
found: [],
outcome: { kind: 'ok', version: '1.48.0' },
},
s
@@ -1295,7 +1365,7 @@ async function clicksKeysAndStates(browser: Browser) {
installComplete: true,
installRunning: false,
installPort: null,
elsewhere: [],
found: [],
outcome: { kind: 'ok', version: '1.48.0' },
},
s
@@ -1425,6 +1495,24 @@ async function clicksKeysAndStates(browser: Browser) {
'Return never starts an install',
(await t.posted('installLocal')).length === 0 && (await t.page.locator('#local-install').isVisible())
);
await t.native(
localStatus({
state: 'runningElsewhere',
version: null,
outcome: { kind: 'refused' },
found: [
{ url: 'https://127.0.0.1:4000/', version: null, state: 'certificate', isDefault: false },
{ url: 'http://127.0.0.1:5000/', version: '1.49.0', state: 'running', isDefault: false },
],
})
);
await t.page.keyboard.press('Enter');
check(
'Return connects to the server found on this Mac when that is the main action',
(await t.posted('connect')).length === 2 && (await t.posted('connect'))[1].url === 'http://127.0.0.1:5000/',
await t.posted('connect')
);
await t.page.keyboard.press('Escape');
await t.native(localStatus({ state: 'stopped', version: null, outcome: { kind: 'refused' } }));
await t.page.focus('#local-check');
await t.page.keyboard.press('Enter');