Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- **Studio: WiFi token memory** β€” Studio now remembers the agent token per discovered device (keyed by Bonjour instance name) in localStorage. After a successful WiFi connect, subsequent mDNS-discovered sessions for that device prefill the token automatically and show a "πŸ”‘ saved" tag. Each remembered device gets a small "βœ•" button to forget the token.

## [0.7.0] - 2026-05-02

### Added
Expand Down
69 changes: 68 additions & 1 deletion studio/frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,47 @@ $("help-overlay").addEventListener("click", (e) => {

type WiFiDevice = { name: string; host: string; port: number; version: string };

const WIFI_TOKENS_KEY = "fp.studio.wifi-tokens";

// Token store: deviceName β†’ token. Persisted in localStorage so a user who
// has connected once doesn't need to paste the token again on subsequent
// launches. Keyed by the Bonjour instance name (typically the device's
// hostname) since the IP can change across DHCP leases but the name is
// stable. Tokens for an LAN-only test agent inside a desktop app are an
// acceptable trust boundary; this is not a credential vault.
function loadTokenStore(): Record<string, string> {
try {
const raw = localStorage.getItem(WIFI_TOKENS_KEY);
return raw ? (JSON.parse(raw) as Record<string, string>) : {};
} catch {
return {};
}
}

function saveTokenStore(store: Record<string, string>) {
try {
localStorage.setItem(WIFI_TOKENS_KEY, JSON.stringify(store));
} catch {
// localStorage full or denied β€” non-fatal, user just re-enters next time
}
}

function rememberToken(deviceName: string, token: string) {
const store = loadTokenStore();
store[deviceName] = token;
saveTokenStore(store);
}

function forgetToken(deviceName: string) {
const store = loadTokenStore();
delete store[deviceName];
saveTokenStore(store);
}

function recalledToken(deviceName: string): string | undefined {
return loadTokenStore()[deviceName];
}

let wifiSelected: WiFiDevice | null = null;
const wifiSeen = new Map<string, WiFiDevice>();

Expand Down Expand Up @@ -337,9 +378,26 @@ function selectWiFiDevice(dev: WiFiDevice) {
for (const li of Array.from($("wifi-devices").children) as HTMLElement[]) {
li.classList.toggle("selected", li.dataset.key === `${dev.host}:${dev.port}`);
}
const remembered = recalledToken(dev.name);
$("wifi-selected-label").textContent = `Connecting to ${dev.name} (${dev.host}:${dev.port})`;
($("wifi-token-row") as HTMLElement).hidden = false;
($("wifi-token-input") as HTMLInputElement).focus();
const input = $("wifi-token-input") as HTMLInputElement;
input.value = remembered ?? "";
input.focus();
}

function renderForgetButton(deviceName: string, container: HTMLElement) {
const btn = document.createElement("button");
btn.className = "btn-mini wifi-forget";
btn.title = `Forget remembered token for ${deviceName}`;
btn.textContent = "βœ•";
btn.addEventListener("click", (e) => {
e.stopPropagation();
forgetToken(deviceName);
btn.remove();
toast(`Forgot token for ${deviceName}`, "info", 1500);
});
container.appendChild(btn);
}

EventsOn("wifi:device-found", (dev: WiFiDevice) => {
Expand All @@ -360,6 +418,14 @@ EventsOn("wifi:device-found", (dev: WiFiDevice) => {
hostEl.className = "wifi-host";
hostEl.textContent = `${dev.host}:${dev.port}${dev.version ? ` Β· v${dev.version}` : ""}`;
li.append(nameEl, hostEl);
if (recalledToken(dev.name)) {
const savedTag = document.createElement("span");
savedTag.className = "wifi-saved-tag";
savedTag.textContent = "πŸ”‘ saved";
savedTag.title = "Token remembered from a previous connection";
li.appendChild(savedTag);
renderForgetButton(dev.name, li);
}
li.addEventListener("click", () => selectWiFiDevice(dev));
list.appendChild(li);
});
Expand All @@ -380,6 +446,7 @@ $("btn-wifi-connect").addEventListener("click", async () => {
setStatus("connecting", `Connecting to ${wifiSelected.host}…`);
try {
await ConnectWiFi(wifiSelected.host, wifiSelected.port, token);
rememberToken(wifiSelected.name, token);
setStatus("connected", `Connected Β· ${wifiSelected.name}`);
closeWiFiOverlay();
startStream();
Expand Down
3 changes: 3 additions & 0 deletions studio/frontend/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,9 @@ body { display: flex; flex-direction: column; overflow: hidden; }
margin-top: 6px;
}
.wifi-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
.wifi-saved-tag { color: var(--fg-dim); font-size: 10px; margin-left: 8px; padding: 1px 6px; background: var(--bg-1); border-radius: var(--radius-sm); }
.wifi-device-list .btn-mini.wifi-forget { float: right; opacity: 0.5; padding: 0 6px; font-size: 11px; }
.wifi-device-list .btn-mini.wifi-forget:hover { opacity: 1; }
kbd {
display: inline-block;
padding: 1px 6px;
Expand Down
Loading