use serde_json::Value;
use topcoat::Result;
use topcoat::view::component;
use topcoat::view::view;
use crate::daemon_config::AccountProfile;
#[component]
pub(crate) async fn goal_modal() -> Result {
view! {
<div id="goal-modal" class="fixed inset-0 z-[70] hidden place-items-center bg-background/75 p-4 backdrop-blur-sm">
<section role="dialog" aria-modal="true" aria-labelledby="goal-modal-title" class="w-full max-w-xl overflow-hidden rounded-2xl border border-border bg-background shadow-sm">
<header class="flex items-start gap-3 border-b border-border px-5 py-4">
<div class="min-w-0 flex-1">
<h2 id="goal-modal-title" class="text-sm font-semibold">"Session goal"</h2>
<p class="mt-1 text-xs text-muted-foreground">"Keep a persistent objective, lifecycle state, and optional token budget for this conversation."</p>
</div>
<button type="button" data-close-modal="goal-modal" class="grid size-8 place-items-center rounded-lg text-muted-foreground hover:bg-foreground/5 hover:text-foreground" aria-label="Close">"×"</button>
</header>
<form id="goal-form" class="grid gap-4 p-5">
<label class="grid gap-1.5 text-xs font-medium">
"Objective"
<textarea id="goal-objective" rows="4" required="" class="resize-y rounded-lg border border-border bg-background px-3 py-2 text-sm leading-6 outline-none focus-visible:ring-2 focus-visible:ring-ring" placeholder="What should Codex keep working toward?"></textarea>
</label>
<div class="grid grid-cols-2 gap-4 max-sm:grid-cols-1">
<label class="grid gap-1.5 text-xs font-medium">
"Status"
<select id="goal-status" class="h-10 rounded-lg border border-border bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring">
<option value="active">"Active"</option>
<option value="paused">"Paused"</option>
<option value="blocked">"Blocked"</option>
<option value="usageLimited">"Usage limited"</option>
<option value="budgetLimited">"Budget limited"</option>
<option value="complete">"Complete"</option>
</select>
</label>
<label class="grid gap-1.5 text-xs font-medium">
"Token budget"
<input id="goal-token-budget" type="number" min="1" step="1" class="h-10 rounded-lg border border-border bg-background px-3 font-mono text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring" placeholder="Unlimited">
</label>
</div>
<div id="goal-usage" class="hidden grid-cols-2 gap-3 rounded-xl border border-border bg-foreground/[0.025] p-4 text-xs text-muted-foreground"></div>
<p id="goal-error" class="hidden text-xs text-destructive"></p>
<footer class="flex items-center gap-2 border-t border-border pt-4">
<button id="goal-clear" type="button" class="rounded-lg px-3 py-2 text-sm text-destructive hover:bg-destructive/10">"Clear goal"</button>
<button type="button" data-close-modal="goal-modal" class="ml-auto rounded-lg border border-border px-3 py-2 text-sm hover:bg-foreground/5">"Cancel"</button>
<button type="submit" class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground">"Save goal"</button>
</footer>
</form>
</section>
</div>
}
}
#[component]
pub(crate) async fn settings_modal(
mcp_servers: &[Value],
accounts: &[AccountProfile],
active_account: &str,
) -> Result {
view! {
<div id="settings-modal" class="fixed inset-0 z-[70] hidden place-items-center bg-background/75 p-4 backdrop-blur-sm">
<section role="dialog" aria-modal="true" aria-labelledby="settings-modal-title" class="flex max-h-[min(48rem,92vh)] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-sm">
<header class="flex items-start gap-3 border-b border-border px-5 py-4">
<div class="min-w-0 flex-1">
<h2 id="settings-modal-title" class="text-sm font-semibold">"Settings"</h2>
<p class="mt-1 text-xs text-muted-foreground">"Appearance, accounts, and MCP connections."</p>
</div>
<button type="button" data-close-modal="settings-modal" class="grid size-8 place-items-center rounded-lg text-muted-foreground hover:bg-foreground/5 hover:text-foreground" aria-label="Close">"×"</button>
</header>
<div class="min-h-0 overflow-y-auto p-5">
<section>
<h3 class="text-sm font-semibold">"Appearance"</h3>
<label class="mt-3 flex items-center justify-between gap-4 rounded-xl border border-border p-4 text-sm">
<span><strong class="block font-medium">"Theme"</strong><span class="mt-1 block text-xs text-muted-foreground">"Dark is used by default, including while pages load."</span></span>
<select id="settings-theme" class="h-9 rounded-lg border border-border bg-background px-3 text-sm">
<option value="dark">"Dark"</option>
<option value="light">"Light"</option>
<option value="system">"System"</option>
</select>
</label>
</section>
<section class="mt-7">
<h3 class="text-sm font-semibold">"Accounts"</h3>
<p class="mt-1 text-xs leading-5 text-muted-foreground">"Profiles are tried from top to bottom when the daemon starts. Sign-in actions apply to the active profile."</p>
<div class="mt-3 divide-y divide-border overflow-hidden rounded-xl border border-border">
for account in accounts {
<div class="flex items-center gap-3 px-4 py-3">
<span class="grid size-7 place-items-center rounded-lg bg-foreground/5 font-mono text-xs">"@"</span>
<span class="min-w-0 flex-1"><strong class="block truncate text-sm font-medium">(account.label.as_str())</strong><span class="block truncate font-mono text-[0.66rem] text-muted-foreground">(account.codex_home.display().to_string())</span></span>
if account.name == active_account {
<span class="rounded-full bg-sky-500/10 px-2 py-1 text-[0.65rem] font-medium text-sky-400">"Active"</span>
}
<span class="flex items-center gap-1">
<button type="button" data-account-move=(account.name.as_str()) data-direction="up" class="grid size-7 place-items-center rounded-md text-muted-foreground hover:bg-foreground/5 hover:text-foreground" title="Move earlier">"↑"</button>
<button type="button" data-account-move=(account.name.as_str()) data-direction="down" class="grid size-7 place-items-center rounded-md text-muted-foreground hover:bg-foreground/5 hover:text-foreground" title="Move later">"↓"</button>
if account.name != active_account {
<button type="button" data-account-remove=(account.name.as_str()) class="grid size-7 place-items-center rounded-md text-destructive hover:bg-destructive/10" title="Remove profile">"×"</button>
}
</span>
</div>
}
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button type="button" data-account-login="" class="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-primary-foreground">"Sign in with ChatGPT"</button>
<button type="button" data-account-api-key="" class="rounded-lg border border-border px-3 py-2 text-sm hover:bg-foreground/5">"Use API key"</button>
<button type="button" data-account-logout="" class="rounded-lg px-3 py-2 text-sm text-destructive hover:bg-destructive/10">"Sign out"</button>
</div>
<p data-account-message="" class="mt-3 hidden text-xs text-muted-foreground"></p>
<details class="mt-4 rounded-xl border border-border p-4">
<summary class="cursor-pointer text-sm font-medium">"Add account profile"</summary>
<form data-account-profile-form="" class="mt-4 grid gap-3">
<div class="grid grid-cols-2 gap-3 max-sm:grid-cols-1">
<label class="grid gap-1 text-xs font-medium">"Name"<input name="name" required="" pattern="[A-Za-z0-9_-]+" class="h-9 rounded-lg border border-border bg-background px-3 font-mono text-sm" placeholder="personal"></label>
<label class="grid gap-1 text-xs font-medium">"Label"<input name="label" class="h-9 rounded-lg border border-border bg-background px-3 text-sm" placeholder="Personal"></label>
</div>
<label class="grid gap-1 text-xs font-medium">"Codex home"<input name="codexHome" required="" class="h-9 rounded-lg border border-border bg-background px-3 font-mono text-sm" placeholder="/home/me/.codex-personal"></label>
<label class="grid gap-1 text-xs font-medium">"HTTP or SOCKS proxy (optional)"<input name="proxy" class="h-9 rounded-lg border border-border bg-background px-3 font-mono text-sm" placeholder="socks5h://127.0.0.1:1080"></label>
<label class="flex items-center gap-2 text-xs"><input name="useSshTunnel" type="checkbox">"Use the daemon's managed SSH tunnel"</label>
<button type="submit" class="w-fit rounded-lg border border-border px-3 py-2 text-sm font-medium hover:bg-foreground/5">"Add profile"</button>
</form>
</details>
<p class="mt-3 text-[0.68rem] leading-5 text-muted-foreground">"Profile order, additions, and removals are saved to daemon.toml and take effect after restarting the daemon."</p>
</section>
<section class="mt-7">
<div class="flex items-baseline gap-2"><h3 class="text-sm font-semibold">"MCP servers"</h3><span class="font-mono text-[0.66rem] text-muted-foreground">(mcp_servers.len())" configured"</span></div>
<div class="mt-3 divide-y divide-border overflow-hidden rounded-xl border border-border">
if mcp_servers.is_empty() {
<p class="px-4 py-5 text-sm text-muted-foreground">"No MCP servers are configured."</p>
}
for server in mcp_servers {
<div class="flex items-center gap-3 px-4 py-3">
<span class=(if needs_login(server) { "size-2 rounded-full bg-amber-500" } else { "size-2 rounded-full bg-emerald-500" })></span>
<span class="min-w-0 flex-1"><strong class="block truncate text-sm font-medium">(field(server, "name"))</strong><span class="block text-xs text-muted-foreground">(if needs_login(server) { "Authentication required" } else { "Connected" })</span></span>
if needs_login(server) {
<button type="button" data-mcp-login=(field(server, "name")) class="rounded-lg border border-border px-3 py-1.5 text-xs hover:bg-foreground/5">"Log in"</button>
}
</div>
}
</div>
</section>
</div>
</section>
</div>
}
}
fn field<'a>(value: &'a Value, name: &str) -> &'a str {
value.get(name).and_then(Value::as_str).unwrap_or_default()
}
fn needs_login(server: &Value) -> bool {
matches!(field(server, "authStatus"), "notAuthenticated" | "expired")
}
#[cfg(test)]
#[path = "panels_tests.rs"]
mod tests;