pilotswarm-web 0.1.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.
- package/README.md +144 -0
- package/auth/authz/engine.js +139 -0
- package/auth/config.js +110 -0
- package/auth/index.js +153 -0
- package/auth/normalize/entra.js +22 -0
- package/auth/providers/entra.js +76 -0
- package/auth/providers/none.js +24 -0
- package/auth.js +10 -0
- package/bin/serve.js +53 -0
- package/config.js +20 -0
- package/dist/app.js +469 -0
- package/dist/assets/index-BSVg-lGb.css +1 -0
- package/dist/assets/index-BXD5YP7A.js +24 -0
- package/dist/assets/msal-CytV9RFv.js +7 -0
- package/dist/assets/pilotswarm-WX3NED6m.js +40 -0
- package/dist/assets/react-jg0oazEi.js +1 -0
- package/dist/index.html +16 -0
- package/node_modules/pilotswarm-ui-core/README.md +6 -0
- package/node_modules/pilotswarm-ui-core/package.json +32 -0
- package/node_modules/pilotswarm-ui-core/src/commands.js +72 -0
- package/node_modules/pilotswarm-ui-core/src/context-usage.js +212 -0
- package/node_modules/pilotswarm-ui-core/src/controller.js +3613 -0
- package/node_modules/pilotswarm-ui-core/src/formatting.js +872 -0
- package/node_modules/pilotswarm-ui-core/src/history.js +571 -0
- package/node_modules/pilotswarm-ui-core/src/index.js +13 -0
- package/node_modules/pilotswarm-ui-core/src/layout.js +196 -0
- package/node_modules/pilotswarm-ui-core/src/reducer.js +1027 -0
- package/node_modules/pilotswarm-ui-core/src/selectors.js +2786 -0
- package/node_modules/pilotswarm-ui-core/src/session-tree.js +109 -0
- package/node_modules/pilotswarm-ui-core/src/state.js +80 -0
- package/node_modules/pilotswarm-ui-core/src/store.js +23 -0
- package/node_modules/pilotswarm-ui-core/src/system-titles.js +24 -0
- package/node_modules/pilotswarm-ui-core/src/themes/catppuccin-mocha.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/cobalt2.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/dark-high-contrast.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/dracula.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/github-dark.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/gruvbox-dark.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-matrix.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-orion-prime.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/helpers.js +77 -0
- package/node_modules/pilotswarm-ui-core/src/themes/index.js +42 -0
- package/node_modules/pilotswarm-ui-core/src/themes/noctis-viola.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/noctis.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/nord.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/solarized-dark.js +56 -0
- package/node_modules/pilotswarm-ui-core/src/themes/tokyo-night.js +56 -0
- package/node_modules/pilotswarm-ui-react/README.md +5 -0
- package/node_modules/pilotswarm-ui-react/package.json +36 -0
- package/node_modules/pilotswarm-ui-react/src/components.js +1316 -0
- package/node_modules/pilotswarm-ui-react/src/index.js +4 -0
- package/node_modules/pilotswarm-ui-react/src/platform.js +15 -0
- package/node_modules/pilotswarm-ui-react/src/use-controller-state.js +38 -0
- package/node_modules/pilotswarm-ui-react/src/web-app.js +2661 -0
- package/package.json +64 -0
- package/runtime.js +146 -0
- package/server.js +311 -0
package/bin/serve.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pilotswarm-web — Starts the Express + WebSocket server and serves the
|
|
5
|
+
* built React portal. In development, use `npm run dev` (Vite) with the
|
|
6
|
+
* server running separately via `node server.js`.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx pilotswarm-web --env .env.remote
|
|
10
|
+
* npx pilotswarm-web --port 3001
|
|
11
|
+
* npx pilotswarm-web --plugin ./plugin
|
|
12
|
+
* npx pilotswarm-web --workers 4 # embedded workers
|
|
13
|
+
* npx pilotswarm-web --workers 0 # remote workers (AKS)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
|
|
21
|
+
// Load env file if --env flag provided
|
|
22
|
+
const envIdx = process.argv.indexOf("--env");
|
|
23
|
+
if (envIdx !== -1 && process.argv[envIdx + 1]) {
|
|
24
|
+
const envPath = path.resolve(process.argv[envIdx + 1]);
|
|
25
|
+
// Node 24+ supports --env-file natively; for programmatic loading we
|
|
26
|
+
// read the file and set process.env entries manually.
|
|
27
|
+
const { readFileSync } = await import("node:fs");
|
|
28
|
+
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
31
|
+
const eq = trimmed.indexOf("=");
|
|
32
|
+
if (eq === -1) continue;
|
|
33
|
+
const key = trimmed.slice(0, eq).trim();
|
|
34
|
+
const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
35
|
+
if (!process.env[key]) process.env[key] = val;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Dynamically import the server (after env is loaded)
|
|
40
|
+
const { startServer } = await import("../server.js");
|
|
41
|
+
|
|
42
|
+
const portFlag = process.argv.indexOf("--port");
|
|
43
|
+
const port = portFlag !== -1 ? parseInt(process.argv[portFlag + 1], 10) : 3001;
|
|
44
|
+
|
|
45
|
+
const pluginFlag = process.argv.indexOf("--plugin");
|
|
46
|
+
if (pluginFlag !== -1 && process.argv[pluginFlag + 1]) {
|
|
47
|
+
process.env.PLUGIN_DIRS = path.resolve(process.argv[pluginFlag + 1]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const workersFlag = process.argv.indexOf("--workers");
|
|
51
|
+
const workers = workersFlag !== -1 ? parseInt(process.argv[workersFlag + 1], 10) : 4;
|
|
52
|
+
|
|
53
|
+
startServer({ port, workers });
|
package/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getPluginDirsFromEnv, resolvePortalConfigBundleFromPluginDirs } from "pilotswarm-cli/portal";
|
|
2
|
+
|
|
3
|
+
let cachedPortalBundle = null;
|
|
4
|
+
|
|
5
|
+
function getPortalBundle() {
|
|
6
|
+
if (!cachedPortalBundle) {
|
|
7
|
+
cachedPortalBundle = resolvePortalConfigBundleFromPluginDirs(getPluginDirsFromEnv());
|
|
8
|
+
}
|
|
9
|
+
return cachedPortalBundle;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getPortalConfig() {
|
|
13
|
+
return getPortalBundle().portalConfig;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getPortalAssetFile(assetName) {
|
|
17
|
+
const key = String(assetName || "").trim();
|
|
18
|
+
if (!key) return null;
|
|
19
|
+
return getPortalBundle().assetFiles?.[key] || null;
|
|
20
|
+
}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { Terminal } from "/xterm/lib/xterm.mjs";
|
|
2
|
+
import { FitAddon } from "/xterm-addon-fit/lib/addon-fit.mjs";
|
|
3
|
+
import { WebLinksAddon } from "/xterm-addon-web-links/lib/addon-web-links.mjs";
|
|
4
|
+
import { DEFAULT_THEME_ID, getTheme, listThemes } from "/ui-core/themes/index.js";
|
|
5
|
+
|
|
6
|
+
// ── Auth state ──────────────────────────────────────────────────
|
|
7
|
+
let authEnabled = false;
|
|
8
|
+
let msalInstance = null;
|
|
9
|
+
let currentAccount = null;
|
|
10
|
+
let accessToken = null;
|
|
11
|
+
|
|
12
|
+
const topbarUser = document.getElementById("topbar-user");
|
|
13
|
+
const topbarSignInBtn = document.getElementById("topbar-signin-btn");
|
|
14
|
+
const topbarSignOutBtn = document.getElementById("topbar-signout-btn");
|
|
15
|
+
const signedOutScreen = document.getElementById("signed-out-screen");
|
|
16
|
+
|
|
17
|
+
async function initAuth() {
|
|
18
|
+
const resp = await fetch("/api/auth-config");
|
|
19
|
+
const config = await resp.json();
|
|
20
|
+
if (!config.enabled) {
|
|
21
|
+
// No auth configured — proceed directly
|
|
22
|
+
authEnabled = false;
|
|
23
|
+
updateAuthUI();
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
authEnabled = true;
|
|
28
|
+
msalInstance = new msal.PublicClientApplication({
|
|
29
|
+
auth: {
|
|
30
|
+
clientId: config.clientId,
|
|
31
|
+
authority: config.authority,
|
|
32
|
+
redirectUri: config.redirectUri,
|
|
33
|
+
},
|
|
34
|
+
cache: {
|
|
35
|
+
cacheLocation: "sessionStorage",
|
|
36
|
+
storeAuthStateInCookie: true,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await msalInstance.initialize();
|
|
41
|
+
|
|
42
|
+
// Handle redirect callback (if returning from redirect flow)
|
|
43
|
+
const redirectResp = await msalInstance.handleRedirectPromise();
|
|
44
|
+
if (redirectResp) {
|
|
45
|
+
currentAccount = redirectResp.account;
|
|
46
|
+
} else {
|
|
47
|
+
const accounts = msalInstance.getAllAccounts();
|
|
48
|
+
if (accounts.length > 0) currentAccount = accounts[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (currentAccount) {
|
|
52
|
+
await acquireToken();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
updateAuthUI();
|
|
56
|
+
return !!currentAccount;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function acquireToken() {
|
|
60
|
+
if (!msalInstance || !currentAccount) return null;
|
|
61
|
+
try {
|
|
62
|
+
const resp = await msalInstance.acquireTokenSilent({
|
|
63
|
+
scopes: [`${msalInstance.getConfiguration().auth.clientId}/.default`],
|
|
64
|
+
account: currentAccount,
|
|
65
|
+
});
|
|
66
|
+
accessToken = resp.accessToken || resp.idToken;
|
|
67
|
+
return accessToken;
|
|
68
|
+
} catch {
|
|
69
|
+
try {
|
|
70
|
+
const resp = await msalInstance.acquireTokenPopup({
|
|
71
|
+
scopes: [`${msalInstance.getConfiguration().auth.clientId}/.default`],
|
|
72
|
+
account: currentAccount,
|
|
73
|
+
});
|
|
74
|
+
accessToken = resp.accessToken || resp.idToken;
|
|
75
|
+
return accessToken;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error("[auth] Token acquisition failed:", err);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function signIn() {
|
|
84
|
+
if (!msalInstance) return;
|
|
85
|
+
try {
|
|
86
|
+
// Mobile browsers block popups — use redirect flow instead
|
|
87
|
+
if (/Mobi|Android/i.test(navigator.userAgent)) {
|
|
88
|
+
await msalInstance.loginRedirect({ scopes: ["User.Read"] });
|
|
89
|
+
return; // page will redirect
|
|
90
|
+
}
|
|
91
|
+
const resp = await msalInstance.loginPopup({
|
|
92
|
+
scopes: ["User.Read"],
|
|
93
|
+
});
|
|
94
|
+
currentAccount = resp.account;
|
|
95
|
+
await acquireToken();
|
|
96
|
+
updateAuthUI();
|
|
97
|
+
connectWebSocket();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error("[auth] Sign-in failed:", err);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function signOut() {
|
|
104
|
+
if (!msalInstance) return;
|
|
105
|
+
msalInstance.logoutPopup({ account: currentAccount });
|
|
106
|
+
currentAccount = null;
|
|
107
|
+
accessToken = null;
|
|
108
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.close();
|
|
109
|
+
ws = null;
|
|
110
|
+
updateAuthUI();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function updateAuthUI() {
|
|
114
|
+
if (!authEnabled) {
|
|
115
|
+
// Auth not configured — hide all auth UI, show terminal
|
|
116
|
+
topbarSignInBtn.classList.add("hidden");
|
|
117
|
+
topbarSignOutBtn.classList.add("hidden");
|
|
118
|
+
topbarUser.textContent = "";
|
|
119
|
+
signedOutScreen.classList.add("hidden");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const signedIn = !!currentAccount;
|
|
124
|
+
topbarSignInBtn.classList.toggle("hidden", signedIn);
|
|
125
|
+
topbarSignOutBtn.classList.toggle("hidden", !signedIn);
|
|
126
|
+
if (signedIn) {
|
|
127
|
+
const name = currentAccount.name || currentAccount.username || "";
|
|
128
|
+
const email = currentAccount.username || currentAccount.idTokenClaims?.preferred_username || "";
|
|
129
|
+
topbarUser.textContent = email && email !== name ? `${name} (${email})` : name;
|
|
130
|
+
} else {
|
|
131
|
+
topbarUser.textContent = "";
|
|
132
|
+
}
|
|
133
|
+
signedOutScreen.classList.toggle("hidden", signedIn);
|
|
134
|
+
|
|
135
|
+
// Hide overlay + terminal when signed out, show signed-out screen
|
|
136
|
+
if (!signedIn) {
|
|
137
|
+
overlayEl.classList.add("hidden");
|
|
138
|
+
container.style.visibility = "hidden";
|
|
139
|
+
} else {
|
|
140
|
+
container.style.visibility = "visible";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Expose for onclick handlers in HTML
|
|
145
|
+
window.__portalSignIn = signIn;
|
|
146
|
+
window.__portalSignOut = signOut;
|
|
147
|
+
|
|
148
|
+
const THEME_STORAGE_KEY = "pilotswarm.portal.theme";
|
|
149
|
+
const FONT_FAMILY = "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace";
|
|
150
|
+
const THEMES = listThemes();
|
|
151
|
+
|
|
152
|
+
function readStoredThemeId() {
|
|
153
|
+
try {
|
|
154
|
+
return window.localStorage.getItem(THEME_STORAGE_KEY);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeStoredThemeId(themeId) {
|
|
161
|
+
try {
|
|
162
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, themeId);
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveInitialTheme() {
|
|
167
|
+
const storedThemeId = readStoredThemeId();
|
|
168
|
+
return getTheme(storedThemeId) || getTheme(DEFAULT_THEME_ID) || THEMES[0];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function setThemeCssVariable(name, value) {
|
|
172
|
+
document.documentElement.style.setProperty(name, value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function applyDocumentTheme(theme) {
|
|
176
|
+
const page = theme.page;
|
|
177
|
+
setThemeCssVariable("--app-background", page.background);
|
|
178
|
+
setThemeCssVariable("--app-foreground", page.foreground);
|
|
179
|
+
setThemeCssVariable("--overlay-background", page.overlayBackground);
|
|
180
|
+
setThemeCssVariable("--overlay-foreground", page.overlayForeground);
|
|
181
|
+
setThemeCssVariable("--hint-color", page.hintColor);
|
|
182
|
+
setThemeCssVariable("--modal-backdrop", page.modalBackdrop);
|
|
183
|
+
setThemeCssVariable("--modal-background", page.modalBackground);
|
|
184
|
+
setThemeCssVariable("--modal-border", page.modalBorder);
|
|
185
|
+
setThemeCssVariable("--modal-foreground", page.modalForeground);
|
|
186
|
+
setThemeCssVariable("--modal-muted", page.modalMuted);
|
|
187
|
+
setThemeCssVariable("--modal-selected-background", page.modalSelectedBackground);
|
|
188
|
+
setThemeCssVariable("--modal-selected-border", page.modalSelectedBorder);
|
|
189
|
+
setThemeCssVariable("--modal-selected-foreground", page.modalSelectedForeground);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const overlayEl = document.getElementById("overlay");
|
|
193
|
+
const dotsEl = document.getElementById("dots");
|
|
194
|
+
const modalBackdropEl = document.getElementById("theme-modal-backdrop");
|
|
195
|
+
const modalOptionsEl = document.getElementById("theme-options");
|
|
196
|
+
const modalTitleEl = document.getElementById("theme-modal-title");
|
|
197
|
+
|
|
198
|
+
let currentTheme = resolveInitialTheme();
|
|
199
|
+
let themeModalOpen = false;
|
|
200
|
+
let modalSelectedThemeId = currentTheme.id;
|
|
201
|
+
|
|
202
|
+
applyDocumentTheme(currentTheme);
|
|
203
|
+
|
|
204
|
+
const term = new Terminal({
|
|
205
|
+
cursorBlink: true,
|
|
206
|
+
cursorStyle: "block",
|
|
207
|
+
fontFamily: FONT_FAMILY,
|
|
208
|
+
fontSize: 14,
|
|
209
|
+
lineHeight: 1.1,
|
|
210
|
+
macOptionIsMeta: true,
|
|
211
|
+
allowProposedApi: true,
|
|
212
|
+
theme: { ...currentTheme.terminal },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const fitAddon = new FitAddon();
|
|
216
|
+
term.loadAddon(fitAddon);
|
|
217
|
+
term.loadAddon(new WebLinksAddon());
|
|
218
|
+
|
|
219
|
+
const container = document.getElementById("terminal");
|
|
220
|
+
term.open(container);
|
|
221
|
+
|
|
222
|
+
// ── WebSocket (created after auth) ──────────────────────────────
|
|
223
|
+
let ws = null;
|
|
224
|
+
|
|
225
|
+
function connectWebSocket() {
|
|
226
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
227
|
+
|
|
228
|
+
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
229
|
+
const url = `${proto}//${location.host}/ws`;
|
|
230
|
+
|
|
231
|
+
// Pass token via sub-protocol if auth is enabled
|
|
232
|
+
if (authEnabled && accessToken) {
|
|
233
|
+
ws = new WebSocket(url, ["access_token", accessToken]);
|
|
234
|
+
} else {
|
|
235
|
+
ws = new WebSocket(url);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
ws.onopen = () => {
|
|
239
|
+
overlayEl.classList.add("hidden");
|
|
240
|
+
// Re-fit now that layout is fully resolved, then send accurate dimensions
|
|
241
|
+
fitAddon.fit();
|
|
242
|
+
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
ws.onmessage = (event) => {
|
|
246
|
+
try {
|
|
247
|
+
const message = JSON.parse(event.data);
|
|
248
|
+
if (message.type === "output") {
|
|
249
|
+
term.write(message.data);
|
|
250
|
+
} else if (message.type === "exit") {
|
|
251
|
+
term.write("\r\n\x1b[90m[Session ended]\x1b[0m\r\n");
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
ws.onclose = (ev) => {
|
|
257
|
+
if (ev.code === 4401) {
|
|
258
|
+
// Auth rejected — show signed-out state
|
|
259
|
+
currentAccount = null;
|
|
260
|
+
accessToken = null;
|
|
261
|
+
updateAuthUI();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
term.write("\r\n\x1b[90m[Disconnected - reload to reconnect]\x1b[0m\r\n");
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
term.onData((data) => {
|
|
269
|
+
if (themeModalOpen) return;
|
|
270
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
271
|
+
ws.send(JSON.stringify({ type: "input", data }));
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
term.onBinary((data) => {
|
|
276
|
+
if (themeModalOpen) return;
|
|
277
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
278
|
+
ws.send(JSON.stringify({ type: "input", data }));
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
window.addEventListener("resize", () => {
|
|
283
|
+
fitAddon.fit();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
term.onResize(({ cols, rows }) => {
|
|
287
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
288
|
+
ws.send(JSON.stringify({ type: "resize", cols, rows }));
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
let dotCount = 0;
|
|
293
|
+
const dotInterval = window.setInterval(() => {
|
|
294
|
+
dotCount = (dotCount + 1) % 4;
|
|
295
|
+
dotsEl.textContent = ".".repeat(dotCount || 1);
|
|
296
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
297
|
+
window.clearInterval(dotInterval);
|
|
298
|
+
}
|
|
299
|
+
}, 400);
|
|
300
|
+
|
|
301
|
+
// ── Boot: auth then connect ─────────────────────────────────────
|
|
302
|
+
(async () => {
|
|
303
|
+
const signedIn = await initAuth();
|
|
304
|
+
if (!authEnabled || signedIn) {
|
|
305
|
+
// Wait for layout to settle before fitting + connecting
|
|
306
|
+
requestAnimationFrame(() => {
|
|
307
|
+
fitAddon.fit();
|
|
308
|
+
term.focus();
|
|
309
|
+
connectWebSocket();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
})();
|
|
313
|
+
|
|
314
|
+
function renderThemeOptions() {
|
|
315
|
+
modalOptionsEl.replaceChildren();
|
|
316
|
+
|
|
317
|
+
for (const theme of THEMES) {
|
|
318
|
+
const optionEl = document.createElement("button");
|
|
319
|
+
optionEl.type = "button";
|
|
320
|
+
optionEl.className = "theme-option";
|
|
321
|
+
if (theme.id === modalSelectedThemeId) optionEl.classList.add("is-selected");
|
|
322
|
+
|
|
323
|
+
const titleRowEl = document.createElement("div");
|
|
324
|
+
titleRowEl.className = "theme-option-title-row";
|
|
325
|
+
|
|
326
|
+
const titleEl = document.createElement("span");
|
|
327
|
+
titleEl.className = "theme-option-title";
|
|
328
|
+
titleEl.textContent = theme.label;
|
|
329
|
+
titleRowEl.appendChild(titleEl);
|
|
330
|
+
|
|
331
|
+
if (theme.id === currentTheme.id) {
|
|
332
|
+
const currentEl = document.createElement("span");
|
|
333
|
+
currentEl.className = "theme-option-current";
|
|
334
|
+
currentEl.textContent = "Current";
|
|
335
|
+
titleRowEl.appendChild(currentEl);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const descriptionEl = document.createElement("div");
|
|
339
|
+
descriptionEl.className = "theme-option-description";
|
|
340
|
+
descriptionEl.textContent = theme.description;
|
|
341
|
+
|
|
342
|
+
const swatchesEl = document.createElement("div");
|
|
343
|
+
swatchesEl.className = "theme-option-swatches";
|
|
344
|
+
for (const color of [
|
|
345
|
+
theme.terminal.background,
|
|
346
|
+
theme.terminal.blue,
|
|
347
|
+
theme.terminal.green,
|
|
348
|
+
theme.terminal.magenta,
|
|
349
|
+
theme.terminal.yellow,
|
|
350
|
+
]) {
|
|
351
|
+
const swatchEl = document.createElement("span");
|
|
352
|
+
swatchEl.className = "theme-swatch";
|
|
353
|
+
swatchEl.style.backgroundColor = color;
|
|
354
|
+
swatchesEl.appendChild(swatchEl);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
optionEl.append(titleRowEl, descriptionEl, swatchesEl);
|
|
358
|
+
optionEl.addEventListener("click", () => {
|
|
359
|
+
modalSelectedThemeId = theme.id;
|
|
360
|
+
renderThemeOptions();
|
|
361
|
+
applySelectedTheme();
|
|
362
|
+
});
|
|
363
|
+
modalOptionsEl.appendChild(optionEl);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function moveThemeSelection(delta) {
|
|
368
|
+
const currentIndex = THEMES.findIndex((theme) => theme.id === modalSelectedThemeId);
|
|
369
|
+
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
370
|
+
const nextIndex = (safeIndex + delta + THEMES.length) % THEMES.length;
|
|
371
|
+
modalSelectedThemeId = THEMES[nextIndex].id;
|
|
372
|
+
renderThemeOptions();
|
|
373
|
+
// Scroll selected option into view
|
|
374
|
+
const selected = modalOptionsEl.querySelector(".is-selected");
|
|
375
|
+
if (selected) selected.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function applyTheme(themeId, { persist = true } = {}) {
|
|
379
|
+
const nextTheme = getTheme(themeId);
|
|
380
|
+
if (!nextTheme) return;
|
|
381
|
+
currentTheme = nextTheme;
|
|
382
|
+
modalSelectedThemeId = nextTheme.id;
|
|
383
|
+
applyDocumentTheme(nextTheme);
|
|
384
|
+
term.options.theme = { ...nextTheme.terminal };
|
|
385
|
+
if (typeof term.clearTextureAtlas === "function") term.clearTextureAtlas();
|
|
386
|
+
term.refresh(0, term.rows - 1);
|
|
387
|
+
if (persist) writeStoredThemeId(nextTheme.id);
|
|
388
|
+
renderThemeOptions();
|
|
389
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
390
|
+
ws.send(JSON.stringify({ type: "theme", themeId: nextTheme.id }));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function openThemeModal() {
|
|
395
|
+
themeModalOpen = true;
|
|
396
|
+
modalSelectedThemeId = currentTheme.id;
|
|
397
|
+
modalTitleEl.textContent = "Theme Picker";
|
|
398
|
+
renderThemeOptions();
|
|
399
|
+
modalBackdropEl.classList.remove("hidden");
|
|
400
|
+
modalBackdropEl.setAttribute("aria-hidden", "false");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function closeThemeModal() {
|
|
404
|
+
themeModalOpen = false;
|
|
405
|
+
modalBackdropEl.classList.add("hidden");
|
|
406
|
+
modalBackdropEl.setAttribute("aria-hidden", "true");
|
|
407
|
+
term.focus();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function applySelectedTheme() {
|
|
411
|
+
applyTheme(modalSelectedThemeId);
|
|
412
|
+
closeThemeModal();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
modalBackdropEl.addEventListener("click", (event) => {
|
|
416
|
+
if (event.target === modalBackdropEl) {
|
|
417
|
+
closeThemeModal();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
window.addEventListener("keydown", (event) => {
|
|
422
|
+
const toggleThemeModal = event.shiftKey
|
|
423
|
+
&& !event.ctrlKey
|
|
424
|
+
&& !event.metaKey
|
|
425
|
+
&& !event.altKey
|
|
426
|
+
&& event.code === "KeyT";
|
|
427
|
+
|
|
428
|
+
if (toggleThemeModal) {
|
|
429
|
+
event.preventDefault();
|
|
430
|
+
event.stopPropagation();
|
|
431
|
+
if (themeModalOpen) {
|
|
432
|
+
closeThemeModal();
|
|
433
|
+
} else {
|
|
434
|
+
openThemeModal();
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!themeModalOpen) return;
|
|
440
|
+
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
event.stopPropagation();
|
|
443
|
+
|
|
444
|
+
if (event.key === "Escape") {
|
|
445
|
+
closeThemeModal();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (event.key === "Enter") {
|
|
449
|
+
applySelectedTheme();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (event.key === "ArrowUp" || event.key === "k" || event.key === "K") {
|
|
453
|
+
moveThemeSelection(-1);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (event.key === "ArrowDown" || event.key === "j" || event.key === "J") {
|
|
457
|
+
moveThemeSelection(1);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (event.key === "Home" || event.key === "g") {
|
|
461
|
+
modalSelectedThemeId = THEMES[0].id;
|
|
462
|
+
renderThemeOptions();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (event.key === "End" || event.key === "G") {
|
|
466
|
+
modalSelectedThemeId = THEMES[THEMES.length - 1].id;
|
|
467
|
+
renderThemeOptions();
|
|
468
|
+
}
|
|
469
|
+
}, true);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--ps-page-background: #0d1117;--ps-page-foreground: #f0f6fc;--ps-surface: #0d1117;--ps-background: #0d1117;--ps-foreground: #f0f6fc;--ps-muted: #8b949e;--ps-border: #6e7681;--ps-selection-background: #58a6ff;--ps-selection-foreground: #0d1117;--ps-highlight-background: #1f6feb;--ps-highlight-foreground: #ffffff;--ps-modal-backdrop: rgba(13, 17, 23, .72);--ps-modal-background: #161b22;--ps-modal-border: #30363d;--ps-modal-foreground: #f0f6fc;--ps-modal-muted: #8b949e;--ps-modal-selected-background: rgba(31, 111, 235, .16);--ps-modal-selected-border: #58a6ff;--ps-modal-selected-foreground: #ffffff;--ps-font-size-base: 13px;--ps-font-size-dense: 12px;--ps-line-height-base: 1.28;--ps-line-height-dense: 1.18;--ps-radius-sm: 6px;--ps-radius-md: 8px;--ps-scrollbar-size: 10px;--ps-scrollbar-track: rgba(110, 118, 129, .14);--ps-scrollbar-thumb: color-mix(in srgb, var(--ps-border) 56%, var(--ps-surface));--ps-scrollbar-thumb-hover: color-mix(in srgb, var(--ps-modal-selected-border) 42%, var(--ps-border))}*{box-sizing:border-box;scrollbar-width:thin;scrollbar-color:var(--ps-scrollbar-thumb) var(--ps-scrollbar-track)}html,body,#root{margin:0;min-height:100%;height:100%;background:var(--ps-page-background);color:var(--ps-page-foreground);font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;font-size:var(--ps-font-size-base);line-height:var(--ps-line-height-base)}body{overflow:hidden;-webkit-text-size-adjust:100%}button,input,textarea{font:inherit}button{cursor:pointer}.portal-app-shell,.portal-gate{min-height:var(--ps-app-height, 100%);height:var(--ps-app-height, 100%);display:flex;flex-direction:column;background:radial-gradient(circle at top left,color-mix(in srgb,var(--ps-highlight-background) 18%,transparent),transparent 42%),radial-gradient(circle at top right,color-mix(in srgb,var(--ps-modal-selected-border) 16%,transparent),transparent 38%),var(--ps-page-background)}.portal-gate{align-items:center;justify-content:center;padding:24px}.portal-gate-card{width:min(480px,100%);border:1px solid var(--ps-modal-border);border-radius:var(--ps-radius-md);background:color-mix(in srgb,var(--ps-modal-background) 92%,transparent);box-shadow:0 28px 80px #00000052;padding:28px;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.portal-gate-brand{display:flex;flex-direction:column;align-items:flex-start;gap:14px;margin-bottom:18px}.portal-gate-kicker,.portal-header-kicker{color:var(--ps-muted);font-size:12px;letter-spacing:.08em;text-transform:uppercase}.portal-gate-title{margin:10px 0 14px;font-size:clamp(28px,5vw,42px);line-height:1.05}.portal-gate-copy{margin:0;color:var(--ps-muted);line-height:1.6}.portal-primary-button,.portal-secondary-button,.ps-toolbar-button,.ps-mini-button,.ps-send-button,.ps-modal-button,.ps-mobile-nav-button,.ps-tab,.ps-filter-option,.ps-list-button,.ps-modal-close{border:1px solid var(--ps-modal-border);border-radius:var(--ps-radius-sm);background:color-mix(in srgb,var(--ps-surface) 94%,transparent);color:var(--ps-foreground);transition:background .12s ease,border-color .12s ease,transform .12s ease}.portal-primary-button,.ps-send-button,.ps-modal-button.is-primary{background:var(--ps-highlight-background);border-color:var(--ps-highlight-background);color:var(--ps-highlight-foreground)}.portal-primary-button,.portal-secondary-button{padding:10px 14px;margin-top:20px}.portal-secondary-button{margin-top:0;flex:0 0 auto}.portal-header{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid color-mix(in srgb,var(--ps-border) 50%,transparent);background:color-mix(in srgb,var(--ps-page-background) 86%,transparent);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px)}.portal-header-brand{display:flex;align-items:center;gap:12px;min-width:0}.portal-header-brand-copy{display:flex;flex-direction:column;gap:3px;min-width:0}.portal-logo-frame{width:38px;height:38px;border-radius:var(--ps-radius-sm);border:1px solid color-mix(in srgb,var(--ps-modal-selected-border) 52%,var(--ps-modal-border));background:radial-gradient(circle at 28% 24%,color-mix(in srgb,var(--ps-modal-selected-border) 20%,transparent),transparent 58%),linear-gradient(135deg,color-mix(in srgb,var(--ps-highlight-background) 20%,var(--ps-surface)),color-mix(in srgb,var(--ps-background) 92%,transparent));box-shadow:inset 0 1px color-mix(in srgb,white 10%,transparent),0 10px 24px #0000002e;display:grid;place-items:center;flex:0 0 auto;overflow:hidden}.portal-logo-frame.is-large{width:76px;height:76px;border-radius:16px}.portal-logo-frame.has-image{background:radial-gradient(circle at 28% 24%,color-mix(in srgb,white 10%,transparent),transparent 58%),linear-gradient(135deg,color-mix(in srgb,var(--ps-surface) 94%,white 6%),color-mix(in srgb,var(--ps-background) 90%,transparent))}.portal-logo{width:24px;height:24px}.portal-logo-frame.is-large .portal-logo{width:44px;height:44px}.portal-logo-image{width:74%;height:74%;object-fit:contain;display:block}.portal-logo-frame.is-large .portal-logo-image{width:82%;height:82%}.portal-logo-ring,.portal-logo-link{stroke:color-mix(in srgb,var(--ps-modal-selected-border) 82%,var(--ps-page-foreground));stroke-linecap:round;stroke-linejoin:round}.portal-logo-ring{stroke-width:3;opacity:.46}.portal-logo-link{stroke-width:2.5;opacity:.9}.portal-logo-core{fill:var(--ps-highlight-background)}.portal-logo-node{stroke:color-mix(in srgb,var(--ps-page-background) 70%,transparent);stroke-width:1.5}.portal-logo-node-a,.portal-logo-node-d{fill:#7ee787}.portal-logo-node-b,.portal-logo-node-e{fill:#58a6ff}.portal-logo-node-c,.portal-logo-node-f{fill:#d2a8ff}.portal-header-user{align-self:start}.portal-header-identity,.portal-header-identity-stack{min-width:0}.portal-header-identity-stack{display:flex;flex-direction:column;align-items:flex-start;gap:1px;text-align:left}.portal-header-name,.portal-header-email,.portal-header-identity{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.portal-header-name{color:var(--ps-foreground);font-size:.9rem;font-weight:700;line-height:1.05}.portal-header-email{color:var(--ps-muted);font-size:.78rem;line-height:1.05}.portal-header-identity{color:var(--ps-foreground);font-size:.84rem}.portal-header-identity.is-muted{color:var(--ps-muted)}.portal-main{flex:1;min-height:0;padding:10px;overflow:hidden}.ps-web-shell{min-height:0;height:100%;display:flex;flex-direction:column;gap:8px;overflow:hidden}.ps-toolbar{display:flex;align-items:center;justify-content:space-between;gap:6px}.ps-toolbar-actions{display:flex;flex-wrap:wrap;gap:6px;min-width:0}.ps-toolbar-status{flex:0 1 auto;color:var(--ps-muted);font-size:10px;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ps-toolbar-button,.ps-mini-button,.ps-modal-button,.ps-mobile-nav-button,.ps-tab,.ps-filter-option,.ps-modal-close{padding:6px 10px;font-size:.92rem}.ps-workspace{flex:1;min-height:0;overflow:hidden}.ps-workspace>*{min-height:0}.ps-workspace-full{min-height:0;height:100%;display:flex}.ps-workspace-full>.ps-panel{flex:1;min-height:0}.ps-workspace-grid{display:grid;gap:8px;min-height:0;height:100%;overflow:hidden}.ps-workspace-column{display:grid;gap:8px;min-height:0;overflow:hidden}.ps-chat-focus-shell{min-height:0;height:100%;display:grid;grid-template-rows:auto minmax(0,1fr);gap:8px}.ps-chat-focus-rail{display:flex;align-items:center;gap:6px;padding:4px 6px;border:1px solid color-mix(in srgb,var(--ps-border) 42%,transparent);border-radius:var(--ps-radius-sm);background:color-mix(in srgb,var(--ps-surface) 94%,transparent)}.ps-chat-focus-button{flex:0 0 auto}.ps-chat-focus-status{margin-left:auto;color:var(--ps-muted);font-size:.72rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ps-chat-focus-body{position:relative;min-height:0;display:flex}.ps-chat-focus-body>.ps-panel{flex:1;min-height:0;min-width:0}.ps-chat-focus-body .ps-panel-body,.ps-chat-focus-body .ps-scroll-panel{min-width:0}.ps-chat-focus-body .ps-line,.ps-chat-focus-body .ps-chat-card-line{overflow-wrap:anywhere}.ps-chat-focus-overlay{position:absolute;top:10px;bottom:10px;width:min(34rem,calc(100% - 20px));max-width:min(34rem,42%);z-index:6;pointer-events:none}.ps-chat-focus-overlay.is-left{left:10px}.ps-chat-focus-overlay.is-right{right:10px}.ps-chat-focus-overlay>.ps-panel{height:100%;pointer-events:auto;box-shadow:0 20px 48px #00000057,inset 0 0 0 1px color-mix(in srgb,var(--ps-page-background) 35%,transparent)}.ps-mobile-workspace{min-height:0;height:100%;display:grid;grid-template-rows:auto minmax(0,1fr);gap:8px}.ps-mobile-session-pane{max-height:min(18vh,124px)}.ps-mobile-session-pane .ps-action-list{min-height:0}.ps-mobile-session-collapsed .ps-panel-body{flex:0 0 auto}.ps-mobile-session-summary{padding:0 10px 10px;color:var(--ps-muted);font-size:var(--ps-font-size-dense);line-height:var(--ps-line-height-dense)}.ps-mobile-chat-pane{min-height:0;display:flex;flex-direction:column;overflow:hidden}.ps-mobile-chat-pane>.ps-panel{flex:1;min-height:0}.ps-mobile-pane-fill{min-height:0;height:100%;display:flex;flex-direction:column;overflow:hidden}.ps-mobile-pane-fill>.ps-panel{flex:1;min-height:0}.ps-column-resizer{align-self:stretch;width:16px;min-height:0;padding:0;border:0;border-radius:999px;background:transparent;display:grid;place-items:center;cursor:col-resize;position:relative}.ps-row-resizer{justify-self:stretch;height:16px;min-width:0;padding:0;border:0;border-radius:999px;background:transparent;display:grid;place-items:center;cursor:row-resize;position:relative}.ps-column-resizer:before{content:"";position:absolute;top:10px;bottom:10px;left:50%;width:1px;transform:translate(-50%);background:color-mix(in srgb,var(--ps-border) 52%,transparent)}.ps-row-resizer:before{content:"";position:absolute;left:10px;right:10px;top:50%;height:1px;transform:translateY(-50%);background:color-mix(in srgb,var(--ps-border) 52%,transparent)}.ps-column-resizer-handle{position:relative;z-index:1;display:grid;gap:4px;padding:10px 4px;border-radius:999px;border:1px solid color-mix(in srgb,var(--ps-modal-border) 78%,transparent);background:color-mix(in srgb,var(--ps-surface) 94%,transparent);box-shadow:0 6px 20px #0000002e}.ps-row-resizer-handle{position:relative;z-index:1;display:flex;gap:4px;padding:4px 10px;border-radius:999px;border:1px solid color-mix(in srgb,var(--ps-modal-border) 78%,transparent);background:color-mix(in srgb,var(--ps-surface) 94%,transparent);box-shadow:0 6px 20px #0000002e}.ps-column-resizer-dot,.ps-row-resizer-dot{width:3px;height:3px;border-radius:999px;background:color-mix(in srgb,var(--ps-modal-selected-border) 84%,var(--ps-page-foreground))}.ps-column-resizer:hover .ps-column-resizer-handle,.ps-column-resizer.is-dragging .ps-column-resizer-handle,.ps-column-resizer:focus-visible .ps-column-resizer-handle{border-color:color-mix(in srgb,var(--ps-modal-selected-border) 72%,var(--ps-modal-border));background:color-mix(in srgb,var(--ps-modal-selected-background) 72%,var(--ps-surface))}.ps-row-resizer:hover .ps-row-resizer-handle,.ps-row-resizer.is-dragging .ps-row-resizer-handle,.ps-row-resizer:focus-visible .ps-row-resizer-handle{border-color:color-mix(in srgb,var(--ps-modal-selected-border) 72%,var(--ps-modal-border));background:color-mix(in srgb,var(--ps-modal-selected-background) 72%,var(--ps-surface))}.ps-column-resizer:focus-visible{outline:none}.ps-row-resizer:focus-visible{outline:none}body.is-resizing-pane-x,body.is-resizing-pane-x *{cursor:col-resize!important;-webkit-user-select:none!important;user-select:none!important}body.is-resizing-pane-y,body.is-resizing-pane-y *{cursor:row-resize!important;-webkit-user-select:none!important;user-select:none!important}.ps-panel{position:relative;min-height:0;display:flex;flex-direction:column;overflow:hidden;border:2px solid var(--ps-panel-accent, var(--ps-border));border-radius:var(--ps-radius-sm);background:color-mix(in srgb,var(--ps-surface) 96%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ps-page-background) 35%,transparent)}.ps-panel.is-focused{box-shadow:0 0 0 1px color-mix(in srgb,var(--ps-panel-accent, var(--ps-border)) 40%,transparent),inset 0 0 0 1px color-mix(in srgb,var(--ps-page-background) 35%,transparent)}.ps-panel-header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 10px 0;margin-bottom:0}.ps-panel-title{display:inline-flex;align-items:center;gap:4px;padding:0;background:transparent;font-size:.94rem;font-weight:700;line-height:1.08}.ps-panel-actions{display:inline-flex;gap:6px;padding-left:0;background:transparent}.ps-panel-body,.ps-scroll-panel{flex:1;min-height:0}.ps-panel-body{display:flex;flex-direction:column;min-height:0}.ps-scroll-panel{overflow:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;touch-action:pan-x pan-y;scrollbar-gutter:stable both-edges;padding:6px 10px 10px;font-size:var(--ps-font-size-dense);line-height:var(--ps-line-height-dense)}.ps-panel-sticky{flex:0 0 auto;padding:6px 10px 0;border-bottom:1px solid color-mix(in srgb,var(--ps-border) 35%,transparent);font-size:var(--ps-font-size-dense);line-height:var(--ps-line-height-dense)}.ps-panel-sticky.is-scroll-sync{overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;touch-action:pan-x;scrollbar-gutter:stable both-edges}.ps-line{min-height:1.16rem;white-space:pre-wrap;line-height:1.16rem;word-break:break-word}.ps-panel.has-preserved-sticky .ps-panel-sticky .ps-line,.ps-scroll-panel.is-preserve .ps-line{white-space:pre;word-break:normal;overflow-wrap:normal}.ps-scroll-panel.is-preserve .ps-line{min-width:max-content}.ps-scroll-panel.is-wrapped .ps-line,.ps-modal-details .ps-line,.ps-filter-column .ps-line{white-space:pre-wrap;word-break:break-word}.ps-chat-card{margin:8px auto 8px 0;border:1px solid color-mix(in srgb,var(--ps-chat-card-accent, var(--ps-border)) 78%,transparent);border-radius:var(--ps-radius-sm);background:color-mix(in srgb,var(--ps-surface) 98%,transparent);overflow:hidden;width:fit-content;max-width:min(100%,82ch)}.ps-chat-card-header{padding:8px 12px 6px;border-bottom:1px solid color-mix(in srgb,var(--ps-chat-card-accent, var(--ps-border)) 28%,transparent);background:color-mix(in srgb,var(--ps-chat-card-accent, var(--ps-surface)) 10%,transparent)}.ps-chat-card-body{padding:8px 12px 10px}.ps-chat-card-line{white-space:pre-wrap;word-break:break-word}.ps-chat-card-line+.ps-chat-card-line{margin-top:6px}.ps-chat-panel .ps-line,.ps-chat-panel .ps-chat-card-line{min-height:1.24rem;line-height:1.24rem}.ps-system-notice{margin:10px 0 12px;color:var(--ps-muted)}.ps-system-notice[open]{margin-bottom:14px}.ps-system-notice-summary{cursor:pointer;list-style:none;-webkit-user-select:none;user-select:none;opacity:.86;transition:opacity .12s ease}.ps-system-notice-summary:hover,.ps-system-notice[open] .ps-system-notice-summary{opacity:1}.ps-system-notice-summary::-webkit-details-marker{display:none}.ps-system-notice-summary-text{display:inline-block;white-space:pre-wrap}.ps-system-notice-summary:before{content:"▸ ";color:color-mix(in srgb,var(--ps-muted) 84%,transparent)}.ps-system-notice[open] .ps-system-notice-summary:before{content:"▾ "}.ps-system-notice-body{margin-top:8px;padding-left:12px;border-left:1px solid color-mix(in srgb,var(--ps-border) 42%,transparent);color:var(--ps-muted)}.ps-system-notice-body .ps-markdown-preview{gap:10px}.ps-system-notice-body .ps-md-paragraph,.ps-system-notice-body .ps-md-list,.ps-system-notice-body .ps-md-quote{color:var(--ps-muted)}.ps-chat-table-wrap{margin:8px auto 8px 0;border:1px solid color-mix(in srgb,var(--ps-border) 55%,transparent);border-radius:var(--ps-radius-sm);overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;touch-action:pan-x;width:fit-content;max-width:100%}.ps-chat-table{width:max-content;border-collapse:collapse;table-layout:auto}.ps-chat-table th,.ps-chat-table td{padding:6px 8px;border-right:1px solid color-mix(in srgb,var(--ps-border) 34%,transparent);border-bottom:1px solid color-mix(in srgb,var(--ps-border) 34%,transparent);white-space:pre-wrap;word-break:break-word;vertical-align:top}.ps-chat-table th:last-child,.ps-chat-table td:last-child{border-right:0}.ps-chat-table tbody tr:last-child td{border-bottom:0}.ps-chat-table th{text-align:left;font-weight:700;color:var(--ps-foreground);background:color-mix(in srgb,var(--ps-highlight-background) 10%,var(--ps-surface))}.ps-chat-code-block{margin:8px auto 8px 0;width:min(100%,120ch);max-width:min(100%,120ch)}.ps-action-list{display:flex;flex-direction:column;gap:4px;padding:6px 10px 10px;overflow:auto;-webkit-overflow-scrolling:touch;touch-action:pan-y;scrollbar-gutter:stable both-edges;font-size:var(--ps-font-size-dense);line-height:var(--ps-line-height-dense)}.ps-list-button{width:100%;text-align:left;padding:7px 9px}.ps-action-list .ps-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ps-session-pane .ps-panel-body{padding:0 8px 8px}.ps-session-pane .ps-session-list{gap:1px;padding:2px 0 6px}.ps-session-pane .ps-session-list-button{border:0;border-radius:2px;background:transparent;box-shadow:none;color:inherit;padding:3px 8px;min-height:1.55rem}.ps-session-pane .ps-session-list-button:hover{transform:none;border-color:transparent;background:color-mix(in srgb,var(--ps-panel-accent, var(--ps-border)) 7%,transparent)}.ps-session-pane .ps-session-list-button.is-selected{border-color:transparent;background:color-mix(in srgb,var(--ps-panel-accent, var(--ps-border)) 15%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ps-panel-accent, var(--ps-border)) 80%,transparent),inset 3px 0 color-mix(in srgb,var(--ps-panel-accent, var(--ps-border)) 90%,transparent);color:inherit}.ps-session-pane .ps-session-list-button:focus-visible{outline:none;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ps-panel-accent, var(--ps-border)) 80%,transparent),inset 3px 0 color-mix(in srgb,var(--ps-panel-accent, var(--ps-border)) 90%,transparent),0 0 0 1px color-mix(in srgb,var(--ps-panel-accent, var(--ps-border)) 35%,transparent)}.ps-session-row-content{display:block}.ps-list-button.is-selected,.ps-toolbar-button.is-active,.ps-mini-button.is-active,.ps-tab.is-active,.ps-mobile-nav-button.is-active,.ps-filter-option.is-selected{border-color:var(--ps-modal-selected-border);background:var(--ps-modal-selected-background);color:var(--ps-modal-selected-foreground)}.ps-empty-state{color:var(--ps-muted);padding:6px 0}.ps-tab-row{display:flex;flex-wrap:wrap;gap:5px;padding:4px 10px 6px;align-items:center}.ps-files-grid{flex:1;min-height:0;display:grid;grid-template-rows:minmax(180px,.9fr) minmax(220px,1.3fr);gap:8px;padding:0 10px 10px}.ps-scroll-panel.is-preview,.ps-markdown-scroll{padding-top:10px}.ps-markdown-preview{display:flex;flex-direction:column;gap:14px;padding-right:4px;line-height:1.55}.ps-md-heading{margin:0;font-weight:800;line-height:1.2;color:var(--ps-foreground)}.ps-md-heading.is-h1{font-size:1.45rem}.ps-md-heading.is-h2{font-size:1.28rem}.ps-md-heading.is-h3{font-size:1.12rem}.ps-md-heading.is-h4,.ps-md-heading.is-h5,.ps-md-heading.is-h6{font-size:1rem}.ps-md-paragraph,.ps-md-quote,.ps-md-list{margin:0}.ps-md-list{padding-left:22px;display:flex;flex-direction:column;gap:6px}.ps-md-list-item{line-height:1.5}.ps-md-quote{margin:0;padding:10px 14px;border-left:3px solid color-mix(in srgb,var(--ps-modal-selected-border) 78%,transparent);background:color-mix(in srgb,var(--ps-highlight-background) 8%,transparent);color:color-mix(in srgb,var(--ps-foreground) 88%,var(--ps-muted))}.ps-md-inline-code{padding:1px 6px;border-radius:999px;background:color-mix(in srgb,var(--ps-highlight-background) 12%,var(--ps-surface));border:1px solid color-mix(in srgb,var(--ps-border) 35%,transparent);font-size:.94em}.ps-md-code-block{border:1px solid color-mix(in srgb,var(--ps-border) 45%,transparent);border-radius:var(--ps-radius-sm);overflow:hidden;background:color-mix(in srgb,var(--ps-background) 96%,black 4%)}.ps-md-code-header{padding:7px 12px;font-size:.82rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ps-muted);border-bottom:1px solid color-mix(in srgb,var(--ps-border) 32%,transparent);background:color-mix(in srgb,var(--ps-surface) 92%,transparent)}.ps-md-code-pre{margin:0;padding:14px 16px;overflow:auto;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;touch-action:pan-x;line-height:1.55;font-size:.95rem;white-space:pre}.ps-md-code-pre code{font:inherit;color:#9fe3ff}.ps-md-table-wrap{overflow-x:auto;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;touch-action:pan-x;border:1px solid color-mix(in srgb,var(--ps-border) 40%,transparent);border-radius:var(--ps-radius-sm)}.ps-md-table{width:100%;border-collapse:collapse}.ps-md-table th,.ps-md-table td{padding:9px 12px;border-bottom:1px solid color-mix(in srgb,var(--ps-border) 25%,transparent);text-align:left;vertical-align:top}.ps-md-table th{background:color-mix(in srgb,var(--ps-highlight-background) 10%,var(--ps-surface));font-weight:700}.ps-md-table tbody tr:last-child td{border-bottom:0}.ps-md-link{text-decoration:underline;text-underline-offset:2px}.ps-chat-panel .ps-panel-header{padding-bottom:8px}.ps-chat-panel .ps-panel-body{padding-top:2px}.ps-chat-panel .ps-scroll-panel{padding-top:6px}.ps-status-strip{display:flex;justify-content:space-between;gap:10px;padding:0 2px;color:var(--ps-muted);font-size:9px}.ps-status-left,.ps-status-right{min-width:0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.ps-footer-shell{display:flex;flex-direction:column;gap:4px;padding:6px 8px 8px;border:1px solid color-mix(in srgb,var(--ps-border) 45%,transparent);border-radius:var(--ps-radius-sm);background:color-mix(in srgb,var(--ps-surface) 94%,transparent)}.ps-prompt-shell{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:center;padding:0}.ps-prompt-shell.is-mobile{grid-template-columns:auto minmax(0,1fr) auto;gap:8px}.ps-hidden-file-input{display:none}.ps-prompt-label{padding:0 4px 0 2px;color:var(--ps-muted);text-transform:uppercase;font-size:10px;letter-spacing:.08em;align-self:center}.ps-prompt-shell.is-mobile .ps-prompt-label{padding:0 2px 0 0}.ps-prompt-input,.ps-modal-input{width:100%;min-height:40px;resize:vertical;border:1px solid color-mix(in srgb,var(--ps-border) 60%,transparent);border-radius:var(--ps-radius-sm);background:color-mix(in srgb,var(--ps-background) 94%,transparent);color:var(--ps-foreground);padding:8px 10px;outline:none;font-size:var(--ps-font-size-dense);line-height:var(--ps-line-height-dense)}.ps-prompt-shell.is-mobile .ps-prompt-input{font-size:16px;line-height:1.25}.ps-prompt-input:focus,.ps-modal-input:focus{border-color:var(--ps-modal-selected-border);box-shadow:0 0 0 3px color-mix(in srgb,var(--ps-modal-selected-border) 20%,transparent)}.ps-prompt-shell.is-drag-over .ps-prompt-input{border-color:var(--ps-modal-selected-border);background:color-mix(in srgb,var(--ps-modal-selected-background) 26%,var(--ps-background));box-shadow:0 0 0 3px color-mix(in srgb,var(--ps-modal-selected-border) 18%,transparent)}.ps-send-button{min-height:40px;padding:0 12px}.ps-send-button.is-inline{min-height:40px;min-width:40px;padding:0;border-radius:var(--ps-radius-sm);font-size:18px;line-height:1;display:inline-flex;align-items:center;justify-content:center}.ps-mobile-nav{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.ps-modal-backdrop{position:fixed;inset:0;background:var(--ps-modal-backdrop);display:flex;align-items:center;justify-content:center;padding:16px;z-index:50}.ps-compose-backdrop{align-items:flex-start;padding-top:max(18px,env(safe-area-inset-top))}.ps-compose-card{width:min(980px,100%);border:1px solid var(--ps-modal-border);border-radius:var(--ps-radius-md);background:var(--ps-modal-background);color:var(--ps-modal-foreground);padding:14px;box-shadow:0 30px 80px #0006;display:flex;flex-direction:column;gap:10px}.ps-compose-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.ps-modal{width:min(900px,100%);max-height:min(86vh,900px);overflow:auto;-webkit-overflow-scrolling:touch;touch-action:pan-y;scrollbar-gutter:stable both-edges;border:1px solid var(--ps-modal-border);border-radius:var(--ps-radius-md);background:var(--ps-modal-background);color:var(--ps-modal-foreground);padding:18px;box-shadow:0 30px 80px #0006}.ps-scroll-panel::-webkit-scrollbar,.ps-action-list::-webkit-scrollbar,.ps-panel-sticky.is-scroll-sync::-webkit-scrollbar,.ps-modal::-webkit-scrollbar{width:var(--ps-scrollbar-size);height:var(--ps-scrollbar-size)}.ps-scroll-panel::-webkit-scrollbar-track,.ps-action-list::-webkit-scrollbar-track,.ps-panel-sticky.is-scroll-sync::-webkit-scrollbar-track,.ps-modal::-webkit-scrollbar-track{background:transparent}.ps-scroll-panel::-webkit-scrollbar-thumb,.ps-action-list::-webkit-scrollbar-thumb,.ps-panel-sticky.is-scroll-sync::-webkit-scrollbar-thumb,.ps-modal::-webkit-scrollbar-thumb{background:var(--ps-scrollbar-thumb);border:2px solid transparent;border-radius:999px;background-clip:padding-box;min-height:28px}.ps-scroll-panel::-webkit-scrollbar-thumb:hover,.ps-action-list::-webkit-scrollbar-thumb:hover,.ps-panel-sticky.is-scroll-sync::-webkit-scrollbar-thumb:hover,.ps-modal::-webkit-scrollbar-thumb:hover{background:var(--ps-scrollbar-thumb-hover);border:2px solid transparent;background-clip:padding-box}.ps-scroll-panel::-webkit-scrollbar-corner,.ps-action-list::-webkit-scrollbar-corner,.ps-panel-sticky.is-scroll-sync::-webkit-scrollbar-corner,.ps-modal::-webkit-scrollbar-corner{background:transparent}.ps-modal.is-narrow{width:min(560px,100%)}.ps-modal.is-wide{width:min(980px,100%)}.ps-modal-header,.ps-modal-footer{display:flex;align-items:center;justify-content:space-between;gap:12px}.ps-modal-title,.ps-modal-details-title,.ps-filter-title{font-size:16px;font-weight:700}.ps-modal-grid{display:grid;grid-template-columns:minmax(280px,1fr) minmax(260px,.9fr);gap:16px;margin:16px 0}.ps-modal-list,.ps-modal-details{display:flex;flex-direction:column;gap:10px}.ps-keybinding-modal{width:min(860px,100%)}.ps-keybinding-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;margin:18px 0}.ps-keybinding-section{display:flex;flex-direction:column;gap:10px}.ps-keybinding-title{margin:0;font-size:14px;font-weight:700;color:var(--ps-page-foreground)}.ps-keybinding-list{display:flex;flex-direction:column;gap:10px}.ps-keybinding-row{display:grid;grid-template-columns:minmax(110px,auto) minmax(0,1fr);align-items:start;gap:12px}.ps-keybinding-kbd{display:inline-flex;align-items:center;justify-content:center;min-height:28px;padding:4px 10px;border-radius:8px;border:1px solid color-mix(in srgb,var(--ps-modal-selected-border) 45%,var(--ps-modal-border));background:color-mix(in srgb,var(--ps-modal-selected-background) 30%,var(--ps-surface));color:var(--ps-page-foreground);font:inherit;font-weight:700}.ps-keybinding-description{color:var(--ps-muted);line-height:1.45}.ps-filter-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin:18px 0}.ps-filter-column{display:flex;flex-direction:column;gap:10px}.ps-modal-close{color:var(--ps-muted)}.portal-primary-button:hover,.portal-secondary-button:hover,.ps-toolbar-button:hover,.ps-mini-button:hover,.ps-send-button:hover,.ps-modal-button:hover,.ps-mobile-nav-button:hover,.ps-tab:hover,.ps-filter-option:hover,.ps-list-button:hover,.ps-modal-close:hover,.ps-column-resizer:hover{transform:translateY(-1px);border-color:color-mix(in srgb,var(--ps-modal-selected-border) 70%,var(--ps-modal-border))}@media(max-width:920px){:root{--ps-font-size-base: 13px;--ps-font-size-dense: 12px}.portal-main{padding:8px}.portal-header{grid-template-columns:minmax(0,1fr) auto;align-items:start;padding:8px 10px}.portal-header-user{align-self:start}.ps-toolbar{align-items:center}.ps-toolbar-status{max-width:34%;font-size:9px}.ps-chat-focus-rail{flex-wrap:wrap}.ps-chat-focus-status{width:100%;margin-left:0}.ps-chat-focus-overlay{left:8px;right:8px;width:auto;max-width:none}.ps-chat-focus-shell .ps-chat-table th,.ps-chat-focus-shell .ps-chat-table td{white-space:nowrap;word-break:normal;overflow-wrap:normal}.ps-footer-shell{padding:4px 6px 6px}.ps-status-right{display:none}.ps-status-strip{justify-content:flex-start}.ps-modal-grid{grid-template-columns:1fr}.ps-files-grid{grid-template-rows:minmax(160px,1fr) minmax(260px,1.4fr)}.ps-column-resizer{display:none}.ps-mobile-session-pane{max-height:min(16vh,108px)}.ps-mobile-pane-fill>.ps-panel .ps-scroll-panel,.ps-mobile-pane-fill>.ps-panel .ps-panel-body{flex:1}}
|