mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 18:39:33 +01:00
refactor(desktop): policy, logo and the connect page, simpler and the same
No behavior change: the connect page renders pixel-identical in eight states (Chromium, before and after), posts the same messages, and passes its jsdom and browser tests. - policy.rs: the connect commands' camelCase fields come from serde's rename_all_fields instead of three per-field renames; parse_ipc's length and id checks are match guards like its other arms, with the message cap named (MAX_MESSAGE_LEN); new_window reads its URL through external_url, which is the same parse-and-http(s) check. - logo.rs: the stubs for other platforms are one module, and Logo, Marker and recover are private to the file. - connect.html: answers() is the one test for a server that answers (six copies), the running and needs-login rows of This Mac share their branch, prepareWizard, needsAgent, newWait and openTerminal each replace two or three copies, a finished install and a finished agent share their path, host() is displayAddress(), state.clock is in the state literal, three CSS rules that matched nothing are gone, and the state comments say what wait and gui hold now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4bde072355
commit
404930d55e
3 files changed
+115
-118
No files matched your search
@@ -53,7 +53,7 @@
|
||||
padding: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.45);
|
||||
}
|
||||
label.field { display: block; font-size: 12px; color: var(--muted); margin: 0 0 6px; }
|
||||
input[type=text], input[type=password], input[type=url] {
|
||||
input[type=text], input[type=password] {
|
||||
width: 100%; height: 38px; padding: 0 12px; border-radius: 9px;
|
||||
border: 1px solid var(--line-strong); background: #0c0d10; color: var(--text);
|
||||
font: inherit; outline: none; -webkit-user-select: text; user-select: text;
|
||||
@@ -61,7 +61,6 @@
|
||||
input:focus { border-color: var(--accent-2); box-shadow: 0 0 0 3px rgba(59,130,246,0.25); }
|
||||
.row { display: flex; gap: 10px; }
|
||||
.row > * { flex: 1; }
|
||||
.gap { height: 14px; }
|
||||
#recent { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
||||
#recent:empty { display: none; }
|
||||
.chip {
|
||||
@@ -128,7 +127,7 @@
|
||||
/* The Codeman servers found on this Mac: a row each, in the look of a choice
|
||||
in the install, with the row's own Connect button. */
|
||||
#local-found-note { margin: 8px 0 0; }
|
||||
#local-also { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
|
||||
#local-also, #elsewhere { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
|
||||
#local-also-head { color: var(--muted); font-size: 12px; margin: 0 0 6px; }
|
||||
.found {
|
||||
display: flex; gap: 10px; align-items: center; padding: 9px 11px; border-radius: 9px;
|
||||
@@ -139,7 +138,6 @@
|
||||
.found b { display: block; font-weight: 600; }
|
||||
.found small { display: block; margin-top: 1px; color: var(--muted); font-size: 12px; }
|
||||
.found button { flex: none; }
|
||||
#elsewhere { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
|
||||
#elsewhere[hidden] { display: none; }
|
||||
#elsewhere summary { color: var(--muted); font-size: 13px; cursor: default; list-style: none; }
|
||||
#elsewhere summary::-webkit-details-marker { display: none; }
|
||||
@@ -231,7 +229,6 @@
|
||||
border: 1px solid var(--line); color: #aab1bb; font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
white-space: pre-wrap; overflow-wrap: anywhere; -webkit-user-select: text; user-select: text;
|
||||
}
|
||||
#wizard .actions .wiz-busy { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; }
|
||||
footer { margin-top: 18px; text-align: center; color: var(--muted); font-size: 12px; }
|
||||
footer a { color: #aab1bb; text-decoration: none; cursor: default; }
|
||||
footer a:hover { color: var(--accent); }
|
||||
@@ -416,9 +413,11 @@
|
||||
// suggestion: the address the suggest button connects to.
|
||||
// tab: 'local' (This Mac) or 'remote' (Another machine). askedFrom: the tab
|
||||
// the connect under way was asked from, where its answer belongs. local: the
|
||||
// last This Mac status from the app. wait: set while an install runs in
|
||||
// Terminal and this page polls for the server it brings up. gui: the
|
||||
// graphical install while its screens are up (newWizard), null otherwise.
|
||||
// last This Mac status from the app. wait: set while this page polls for the
|
||||
// server an install brings up (one in Terminal, or a graphical one that
|
||||
// finished; newWait). gui: the graphical install or the agent chooser while
|
||||
// its screens are up (newWizard), null otherwise. clock: the timer of the
|
||||
// running phase's clock.
|
||||
// reopen: a connect to this Mac was refused, so open it if it turns out to
|
||||
// be starting. startingLooks: how often the page has looked again at a
|
||||
// starting server. askedFound: the connect under way was asked from the row
|
||||
@@ -427,7 +426,7 @@
|
||||
servers: [], busy: false, lastUrl: '', startUrl: '', suggestion: '',
|
||||
tab: 'local', askedFrom: 'local', askedFound: false, local: null, localChecking: false, opening: false,
|
||||
wait: null, waitTimer: 0, stoppedWaiting: false, gui: null, wantAgents: false,
|
||||
reopen: false, startingLooks: 0, startingTimer: 0
|
||||
reopen: false, startingLooks: 0, startingTimer: 0, clock: 0
|
||||
};
|
||||
// While an install runs: how often to look, how long a finished install may
|
||||
// take to answer before Start is offered, and when to stop looking.
|
||||
@@ -511,14 +510,15 @@
|
||||
els.suggest.hidden = true;
|
||||
}
|
||||
|
||||
function host(url) {
|
||||
function displayAddress(url) {
|
||||
try { var u = new URL(url); return u.host + (u.pathname === '/' ? '' : u.pathname.replace(/\/$/, '')); } catch (e) { return url; }
|
||||
}
|
||||
|
||||
// The address as the app normalizes it (policy::normalize_server_url):
|
||||
// http:// by default, https:// for a .ts.net name without a port, no query
|
||||
// or fragment, a trailing slash. Only used to tell whether two addresses
|
||||
// are one server.
|
||||
// or fragment, a trailing slash. Used to tell whether two addresses are one
|
||||
// server, and which scheme and host a typed address ends up with
|
||||
// (isLocalUrl, the http:// warning).
|
||||
function normalize(raw) {
|
||||
var s = String(raw || '').trim();
|
||||
if (!s) return '';
|
||||
@@ -553,7 +553,7 @@
|
||||
chip.className = 'chip';
|
||||
var pick = document.createElement('button');
|
||||
pick.type = 'button';
|
||||
pick.textContent = host(s.url) + (s.username ? ' (' + s.username + ')' : '');
|
||||
pick.textContent = displayAddress(s.url) + (s.username ? ' (' + s.username + ')' : '');
|
||||
pick.title = 'Connect to ' + s.url;
|
||||
pick.addEventListener('click', function () {
|
||||
if (state.busy) return;
|
||||
@@ -565,7 +565,7 @@
|
||||
x.className = 'x';
|
||||
x.textContent = '×';
|
||||
x.title = 'Forget this server' + (s.hasSavedPassword ? ' and its saved password' : '');
|
||||
x.setAttribute('aria-label', 'Forget ' + host(s.url));
|
||||
x.setAttribute('aria-label', 'Forget ' + displayAddress(s.url));
|
||||
x.addEventListener('click', function () { if (!state.busy) post({ cmd: 'forget', url: s.url }); });
|
||||
chip.appendChild(pick);
|
||||
chip.appendChild(x);
|
||||
@@ -782,8 +782,11 @@
|
||||
return (s.found || []).filter(function (f) { return f && f.url !== s.url; });
|
||||
}
|
||||
|
||||
function canConnect(f) {
|
||||
return f.state === 'running' || f.state === 'needsLogin';
|
||||
// Codeman answers there, with or without a login, so the app can connect
|
||||
// to it. Read the same way for a found server's row and for the This Mac
|
||||
// status.
|
||||
function answers(s) {
|
||||
return s.state === 'running' || s.state === 'needsLogin';
|
||||
}
|
||||
|
||||
// What a found server's row says under its address: its version, or why the
|
||||
@@ -815,7 +818,7 @@
|
||||
text.appendChild(small);
|
||||
}
|
||||
row.appendChild(text);
|
||||
if (canConnect(f)) {
|
||||
if (answers(f)) {
|
||||
var button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = primary ? 'primary' : 'secondary';
|
||||
@@ -848,7 +851,7 @@
|
||||
} else if (!s) {
|
||||
dot = 'busy';
|
||||
title = 'Looking for Codeman on this Mac…';
|
||||
} else if (s.installRunning && s.state !== 'running' && s.state !== 'needsLogin') {
|
||||
} else if (s.installRunning && !answers(s)) {
|
||||
dot = 'busy';
|
||||
title = 'Codeman is being installed in Terminal.';
|
||||
detail = 'Finish the installer there, then Check again.';
|
||||
@@ -861,7 +864,7 @@
|
||||
} else if (s.state === 'runningElsewhere') {
|
||||
// Not at the address the app opens by default: each one gets a row.
|
||||
main = (s.found || []).filter(Boolean);
|
||||
dot = main.some(canConnect) ? 'ok' : 'warn';
|
||||
dot = main.some(answers) ? 'ok' : 'warn';
|
||||
title = 'Codeman is already running on this Mac.';
|
||||
// No row: the app found the server's process, but no address to try.
|
||||
if (!main.length) {
|
||||
@@ -869,16 +872,12 @@
|
||||
'Starting another server here would take over its sessions.';
|
||||
}
|
||||
show.check = true;
|
||||
} else if (s.state === 'running') {
|
||||
} else if (answers(s)) {
|
||||
dot = 'ok';
|
||||
title = 'Codeman is running on this Mac.';
|
||||
detail = s.url + (s.version ? ' · version ' + s.version : '');
|
||||
show.open = true;
|
||||
also = otherServers(s);
|
||||
} else if (s.state === 'needsLogin') {
|
||||
dot = 'ok';
|
||||
title = 'Codeman is running on this Mac.';
|
||||
detail = s.url + ' asks for a login: Open asks for the password.';
|
||||
detail = s.state === 'needsLogin'
|
||||
? s.url + ' asks for a login: Open asks for the password.'
|
||||
: s.url + (s.version ? ' · version ' + s.version : '');
|
||||
show.open = true;
|
||||
also = otherServers(s);
|
||||
} else if (s.state === 'stopped') {
|
||||
@@ -915,7 +914,7 @@
|
||||
if (o.kind === 'notCodeman') {
|
||||
title = 'Something else answers at ' + s.url;
|
||||
detail = 'It is not Codeman (HTTP ' + o.status + '). Stop what runs on that port, or connect to ' +
|
||||
(also.some(canConnect) ? 'the Codeman listed below.' : 'Codeman’s real address under Another machine.');
|
||||
(also.some(answers) ? 'the Codeman listed below.' : 'Codeman’s real address under Another machine.');
|
||||
} else {
|
||||
var e = explain[o.kind];
|
||||
title = e ? e[1] : 'Could not reach Codeman on this Mac.';
|
||||
@@ -959,8 +958,8 @@
|
||||
}
|
||||
// An install from this app is still running (this page was shown again
|
||||
// while it ran): wait for it, unless the user stopped waiting.
|
||||
if (!state.wait && s && s.installRunning && !s.installInApp && !state.stoppedWaiting && s.state !== 'running' && s.state !== 'needsLogin') {
|
||||
state.wait = { since: Date.now(), finishedAt: 0, port: s.installPort || 3000 };
|
||||
if (!state.wait && s && s.installRunning && !s.installInApp && !state.stoppedWaiting && !answers(s)) {
|
||||
state.wait = newWait(s.installPort || 3000);
|
||||
renderLocal();
|
||||
scheduleWait();
|
||||
}
|
||||
@@ -989,7 +988,7 @@
|
||||
}
|
||||
return false;
|
||||
}
|
||||
var up = !!s && (s.state === 'running' || s.state === 'needsLogin');
|
||||
var up = !!s && answers(s);
|
||||
var open = up && state.reopen && state.startingLooks > 0 && state.tab === 'local' && !state.busy;
|
||||
state.startingLooks = 0;
|
||||
state.reopen = false;
|
||||
@@ -1041,10 +1040,15 @@
|
||||
var network = !!state.gui && state.gui.reach === 'network';
|
||||
closeWizard();
|
||||
selectTab('local');
|
||||
openTerminal({ cmd: 'installLocal', network: network });
|
||||
}
|
||||
|
||||
// Both Terminal installs: no second one while Terminal opens.
|
||||
function openTerminal(msg) {
|
||||
state.opening = true;
|
||||
syncLocalButtons();
|
||||
setStatus('busy', 'Opening Terminal…');
|
||||
post({ cmd: 'installLocal', network: network });
|
||||
post(msg);
|
||||
}
|
||||
|
||||
// mode 'install': Codeman itself. mode 'agent': one coding agent for a Mac
|
||||
@@ -1056,17 +1060,22 @@
|
||||
};
|
||||
}
|
||||
|
||||
// The first screen of the graphical install ('install') or of adding an
|
||||
// agent ('agent'), which asks the installer for its plan.
|
||||
function prepareWizard(mode) {
|
||||
state.gui = newWizard('preparing', mode);
|
||||
post({ cmd: mode === 'agent' ? 'agentPrepare' : 'installPrepare' });
|
||||
}
|
||||
|
||||
function openWizard() {
|
||||
if (state.busy || tooOld || state.opening) return;
|
||||
state.gui = newWizard('preparing');
|
||||
post({ cmd: 'installPrepare' });
|
||||
prepareWizard('install');
|
||||
renderWizard();
|
||||
}
|
||||
|
||||
function openAgentChooser() {
|
||||
if (state.busy || tooOld || state.opening) return;
|
||||
state.gui = newWizard('preparing', 'agent');
|
||||
post({ cmd: 'agentPrepare' });
|
||||
prepareWizard('agent');
|
||||
renderWizard();
|
||||
}
|
||||
|
||||
@@ -1086,14 +1095,14 @@
|
||||
renderLocal();
|
||||
}
|
||||
|
||||
// Try again keeps what was chosen.
|
||||
function retryWizard() {
|
||||
var reach = state.gui ? state.gui.reach : 'local';
|
||||
var agent = state.gui ? state.gui.agent : '';
|
||||
var mode = state.gui ? state.gui.mode : 'install';
|
||||
state.gui = newWizard('preparing', mode);
|
||||
state.gui.reach = reach;
|
||||
state.gui.agent = agent;
|
||||
post({ cmd: mode === 'agent' ? 'agentPrepare' : 'installPrepare' });
|
||||
var old = state.gui;
|
||||
prepareWizard(old ? old.mode : 'install');
|
||||
if (old) {
|
||||
state.gui.reach = old.reach;
|
||||
state.gui.agent = old.agent;
|
||||
}
|
||||
renderWizard();
|
||||
}
|
||||
|
||||
@@ -1111,6 +1120,12 @@
|
||||
return offeredAgents().filter(function (c) { return c.id === id; })[0] || null;
|
||||
}
|
||||
|
||||
// This Mac has no coding agent and the installer offers one, so an install
|
||||
// asks which one.
|
||||
function needsAgent() {
|
||||
return !installedAgents().length && offeredAgents().length > 0;
|
||||
}
|
||||
|
||||
// A run this page did not start has no plan, so no name for the agent.
|
||||
function agentName() {
|
||||
var c = agentById(state.gui.agent);
|
||||
@@ -1129,7 +1144,7 @@
|
||||
g.step = 'agent';
|
||||
} else if (plan.blocker) {
|
||||
g.step = 'blocked';
|
||||
} else if (!installedAgents().length && offeredAgents().length) {
|
||||
} else if (needsAgent()) {
|
||||
// Kept across Try again; otherwise the first of the catalogue's order.
|
||||
if (!agentById(g.agent)) g.agent = offeredAgents()[0].id;
|
||||
g.step = 'agent';
|
||||
@@ -1229,7 +1244,7 @@
|
||||
PHASES.forEach(function (id) {
|
||||
var label = {
|
||||
runtime: 'Getting Node.js and tmux',
|
||||
agent: 'Installing ' + (chosen ? chosen.label : 'a coding agent'),
|
||||
agent: 'Installing ' + agentName(),
|
||||
source: 'Downloading Codeman',
|
||||
packages: 'Installing packages',
|
||||
build: 'Building Codeman',
|
||||
@@ -1313,21 +1328,14 @@
|
||||
if (!g) return;
|
||||
appendLog(msg.log);
|
||||
clearInterval(state.clock);
|
||||
if (msg.ok && g.mode === 'agent') {
|
||||
g.ok = true;
|
||||
g.step = 'added';
|
||||
renderWizard();
|
||||
// Whether there is a Codeman to open from here.
|
||||
checkLocal('quick');
|
||||
return;
|
||||
}
|
||||
if (msg.ok) {
|
||||
g.ok = true;
|
||||
g.step = 'finishing';
|
||||
g.step = g.mode === 'agent' ? 'added' : 'finishing';
|
||||
renderWizard();
|
||||
// The same watch as a Terminal install: it opens Codeman at the first
|
||||
// answer, and offers Start when none comes.
|
||||
state.wait = { since: Date.now(), finishedAt: 0, port: g.port };
|
||||
// An install gets the same watch as a Terminal install: it opens Codeman
|
||||
// at the first answer, and offers Start when none comes. An added agent
|
||||
// only asks whether there is a Codeman to open from here.
|
||||
if (g.mode !== 'agent') state.wait = newWait(g.port);
|
||||
checkLocal('quick');
|
||||
return;
|
||||
}
|
||||
@@ -1346,7 +1354,8 @@
|
||||
renderLog();
|
||||
}
|
||||
|
||||
// Each screen of the graphical install, drawn from state.gui alone.
|
||||
// Each screen of the graphical install and of adding an agent, drawn from
|
||||
// state.gui (and state.local for Open Codeman).
|
||||
function renderWizard() {
|
||||
var g = state.gui;
|
||||
els.card.classList.toggle('wizard-on', !!g);
|
||||
@@ -1418,10 +1427,6 @@
|
||||
else els.wizTitle.focus();
|
||||
}
|
||||
|
||||
function localAnswers() {
|
||||
return !!state.local && (state.local.state === 'running' || state.local.state === 'needsLogin');
|
||||
}
|
||||
|
||||
function renderWizardButtons() {
|
||||
var g = state.gui;
|
||||
var step = g.step;
|
||||
@@ -1430,7 +1435,7 @@
|
||||
var next = {
|
||||
agent: agentMode ? (chosen ? 'Install ' + chosen.label : '') : 'Continue',
|
||||
reach: 'Install Codeman', failed: 'Try again', prepareFailed: 'Try again', blocked: 'Install in Terminal',
|
||||
ready: 'Open Codeman', added: localAnswers() ? 'Open Codeman' : ''
|
||||
ready: 'Open Codeman', added: state.local && answers(state.local) ? 'Open Codeman' : ''
|
||||
}[step];
|
||||
els.wizNext.hidden = !next;
|
||||
els.wizNext.textContent = next || '';
|
||||
@@ -1478,7 +1483,7 @@
|
||||
els.wizPw.value = '';
|
||||
els.wizPw2.value = '';
|
||||
// Skipped: the first thing to say once Codeman answers.
|
||||
g.noAgent = !installedAgents().length && !msg.aiCli && offeredAgents().length > 0;
|
||||
g.noAgent = needsAgent() && !msg.aiCli;
|
||||
beginRun();
|
||||
post(msg);
|
||||
}
|
||||
@@ -1524,7 +1529,7 @@
|
||||
function wizardBack() {
|
||||
var g = state.gui;
|
||||
if (!g) return;
|
||||
if (g.step === 'reach' && !installedAgents().length && offeredAgents().length && !g.plan.blocker) {
|
||||
if (g.step === 'reach' && needsAgent() && !g.plan.blocker) {
|
||||
g.step = 'agent';
|
||||
renderWizard();
|
||||
} else {
|
||||
@@ -1532,6 +1537,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// A wait for the server an install brings up, on `port` until the
|
||||
// installer says otherwise.
|
||||
function newWait(port) {
|
||||
return { since: Date.now(), finishedAt: 0, port: port };
|
||||
}
|
||||
|
||||
function scheduleWait() {
|
||||
clearTimeout(state.waitTimer);
|
||||
state.waitTimer = setTimeout(function () { if (state.wait) checkLocal('quick'); }, WAIT_POLL_MS);
|
||||
@@ -1560,7 +1571,7 @@
|
||||
// Until the installer writes its LaunchAgent, the app looks at port 3000,
|
||||
// where something else may answer when the install took another port.
|
||||
var ours = portOf(s.url) === w.port;
|
||||
if (ours && (s.state === 'running' || s.state === 'needsLogin')) {
|
||||
if (ours && answers(s)) {
|
||||
var g = state.gui;
|
||||
if (g && g.step === 'finishing' && g.noAgent) {
|
||||
clearTimeout(state.waitTimer);
|
||||
@@ -1608,10 +1619,7 @@
|
||||
els.ssh.focus();
|
||||
return;
|
||||
}
|
||||
state.opening = true;
|
||||
syncLocalButtons();
|
||||
setStatus('busy', 'Opening Terminal…');
|
||||
post({ cmd: 'installRemote', target: target });
|
||||
openTerminal({ cmd: 'installRemote', target: target });
|
||||
}
|
||||
|
||||
window.__codemanDesktop = {
|
||||
@@ -1690,7 +1698,7 @@
|
||||
case 'installStarted':
|
||||
state.opening = false;
|
||||
setStatus('', '');
|
||||
state.wait = { since: Date.now(), finishedAt: 0, port: Number(msg.port) || 3000 };
|
||||
state.wait = newWait(Number(msg.port) || 3000);
|
||||
selectTab('local');
|
||||
renderLocal();
|
||||
scheduleWait();
|
||||
|
||||
@@ -270,7 +270,7 @@ const SETTLE: Duration = Duration::from_secs(3);
|
||||
const LOOK_EVERY: Duration = Duration::from_millis(250);
|
||||
|
||||
/// What a blink needs from the machine, so its decisions can be tested.
|
||||
pub trait Logo {
|
||||
trait Logo {
|
||||
/// The current mode, None if it could not be read.
|
||||
fn mode(&self) -> Option<i64>;
|
||||
/// Set the mode; false if the daemon did not take it.
|
||||
@@ -281,7 +281,7 @@ pub trait Logo {
|
||||
}
|
||||
|
||||
/// The file that says a blink may have left the logo dark.
|
||||
pub struct Marker(Option<PathBuf>);
|
||||
struct Marker(Option<PathBuf>);
|
||||
|
||||
impl Marker {
|
||||
fn default_location() -> Self {
|
||||
@@ -363,7 +363,7 @@ fn blink(logo: &impl Logo, marker: &Marker, pattern: &[Beat], mut wait: impl FnM
|
||||
}
|
||||
|
||||
/// Put back a logo that a blink could not finish with.
|
||||
pub fn recover(logo: &impl Logo, marker: &Marker) {
|
||||
fn recover(logo: &impl Logo, marker: &Marker) {
|
||||
if !marker.is_set() {
|
||||
return;
|
||||
}
|
||||
@@ -507,25 +507,24 @@ fn spawn_child(connection: Connection) -> Option<(Connection, ChildStdin)> {
|
||||
Some((connection, stdin))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use system::{available, blink_main, recover_at_launch, switched_off};
|
||||
|
||||
/// No lit logo to blink outside macOS.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn available() -> bool {
|
||||
false
|
||||
}
|
||||
mod system {
|
||||
pub fn available() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn blink_main() -> ! {
|
||||
std::process::exit(0)
|
||||
}
|
||||
pub fn blink_main() -> ! {
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn recover_at_launch() {}
|
||||
pub fn recover_at_launch() {}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn switched_off() -> bool {
|
||||
false
|
||||
pub fn switched_off() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -21,6 +21,7 @@ pub const CONNECT_PAGE: &str = "codemanapp://localhost/index.html";
|
||||
/// Caps on what a page may hand the native side.
|
||||
pub const MAX_URL_LEN: usize = 4096;
|
||||
pub const MAX_FIELD_LEN: usize = 1024;
|
||||
const MAX_MESSAGE_LEN: usize = 16 * 1024;
|
||||
|
||||
/// Where the main window is in reaching a server. The gates below take it
|
||||
/// instead of "is a server set", because what may talk to the native side
|
||||
@@ -552,12 +553,9 @@ pub fn new_window(target: &str, phase: &Phase, via: Via) -> NewWindow {
|
||||
if target.len() > MAX_URL_LEN {
|
||||
return NewWindow::Deny;
|
||||
}
|
||||
let Ok(url) = Url::parse(target) else {
|
||||
let Some(url) = external_url(target) else {
|
||||
return NewWindow::Deny;
|
||||
};
|
||||
if !matches!(url.scheme(), "http" | "https") {
|
||||
return NewWindow::Deny;
|
||||
}
|
||||
let Some(srv) = phase.loaded() else {
|
||||
return NewWindow::Deny;
|
||||
};
|
||||
@@ -656,7 +654,12 @@ pub enum Ipc {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[serde(
|
||||
tag = "cmd",
|
||||
rename_all = "camelCase",
|
||||
rename_all_fields = "camelCase",
|
||||
deny_unknown_fields
|
||||
)]
|
||||
enum RawConnect {
|
||||
Ready,
|
||||
Connect {
|
||||
@@ -696,18 +699,15 @@ enum RawConnect {
|
||||
},
|
||||
InstallPrepare,
|
||||
InstallStart {
|
||||
// rename_all above reaches variant names only, and deny_unknown_fields
|
||||
// would refuse the page's spelling of these.
|
||||
#[serde(default, rename = "networkPassword")]
|
||||
#[serde(default)]
|
||||
network_password: Option<String>,
|
||||
#[serde(default, rename = "aiCli")]
|
||||
#[serde(default)]
|
||||
ai_cli: Option<String>,
|
||||
},
|
||||
InstallAttach,
|
||||
InstallCancel,
|
||||
AgentPrepare,
|
||||
AgentStart {
|
||||
#[serde(rename = "aiCli")]
|
||||
ai_cli: String,
|
||||
},
|
||||
AgentAttach,
|
||||
@@ -761,7 +761,7 @@ fn is_connect_page(frame: &Url) -> bool {
|
||||
/// - A server page is listened to only once it committed, and only from a
|
||||
/// frame running Codeman's own app shell (trusted_frame).
|
||||
pub fn parse_ipc(frame_url: &str, body: &str, phase: &Phase, is_main_window: bool) -> Option<Ipc> {
|
||||
if body.len() > 16 * 1024 {
|
||||
if body.len() > MAX_MESSAGE_LEN {
|
||||
return None;
|
||||
}
|
||||
let frame = Url::parse(frame_url).ok()?;
|
||||
@@ -776,17 +776,12 @@ pub fn parse_ipc(frame_url: &str, body: &str, phase: &Phase, is_main_window: boo
|
||||
username,
|
||||
password,
|
||||
remember,
|
||||
} => {
|
||||
if url.len() > MAX_URL_LEN {
|
||||
return None;
|
||||
}
|
||||
ConnectCmd::Connect {
|
||||
url,
|
||||
username: bounded(username, MAX_FIELD_LEN)?,
|
||||
password: bounded(password, MAX_FIELD_LEN)?,
|
||||
remember,
|
||||
}
|
||||
}
|
||||
} if url.len() <= MAX_URL_LEN => ConnectCmd::Connect {
|
||||
url,
|
||||
username: bounded(username, MAX_FIELD_LEN)?,
|
||||
password: bounded(password, MAX_FIELD_LEN)?,
|
||||
remember,
|
||||
},
|
||||
RawConnect::Cancel => ConnectCmd::Cancel,
|
||||
RawConnect::StartServer { url } if url.len() <= MAX_URL_LEN => ConnectCmd::StartServer { url },
|
||||
RawConnect::Forget { url } if url.len() <= MAX_URL_LEN => ConnectCmd::Forget { url },
|
||||
@@ -799,15 +794,10 @@ pub fn parse_ipc(frame_url: &str, body: &str, phase: &Phase, is_main_window: boo
|
||||
RawConnect::InstallStart {
|
||||
network_password,
|
||||
ai_cli,
|
||||
} => {
|
||||
if ai_cli.as_deref().is_some_and(|id| !is_cli_id(id)) {
|
||||
return None;
|
||||
}
|
||||
ConnectCmd::InstallStart {
|
||||
network_password: bounded(network_password, MAX_FIELD_LEN)?.filter(|p| !p.is_empty()),
|
||||
ai_cli,
|
||||
}
|
||||
}
|
||||
} if ai_cli.as_deref().is_none_or(is_cli_id) => ConnectCmd::InstallStart {
|
||||
network_password: bounded(network_password, MAX_FIELD_LEN)?.filter(|p| !p.is_empty()),
|
||||
ai_cli,
|
||||
},
|
||||
RawConnect::InstallAttach => ConnectCmd::InstallAttach,
|
||||
RawConnect::InstallCancel => ConnectCmd::InstallCancel,
|
||||
RawConnect::AgentPrepare => ConnectCmd::AgentPrepare,
|
||||
|
||||
Reference in new issue
Block a user