pi-orchestrator-config 4.20.1 → 4.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +8 -0
  2. package/extensions/coms/coms-p2p.ts +44 -1
  3. package/extensions/coms/coms-shared.ts +35 -0
  4. package/extensions/orchestrator/enforcement.ts +16 -8
  5. package/extensions/orchestrator/index.ts +32 -0
  6. package/extensions/orchestrator/subagent-tool.ts +7 -1
  7. package/extensions/pidash/browser-ops.ts +231 -0
  8. package/extensions/pidash/pidash-ui/dist/assets/index-BKsBLUuf.css +2 -0
  9. package/extensions/pidash/pidash-ui/dist/assets/index-DzT6_qJc.js +23 -0
  10. package/extensions/pidash/pidash-ui/dist/index.html +2 -2
  11. package/extensions/pidash/pidash-ui/src/App.tsx +81 -15
  12. package/extensions/pidash/pidash-ui/src/components/ComsPanel.tsx +162 -0
  13. package/extensions/pidash/pidash-ui/src/components/InfoBar.tsx +162 -70
  14. package/extensions/pidash/pidash-ui/src/components/InputBar.tsx +5 -5
  15. package/extensions/pidash/pidash-ui/src/components/KeybindingSettings.tsx +3 -0
  16. package/extensions/pidash/pidash-ui/src/components/McpPanel.tsx +102 -0
  17. package/extensions/pidash/pidash-ui/src/components/MemoryPanel.tsx +68 -0
  18. package/extensions/pidash/pidash-ui/src/components/MessageList.tsx +103 -54
  19. package/extensions/pidash/pidash-ui/src/components/NewSessionDialog.tsx +102 -0
  20. package/extensions/pidash/pidash-ui/src/components/SessionSidebar.tsx +243 -83
  21. package/extensions/pidash/pidash-ui/src/components/SettingsPanel.tsx +310 -0
  22. package/extensions/pidash/pidash-ui/src/components/SidePanel.tsx +164 -0
  23. package/extensions/pidash/pidash-ui/src/components/TasksPanel.tsx +119 -0
  24. package/extensions/pidash/pidash-ui/src/hooks/useKeybindings.ts +24 -3
  25. package/extensions/pidash/pidash-ui/src/hooks/useKeyboardNavigation.ts +21 -4
  26. package/extensions/pidash/pidash-ui/src/hooks/useMessageHandler.ts +78 -8
  27. package/extensions/pidash/pidash-ui/src/lib/settings-edit.ts +73 -0
  28. package/extensions/pidash/pidash-ui/src/lib/theme.ts +53 -0
  29. package/extensions/pidash/pidash-ui/src/main.tsx +9 -0
  30. package/extensions/pidash/pidash-ui/src/types.ts +58 -0
  31. package/extensions/pidash/pidash.ts +161 -0
  32. package/extensions/pitasks/index.ts +60 -9
  33. package/extensions/pitasks/task-store.ts +37 -12
  34. package/extensions/shared/ui-bridge.ts +232 -0
  35. package/package.json +1 -1
  36. package/scripts/pidash-server.ts +8 -0
  37. package/extensions/pidash/pidash-ui/dist/assets/index-CI6UL4g4.js +0 -23
  38. package/extensions/pidash/pidash-ui/dist/assets/index-hM6gC5Yg.css +0 -2
package/README.md CHANGED
@@ -661,6 +661,14 @@ Pidash is a web-based dashboard that runs alongside the TUI, accessible from any
661
661
  - Per-project diff viewer (pidiff) — opens in browser via clickable `pi-diff` status-line link, powered by `@pierre/diffs` + `@pierre/trees`, with review comments
662
662
  - Collapsible thinking and tool blocks with copy buttons
663
663
  - ask_user tool bridging (answer from browser or TUI)
664
+ - **Full TUI parity (#968)** — every interactive prompt and surface is answerable/operable from either TUI or browser, first answer wins:
665
+ - Dangerous-command approvals, commit-trailer selection, project-agent confirmations, and `/tasks` menus render in both surfaces
666
+ - Tasks panel (view/create/start/complete/delete/clear) driven through the live TaskStore with live updates
667
+ - Coms peer list (busy state, queue depth, context %) + send message to a peer (same delivery path as `coms_send`)
668
+ - New-session creation (name + initial model) and `/compact` from the browser
669
+ - Memory status (usage, topics, situation report)
670
+ - MCP server management (list/login/logout)
671
+ - Full settings page — every key from `settings-keys.json`, project + global scopes, typed editors, written through the live settings module (same semantics as `/pi-config-settings`)
664
672
  - Mobile responsive
665
673
  - Event buffering for message replay on refresh
666
674
  - Browser push notifications for background events (configurable per event type)
@@ -19,7 +19,8 @@ import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@ea
19
19
  import { Text, truncateToWidth } from "@earendil-works/pi-tui";
20
20
  import { Type } from "typebox";
21
21
  import { applyExtensionDefaults } from "./themeMap.js";
22
- import { ulid, hexFg, isValidHex, fallbackColor, comsParseYamlFrontmatter as parseFrontmatter, nowIso, abbreviateModel, findSystemPromptPath, readFrontmatterFromArgv, readTaskSummary, buildInboundContent, renderTasksPart, renderQueuePart, formatQueueStr, formatComsResponseText, formatComsResponseType, formatComsResponseBody, formatComsInboundType, sanitizeComsName, createComsInboundTasks, shouldEvictOnLeaving, shouldAnnouncePeerLeft, isSessionStillRegistered, FALLBACK_PALETTE, type TasksSummary, insertInboundByPriority, shouldAcceptSteer, summarizeBroadcast, formatBusyStr, selectSenderBundle, type BroadcastTargetResult } from "./coms-shared.js";
22
+ import {
23
+ buildComsPeersPayload, ulid, hexFg, isValidHex, fallbackColor, comsParseYamlFrontmatter as parseFrontmatter, nowIso, abbreviateModel, findSystemPromptPath, readFrontmatterFromArgv, readTaskSummary, buildInboundContent, renderTasksPart, renderQueuePart, formatQueueStr, formatComsResponseText, formatComsResponseType, formatComsResponseBody, formatComsInboundType, sanitizeComsName, createComsInboundTasks, shouldEvictOnLeaving, shouldAnnouncePeerLeft, isSessionStillRegistered, FALLBACK_PALETTE, type TasksSummary, insertInboundByPriority, shouldAcceptSteer, summarizeBroadcast, formatBusyStr, selectSenderBundle, type BroadcastTargetResult } from "./coms-shared.js";
23
24
  import { appendHistoryRecord, queryHistoryRecords, truncatePreview, type HistoryRecord } from "./coms-history.js";
24
25
  import { openListDetailOverlay, OverlayScrollDetail } from "../orchestrator/overlay-dashboard.js";
25
26
  import * as net from "node:net";
@@ -785,6 +786,7 @@ export default function (pi: ExtensionAPI) {
785
786
  const key = `pending=${pc}|busy=${busyKey}|prog=${progressKey}`;
786
787
  if (key === lastPoolSnapshot) return;
787
788
  lastPoolSnapshot = key;
789
+ emitComsPeers();
788
790
  try { installPoolWidget(currentCtx); } catch {}
789
791
  }
790
792
 
@@ -1803,6 +1805,10 @@ Do not respond to this message.`;
1803
1805
  existing.staleCount = 0;
1804
1806
  existing.model = entry.model ?? existing.model;
1805
1807
  log.debug("fs_watch_heartbeat", entry.name, "ctx", entry.context_used_pct, "busy", existing.busy);
1808
+ // Heartbeats mutate peer fields without touching the widget
1809
+ // refresh snapshot key — re-emit so browser peer cards stay
1810
+ // current (Qodo R1: stale peer status otherwise lingers).
1811
+ emitComsPeers();
1806
1812
  } else if (entry && entry.coms_session_id && !peerCards.has(entry.coms_session_id)) {
1807
1813
  const card: AgentCard & { staleCount: number } = {
1808
1814
  name: entry.name, purpose: entry.purpose, model: entry.model,
@@ -3613,6 +3619,43 @@ Do not respond to this message.`;
3613
3619
  handler: comsRefreshHandler,
3614
3620
  });
3615
3621
 
3622
+ // ━━ #968 part C: pidash browser bridge ─━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3623
+ // Peer list + messaging from the browser, routed through the SAME
3624
+ // resolveTarget/deliverPrompt path as coms_send — identical delivery
3625
+ // semantics, browser is just another transport.
3626
+ function emitComsPeers(): void {
3627
+ try {
3628
+ pi.events.emit("pidash:coms-peers", buildComsPeersPayload(
3629
+ peerCards.values(),
3630
+ identity ? { name: identity.name, purpose: identity.purpose, project: identity.project } : null,
3631
+ ));
3632
+ } catch { /* pidash not loaded */ }
3633
+ }
3634
+ pi.events.on("pidash:coms-peers-request", () => emitComsPeers());
3635
+ pi.events.on("pidash:coms-send", async (data: unknown) => {
3636
+ const d = data as { requestId?: string; target?: string; prompt?: string; steer?: boolean; priority?: boolean };
3637
+ if (!d?.target || !d?.prompt) {
3638
+ pi.events.emit("pidash:coms-send-result", { requestId: d?.requestId, ok: false, error: "target and prompt required" });
3639
+ return;
3640
+ }
3641
+ try {
3642
+ if (!identity) throw new Error("coms not initialised");
3643
+ const target = await resolveTarget(d.target);
3644
+ if (!target) throw new Error(`peer not found: ${d.target}`);
3645
+ const { msg_id } = await deliverPrompt(target, {
3646
+ prompt: d.prompt,
3647
+ ...(d.steer ? { steer: true } : {}),
3648
+ ...(d.priority ? { priority: true } : {}),
3649
+ });
3650
+ log.info("pidash_coms_send", { target: target.name, msg_id, steer: !!d.steer });
3651
+ pi.events.emit("pidash:coms-send-result", { requestId: d.requestId, ok: true, msg_id, target: target.name });
3652
+ } catch (e) {
3653
+ const error = e instanceof Error ? e.message : String(e);
3654
+ log.warn("pidash_coms_send_failed", { target: d.target, error });
3655
+ pi.events.emit("pidash:coms-send-result", { requestId: d.requestId, ok: false, error });
3656
+ }
3657
+ });
3658
+
3616
3659
  // ━━ /coms-queue command — view and kill queued messages ━━━━━━━━━━━━━━━
3617
3660
  let comsQueueCmdHandler: any;
3618
3661
  pi.registerCommand("coms-queue", {
@@ -551,6 +551,41 @@ export function formatComsResponseText(targetName: string, response: any, error:
551
551
  : `[coms response from ${targetName}${arrow}${queueNote}] ${JSON.stringify(response, null, 2)}`;
552
552
  }
553
553
 
554
+ /** #968 C: pidash browser peer-list payload — pure projection of live cards. */
555
+ export interface ComsPeerCard {
556
+ name: string;
557
+ purpose: string;
558
+ model: string;
559
+ color: string;
560
+ context_used_pct?: number;
561
+ queue_depth?: number;
562
+ busy?: boolean;
563
+ tasks_summary?: unknown;
564
+ staleCount: number;
565
+ }
566
+
567
+ export function buildComsPeersPayload(
568
+ cards: Iterable<ComsPeerCard>,
569
+ self: { name: string; purpose?: string; project?: string } | null,
570
+ ): { self: { name: string; purpose: string; project: string } | null; peers: Array<Record<string, unknown>> } {
571
+ const peers: Array<Record<string, unknown>> = [];
572
+ for (const card of cards) {
573
+ peers.push({
574
+ name: card.name,
575
+ purpose: card.purpose,
576
+ model: card.model,
577
+ busy: card.busy ?? false,
578
+ queueDepth: card.queue_depth ?? 0,
579
+ contextUsedPct: card.context_used_pct ?? 0,
580
+ tasksSummary: card.tasks_summary ?? null,
581
+ });
582
+ }
583
+ return {
584
+ self: self ? { name: self.name, purpose: self.purpose || "", project: self.project || "" } : null,
585
+ peers,
586
+ };
587
+ }
588
+
554
589
  /** Sanitize a coms identity name for safe embedding in header strings.
555
590
  * Strips all control characters (C0/C1), brackets; replaces whitespace with hyphens. */
556
591
  export function sanitizeComsName(name: string): string {
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { createLogger } from "../shared/logger.js";
9
+ import { createBridgedUi } from "../shared/ui-bridge.js";
9
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
11
  import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
11
12
  import { Type } from "typebox";
@@ -119,6 +120,9 @@ function commitTrailerModelId(ctx: any): string {
119
120
  }
120
121
 
121
122
  /** Inject commit trailer into command (Assisted-by, Co-authored-by, etc.) */
123
+ /** #968 B: set by registerEnforcement; null → plain ctx.ui prompts (tests, unregistered). */
124
+ let bridgedUi: ReturnType<typeof createBridgedUi> | null = null;
125
+
122
126
  async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd: string): Promise<EnforcementResult> {
123
127
  // #932: rewrite a literal `$PI_MODEL` in the trailer identity — an agent
124
128
  // following the git-expert trailer instruction may write the placeholder
@@ -159,10 +163,9 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
159
163
  // No UI available — default to first option
160
164
  trailerName = options[0];
161
165
  } else {
162
- const selected = await ctx.ui.select(
163
- "Select commit trailer:",
164
- options,
165
- );
166
+ const selected = bridgedUi
167
+ ? await bridgedUi.select(ctx, "Select commit trailer:", options)
168
+ : await ctx.ui.select("Select commit trailer:", options);
166
169
  if (!selected || !options.includes(selected)) {
167
170
  return {
168
171
  block: true,
@@ -579,10 +582,10 @@ async function checkDangerousCommands(command: string, cwd: string, ctx: any): P
579
582
  : []),
580
583
  "No",
581
584
  ];
582
- const ok = await ctx.ui.select(
583
- `⚠️ Dangerous statement${remaining.length > 1 ? "s" : ""} (of the full command):\n\n${list}${scopeNote}\n\nAllow?`,
584
- options,
585
- );
585
+ const promptTitle = `⚠️ Dangerous statement${remaining.length > 1 ? "s" : ""} (of the full command):\n\n${list}${scopeNote}\n\nAllow?`;
586
+ const ok = bridgedUi
587
+ ? await bridgedUi.select(ctx, promptTitle, options)
588
+ : await ctx.ui.select(promptTitle, options);
586
589
  if (ok === "No" || ok === undefined)
587
590
  return { block: true, reason: "Blocked by user. Do NOT retry with an equivalent command (e.g., find -delete, perl, python os.remove). Stop and report this block to the user." };
588
591
  if (ok === "Yes — run once" || ok.startsWith("Always allow all of the above")) {
@@ -639,6 +642,11 @@ async function checkDangerousCommands(command: string, cwd: string, ctx: any): P
639
642
  }
640
643
 
641
644
  export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): void {
645
+ // #968 part B: enforcement prompts (dangerous-command approvals, trailer
646
+ // selection) are bridged — answerable from TUI or pidash browser, first
647
+ // answer wins, same enforcement semantics either way. Module-level check
648
+ // functions read this ref; null (unregistered / tests) falls back to plain ui.
649
+ bridgedUi = createBridgedUi(pi);
642
650
 
643
651
  pi.on("session_start", (_event, ctx) => {
644
652
  cachedTrailerSelection = null;
@@ -34,6 +34,8 @@ import { registerMemoryTools } from "./memory-tools.js";
34
34
  import { registerReviewUI } from "./review-ui.js";
35
35
  import { registerSessionSearch } from "./session-search.js";
36
36
  import { registerSettingsTui } from "./settings-tui.js";
37
+ import { buildSituationReport } from "./situation-report.js";
38
+ import { listTopics } from "./memory-tree.js";
37
39
  import { registerMarkdownTransformer } from "./markdown-transformer.js";
38
40
  import { registerGraft } from "./graft.js";
39
41
  import { registerReviewLearning } from "./review-learning.js";
@@ -89,6 +91,36 @@ export default function (pi: ExtensionAPI) {
89
91
  }
90
92
  });
91
93
 
94
+ // ── #968 part E: memory status surface for pidash ──────────────────
95
+ // Read-only snapshot through the live orchestrator memory modules —
96
+ // situation report, topic hotness, entry counts, budget usage.
97
+ // The requesting session's cwd comes in the event payload (the pidash
98
+ // browser may be watching a session in a different project than this
99
+ // process's cwd).
100
+ pi.events.on("pidash:memory-status-request", (data: unknown) => {
101
+ try {
102
+ const payload = (typeof data === "object" && data !== null ? data : {}) as { cwd?: unknown };
103
+ const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
104
+ const report = buildSituationReport(cwd);
105
+ const topics = listTopics(cwd).map((t) => ({
106
+ name: t.name,
107
+ entryCount: t.entryCount,
108
+ hotness: Math.round(t.hotness),
109
+ newestEntry: t.newestEntry,
110
+ }));
111
+ const usageMatch = report.match(/# Project Memory \[([0-9]+)%[^\]]*\]/);
112
+ pi.events.emit("pidash:memory-status", {
113
+ topics,
114
+ usagePct: usageMatch ? Number(usageMatch[1]) : null,
115
+ report,
116
+ });
117
+ log.info("pidash_memory_status_sent", { topics: topics.length });
118
+ } catch (e: any) {
119
+ log.warn("pidash_memory_status_failed", { error: e?.message });
120
+ pi.events.emit("pidash:memory-status", { topics: [], usagePct: null, report: "", error: e?.message });
121
+ }
122
+ });
123
+
92
124
  // Extended autocomplete must register FIRST — it wraps registerCommand
93
125
  // to inject getArgumentCompletions before other modules register their commands.
94
126
  registerExtendedAutocomplete(pi);
@@ -17,6 +17,7 @@ import type { Message } from "@earendil-works/pi-ai";
17
17
  import { StringEnum } from "@earendil-works/pi-ai";
18
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
19
  import { createLogger } from "../shared/logger.js";
20
+ import { createBridgedUi } from "../shared/ui-bridge.js";
20
21
  import {
21
22
  getMarkdownTheme,
22
23
  withFileMutationQueue,
@@ -602,6 +603,10 @@ export function registerSubagentTool(
602
603
  // Child processes set PI_SUBAGENT_CHILD=1 to prevent infinite recursion.
603
604
  if (process.env.PI_SUBAGENT_CHILD === "1") return;
604
605
 
606
+ // #968 part B: the project-agent approval is bridged — answerable from
607
+ // TUI or pidash browser, first answer wins.
608
+ const bridgedUi = createBridgedUi(pi);
609
+
605
610
  // ── Extracted execute helpers (closure over pi, spawnAsyncAgent, killAsyncAgent) ──
606
611
 
607
612
  // asyncKill is an LLM-issued decision, not a user action.
@@ -1627,7 +1632,8 @@ export function registerSubagentTool(
1627
1632
  .map((n) => agents.find((a) => a.name === n))
1628
1633
  .filter((a): a is AgentConfig => a?.source === "project");
1629
1634
  if (projAgents.length > 0) {
1630
- const ok = await ctx.ui.confirm(
1635
+ const ok = await bridgedUi.confirm(
1636
+ ctx,
1631
1637
  "Run project-local agents?",
1632
1638
  `Agents: ${projAgents.map((a) => a.name).join(", ")}\nSource: ${discovery.projectAgentsDir}`,
1633
1639
  );
@@ -0,0 +1,231 @@
1
+ /**
2
+ * browser-ops — pidash browser operations that need config/CLI access (#968 D/F/G).
3
+ *
4
+ * Settings: read/write through the SAME project-settings modules the TUI uses
5
+ * (settings-keys.json is the single source of truth; writeSettingsFile is the
6
+ * atomic writer). Never a direct ad-hoc file write from the daemon.
7
+ * MCP: `pi mcp` CLI (list --json / login / logout).
8
+ */
9
+
10
+ import { execFile } from "node:child_process";
11
+ import { promisify } from "node:util";
12
+
13
+ const execFileAsync = promisify(execFile);
14
+ import { createLogger } from "../shared/logger.js";
15
+ import {
16
+ SETTINGS_KEYS,
17
+ clearSettingsCache,
18
+ getGlobalSettingsPath,
19
+ getSettingsPath,
20
+ loadGlobalSettings,
21
+ loadProjectSettings,
22
+ parseSettingsFile,
23
+ } from "../orchestrator/project-settings.js";
24
+ import { readSettingsFile, writeSettingsFile } from "../orchestrator/settings-tui-helpers.js";
25
+
26
+ const log = createLogger("pidash-browser-ops");
27
+
28
+ export type SettingsScope = "project" | "global";
29
+
30
+ export interface SettingsKeyPayload {
31
+ key: string;
32
+ description: string;
33
+ type: string;
34
+ env?: string;
35
+ default: unknown;
36
+ group: string;
37
+ enum?: string[];
38
+ min?: number;
39
+ max?: number;
40
+ /** Value stored in the scope's file (undefined = not set there). */
41
+ stored: unknown;
42
+ }
43
+
44
+ export interface SettingsPayload {
45
+ scope: SettingsScope;
46
+ keys: SettingsKeyPayload[];
47
+ error?: string;
48
+ /** Resolved settings file path (project scope) — lets browsers decide
49
+ * whether a broadcast describes THEIR settings file even when their session
50
+ * cwd is a different worktree/subdir of the same repo (Qodo R5). */
51
+ settingsPath?: string;
52
+ }
53
+
54
+ function isSettingsScope(v: unknown): v is SettingsScope {
55
+ return v === "project" || v === "global";
56
+ }
57
+
58
+ /** Snapshot of every settings key for a scope, with the value stored in that scope's file. */
59
+ export function buildSettingsPayload(cwd: string, scope: unknown): SettingsPayload {
60
+ if (!isSettingsScope(scope)) return { scope: "project", keys: [], error: "invalid scope" };
61
+ try {
62
+ clearSettingsCache();
63
+ let stored: Record<string, unknown> = {};
64
+ if (scope === "project") {
65
+ stored = parseSettingsFile(getSettingsPath(cwd)) as unknown as Record<string, unknown>;
66
+ } else {
67
+ stored = loadGlobalSettings() as unknown as Record<string, unknown>;
68
+ }
69
+ const keys: SettingsKeyPayload[] = Object.entries(SETTINGS_KEYS).map(([key, def]) => ({
70
+ key,
71
+ description: def.description,
72
+ type: def.type,
73
+ env: def.env,
74
+ default: def.default,
75
+ group: (def as any).group ?? "General",
76
+ enum: def.enum,
77
+ min: def.min,
78
+ max: def.max,
79
+ stored: key in stored ? stored[key] : undefined,
80
+ }));
81
+ keys.sort((a, b) => (a.group === b.group ? a.key.localeCompare(b.key) : a.group.localeCompare(b.group)));
82
+ log.debug("settings_payload_built", { scope, keys: keys.length });
83
+ // Resolved file path (project scope only) — identity of the settings
84
+ // file being described, independent of the session's cwd (Qodo R5).
85
+ return { scope, keys, settingsPath: scope === "project" ? getSettingsPath(cwd) : undefined };
86
+ } catch (e: any) {
87
+ log.warn("settings_payload_failed", { scope, error: e?.message });
88
+ return { scope, keys: [], error: e?.message };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Validate a browser-supplied value against the settings-keys schema.
94
+ * Mirrors the checks `parseSettingsFile` applies on read, so the browser can
95
+ * never persist a value the terminal settings editor would reject or drop.
96
+ * Returns an error string, or null when the value is valid.
97
+ */
98
+ export function validateSettingValue(key: string, value: unknown): string | null {
99
+ const def = (SETTINGS_KEYS as Record<string, any>)[key];
100
+ if (!def) return `unknown setting key: ${key}`;
101
+ // Security-sensitive: the dangerous-command allowlist has a dedicated
102
+ // writer with trust/escaping checks (settings-tui) — never accept it over
103
+ // the browser websocket.
104
+ if (key === "dangerous_command_allowlist") {
105
+ return "dangerous_command_allowlist is managed by the terminal settings editor only";
106
+ }
107
+ if (value === null) return null; // clear — removing the key is always valid
108
+ const t = def.type as string;
109
+ const isStr = (v: unknown) => typeof v === "string" && v.trim().length > 0;
110
+ switch (t) {
111
+ case "bool":
112
+ case "bool_enable":
113
+ return typeof value === "boolean" ? null : `${key} must be a boolean`;
114
+ case "int":
115
+ if (typeof value !== "number" || !Number.isInteger(value)) return `${key} must be an integer`;
116
+ if (def.min !== undefined && value < def.min) return `${key} must be >= ${def.min}`;
117
+ if (def.max !== undefined && value > def.max) return `${key} must be <= ${def.max}`;
118
+ return null;
119
+ case "number":
120
+ if (typeof value !== "number" || !Number.isFinite(value)) return `${key} must be a finite number`;
121
+ if (def.min !== undefined && value < def.min) return `${key} must be >= ${def.min}`;
122
+ if (def.max !== undefined && value > def.max) return `${key} must be <= ${def.max}`;
123
+ return null;
124
+ case "port":
125
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535
126
+ ? null
127
+ : `${key} must be an integer between 1 and 65535`;
128
+ case "string":
129
+ case "enum":
130
+ case "model":
131
+ case "provider":
132
+ if (!isStr(value)) return `${key} must be a non-empty string`;
133
+ if (Array.isArray(def.enum) && def.enum.length > 0 && !def.enum.includes(value)) {
134
+ return `${key} must be one of: ${def.enum.join(", ")}`;
135
+ }
136
+ return null;
137
+ case "bool_or_string":
138
+ return typeof value === "boolean" || isStr(value) ? null : `${key} must be a boolean or non-empty string`;
139
+ case "string_list":
140
+ if (!Array.isArray(value) || !value.every((v: unknown) => typeof v === "string")) {
141
+ return `${key} must be an array of strings`;
142
+ }
143
+ return null;
144
+ case "agent_list":
145
+ if (typeof value === "string" || (Array.isArray(value) && value.every((v: unknown) => typeof v === "string"))) {
146
+ return null;
147
+ }
148
+ return `${key} must be a string or array of strings`;
149
+ case "agent_overrides":
150
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
151
+ return `${key} must be an object mapping agent name to {provider?, model?}`;
152
+ }
153
+ return null;
154
+ default:
155
+ return `${key} has unsupported type ${t} for browser edits`;
156
+ }
157
+ }
158
+
159
+ /** Apply one setting to a scope's file. value === null removes the key. */
160
+ export function applySetting(
161
+ cwd: string,
162
+ scope: unknown,
163
+ key: string,
164
+ value: unknown,
165
+ ): { ok: boolean; error?: string } {
166
+ if (!isSettingsScope(scope)) return { ok: false, error: "invalid scope" };
167
+ if (!(key in SETTINGS_KEYS)) return { ok: false, error: `unknown setting key: ${key}` };
168
+ const validationError = validateSettingValue(key, value);
169
+ if (validationError) {
170
+ log.warn("browser_setting_rejected", { scope, key, error: validationError });
171
+ return { ok: false, error: validationError };
172
+ }
173
+ try {
174
+ clearSettingsCache();
175
+ const filePath = scope === "project" ? getSettingsPath(cwd) : getGlobalSettingsPath();
176
+ // Read the RAW file — parseSettingsFile drops unknown keys and invalid
177
+ // values, so a read-modify-write through it would rewrite the file
178
+ // without them; and for malformed JSON it returns {} which would replace
179
+ // the user's file with a one-key file. readSettingsFile preserves every
180
+ // key and returns null for malformed files — refuse to save instead.
181
+ const data = readSettingsFile(filePath);
182
+ if (data === null) {
183
+ log.warn("browser_setting_save_refused_malformed_file", { scope, key, filePath });
184
+ return { ok: false, error: `${filePath} is malformed (invalid JSON or not an object) — fix it before saving from the browser` };
185
+ }
186
+ if (value === null) delete data[key];
187
+ else data[key] = value;
188
+ writeSettingsFile(filePath, data);
189
+ clearSettingsCache();
190
+ log.info("browser_setting_applied", { scope, key, cleared: value === null });
191
+ return { ok: true };
192
+ } catch (e: any) {
193
+ log.error("browser_setting_apply_failed", { scope, key, error: e?.message });
194
+ return { ok: false, error: e?.message };
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Run `pi mcp <args>` and resolve stdout (rejects with .stdout attached on failure).
200
+ *
201
+ * Async on purpose: this runs inside the live Pi process's websocket handler,
202
+ * and `pi mcp login` can wait on OAuth browser completion for minutes — a
203
+ * synchronous exec would block the event loop (TUI, agent streaming, timers,
204
+ * every pidash socket) for the whole timeout window.
205
+ */
206
+ export async function runMcp(args: string[], cwd: string, timeoutMs = 60000): Promise<string> {
207
+ const { stdout } = await execFileAsync("pi", ["mcp", ...args], {
208
+ cwd,
209
+ encoding: "utf-8",
210
+ timeout: timeoutMs,
211
+ env: { ...process.env, PI_MCP_NO_BROWSER: process.env.PI_MCP_NO_BROWSER },
212
+ });
213
+ return stdout;
214
+ }
215
+
216
+ /** Parse `pi mcp list --json` output. Returns servers array or error. */
217
+ export async function mcpList(cwd: string): Promise<{ servers?: unknown[]; raw?: string; error?: string }> {
218
+ try {
219
+ const out = await runMcp(["list", "--json"], cwd);
220
+ try {
221
+ const parsed = JSON.parse(out);
222
+ return { servers: Array.isArray(parsed) ? parsed : parsed?.servers ?? [parsed] };
223
+ } catch {
224
+ // Not JSON — return raw text for display
225
+ return { raw: out };
226
+ }
227
+ } catch (e: any) {
228
+ log.warn("mcp_list_failed", { error: e?.message });
229
+ return { error: (e?.stderr || e?.message || String(e)).slice(0, 2000) };
230
+ }
231
+ }
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){:root,:host{--shimmer-angle:20deg}*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--scroll-fade-t:0px;--scroll-fade-b:0px;--scroll-fade-s:0px;--scroll-fade-e:0px;--scroll-fade-mask:initial;--shimmer-image:initial;--shimmer-text-fill:initial}}}@layer theme{:root,:host{--font-sans:"Geist Variable", sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-700:oklch(52% .105 223.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-pink-400:oklch(71.8% .202 349.761);--color-black:#000;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-normal:0em;--tracking-wider:.05em;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"Geist Variable", sans-serif;--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){*{outline-color:color-mix(in oklab, var(--ring) 50%, transparent)}}body{background-color:var(--background);color:var(--foreground)}html{font-family:Geist Variable,sans-serif}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.-top-1{top:calc(var(--spacing) * -1)}.top-0{top:0}.top-1\.5{top:calc(var(--spacing) * 1.5)}.-right-1{right:calc(var(--spacing) * -1)}.right-0{right:0}.right-1\.5{right:calc(var(--spacing) * 1.5)}.bottom-0{bottom:0}.bottom-full{bottom:100%}.left-0{left:0}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-0\.5{margin-inline:calc(var(--spacing) * .5)}.mx-3{margin-inline:calc(var(--spacing) * 3)}.-mt-0\.5{margin-top:calc(var(--spacing) * -.5)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-\[38px\]{height:38px}.h-full{height:100%}.max-h-32{max-height:calc(var(--spacing) * 32)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-\[75vh\]{max-height:75vh}.max-h-\[300px\]{max-height:300px}.min-h-0{min-height:0}.min-h-\[38px\]{min-height:38px}.min-h-\[60px\]{min-height:60px}.w-1{width:var(--spacing)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-56{width:calc(var(--spacing) * 56)}.w-72{width:calc(var(--spacing) * 72)}.w-\[6\.5rem\]{width:6.5rem}.w-\[8\.5rem\]{width:8.5rem}.w-\[38px\]{width:38px}.w-\[130px\]{width:130px}.w-\[320px\]{width:320px}.w-\[380px\]{width:380px}.w-\[520px\]{width:520px}.w-\[600px\]{width:600px}.w-\[var\(--panel-w\)\]{width:var(--panel-w)}.w-full{width:100%}.w-max{width:max-content}.w-screen{width:100vw}.max-w-\[5rem\]{max-width:5rem}.max-w-\[6\.5rem\]{max-width:6.5rem}.max-w-\[22rem\]{max-width:22rem}.max-w-\[90vw\]{max-width:90vw}.max-w-\[200px\]{max-width:200px}.min-w-0{min-width:0}.min-w-28{min-width:calc(var(--spacing) * 28)}.min-w-\[3\.5em\]{min-width:3.5em}.min-w-\[4\.75rem\]{min-width:4.75rem}.min-w-\[9\.5em\]{min-width:9.5em}.min-w-\[9rem\]{min-width:9rem}.min-w-\[200px\]{min-width:200px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.animate-bounce{animation:var(--animate-bounce)}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.cursor-col-resize{cursor:col-resize}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.resize-y{resize:vertical}.scrollbar-none{scrollbar-width:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--border)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) * .8)}.rounded-sm{border-radius:calc(var(--radius) * .6)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-3,.border-l-\[3px\]{border-left-style:var(--tw-border-style);border-left-width:3px}.border-border,.border-border\/30{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.border-border\/30{border-color:color-mix(in oklab, var(--border) 30%, transparent)}}.border-border\/50{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.border-border\/50{border-color:color-mix(in oklab, var(--border) 50%, transparent)}}.border-cyan-400{border-color:var(--color-cyan-400)}.border-green-500\/30{border-color:#00c7584d}@supports (color:color-mix(in lab, red, red)){.border-green-500\/30{border-color:color-mix(in oklab, var(--color-green-500) 30%, transparent)}}.border-green-500\/40{border-color:#00c75866}@supports (color:color-mix(in lab, red, red)){.border-green-500\/40{border-color:color-mix(in oklab, var(--color-green-500) 40%, transparent)}}.border-green-600{border-color:var(--color-green-600)}.border-orange-500{border-color:var(--color-orange-500)}.border-primary{border-color:var(--primary)}.border-purple-500{border-color:var(--color-purple-500)}.border-red-500{border-color:var(--color-red-500)}.border-red-500\/30{border-color:#fb2c364d}@supports (color:color-mix(in lab, red, red)){.border-red-500\/30{border-color:color-mix(in oklab, var(--color-red-500) 30%, transparent)}}.border-yellow-500\/20{border-color:#edb20033}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/20{border-color:color-mix(in oklab, var(--color-yellow-500) 20%, transparent)}}.border-l-primary{border-left-color:var(--primary)}.border-l-transparent{border-left-color:#0000}.bg-accent,.bg-accent\/30{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/30{background-color:color-mix(in oklab, var(--accent) 30%, transparent)}}.bg-accent\/50{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/50{background-color:color-mix(in oklab, var(--accent) 50%, transparent)}}.bg-background{background-color:var(--background)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-card{background-color:var(--card)}.bg-cyan-400{background-color:var(--color-cyan-400)}.bg-cyan-500\/15{background-color:#00b7d726}@supports (color:color-mix(in lab, red, red)){.bg-cyan-500\/15{background-color:color-mix(in oklab, var(--color-cyan-500) 15%, transparent)}}.bg-destructive{background-color:var(--destructive)}.bg-green-400{background-color:var(--color-green-400)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-500\/10{background-color:#00c7581a}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/10{background-color:color-mix(in oklab, var(--color-green-500) 10%, transparent)}}.bg-green-500\/15{background-color:#00c75826}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/15{background-color:color-mix(in oklab, var(--color-green-500) 15%, transparent)}}.bg-green-500\/20{background-color:#00c75833}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/20{background-color:color-mix(in oklab, var(--color-green-500) 20%, transparent)}}.bg-muted,.bg-muted\/40{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/40{background-color:color-mix(in oklab, var(--muted) 40%, transparent)}}.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/50{background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}.bg-orange-400{background-color:var(--color-orange-400)}.bg-orange-400\/15{background-color:#ff8b1a26}@supports (color:color-mix(in lab, red, red)){.bg-orange-400\/15{background-color:color-mix(in oklab, var(--color-orange-400) 15%, transparent)}}.bg-primary{background-color:var(--primary)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/10{background-color:color-mix(in oklab, var(--color-red-500) 10%, transparent)}}.bg-red-500\/20{background-color:#fb2c3633}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/20{background-color:color-mix(in oklab, var(--color-red-500) 20%, transparent)}}.bg-transparent{background-color:#0000}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500\/10{background-color:#edb2001a}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/10{background-color:color-mix(in oklab, var(--color-yellow-500) 10%, transparent)}}.bg-yellow-500\/15{background-color:#edb20026}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/15{background-color:color-mix(in oklab, var(--color-yellow-500) 15%, transparent)}}.bg-yellow-500\/20{background-color:#edb20033}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/20{background-color:color-mix(in oklab, var(--color-yellow-500) 20%, transparent)}}.bg-yellow-500\/30{background-color:#edb2004d}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/30{background-color:color-mix(in oklab, var(--color-yellow-500) 30%, transparent)}}.fill-current{fill:currentColor}.object-cover{object-fit:cover}.p-0{padding:0}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-8{padding:calc(var(--spacing) * 8)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0{padding-block:0}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-\[10vh\]{padding-top:10vh}.pt-\[15vh\]{padding-top:15vh}.pt-\[env\(safe-area-inset-top\,12px\)\]{padding-top:env(safe-area-inset-top,12px)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pb-1\.5{padding-bottom:calc(var(--spacing) * 1.5)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-7{padding-left:calc(var(--spacing) * 7)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-blue-400{color:var(--color-blue-400)}.text-border{color:var(--border)}.text-cyan-300{color:var(--color-cyan-300)}.text-cyan-400{color:var(--color-cyan-400)}.text-cyan-700{color:var(--color-cyan-700)}.text-foreground{color:var(--foreground)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-inherit{color:inherit}.text-muted-foreground,.text-muted-foreground\/50{color:var(--muted-foreground)}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/50{color:color-mix(in oklab, var(--muted-foreground) 50%, transparent)}}.text-muted-foreground\/70{color:var(--muted-foreground)}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/70{color:color-mix(in oklab, var(--muted-foreground) 70%, transparent)}}.text-orange-400{color:var(--color-orange-400)}.text-pink-400{color:var(--color-pink-400)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-purple-400{color:var(--color-purple-400)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-yellow-300{color:var(--color-yellow-300)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-400\/80{color:#fac800cc}@supports (color:color-mix(in lab, red, red)){.text-yellow-400\/80{color:color-mix(in oklab, var(--color-yellow-400) 80%, transparent)}}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.accent-primary{accent-color:var(--primary)}.opacity-0{opacity:0}.opacity-35{opacity:.35}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.running{animation-play-state:running}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *),.group-hover\/session\:opacity-100:is(:where(.group\/session):hover *){opacity:1}}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}@media (hover:hover){.hover\:border-primary\/50:hover{border-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--primary) 50%, transparent)}}.hover\:bg-accent:hover,.hover\:bg-accent\/20:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/20:hover{background-color:color-mix(in oklab, var(--accent) 20%, transparent)}}.hover\:bg-accent\/30:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/30:hover{background-color:color-mix(in oklab, var(--accent) 30%, transparent)}}.hover\:bg-accent\/50:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/50:hover{background-color:color-mix(in oklab, var(--accent) 50%, transparent)}}.hover\:bg-green-500\/40:hover{background-color:#00c75866}@supports (color:color-mix(in lab, red, red)){.hover\:bg-green-500\/40:hover{background-color:color-mix(in oklab, var(--color-green-500) 40%, transparent)}}.hover\:bg-muted:hover{background-color:var(--muted)}.hover\:bg-primary\/30:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/30:hover{background-color:color-mix(in oklab, var(--primary) 30%, transparent)}}.hover\:bg-primary\/40:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/40:hover{background-color:color-mix(in oklab, var(--primary) 40%, transparent)}}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--primary) 90%, transparent)}}.hover\:bg-red-500\/40:hover{background-color:#fb2c3666}@supports (color:color-mix(in lab, red, red)){.hover\:bg-red-500\/40:hover{background-color:color-mix(in oklab, var(--color-red-500) 40%, transparent)}}.hover\:bg-yellow-500\/40:hover{background-color:#edb20066}@supports (color:color-mix(in lab, red, red)){.hover\:bg-yellow-500\/40:hover{background-color:color-mix(in oklab, var(--color-yellow-500) 40%, transparent)}}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:text-muted-foreground:hover{color:var(--muted-foreground)}.hover\:text-primary:hover,.hover\:text-primary\/80:hover{color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:text-primary\/80:hover{color:color-mix(in oklab, var(--primary) 80%, transparent)}}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-yellow-300:hover{color:var(--color-yellow-300)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}}.focus\:border-primary:focus{border-color:var(--primary)}.focus\:bg-accent:focus{background-color:var(--accent)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-primary:focus{--tw-ring-color:var(--primary)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus\:ring-inset:focus{--tw-ring-inset:inset}.disabled\:opacity-50:disabled{opacity:.5}@media not all and (width>=48rem){.max-md\:absolute{position:absolute}.max-md\:fixed{position:fixed}.max-md\:inset-0{inset:0}.max-md\:inset-y-0{inset-block:0}.max-md\:left-0{left:0}.max-md\:z-20{z-index:20}.max-md\:z-30{z-index:30}.max-md\:block{display:block}.max-md\:hidden{display:none}.max-md\:w-\[85vw\]{width:85vw}.max-md\:w-full{width:100%}.max-md\:max-w-\[90px\]{max-width:90px}.max-md\:max-w-\[320px\]{max-width:320px}.max-md\:min-w-0{min-width:0}.max-md\:gap-1\.5{gap:calc(var(--spacing) * 1.5)}.max-md\:px-2{padding-inline:calc(var(--spacing) * 2)}.max-md\:text-\[10px\]{font-size:10px}}@media (width>=48rem){.md\:w-10{width:calc(var(--spacing) * 10)}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.dark\:text-cyan-400\/90:is(.dark *){color:#00d2efe6}@supports (color:color-mix(in lab, red, red)){.dark\:text-cyan-400\/90:is(.dark *){color:color-mix(in oklab, var(--color-cyan-400) 90%, transparent)}}.dark\:text-green-400:is(.dark *){color:var(--color-green-400)}.dark\:text-yellow-400:is(.dark *){color:var(--color-yellow-400)}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}@property --scroll-fade-t{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-b{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-s{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-e{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-mask{syntax:"*";inherits:false}@property --shimmer-angle{syntax:"<angle>";inherits:true;initial-value:20deg}@property --shimmer-image{syntax:"*";inherits:false}@property --shimmer-text-fill{syntax:"*";inherits:false}@media (prefers-reduced-motion:reduce){.shimmer{-webkit-text-fill-color:currentColor;background-image:none;animation:none}}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-wght-normal-BgDaEnEv.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.5% 0 0);--primary:oklch(20.5% 0 0);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0);--chart-1:oklch(87% 0 0);--chart-2:oklch(55.6% 0 0);--chart-3:oklch(43.9% 0 0);--chart-4:oklch(37.1% 0 0);--chart-5:oklch(26.9% 0 0);--radius:.625rem;--sidebar:oklch(98.5% 0 0);--sidebar-foreground:oklch(14.5% 0 0);--sidebar-primary:oklch(20.5% 0 0);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(97% 0 0);--sidebar-accent-foreground:oklch(20.5% 0 0);--sidebar-border:oklch(92.2% 0 0);--sidebar-ring:oklch(70.8% 0 0)}.dark{--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--popover:oklch(20.5% 0 0);--popover-foreground:oklch(98.5% 0 0);--primary:oklch(92.2% 0 0);--primary-foreground:oklch(20.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0);--chart-1:oklch(87% 0 0);--chart-2:oklch(55.6% 0 0);--chart-3:oklch(43.9% 0 0);--chart-4:oklch(37.1% 0 0);--chart-5:oklch(26.9% 0 0);--sidebar:oklch(20.5% 0 0);--sidebar-foreground:oklch(98.5% 0 0);--sidebar-primary:oklch(48.8% .243 264.376);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(26.9% 0 0);--sidebar-accent-foreground:oklch(98.5% 0 0);--sidebar-border:oklch(100% 0 0/.1);--sidebar-ring:oklch(55.6% 0 0)}body{font-family:SF Mono,Fira Code,JetBrains Mono,Cascadia Code,monospace!important}@media (width<=768px){body{font-size:16px}.body{padding:10px 12px;font-size:15px}.role,.think-toggle{font-size:12px}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:oklch(40% 0 0);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:oklch(50% 0 0)}*{scrollbar-width:thin;scrollbar-color:oklch(40% 0 0) transparent}html,body,#root{height:100vh}@supports (height:100dvh){html,body,#root{height:100dvh}}.scrollbar-none{-ms-overflow-style:none;scrollbar-width:none}.scrollbar-none::-webkit-scrollbar{display:none}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}