@echomem/echo-memory-cloud-openclaw-plugin 0.2.1 → 0.2.2
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/clawdbot.plugin.json +1 -1
- package/index.js +19 -10
- package/lib/config.js +4 -0
- package/lib/local-server.js +224 -83
- package/lib/local-ui/dist/assets/index-CKT0_swv.css +1 -0
- package/lib/local-ui/dist/assets/index-D9Bbjf7A.js +54 -0
- package/lib/local-ui/dist/index.html +2 -2
- package/lib/state.js +59 -4
- package/lib/sync.js +42 -15
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/lib/local-ui/dist/assets/index-BoyzFR9Q.css +0 -1
- package/lib/local-ui/dist/assets/index-Cnb-zSN2.js +0 -54
package/clawdbot.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "echo-memory-cloud-openclaw-plugin",
|
|
3
3
|
"name": "Echo Memory Cloud OpenClaw Plugin",
|
|
4
4
|
"description": "Sync OpenClaw local markdown memory files to Echo cloud",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.2",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { buildConfig, getEnvFileStatus } from "./lib/config.js";
|
|
3
|
+
import { buildConfig, getEnvFileStatus, getOpenClawHome } from "./lib/config.js";
|
|
4
4
|
import { createApiClient } from "./lib/api-client.js";
|
|
5
5
|
import { formatSearchResultsText } from "./lib/echo-memory-search.js";
|
|
6
6
|
import {
|
|
@@ -16,13 +16,15 @@ import { buildOnboardingText } from "./lib/onboarding.js";
|
|
|
16
16
|
import { createSyncRunner, formatStatusText } from "./lib/sync.js";
|
|
17
17
|
import { readLastSyncState } from "./lib/state.js";
|
|
18
18
|
import {
|
|
19
|
+
hasRecentLocalUiPresence,
|
|
19
20
|
openUrlInDefaultBrowser,
|
|
20
21
|
startLocalServer,
|
|
21
22
|
stopLocalServer,
|
|
22
23
|
waitForLocalUiClient,
|
|
23
24
|
} from "./lib/local-server.js";
|
|
24
25
|
|
|
25
|
-
const LOCAL_UI_RECONNECT_GRACE_MS =
|
|
26
|
+
const LOCAL_UI_RECONNECT_GRACE_MS = 4000;
|
|
27
|
+
const LOCAL_UI_PRESENCE_GRACE_MS = 75000;
|
|
26
28
|
|
|
27
29
|
function resolveCommandLabel(channel) {
|
|
28
30
|
return channel === "discord" ? "/echomemory" : "/echo-memory";
|
|
@@ -54,13 +56,15 @@ export default {
|
|
|
54
56
|
const cfg = buildConfig(api.pluginConfig);
|
|
55
57
|
const client = createApiClient(cfg);
|
|
56
58
|
const workspaceDir = path.resolve(path.dirname(cfg.memoryDir), "..");
|
|
57
|
-
const openclawHome =
|
|
58
|
-
const
|
|
59
|
+
const openclawHome = getOpenClawHome();
|
|
60
|
+
const legacyPluginStateDir = path.join(openclawHome, "state", "plugins", "echo-memory-cloud-openclaw-plugin");
|
|
61
|
+
const stableStateDir = path.join(openclawHome, "state", "echo-memory-cloud-openclaw-plugin");
|
|
59
62
|
const syncRunner = createSyncRunner({
|
|
60
63
|
api,
|
|
61
64
|
cfg,
|
|
62
65
|
client,
|
|
63
|
-
fallbackStateDir,
|
|
66
|
+
fallbackStateDir: legacyPluginStateDir,
|
|
67
|
+
stableStateDir,
|
|
64
68
|
});
|
|
65
69
|
let startupBrowserOpenAttempted = false;
|
|
66
70
|
let backgroundStarted = false;
|
|
@@ -82,11 +86,16 @@ export default {
|
|
|
82
86
|
let openedInBrowser = false;
|
|
83
87
|
let openReason = "not_requested";
|
|
84
88
|
if (openInBrowser) {
|
|
85
|
-
const
|
|
86
|
-
? await
|
|
89
|
+
const existingPageDetected = trigger === "gateway-start"
|
|
90
|
+
? await hasRecentLocalUiPresence(syncRunner, { maxAgeMs: LOCAL_UI_PRESENCE_GRACE_MS })
|
|
87
91
|
: false;
|
|
92
|
+
const existingClientDetected = existingPageDetected || (
|
|
93
|
+
trigger === "gateway-start"
|
|
94
|
+
? await waitForLocalUiClient({ timeoutMs: LOCAL_UI_RECONNECT_GRACE_MS })
|
|
95
|
+
: false
|
|
96
|
+
);
|
|
88
97
|
const openResult = existingClientDetected
|
|
89
|
-
? { opened: false, reason: "existing_client_reconnected" }
|
|
98
|
+
? { opened: false, reason: existingPageDetected ? "existing_page_detected" : "existing_client_reconnected" }
|
|
90
99
|
: await openUrlInDefaultBrowser(url, {
|
|
91
100
|
logger: api.logger,
|
|
92
101
|
force: trigger !== "gateway-start",
|
|
@@ -165,7 +174,7 @@ export default {
|
|
|
165
174
|
return;
|
|
166
175
|
}
|
|
167
176
|
backgroundStarted = true;
|
|
168
|
-
await syncRunner.initialize(stateDir ||
|
|
177
|
+
await syncRunner.initialize(stateDir || legacyPluginStateDir);
|
|
169
178
|
|
|
170
179
|
try {
|
|
171
180
|
const shouldOpenBrowser = cfg.localUiAutoOpenOnGatewayStart && !startupBrowserOpenAttempted;
|
|
@@ -219,7 +228,7 @@ export default {
|
|
|
219
228
|
if (serviceStartObserved) {
|
|
220
229
|
return;
|
|
221
230
|
}
|
|
222
|
-
startBackgroundFeatures({ stateDir:
|
|
231
|
+
startBackgroundFeatures({ stateDir: legacyPluginStateDir, trigger: "compat-startup" }).catch((error) => {
|
|
223
232
|
api.logger?.warn?.(`[echo-memory] compatibility startup failed: ${String(error?.message ?? error)}`);
|
|
224
233
|
});
|
|
225
234
|
});
|
package/lib/config.js
CHANGED
|
@@ -122,6 +122,10 @@ export function getEnvFileStatus() {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export function getOpenClawHome() {
|
|
126
|
+
return OPENCLAW_HOME;
|
|
127
|
+
}
|
|
128
|
+
|
|
125
129
|
function resolveConfigValue(pluginConfig, configKey, envKey, fallback) {
|
|
126
130
|
if (pluginConfig?.[configKey] !== undefined && pluginConfig?.[configKey] !== null && pluginConfig?.[configKey] !== "") {
|
|
127
131
|
return {
|
package/lib/local-server.js
CHANGED
|
@@ -3,10 +3,15 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import fsSync from "node:fs";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { getLocalUiSetupState, saveLocalUiSetup } from "./config.js";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { getLocalUiSetupState, saveLocalUiSetup } from "./config.js";
|
|
8
8
|
import { scanFullWorkspace, scanWorkspaceMarkdownFile } from "./openclaw-memory-scan.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
readLastSyncState,
|
|
11
|
+
readLocalUiPresence,
|
|
12
|
+
resolveUiPresencePath,
|
|
13
|
+
writeLocalUiPresence,
|
|
14
|
+
} from "./state.js";
|
|
10
15
|
|
|
11
16
|
const BASE_PORT = 17823;
|
|
12
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -15,9 +20,11 @@ const UI_WORKDIR = path.join(__dirname, "local-ui");
|
|
|
15
20
|
const UI_DIST_DIR = path.join(__dirname, "local-ui", "dist");
|
|
16
21
|
const UI_NODE_MODULES_DIR = path.join(UI_WORKDIR, "node_modules");
|
|
17
22
|
|
|
18
|
-
let _instance = null;
|
|
19
|
-
let _bootstrapPromise = null;
|
|
20
|
-
let _lastOpenedUrl = null;
|
|
23
|
+
let _instance = null;
|
|
24
|
+
let _bootstrapPromise = null;
|
|
25
|
+
let _lastOpenedUrl = null;
|
|
26
|
+
const BACKEND_SOURCE_LOOKUP_TIMEOUT_MS = 4000;
|
|
27
|
+
const LOCAL_UI_PRESENCE_STALE_MS = 75000;
|
|
21
28
|
|
|
22
29
|
/* ── File Watcher + SSE ────────────────────────────────── */
|
|
23
30
|
|
|
@@ -261,17 +268,17 @@ function getBrowserOpenCommand(url) {
|
|
|
261
268
|
return null;
|
|
262
269
|
}
|
|
263
270
|
|
|
264
|
-
export async function openUrlInDefaultBrowser(url, opts = {}) {
|
|
265
|
-
const { logger, force = false } = opts;
|
|
266
|
-
if (!force) {
|
|
267
|
-
const skipReason = detectBrowserOpenSkipReason();
|
|
268
|
-
if (skipReason) {
|
|
269
|
-
return { opened: false, reason: skipReason };
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (_lastOpenedUrl === url) {
|
|
273
|
-
return { opened: false, reason: "already_opened" };
|
|
274
|
-
}
|
|
271
|
+
export async function openUrlInDefaultBrowser(url, opts = {}) {
|
|
272
|
+
const { logger, force = false } = opts;
|
|
273
|
+
if (!force) {
|
|
274
|
+
const skipReason = detectBrowserOpenSkipReason();
|
|
275
|
+
if (skipReason) {
|
|
276
|
+
return { opened: false, reason: skipReason };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!force && _lastOpenedUrl === url) {
|
|
280
|
+
return { opened: false, reason: "already_opened" };
|
|
281
|
+
}
|
|
275
282
|
const command = getBrowserOpenCommand(url);
|
|
276
283
|
if (!command) {
|
|
277
284
|
return { opened: false, reason: "unsupported_platform" };
|
|
@@ -327,34 +334,143 @@ function readBody(req) {
|
|
|
327
334
|
});
|
|
328
335
|
}
|
|
329
336
|
|
|
330
|
-
function resolveStoredFilePath(workspaceDir, entry) {
|
|
331
|
-
const rawPath = entry?.filePath || entry?.file_path || entry?.path || null;
|
|
332
|
-
if (!rawPath) {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
return path.isAbsolute(rawPath) ? path.normalize(rawPath) : path.resolve(workspaceDir, rawPath);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
337
|
+
function resolveStoredFilePath(workspaceDir, entry) {
|
|
338
|
+
const rawPath = entry?.filePath || entry?.file_path || entry?.path || null;
|
|
339
|
+
if (!rawPath) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
return path.isAbsolute(rawPath) ? path.normalize(rawPath) : path.resolve(workspaceDir, rawPath);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function toPathKey(targetPath) {
|
|
346
|
+
const normalized = path.normalize(String(targetPath || ""));
|
|
347
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolveBackendFilePath(workspaceDir, rawPath) {
|
|
351
|
+
if (!rawPath) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return path.isAbsolute(rawPath)
|
|
355
|
+
? path.normalize(rawPath)
|
|
356
|
+
: path.resolve(workspaceDir, String(rawPath));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function readBackendSourceMap(apiClient, workspaceDir) {
|
|
360
|
+
const sources = new Map();
|
|
361
|
+
if (!apiClient) {
|
|
362
|
+
return sources;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const addSource = (sourcePath, latestAt = null) => {
|
|
366
|
+
const resolvedPath = resolveBackendFilePath(workspaceDir, sourcePath);
|
|
367
|
+
if (!resolvedPath) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const pathKey = toPathKey(resolvedPath);
|
|
371
|
+
const existing = sources.get(pathKey);
|
|
372
|
+
if (!existing || (latestAt && (!existing.latestAt || latestAt > existing.latestAt))) {
|
|
373
|
+
sources.set(pathKey, {
|
|
374
|
+
latestAt: latestAt || existing?.latestAt || null,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const data = await apiClient.listAllSources();
|
|
381
|
+
for (const source of data?.paths || []) {
|
|
382
|
+
addSource(source?.file_path, source?.latest_at ?? null);
|
|
383
|
+
}
|
|
384
|
+
return sources;
|
|
385
|
+
} catch {
|
|
386
|
+
// Fall through to import-status fallback.
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const status = await apiClient.getImportStatus();
|
|
391
|
+
for (const source of status?.recent_sources || []) {
|
|
392
|
+
addSource(source?.file_path, source?.created_at ?? null);
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
// Ignore backend status fallback failures in the local UI.
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return sources;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function resolveWithin(timeoutMs, value) {
|
|
402
|
+
return new Promise((resolve) => {
|
|
403
|
+
setTimeout(() => resolve(value), timeoutMs);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getLocalUiPresencePath(syncRunner) {
|
|
408
|
+
try {
|
|
409
|
+
const statePath = syncRunner?.getStatePath?.();
|
|
410
|
+
return statePath ? resolveUiPresencePath(path.dirname(statePath)) : null;
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function updateLocalUiPresence(syncRunner, payload = {}) {
|
|
417
|
+
const presencePath = getLocalUiPresencePath(syncRunner);
|
|
418
|
+
if (!presencePath) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const previous = await readLocalUiPresence(presencePath);
|
|
423
|
+
const next = {
|
|
424
|
+
clientId: payload.clientId || previous?.clientId || null,
|
|
425
|
+
serverInstanceId: payload.serverInstanceId || previous?.serverInstanceId || null,
|
|
426
|
+
active: payload.active !== undefined ? Boolean(payload.active) : true,
|
|
427
|
+
lastSeenAt: payload.lastSeenAt || new Date().toISOString(),
|
|
428
|
+
};
|
|
429
|
+
await writeLocalUiPresence(presencePath, next);
|
|
430
|
+
return next;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export async function hasRecentLocalUiPresence(syncRunner, { maxAgeMs = LOCAL_UI_PRESENCE_STALE_MS } = {}) {
|
|
434
|
+
const presencePath = getLocalUiPresencePath(syncRunner);
|
|
435
|
+
if (!presencePath) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const presence = await readLocalUiPresence(presencePath);
|
|
440
|
+
if (!presence?.lastSeenAt || presence.active === false) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const ageMs = Date.now() - new Date(presence.lastSeenAt).getTime();
|
|
445
|
+
return Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= maxAgeMs;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function buildWorkspaceSyncView({ workspaceDir, syncMemoryDir, statePath, apiClient }) {
|
|
449
|
+
const backendSourcesPromise = readBackendSourceMap(apiClient, workspaceDir).catch(() => new Map());
|
|
450
|
+
const [lastState, files, backendSources] = await Promise.all([
|
|
451
|
+
statePath ? readLastSyncState(statePath) : Promise.resolve(null),
|
|
452
|
+
scanFullWorkspace(workspaceDir),
|
|
453
|
+
Promise.race([
|
|
454
|
+
backendSourcesPromise,
|
|
455
|
+
resolveWithin(BACKEND_SOURCE_LOOKUP_TIMEOUT_MS, new Map()),
|
|
456
|
+
]),
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
const storedResults = Array.isArray(lastState?.results) ? lastState.results : [];
|
|
460
|
+
const resultMap = new Map();
|
|
461
|
+
for (const entry of storedResults) {
|
|
462
|
+
const storedPath = resolveStoredFilePath(workspaceDir, entry);
|
|
463
|
+
if (!storedPath) continue;
|
|
464
|
+
resultMap.set(toPathKey(storedPath), entry);
|
|
465
|
+
}
|
|
351
466
|
|
|
352
467
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}/;
|
|
353
468
|
const eligibleAbsolutePaths = new Set();
|
|
354
469
|
const eligibleRelativePaths = [];
|
|
355
470
|
|
|
356
471
|
const fileStatuses = files.map((f) => {
|
|
357
|
-
const absPath = path.resolve(workspaceDir, f.relativePath);
|
|
472
|
+
const absPath = path.resolve(workspaceDir, f.relativePath);
|
|
473
|
+
const pathKey = toPathKey(absPath);
|
|
358
474
|
const isPrivate =
|
|
359
475
|
f.relativePath.startsWith("memory/private/") ||
|
|
360
476
|
f.privacyLevel === "private";
|
|
@@ -362,16 +478,22 @@ async function buildWorkspaceSyncView({ workspaceDir, syncMemoryDir, statePath }
|
|
|
362
478
|
Boolean(syncMemoryDir) && path.dirname(absPath) === syncMemoryDir;
|
|
363
479
|
const isDaily =
|
|
364
480
|
f.relativePath.startsWith("memory/") && DATE_RE.test(f.fileName);
|
|
365
|
-
const stored = resultMap.get(
|
|
481
|
+
const stored = resultMap.get(pathKey) ?? null;
|
|
366
482
|
const storedStatus = String(stored?.status || "").trim().toLowerCase() || null;
|
|
367
483
|
const attemptedHash = stored?.contentHash || stored?.content_hash || null;
|
|
368
|
-
const successfulHash =
|
|
369
|
-
stored?.lastSuccessfulContentHash
|
|
370
|
-
|| stored?.last_successful_content_hash
|
|
371
|
-
|| (storedStatus && storedStatus !== "failed" ? attemptedHash : null);
|
|
372
|
-
const lastError = stored?.lastError || stored?.last_error || stored?.error || null;
|
|
373
|
-
const lastAttemptAt = stored?.lastAttemptAt || stored?.last_attempt_at || null;
|
|
374
|
-
const
|
|
484
|
+
const successfulHash =
|
|
485
|
+
stored?.lastSuccessfulContentHash
|
|
486
|
+
|| stored?.last_successful_content_hash
|
|
487
|
+
|| (storedStatus && storedStatus !== "failed" ? attemptedHash : null);
|
|
488
|
+
const lastError = stored?.lastError || stored?.last_error || stored?.error || null;
|
|
489
|
+
const lastAttemptAt = stored?.lastAttemptAt || stored?.last_attempt_at || null;
|
|
490
|
+
const backendSource = backendSources.get(pathKey) ?? null;
|
|
491
|
+
const lastSuccessAt =
|
|
492
|
+
stored?.lastSuccessAt
|
|
493
|
+
|| stored?.last_success_at
|
|
494
|
+
|| backendSource?.latestAt
|
|
495
|
+
|| lastState?.finished_at
|
|
496
|
+
|| null;
|
|
375
497
|
|
|
376
498
|
if (isPrivate) {
|
|
377
499
|
return {
|
|
@@ -396,14 +518,16 @@ async function buildWorkspaceSyncView({ workspaceDir, syncMemoryDir, statePath }
|
|
|
396
518
|
eligibleAbsolutePaths.add(absPath);
|
|
397
519
|
eligibleRelativePaths.push(f.relativePath);
|
|
398
520
|
|
|
399
|
-
let status = "new";
|
|
400
|
-
if (storedStatus === "failed" && attemptedHash && attemptedHash === f.contentHash) {
|
|
401
|
-
status = "failed";
|
|
402
|
-
} else if (successfulHash) {
|
|
403
|
-
status = isDaily || successfulHash === f.contentHash ? "synced" : "modified";
|
|
404
|
-
} else if (
|
|
405
|
-
status = "
|
|
406
|
-
}
|
|
521
|
+
let status = "new";
|
|
522
|
+
if (storedStatus === "failed" && attemptedHash && attemptedHash === f.contentHash) {
|
|
523
|
+
status = "failed";
|
|
524
|
+
} else if (successfulHash) {
|
|
525
|
+
status = isDaily || successfulHash === f.contentHash ? "synced" : "modified";
|
|
526
|
+
} else if (backendSource) {
|
|
527
|
+
status = "synced";
|
|
528
|
+
} else if (storedStatus === "failed") {
|
|
529
|
+
status = "modified";
|
|
530
|
+
}
|
|
407
531
|
|
|
408
532
|
return {
|
|
409
533
|
fileName: f.fileName,
|
|
@@ -451,7 +575,7 @@ function createRequestHandler(workspaceDir, htmlContent, opts = {}) {
|
|
|
451
575
|
return;
|
|
452
576
|
}
|
|
453
577
|
|
|
454
|
-
// SSE endpoint — push file-change events to the frontend
|
|
578
|
+
// SSE endpoint — push file-change events to the frontend
|
|
455
579
|
if (url.pathname === "/api/events") {
|
|
456
580
|
res.writeHead(200, {
|
|
457
581
|
"Content-Type": "text/event-stream",
|
|
@@ -470,8 +594,28 @@ function createRequestHandler(workspaceDir, htmlContent, opts = {}) {
|
|
|
470
594
|
}
|
|
471
595
|
return;
|
|
472
596
|
}
|
|
473
|
-
|
|
474
|
-
if (url.pathname === "/") {
|
|
597
|
+
|
|
598
|
+
if (url.pathname === "/api/ui-presence" && req.method === "POST") {
|
|
599
|
+
try {
|
|
600
|
+
const bodyResult = await readBody(req);
|
|
601
|
+
if (!bodyResult.ok) {
|
|
602
|
+
sendJson(res, { ok: false, error: "invalid_json" });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
await updateLocalUiPresence(syncRunner, {
|
|
606
|
+
clientId: typeof bodyResult.body?.clientId === "string" ? bodyResult.body.clientId : null,
|
|
607
|
+
serverInstanceId: typeof bodyResult.body?.serverInstanceId === "string" ? bodyResult.body.serverInstanceId : serverInstanceId,
|
|
608
|
+
active: bodyResult.body?.active,
|
|
609
|
+
});
|
|
610
|
+
sendJson(res, { ok: true });
|
|
611
|
+
} catch (error) {
|
|
612
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
613
|
+
res.end(JSON.stringify({ ok: false, error: String(error?.message ?? error) }));
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (url.pathname === "/") {
|
|
475
619
|
// Always serve the built React app from dist/
|
|
476
620
|
let content;
|
|
477
621
|
try {
|
|
@@ -548,21 +692,16 @@ function createRequestHandler(workspaceDir, htmlContent, opts = {}) {
|
|
|
548
692
|
return;
|
|
549
693
|
}
|
|
550
694
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
});
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
sendJson(res, { fileName: fileScan.fileName, content: fileScan.content });
|
|
695
|
+
sendJson(res, {
|
|
696
|
+
fileName: fileScan.fileName,
|
|
697
|
+
content: fileScan.content,
|
|
698
|
+
privacyLevel: fileScan.privacyLevel,
|
|
699
|
+
privacyAutoUpgraded: fileScan.privacyAutoUpgraded,
|
|
700
|
+
hasSensitiveContent: fileScan.hasSensitiveContent,
|
|
701
|
+
hasHighRiskSensitiveContent: fileScan.hasHighRiskSensitiveContent,
|
|
702
|
+
sensitiveSummary: fileScan.sensitiveSummary,
|
|
703
|
+
sensitiveFindings: fileScan.sensitiveFindings,
|
|
704
|
+
});
|
|
566
705
|
return;
|
|
567
706
|
}
|
|
568
707
|
|
|
@@ -679,11 +818,12 @@ function createRequestHandler(workspaceDir, htmlContent, opts = {}) {
|
|
|
679
818
|
if (url.pathname === "/api/sync-status") {
|
|
680
819
|
try {
|
|
681
820
|
const statePath = syncRunner?.getStatePath() ?? null;
|
|
682
|
-
const syncView = await buildWorkspaceSyncView({
|
|
683
|
-
workspaceDir,
|
|
684
|
-
syncMemoryDir,
|
|
685
|
-
statePath,
|
|
686
|
-
|
|
821
|
+
const syncView = await buildWorkspaceSyncView({
|
|
822
|
+
workspaceDir,
|
|
823
|
+
syncMemoryDir,
|
|
824
|
+
statePath,
|
|
825
|
+
apiClient,
|
|
826
|
+
});
|
|
687
827
|
sendJson(res, {
|
|
688
828
|
lastSyncAt: syncView.lastState?.finished_at ?? null,
|
|
689
829
|
syncedFileCount: syncView.fileStatuses.filter((status) => status.status === 'synced').length,
|
|
@@ -770,11 +910,12 @@ function createRequestHandler(workspaceDir, htmlContent, opts = {}) {
|
|
|
770
910
|
}
|
|
771
911
|
|
|
772
912
|
const statePath = syncRunner?.getStatePath() ?? null;
|
|
773
|
-
const syncView = await buildWorkspaceSyncView({
|
|
774
|
-
workspaceDir,
|
|
775
|
-
syncMemoryDir,
|
|
776
|
-
statePath,
|
|
777
|
-
|
|
913
|
+
const syncView = await buildWorkspaceSyncView({
|
|
914
|
+
workspaceDir,
|
|
915
|
+
syncMemoryDir,
|
|
916
|
+
statePath,
|
|
917
|
+
apiClient,
|
|
918
|
+
});
|
|
778
919
|
const statusMap = new Map(syncView.fileStatuses.map((status) => [status.relativePath, status]));
|
|
779
920
|
const requestedFilterPaths = new Set();
|
|
780
921
|
const requestedInvalidPaths = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.card{position:absolute;border-radius:4px;overflow:hidden;box-shadow:0 1px 3px #0000002e;cursor:pointer;display:flex;flex-direction:column;padding:8px 10px;contain:layout style paint;animation:cardFadeIn .5s ease-out both}@keyframes cardFadeIn{0%{opacity:0}to{opacity:1}}.card:hover{box-shadow:0 4px 16px #0000004d;z-index:10}.card-selected{transform:scale(1.08);transform-origin:center center;box-shadow:0 8px 32px #a78bfa59,0 0 0 2px #a78bfa99;z-index:50!important;transition:transform .25s cubic-bezier(.34,1.56,.64,1),box-shadow .25s ease}.card-dimmed{opacity:.35;transition:opacity .3s ease}.card-dimmed:hover{opacity:.7}.card-expand-btn{flex-shrink:0;width:20px;height:20px;border:none;border-radius:4px;background:#a78bfa33;color:#a78bfa;font-size:12px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s,transform .15s;line-height:1}.card-expand-btn:hover{background:#a78bfa66;transform:scale(1.15)}.card-checkbox{flex-shrink:0;font-size:14px;cursor:pointer;color:#999;line-height:1;transition:color .15s}.card-checkbox-on{color:#a78bfa}.card-lod0{position:absolute;border-radius:3px;contain:strict;pointer-events:none}.card-header{display:flex;align-items:center;gap:6px;flex-shrink:0;margin-bottom:4px}.card-name{font:600 11px/1.3 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.card-warning-toggle{flex-shrink:0;max-width:48%;display:inline-flex;align-items:center;gap:4px;border:1px solid rgba(160,112,48,.35);border-radius:999px;background:#a070301a;color:#8b6128;padding:2px 6px;font:600 8px/1.1 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;cursor:pointer;min-width:0}.card-warning-toggle-high{border-color:#b040406b;background:#b040401a;color:#9c3d3d}.card-warning-toggle__icon{flex-shrink:0}.card-warning-toggle__text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-private-badge{flex-shrink:0;border:1px solid rgba(156,61,61,.35);border-radius:999px;background:#9c3d3d14;color:#9c3d3d;padding:2px 6px;font:700 8px/1.1 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.card-cluster-badge{flex-shrink:0;border-radius:999px;padding:2px 6px;font:700 8px/1.1 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;letter-spacing:.3px;border:1px solid rgba(255,255,255,.18);background:#ffffff1f;color:#d6d0c6;max-width:38%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-cluster-badge-identity{color:#fca5a5;border-color:#fca5a547;background:#fca5a514}.card-cluster-badge-long-term{color:#fdba74;border-color:#fdba7447;background:#fdba7414}.card-cluster-badge-journal{color:#fde68a;border-color:#fde68a47;background:#fde68a14}.card-cluster-badge-goals{color:#d6b388;border-color:#d6b38847;background:#d6b38814}.card-cluster-badge-technical{color:#93c5fd;border-color:#93c5fd47;background:#93c5fd14}.card-cluster-badge-thematic{color:#d8b4fe;border-color:#d8b4fe47;background:#d8b4fe14}.card-cluster-badge-knowledge{color:#86efac;border-color:#86efac47;background:#86efac14}.card-cluster-badge-system{color:#cbd5e1;border-color:#cbd5e147;background:#cbd5e114}.card-warning-panel{margin-bottom:6px;border:1px solid rgba(176,64,64,.2);border-radius:4px;background:#fff8f4eb;padding:6px 8px;font:500 9px/1.35 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#6b443d;pointer-events:none}.card-warning-panel__header,.card-warning-panel__row{display:flex;align-items:center;justify-content:space-between;gap:8px}.card-warning-panel__header{font-weight:700;margin-bottom:4px}.card-warning-panel__privacy{color:#9c3d3d}.card-warning-panel__row+.card-warning-panel__row{margin-top:2px}.card-content{flex:1;overflow:hidden;font:400 9.5px/1.45 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;letter-spacing:.01em;white-space:pre-wrap;word-break:break-word;-webkit-mask-image:linear-gradient(to bottom,#000 60%,transparent 100%);mask-image:linear-gradient(to bottom,#000 60%,transparent 100%);opacity:.72}.card-session-log{animation:cardFadeInLog .5s ease-out both}.card-session-log:hover{opacity:.75}@keyframes cardFadeInLog{0%{opacity:0}to{opacity:.5}}.session-badge{font-size:9px;flex-shrink:0}.card-content-log{flex:1;overflow:hidden;font:400 8px/1.35 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#aaa;white-space:pre-wrap;word-break:break-word;-webkit-mask-image:linear-gradient(to bottom,#000 40%,transparent 90%);mask-image:linear-gradient(to bottom,#000 40%,transparent 90%);opacity:.5}.stamp{flex-shrink:0;font:700 7px/1 -apple-system,sans-serif;letter-spacing:.5px;padding:2px 5px;border-radius:1px;pointer-events:none;border:1.5px solid;text-transform:uppercase;transform:rotate(-2deg);opacity:.75}.stamp-new,.stamp-mod{color:#a07030;border-color:#a07030;background:#a0703014}.stamp-synced{color:#9090a0;border-color:#9090a0;background:#9090a00f;opacity:.5}.stamp-local{color:#8d7e67;border-color:#8d7e67;background:#8d7e6714}.stamp-transient{opacity:.92}.stamp-queued{color:#7da2d6;border-color:#7da2d6;background:#7da2d61f}.stamp-syncing{color:#67d5c1;border-color:#67d5c1;background:#67d5c124}.stamp-done{color:#74c98a;border-color:#74c98a;background:#74c98a1f}.stamp-failed{color:#e58a8a;border-color:#e58a8a;background:#e58a8a24}.card-unselectable{opacity:.88}.card-checkbox-disabled{opacity:.35}.stamp-overlay{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) rotate(-8deg);font:800 14px/1 -apple-system,Songti SC,"Noto Serif SC",serif;letter-spacing:2px;color:#b04040;border:2.5px solid #b04040;padding:4px 10px;border-radius:2px;opacity:.55;pointer-events:none;white-space:nowrap;text-transform:uppercase}.stamp-overlay.stamp-sealed{text-shadow:0 0 2px rgba(176,64,64,.3);box-shadow:inset 0 0 4px #b0404026}.card-sealed .card-content{filter:blur(2px);opacity:.4}.minimap{position:absolute;bottom:40px;right:12px;border-radius:6px;overflow:hidden;border:1px solid #2a2a36;box-shadow:0 4px 16px #0006;z-index:80;cursor:pointer;opacity:.85;transition:opacity .15s}.minimap:hover{opacity:1}.minimap-canvas{display:block;width:180px;height:120px}.viewport-root{flex:1;position:relative;overflow:hidden}.viewport{width:100%;height:100%;cursor:grab;-webkit-user-select:none;user-select:none;overflow:hidden}.viewport:active{cursor:grabbing}.canvas{position:absolute;top:0;left:0;transform-origin:0 0;will-change:transform}.section-label{position:absolute;font-size:13px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;opacity:.3;pointer-events:none;white-space:nowrap;display:flex;align-items:baseline;gap:8px}.section-count{font-size:11px;font-weight:500;opacity:.6}.zoom-indicator{position:absolute;bottom:8px;right:8px;font-size:11px;color:#555;background:#0a0a0fcc;padding:3px 8px;border-radius:4px;pointer-events:none;z-index:50}.viewport-controls{position:absolute;right:12px;top:12px;display:flex;flex-direction:column;gap:8px;z-index:60}.viewport-control-pad{display:flex;flex-direction:column;gap:6px}.viewport-control-pad__row{display:flex;gap:6px}.viewport-control{min-width:64px;height:34px;border:1px solid rgba(90,86,78,.24);border-radius:8px;background:#faf8f3eb;color:#3a342d;font:600 12px/1 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;cursor:pointer;box-shadow:0 8px 20px #2d28201f;transition:transform .12s ease,background .12s ease,border-color .12s ease}.viewport-control:hover{background:#fffcf5fa;border-color:#7a623066;transform:translateY(-1px)}.viewport-control:active{transform:translateY(0)}.viewport-control-fit{min-width:70px}.reading-panel-wrapper{flex:1;display:flex;overflow:hidden;background:#faf9f6;animation:rpFadeIn .3s ease-out}@keyframes rpFadeIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.reading-panel{flex:1;display:flex;flex-direction:column;max-width:800px;margin:0 auto;width:100%}.rp-header{flex-shrink:0;padding:24px 32px 16px;border-bottom:1px solid #e0ddd8;display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.rp-back{flex-shrink:0;width:32px;height:32px;border:none;border-radius:6px;background:#0000000f;color:#666;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s;margin-right:8px}.rp-back:hover{background:#0000001f;color:#333}.rp-title{font:700 24px/1.2 Georgia,"Noto Serif",serif;color:#2a2520;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rp-path{font:400 11px/1 -apple-system,sans-serif;color:#a09888;word-break:break-all;width:100%;padding-left:40px}.rp-body{flex:1;overflow-y:auto;padding:32px 32px 64px;font:400 16px/1.8 Georgia,"Noto Serif",serif;color:#3a3530;-webkit-font-smoothing:antialiased}.rp-h1{font-size:28px;font-weight:800;margin:36px 0 14px;color:#1a1510;line-height:1.3}.rp-h2{font-size:22px;font-weight:700;margin:32px 0 12px;color:#2a2520;line-height:1.3;border-bottom:1px solid #e8e4df;padding-bottom:8px}.rp-h3{font-size:18px;font-weight:700;margin:24px 0 8px;color:#3a3530;line-height:1.3}.rp-h4{font-size:16px;font-weight:700;margin:20px 0 6px;color:#4a4540;line-height:1.4}.rp-h5{font-size:15px;font-weight:700;margin:16px 0 4px;color:#5a5550}.rp-h6{font-size:13px;font-weight:600;margin:14px 0 4px;color:#6a6560;text-transform:uppercase;letter-spacing:.5px}.rp-p{margin:0 0 4px}.rp-spacer{height:14px}.rp-list{margin:4px 0 8px 24px;padding:0}.rp-list li{margin:4px 0}.rp-quote{margin:14px 0;padding:10px 20px;border-left:3px solid #c8b898;background:#c8b89814;color:#6a6050;font-style:italic}.rp-code-block{margin:14px 0;padding:16px 18px;background:#f0ede8;border-radius:6px;overflow-x:auto;font:400 13.5px/1.55 SF Mono,Fira Code,Consolas,monospace;color:#4a4540}.rp-inline-code{background:#ede9e3;padding:2px 6px;border-radius:3px;font:400 13.5px/1 SF Mono,Fira Code,Consolas,monospace;color:#6a5540}.rp-body a{color:#7c6a4f;text-decoration:underline;text-decoration-color:#7c6a4f4d;transition:text-decoration-color .15s}.rp-body a:hover{text-decoration-color:#7c6a4fcc}.rp-hr{border:none;border-top:1px solid #ddd8d0;margin:28px 0}.rp-body strong{font-weight:700;color:#2a2520}.rp-body em{font-style:italic}.rp-body del{text-decoration:line-through;opacity:.5}.rp-warning{max-width:640px;margin:0 auto 24px;border:1px solid #d7c49a;border-radius:10px;background:#fff9eb;padding:24px 26px;box-shadow:0 10px 30px #82641e14}.rp-warning__title{font:700 22px/1.25 Georgia,"Noto Serif",serif;color:#7a5a1f;margin-bottom:10px}.rp-warning__copy{margin:0 0 14px}.rp-warning__privacy{font:600 13px/1.4 -apple-system,sans-serif;color:#8a6421;margin-bottom:12px}.rp-warning__row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:8px 0;border-top:1px solid rgba(138,100,33,.14);font:600 13px/1.4 -apple-system,sans-serif}.rp-body::-webkit-scrollbar{width:6px}.rp-body::-webkit-scrollbar-track{background:transparent}.rp-body::-webkit-scrollbar-thumb{background:#d0ccc4;border-radius:3px}.rp-body::-webkit-scrollbar-thumb:hover{background:#b0aaa0}:root{--canvas-bg: #1a1a22;--chrome-bg: rgba(10, 10, 15, .92);--chrome-border: #2a2a36;--accent: #a78bfa;--setup-width: 340px}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}body{background:var(--canvas-bg);color:#e0e0f0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;height:100vh;overflow:hidden}#root{display:flex;flex-direction:column;height:100vh}.setup-sidebar{position:fixed;top:44px;left:0;bottom:32px;width:22px;z-index:120;transition:width .22s ease}.setup-sidebar:hover{width:var(--setup-width)}.setup-sidebar__rail{position:absolute;inset:0 auto 0 0;width:22px;display:flex;align-items:center;justify-content:center;writing-mode:vertical-rl;transform:rotate(180deg);background:#16161feb;border-right:1px solid var(--chrome-border);color:#9389c9;font-size:10px;letter-spacing:.18em;text-transform:uppercase}.setup-sidebar__panel{position:absolute;inset:0 auto 0 22px;width:calc(var(--setup-width) - 22px);padding:18px 16px 16px;background:linear-gradient(180deg,#101019fa,#0b0b12fa),radial-gradient(circle at top,rgba(167,139,250,.18),transparent 42%);border-right:1px solid var(--chrome-border);overflow-y:auto;transform:translate(calc(-100% - 22px));transition:transform .22s ease}.setup-sidebar:hover .setup-sidebar__panel{transform:translate(0)}.setup-sidebar__header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}.setup-sidebar__header h2{font-size:16px;color:#f3efff;margin-bottom:4px}.setup-sidebar__header p{color:#aaa2c8;font-size:12px;line-height:1.45}.setup-pill{border:1px solid #50476a;border-radius:999px;color:#d0c7f4;font-size:10px;padding:5px 9px;white-space:nowrap}.setup-pill--ok{border-color:#2d6f4b;color:#9ff0be}.setup-card{background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px;margin-bottom:12px;box-shadow:0 10px 30px #0000002e}.setup-card__title{color:#f1eaff;font-size:12px;font-weight:700;margin-bottom:8px;text-transform:uppercase;letter-spacing:.08em}.setup-copy{color:#c4bedc;font-size:12px;line-height:1.55}.setup-copy code{color:#fff;font-size:11px;word-break:break-all}.setup-steps{margin-left:16px;margin-bottom:8px;color:#d7d0ee;font-size:12px;line-height:1.6}.setup-field{display:flex;flex-direction:column;gap:5px;margin-bottom:10px}.setup-field span{color:#efe9ff;font-size:11px;font-weight:600}.setup-field input{background:#09090fbf;border:1px solid #3a3450;border-radius:10px;color:#f4f0ff;padding:10px 12px;font-size:12px;outline:none}.setup-field input:focus{border-color:var(--accent)}.setup-field small{color:#9087af;font-size:10px}.setup-save-btn{width:100%;border:1px solid #3d2f69;border-radius:10px;background:linear-gradient(135deg,#34255f,#5b43a0);color:#fff;font-weight:700;font-size:12px;padding:10px 12px;cursor:pointer}.setup-save-btn:disabled{opacity:.65;cursor:default}.setup-msg{margin-top:10px;font-size:11px;line-height:1.4}.setup-msg--ok{color:#8df0b2}.setup-msg--error{color:#ff9898}.hdr{height:44px;background:var(--chrome-bg);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid var(--chrome-border);display:flex;align-items:center;padding:0 16px;gap:12px;z-index:100;flex-shrink:0}.hdr-icon{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8d84b8}.hdr-title{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.3px}.hdr-title-system{color:#9ca3af}.hdr-back{font-size:12px;color:#888;cursor:pointer;padding:3px 8px;border-radius:4px;transition:background .15s,color .15s}.hdr-back:hover{background:#ffffff14;color:var(--accent)}.hdr-search{background:#16161f;border:1px solid var(--chrome-border);border-radius:6px;color:#8f8fb0;font-size:12px;padding:5px 10px;width:180px;outline:none}.hdr-spacer{flex:1}.hdr-meta{font-size:11px;color:#555}.hdr-meta b{color:#777;font-weight:500}.hdr-conn{font-size:11px}.conn-ok{color:#4ade80}.conn-off{color:#f59e0b}.ftr{height:32px;background:var(--chrome-bg);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-top:1px solid var(--chrome-border);display:flex;align-items:center;padding:0 16px;gap:16px;font-size:11px;color:#555;z-index:100;flex-shrink:0}.ftr b{color:#777;font-weight:500}.ftr-spacer{flex:1}.sync-btn{background:#2d1b69;color:var(--accent);border:1px solid #3b2a7a;border-radius:4px;padding:3px 12px;font-size:11px;font-weight:600;cursor:pointer}.sync-btn:hover{background:#3b2a7a}.sync-btn:disabled{opacity:.4;cursor:default}.ftr-system{color:#666;font-size:10px;cursor:pointer;padding:2px 8px;border-radius:4px;background:#ffffff0a;border:1px solid #2a2a36}.ftr-system:hover{background:#ffffff14;color:#888}.ftr-select-toggle{background:none;border:1px solid #333;border-radius:4px;color:#888;font-size:11px;padding:2px 8px;cursor:pointer}.ftr-select-toggle:hover{background:#ffffff0f;color:#bbb}.explore-btn{background:linear-gradient(135deg,#6d28d9,#a855f7);color:#fff;border:none;border-radius:4px;padding:3px 12px;font-size:11px;font-weight:600;cursor:pointer;text-decoration:none}.explore-btn:hover{opacity:.88;transform:translateY(-1px)}.explore-btn[aria-disabled=true]{opacity:.5;cursor:default;transform:none}.sync-result{color:#4ade80}.sync-error{color:#f87171}.sync-progress-dock{height:56px;background:#0d0d13f2;border-top:1px solid var(--chrome-border);border-bottom:1px solid var(--chrome-border);padding:10px 16px 8px;display:flex;flex-direction:column;gap:6px;z-index:100;flex-shrink:0}.sync-progress-top{display:flex;align-items:center;gap:12px;font-size:11px;color:#b8b8c8;flex-wrap:wrap}.sync-progress-title{color:#f0eff8;font-weight:700}.sync-progress-meta{color:#9a98ad}.sync-progress-track{width:100%;height:8px;border-radius:999px;background:#ffffff14;overflow:hidden}.sync-progress-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,#4d6cff,#68dfc4);transition:width .18s ease}.sync-progress-detail{color:#f3b2b2;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.empty-state{flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:14px}.selection-copy{color:#a78bfa}@media(max-width:900px){.setup-sidebar{width:18px}.setup-sidebar:hover{width:min(92vw,var(--setup-width))}.hdr-search{width:120px}.hdr-meta{display:none}}
|