CodeTrial also ships as a desktop app for macOS, Windows and Linux: one window,
a server it starts and stops itself, and nothing to open in a browser by hand.
It is the same server as codetrial web, so the interview, the reports and the
rules in the other documents here are unchanged. What follows is what the app
adds around that server, where it keeps things, and what it has not been
qualified for yet.
The browser deployment is not going anywhere. Everything below is an alternative way to run the same program on one person's machine.
desktop/ is a Tauri 2 app. It does not link the
server in: it runs the codetrial executable that ships beside it as a child
process, and its window shows the page that child serves on
http://127.0.0.1:3101.
A child process rather than a library call, because the server's startup lives
in src/main.rs (config search, the Setup handover, every refusal it makes
before serving) and moving that into the library would have been a rewrite of
the file the CLI's behavior is pinned to. As a child it is the binary the CLI
tests already cover, started with five things a launcher needs that the CLI
did not have. Each is decided in the server, so the app keeps no copy of the
server's rules:
--data-dir DIR: the config file, the account database, the session key, the error log and anyweb/override all live inDIR, unless the config file there names another place withCODETRIAL_DB_PATHorCODETRIAL_WEB_DIR(a relative one resolves against the working directory, which the app sets toDIR). The working directory and the executable's folder are never searched or written, because a launcher chooses neither, and no configuration is read from the environment, because a shell that exportedCODETRIAL_DB_PATHorNODE_ENVfor a CLI deployment would otherwise move this install's reports depending on how it was started. The one exception is a first launch: whenDIRhas no config yet andLIVEKIT_URL,LIVEKIT_API_KEYandLIVEKIT_API_SECRETare exported, those (andGOOGLE_API_KEYorGOOGLE_API_KEYS, if set) are written toDIR's config as Setup would write them, checked the way Setup checks them but not probed, and Setup is skipped. Nothing else from the environment goes in, and later launches read only the file.--import-config PATH: a config Setup offers to use. The page namesPATHand the scheme and host of its LiveKit URL, and says whether it holds a Google key, but never shows a key, the rest of the URL or a line it could not read: any account on the machine can reach the port while Setup serves. "Use these keys" has the server readPATHand save its Setup keys, checked as typed ones are, and nothing else in it, so a CLI deployment's database path or room prefix stays the CLI's. APATHthat cannot be used is named with the reason instead. Offered, never applied by itself: a file found where the app started could be any project's, and its LiveKit project is where interviews would go, so it is used only when the page asks for it. Any account on the machine that can reach the port while Setup serves can make that request, as it can already post keys of its own. The app passes the CLI config in the folder it was started in,config/codetrial.env.localorcodetrial.env.local, and looks nowhere else, so a file in a folder above is never offered. A development run from a checkout'sdesktop/uses the file picker, or starts from the checkout's root.--allowed-hosts LIST(orCODETRIAL_ALLOWED_HOSTS): theHostvalues the server answers. A page on another origin can repoint its own name at 127.0.0.1 and read a loopback server as same origin; the name it sends is the one thing that still gives it away. The app passes127.0.0.1:3101and nothing else. While Setup serves, before any config exists, it applies its own rule instead: a loopback name on the port it bound, answered with 403 otherwise. The list applies once the full app runs.CODETRIAL_EXIT_WITH_STDIN: the server exits when its standard input closes. The app holds the other end of that pipe, so when the app goes, by quitting or by crashing, the server goes too and does not keep the port. This one stays in the environment even for--data-dir, because it is not configuration but a description of the pipe the launcher handed over.--no-update-check: the lobby never offers a newer release, whatever the config file says aboutCODETRIAL_UPDATE_CHECK. That check compares the server with thelatestrelease, and the app is published asdesktop-latest, so it would offer every desktop install an update to the CLI binary.
All five are ordinary server options, and a CLI install can use them the same
way. codetrial --help lists the four flags, and the README's configuration
table lists CODETRIAL_ALLOWED_HOSTS.
One folder per user, chosen by the operating system. On Windows it is the local one, not the roaming profile, because the folder holds the keys and the session signing key and a roaming profile is copied to a server:
| Platform | Folder |
|---|---|
| macOS | ~/Library/Application Support/org.sysprog21.codetrial |
| Windows | %LOCALAPPDATA%\org.sysprog21.codetrial |
| Linux | $XDG_DATA_HOME/org.sysprog21.codetrial, usually ~/.local/share/org.sysprog21.codetrial |
In it:
codetrial.env.local: the keys the Setup page takes, in plain text, the same file the CLI writes. Edit it to change a key or add an option. The first launch writes it from exported LiveKit keys without asking (see--data-dirabove); otherwise Setup asks, and offers to use a CLI config found in the folder the app was started in (see--import-config). An app opened from the Dock, Finder or the Start menu sees no shell exports and starts where no CLI config is, so Setup's "Import from a config file" button is the way to bring one in: it fills the form from a file you choose. Either way it asks once: later launches read this file.codetrial.db: accounts and saved reports.session-secret: the key session cookies are signed with, which the server makes on its first--data-dirlaunch when the config file sets noSESSION_SECRET, owner-only on macOS and Linux. The CLI's built-in default is a published string, and any program on the machine can reach the port, so a data folder never uses it. A damaged file stops the server rather than being replaced, because replacing it signs everyone out and hides what damaged it.codetrial-server.log: everything the server printed on its last launch.codetrial-error.log: why the last launch failed, when it did.
What the page itself stores (practice history among it) is the web view's,
keyed by the origin http://127.0.0.1:3101. That is why the port is fixed: a
port chosen per launch would be a new origin, and an empty history, every time.
The CLI's default is 3000, so both can run on one machine. If something else
holds 3101, the app says so and does not start.
The config file in the folder above is the only configuration the server has;
see --data-dir above.
The page gets no native access at all: no Tauri command, no capability, no plugin permission is granted to it. Everything the app does, it decides from outside the page.
- Only two origins load in the window: the server's, and the app's own
starting page. Any other web page opens in the default browser, and anything
that is not a web page is refused.
target="_blank"andwindow.opennever make a second window. - The camera and the microphone are granted to the server's page and to
nothing else. The operating system still asks before the app first gets
either; on macOS the reasons shown are in
desktop/Info.plist. - A download, such as an exported report, is saved to the Downloads folder under the name the page offered, numbered rather than overwriting.
Closing the window, or quitting from the menu or the dock, asks the page's own
leave-page guard (web/before-unload.js) whether an interview's report is
still unsaved. If it is, the app asks before quitting; if the page does not
answer within five seconds, it asks as well. When the interview is in the
browser instead (see below) the app cannot ask that page, so it always asks.
Then it closes the server's stdin, gives it five seconds, and kills it if it is
still there.
The interview is a LiveKit call, which is WebRTC. On Linux the window is WebKitGTK, where WebRTC is a build-time option that a distribution's package may leave off. Without it the page loads, takes the camera, and then cannot connect.
So the app's starting page reports whether RTCPeerConnection exists before
it shows the interview. If it does not, the app opens the interview in the
default browser instead, keeps the server running, and its window says so,
with a link to open the browser again. The browser and the window are separate
origins as far as stored history goes: a machine that uses the browser route
keeps its history in that browser.
macOS (WKWebView) and Windows (WebView2) have WebRTC, so this only changes anything on Linux.
- GitHub sign-in. The OAuth page opens in the browser, and the session it
creates lands there, not in the window. A config with
GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETset is a deployment for other people, which is what the CLI is for. - Recording, which needs that sign-in (see recording-contract.md).
- Signed installers. Until they are signed, macOS refuses the first launch as from an unidentified developer (allow it under System Settings, Privacy & Security), and Windows SmartScreen warns.
- Automatic updates. A new version is a new installer.
None of this makes an interview work offline: LiveKit and Gemini are still needed, and C, C++ and Java runs still go to Compiler Explorer.
The checks in the gate and the smoke test below prove the app starts its server, serves only to its own origin, and does not leave the server behind. They do not prove an interview works in a given web view. Before a release is called more than a preview, each platform needs these run on real hardware, with the outcome recorded per case:
- The camera and microphone prompts appear, and granting them works.
- The media preflight completes: the meter moves, the face check runs or says it is unavailable.
- The interviewer is heard, and the candidate's speech is transcribed.
- Speaking over the interviewer interrupts it.
- A Python test run passes.
- Ending the interview renders the report, and its save is confirmed.
- Quitting and relaunching lists that report in history.
Then closing the window and quitting from the menu, in each phase of an interview, with the report-saved state checked after each. A Chromium run under Playwright says nothing about WKWebView, WebView2 or WebKitGTK.
The server first, since the app needs one to start:
make build # target/release/codetrialOn Linux, the app also needs WebKitGTK 4.1 and its friends (Debian and Ubuntu names; Ubuntu 22.04 is the oldest with 4.1):
sudo apt-get install libwebkit2gtk-4.1-dev libxdo-dev libssl-dev librsvg2-devEverything the app's build produces lands in target/desktop/, beside the
server's own target/: desktop/.cargo/config.toml says so for cargo and the
Tauri CLI run from desktop/, and scripts/test.sh passes the same directory
when it builds from the root. Nothing is written under desktop/ itself.
A development run, pointing the app at that server with
CODETRIAL_DESKTOP_SERVER because a development build has no bundle for it to
sit in:
cd desktop
CODETRIAL_DESKTOP_SERVER=$PWD/../target/release/codetrial cargo runInstallers come from the Tauri CLI, with the server copied to the name it
expects under target/desktop/sidecar/. tauri.bundle.json is what adds the
server to the bundle; it is kept out of tauri.conf.json so that a plain
cargo build of the app does not need a server built first.
triple=$(rustc -vV | sed -n 's/^host: //p')
mkdir -p target/desktop/sidecar
cp target/release/codetrial "target/desktop/sidecar/codetrial-$triple"
cd desktop
npx @tauri-apps/cli@2.12.3 build --config tauri.bundle.jsonThe installers are under target/desktop/release/bundle/. Linux gets .deb,
.rpm and AppImage; macOS an .app in a .dmg; Windows an MSI and an NSIS
installer. Each puts codetrial beside codetrial-desktop,
so the .deb and .rpm also put the CLI on the PATH.
scripts/test.sh reaches the app through two gates, because the root's
fmt, clippy and cargo test never see a crate with its own manifest:
desktop-fmt always, and desktop (clippy and the unit tests) where the
platform's web view can be linked, which on Linux means libwebkit2gtk-4.1-dev
is installed. Without it the gate skips and says so. The formatter chain and
cargo audit cover desktop/ as well.
scripts/desktop-smoke.sh APP is outside the gate because it needs a built app
and a display. It starts the app, checks that its server answers with the Setup
page and refuses a foreign Host, then kills the app with SIGKILL and checks
the server went with it. On Linux, run it under xvfb-run -a.
.github/workflows/desktop.yml builds the installers on all three platforms
and runs the smoke test against each build, for pull requests that touch the
app and for pushes to main that change more than documentation. Two releases
come out of it:
- Each such push to
mainreplaces the rollingdesktop-latestprerelease with that commit's installers, like the CLI'slatest. That release, on the repository's releases page, is where to download the app. It is not marked Latest, and it does not wait for the gates and the audit incheck.yml, so a commit that fails them can be published until the next one replaces it. - A tag
desktop-vX.Y.Zpublishes a versioned prerelease, after checking thatX.Y.Zis the version indesktop/Cargo.toml.
A pull request's installers are workflow artifacts, kept for a limited time and never released.