coding-tool-x 3.7.5 → 3.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/web/assets/{Analytics-DnnufrQi.js → Analytics-wCRLv26m.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-BTVhMSfe.js → ConfigTemplates-aa1blEHh.js} +1 -1
  3. package/dist/web/assets/{Home-D03PEREx.js → Home-eIxO50-k.js} +1 -1
  4. package/dist/web/assets/{PluginManager-BiMqEiyQ.js → PluginManager-BAwGFNjN.js} +1 -1
  5. package/dist/web/assets/ProjectList-CRiAEixW.js +1 -0
  6. package/dist/web/assets/ProjectList-Dw2omgOH.css +1 -0
  7. package/dist/web/assets/SessionList-BQ0FW6Gd.js +1 -0
  8. package/dist/web/assets/SessionList-kYJ6-Ies.css +1 -0
  9. package/dist/web/assets/{SkillManager-DAy8k5CL.js → SkillManager-Bhzs1O5S.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Bs2D6R9j.js → WorkspaceManager-CLdX-pTT.js} +1 -1
  11. package/dist/web/assets/index-B_nFqixU.css +1 -0
  12. package/dist/web/assets/index-ClMNA9Hf.js +2 -0
  13. package/dist/web/index.html +2 -2
  14. package/package.json +1 -1
  15. package/src/server/api/omp-channels.js +50 -7
  16. package/src/server/services/dashboard-snapshot-worker.js +1 -1
  17. package/src/server/services/native-oauth-adapters.js +2 -1
  18. package/src/server/services/omp-auth-provider-worker.js +29 -0
  19. package/src/server/services/omp-auth-providers.js +137 -11
  20. package/src/server/services/omp-config.js +7 -4
  21. package/src/server/services/omp-sessions.js +18 -14
  22. package/src/server/services/plugins-service.js +2 -1
  23. package/src/server/services/project-snapshots.js +8 -1
  24. package/src/server/services/session-snapshots.js +9 -1
  25. package/src/server/services/snapshot-cache.js +47 -3
  26. package/dist/web/assets/ProjectList-C6BA3V49.js +0 -1
  27. package/dist/web/assets/ProjectList-DQ5ZCJO2.css +0 -1
  28. package/dist/web/assets/SessionList--9Bl6EhV.css +0 -1
  29. package/dist/web/assets/SessionList-DChG04jz.js +0 -1
  30. package/dist/web/assets/index-DnDBHGCC.js +0 -2
  31. package/dist/web/assets/index-U3TOywtB.css +0 -1
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>coding-tool-x · AI 编程工作台</title>
8
- <script type="module" crossorigin src="/assets/index-DnDBHGCC.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-ClMNA9Hf.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-CVyvdmg1.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/icons-nGdSKQn2.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/vendors-DQA4Hw9v.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-U3TOywtB.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-B_nFqixU.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.7.5",
3
+ "version": "3.7.6",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -23,9 +23,13 @@ const {
23
23
  } = require('../services/model-detector');
24
24
  const {
25
25
  findAuthProviderForKey,
26
- getOmpAuthProviderSnapshot
26
+ getCachedOmpAuthProviderSnapshot,
27
+ getOmpAuthProviderCacheMeta,
28
+ warmOmpAuthProviderSnapshot
27
29
  } = require('../services/omp-auth-providers');
28
30
 
31
+ const CHANNEL_LIST_AUTH_OPTIONS = { accountCheck: false, includeStatus: false };
32
+
29
33
  function uniqueModels(models = []) {
30
34
  const seen = new Set();
31
35
  const result = [];
@@ -136,19 +140,53 @@ function attachAuthProvider(channel, snapshot) {
136
140
  };
137
141
  }
138
142
 
143
+ function getAuthSnapshotForChannelList() {
144
+ const snapshot = getCachedOmpAuthProviderSnapshot(CHANNEL_LIST_AUTH_OPTIONS);
145
+ const shouldWarm = !snapshot || snapshot.stale;
146
+ if (shouldWarm) {
147
+ warmOmpAuthProviderSnapshot(CHANNEL_LIST_AUTH_OPTIONS);
148
+ }
149
+ const meta = getOmpAuthProviderCacheMeta(CHANNEL_LIST_AUTH_OPTIONS);
150
+ return {
151
+ snapshot,
152
+ meta: {
153
+ ...meta,
154
+ stale: !snapshot || Boolean(snapshot.stale),
155
+ refreshing: shouldWarm || meta.refreshing,
156
+ fallback: !snapshot
157
+ }
158
+ };
159
+ }
160
+
161
+ function buildUnavailableAuthProviderMeta(error) {
162
+ return {
163
+ cached: false,
164
+ stale: false,
165
+ refreshing: false,
166
+ fallback: true,
167
+ checkedAt: new Date().toISOString(),
168
+ error: error || 'omp-not-available'
169
+ };
170
+ }
171
+
139
172
  module.exports = () => {
140
173
  router.get('/', (req, res) => {
141
174
  try {
142
175
  if (!isOmpInstalled()) {
143
- return res.json({ channels: [], installed: false, error: 'OMP CLI not installed' });
176
+ return res.json({
177
+ channels: [],
178
+ installed: false,
179
+ error: 'OMP CLI not installed',
180
+ authProviderMeta: buildUnavailableAuthProviderMeta('OMP CLI not installed')
181
+ });
144
182
  }
145
183
  const data = getChannels();
146
- const authSnapshot = getOmpAuthProviderSnapshot({ accountCheck: false, includeStatus: false });
184
+ const { snapshot: authSnapshot, meta: authProviderMeta } = getAuthSnapshotForChannelList();
147
185
  const channels = (data.channels || []).map(ch => ({
148
186
  ...attachAuthProvider(ch, authSnapshot),
149
187
  health: getChannelHealthStatus(ch.id, 'omp')
150
188
  }));
151
- res.json({ channels, installed: true });
189
+ res.json({ channels, installed: true, authProviderMeta });
152
190
  } catch (err) {
153
191
  console.error('[OMP Channels API] Failed to get channels:', err);
154
192
  res.status(500).json({ error: err.message });
@@ -158,14 +196,19 @@ module.exports = () => {
158
196
  router.get('/enabled', (req, res) => {
159
197
  try {
160
198
  if (!isOmpInstalled()) {
161
- return res.json({ channels: [], installed: false, error: 'OMP CLI not installed' });
199
+ return res.json({
200
+ channels: [],
201
+ installed: false,
202
+ error: 'OMP CLI not installed',
203
+ authProviderMeta: buildUnavailableAuthProviderMeta('OMP CLI not installed')
204
+ });
162
205
  }
163
206
  const data = getChannels();
164
- const authSnapshot = getOmpAuthProviderSnapshot({ accountCheck: false, includeStatus: false });
207
+ const { snapshot: authSnapshot, meta: authProviderMeta } = getAuthSnapshotForChannelList();
165
208
  const channels = (data.channels || [])
166
209
  .filter(ch => ch.enabled !== false)
167
210
  .map(ch => ({ ...attachAuthProvider(ch, authSnapshot), health: getChannelHealthStatus(ch.id, 'omp') }));
168
- res.json({ channels, installed: true });
211
+ res.json({ channels, installed: true, authProviderMeta });
169
212
  } catch (err) {
170
213
  console.error('[OMP Channels API] Failed to get enabled channels:', err);
171
214
  res.status(500).json({ error: err.message });
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { fork } = require('child_process');
4
4
 
5
- const DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS = 120 * 1000;
5
+ const DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS = 180 * 1000;
6
6
 
7
7
  function sortClaudeProjects(projects, order = []) {
8
8
  if (!Array.isArray(order) || order.length === 0) {
@@ -1029,7 +1029,8 @@ function runOmpCommand(args = [], options = {}) {
1029
1029
  ...process.env,
1030
1030
  ...(options.env || {})
1031
1031
  },
1032
- timeout: options.timeout || 10000
1032
+ timeout: options.timeout || 10000,
1033
+ windowsHide: true
1033
1034
  });
1034
1035
  const status = result?.status === undefined || result?.status === null ? 0 : result.status;
1035
1036
  return {
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const { getOmpAuthProviderSnapshot } = require('./omp-auth-providers');
4
+
5
+ function attachWorkerHandler() {
6
+ process.on('message', (options = {}) => {
7
+ try {
8
+ const value = getOmpAuthProviderSnapshot(options);
9
+ if (process.send) {
10
+ process.send({ ok: true, value }, () => process.exit(0));
11
+ return;
12
+ }
13
+ } catch (error) {
14
+ if (process.send) {
15
+ process.send({ ok: false, error: error?.message || String(error) }, () => process.exit(1));
16
+ return;
17
+ }
18
+ }
19
+ process.exit(1);
20
+ });
21
+ }
22
+
23
+ if (process.env.CC_TOOL_OMP_AUTH_PROVIDER_WORKER === '1' || require.main === module) {
24
+ attachWorkerHandler();
25
+ }
26
+
27
+ module.exports = {
28
+ attachWorkerHandler
29
+ };
@@ -1,9 +1,11 @@
1
- const { spawnSync } = require('child_process');
1
+ const path = require('path');
2
+ const { fork, spawnSync } = require('child_process');
2
3
  const ompConfig = require('./omp-config');
3
4
 
4
5
  const COMMAND_TIMEOUT_MS = 5000;
5
6
  const TOKEN_LIST_TIMEOUT_MS = 3000;
6
7
  const CACHE_TTL_MS = 15000;
8
+ const WARMUP_WORKER_TIMEOUT_MS = COMMAND_TIMEOUT_MS + 1000;
7
9
 
8
10
  const LOGIN_PROVIDER_IDS = new Set([
9
11
  'openai-codex',
@@ -50,6 +52,20 @@ const PROVIDER_ALIASES = {
50
52
  };
51
53
 
52
54
  let snapshotCache = {};
55
+ const warmupTimers = new Map();
56
+ const warmupErrors = new Map();
57
+
58
+ function getSnapshotCacheKey(options = {}) {
59
+ return options.accountCheck === false
60
+ ? (options.includeStatus === false ? 'metadata' : 'metadata-status')
61
+ : 'full';
62
+ }
63
+
64
+ function writeSnapshotCache(cacheKey, loadedAt, value) {
65
+ snapshotCache[cacheKey] = { loadedAt, value };
66
+ warmupErrors.delete(cacheKey);
67
+ return value;
68
+ }
53
69
 
54
70
  function normalizeProviderId(value = '') {
55
71
  return String(value || '')
@@ -95,7 +111,8 @@ function runCommand(command, args = [], options = {}) {
95
111
  ...process.env,
96
112
  ...(options.env || {})
97
113
  },
98
- timeout: options.timeout || COMMAND_TIMEOUT_MS
114
+ timeout: options.timeout || COMMAND_TIMEOUT_MS,
115
+ windowsHide: true
99
116
  });
100
117
  if (typeof result === 'string' || Buffer.isBuffer(result)) {
101
118
  return { status: 0, stdout: normalizeCommandOutput(result), stderr: '' };
@@ -205,9 +222,7 @@ function buildEmptySnapshot(runtime, reason) {
205
222
 
206
223
  function getOmpAuthProviderSnapshot(options = {}) {
207
224
  const now = Date.now();
208
- const cacheKey = options.accountCheck === false
209
- ? (options.includeStatus === false ? 'metadata' : 'metadata-status')
210
- : 'full';
225
+ const cacheKey = getSnapshotCacheKey(options);
211
226
  const cached = snapshotCache[cacheKey];
212
227
  if (!options.forceRefresh && cached && now - cached.loadedAt < CACHE_TTL_MS) {
213
228
  return cached.value;
@@ -217,16 +232,14 @@ function getOmpAuthProviderSnapshot(options = {}) {
217
232
  const runtime = options.runtime || ompConfig.resolveOmpRuntime(env, options.runtimeOptions || {});
218
233
  if (!runtime || runtime.runtime !== 'omp' || !runtime.installed) {
219
234
  const value = buildEmptySnapshot(runtime || null, 'omp-not-available');
220
- snapshotCache[cacheKey] = { loadedAt: now, value };
221
- return value;
235
+ return writeSnapshotCache(cacheKey, now, value);
222
236
  }
223
237
 
224
238
  const command = runtime.command;
225
239
  const listResult = parseJsonCommand(command, ['auth-broker', 'list', '--json'], options);
226
240
  if (!listResult.ok || !Array.isArray(listResult.value)) {
227
241
  const value = buildEmptySnapshot(runtime, listResult.error || listResult.stderr || 'auth-provider-list-failed');
228
- snapshotCache[cacheKey] = { loadedAt: now, value };
229
- return value;
242
+ return writeSnapshotCache(cacheKey, now, value);
230
243
  }
231
244
 
232
245
  const supportedProviders = listResult.value
@@ -268,8 +281,112 @@ function getOmpAuthProviderSnapshot(options = {}) {
268
281
  aliases: { ...PROVIDER_ALIASES },
269
282
  checkedAt: new Date().toISOString()
270
283
  };
271
- snapshotCache[cacheKey] = { loadedAt: now, value };
272
- return value;
284
+ return writeSnapshotCache(cacheKey, now, value);
285
+ }
286
+
287
+ function getCachedOmpAuthProviderSnapshot(options = {}) {
288
+ const cacheKey = getSnapshotCacheKey(options);
289
+ const cached = snapshotCache[cacheKey];
290
+ if (!cached) return null;
291
+ return {
292
+ ...cached.value,
293
+ stale: Date.now() - cached.loadedAt >= CACHE_TTL_MS
294
+ };
295
+ }
296
+
297
+ function getOmpAuthProviderCacheMeta(options = {}) {
298
+ const cacheKey = getSnapshotCacheKey(options);
299
+ const cached = snapshotCache[cacheKey];
300
+ const stale = !cached || Date.now() - cached.loadedAt >= CACHE_TTL_MS;
301
+ const cachedReason = cached?.value?.available === false ? cached.value.reason || null : null;
302
+ return {
303
+ cached: Boolean(cached),
304
+ stale,
305
+ refreshing: warmupTimers.has(cacheKey),
306
+ fallback: !cached,
307
+ checkedAt: cached?.value?.checkedAt || null,
308
+ error: warmupErrors.get(cacheKey) || cachedReason || null
309
+ };
310
+ }
311
+
312
+ function warmOmpAuthProviderSnapshot(options = {}) {
313
+ const cacheKey = getSnapshotCacheKey(options);
314
+ if (warmupTimers.has(cacheKey)) return;
315
+
316
+ const finishWarmup = (task, error = null, value = null) => {
317
+ if (!warmupTimers.has(cacheKey)) return;
318
+ if (task.timeout) clearTimeout(task.timeout);
319
+ if (task.child) {
320
+ task.child.removeAllListeners();
321
+ if (!task.child.killed) task.child.kill();
322
+ }
323
+ warmupTimers.delete(cacheKey);
324
+ if (error) {
325
+ warmupErrors.set(cacheKey, sanitizeIdentity(error?.message || String(error || 'auth-provider-warmup-failed')));
326
+ return;
327
+ }
328
+ if (value) {
329
+ writeSnapshotCache(cacheKey, Date.now(), value);
330
+ }
331
+ };
332
+
333
+ // Tests and callers that supply a function-based command runner cannot cross
334
+ // the process boundary. Production channel-list warmups use the worker path.
335
+ if (process.env.NODE_ENV === 'test' || options.commandRunner) {
336
+ const task = { timeout: null, child: null };
337
+ const timer = setTimeout(() => {
338
+ try {
339
+ finishWarmup(task, null, getOmpAuthProviderSnapshot(options));
340
+ } catch (error) {
341
+ finishWarmup(task, error);
342
+ }
343
+ }, 0);
344
+ task.timeout = timer;
345
+ warmupTimers.set(cacheKey, task);
346
+ return;
347
+ }
348
+
349
+ const workerPath = path.join(__dirname, 'omp-auth-provider-worker.js');
350
+ const task = { timeout: null, child: null };
351
+ try {
352
+ const child = fork(workerPath, [], {
353
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
354
+ env: {
355
+ ...process.env,
356
+ CC_TOOL_OMP_AUTH_PROVIDER_WORKER: '1'
357
+ }
358
+ });
359
+ task.child = child;
360
+ task.timeout = setTimeout(() => {
361
+ finishWarmup(task, new Error('auth-provider-warmup-timed-out'));
362
+ }, WARMUP_WORKER_TIMEOUT_MS);
363
+ warmupTimers.set(cacheKey, task);
364
+
365
+ child.on('message', (message) => {
366
+ if (message?.ok) {
367
+ finishWarmup(task, null, message.value);
368
+ } else {
369
+ finishWarmup(task, new Error(message?.error || 'auth-provider-warmup-failed'));
370
+ }
371
+ });
372
+ child.on('error', (error) => finishWarmup(task, error));
373
+ child.on('exit', (code, signal) => {
374
+ if (warmupTimers.get(cacheKey) === task) {
375
+ finishWarmup(task, new Error(`auth-provider-warmup-worker-exited-${code || signal || 'unknown'}`));
376
+ }
377
+ });
378
+ child.send({
379
+ accountCheck: options.accountCheck,
380
+ includeStatus: options.includeStatus,
381
+ forceRefresh: options.forceRefresh === true,
382
+ runtime: options.runtime,
383
+ runtimeOptions: options.runtimeOptions
384
+ });
385
+ } catch (error) {
386
+ // `fork` itself can fail before the task is registered. Preserve the
387
+ // failure for the non-blocking status endpoint instead of dropping it.
388
+ warmupErrors.set(cacheKey, sanitizeIdentity(error?.message || String(error || 'auth-provider-warmup-failed')));
389
+ }
273
390
  }
274
391
 
275
392
  function findAuthProviderForKey(providerKey, snapshot) {
@@ -280,6 +397,12 @@ function findAuthProviderForKey(providerKey, snapshot) {
280
397
 
281
398
  function clearOmpAuthProviderCache() {
282
399
  snapshotCache = {};
400
+ for (const task of warmupTimers.values()) {
401
+ if (task.timeout) clearTimeout(task.timeout);
402
+ if (task.child && !task.child.killed) task.child.kill();
403
+ }
404
+ warmupTimers.clear();
405
+ warmupErrors.clear();
283
406
  }
284
407
 
285
408
  module.exports = {
@@ -287,7 +410,10 @@ module.exports = {
287
410
  ACCOUNT_CHECK_PROVIDER_IDS,
288
411
  clearOmpAuthProviderCache,
289
412
  findAuthProviderForKey,
413
+ getCachedOmpAuthProviderSnapshot,
414
+ getOmpAuthProviderCacheMeta,
290
415
  getOmpAuthProviderSnapshot,
416
+ warmOmpAuthProviderSnapshot,
291
417
  normalizeProviderId,
292
418
  resolveProviderAlias,
293
419
  sanitizeIdentity
@@ -61,7 +61,10 @@ function runOmpCommand(command, args = [], env = process.env, options = {}, stdi
61
61
  encoding: 'utf8',
62
62
  env: buildCommandEnv(env),
63
63
  stdio,
64
- timeout: options.timeout || 3000
64
+ timeout: options.timeout || 3000,
65
+ // On Windows, an npm/PowerShell command shim can otherwise create a visible
66
+ // console every time we probe OMP or resolve its configuration directory.
67
+ windowsHide: true
65
68
  });
66
69
  }
67
70
 
@@ -76,8 +79,8 @@ function readOmpAgentDirFromCommand(command, env = process.env, options = {}) {
76
79
  }
77
80
 
78
81
  function getOmpAgentDir(env = process.env, options = {}) {
79
- const runtime = options.runtime || resolveOmpRuntime(env, options);
80
- if (runtime.runtime === 'omp' && runtime.installed) {
82
+ const runtime = options.runtime || (options.resolveRuntime === false ? null : resolveOmpRuntime(env, options));
83
+ if (runtime?.runtime === 'omp' && runtime.installed) {
81
84
  const commandAgentDir = readOmpAgentDirFromCommand(runtime.command, env, options);
82
85
  if (commandAgentDir) {
83
86
  return commandAgentDir;
@@ -87,7 +90,7 @@ function getOmpAgentDir(env = process.env, options = {}) {
87
90
  }
88
91
 
89
92
  function getOmpPaths(env = process.env, options = {}) {
90
- const runtime = options.runtime || resolveOmpRuntime(env, options);
93
+ const runtime = options.runtime || (options.resolveRuntime === false ? null : resolveOmpRuntime(env, options));
91
94
  const agentDir = getOmpAgentDir(env, { ...options, runtime });
92
95
  return {
93
96
  agentDir,
@@ -1,8 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { execFileSync } = require('child_process');
4
3
  const { PATHS, HOME_DIR } = require('../../config/paths');
5
- const { getOmpCommand, getOmpPaths, isOmpInstalled, resolveOmpRuntime } = require('./omp-config');
4
+ const { getOmpCommand, getOmpPaths, resolveOmpRuntime } = require('./omp-config');
6
5
 
7
6
  const PROJECT_ORDER_FILE = PATHS.ompProjectOrder;
8
7
  const SESSION_ORDER_FILE = PATHS.ompSessionOrder;
@@ -213,7 +212,20 @@ function parseSessionFile(filePath) {
213
212
  };
214
213
  }
215
214
 
216
- function scanSessionFiles(rootDir = getOmpPaths().sessions) {
215
+ function getOmpSessionPaths() {
216
+ // Reading historical sessions normally only needs OMP's native directory
217
+ // convention. Avoid starting the CLI here: project/count snapshot workers
218
+ // invoke this path independently, which made Windows repeatedly open
219
+ // PowerShell shims. Keep the CLI lookup as a fallback for non-standard OMP
220
+ // locations that cannot be derived from the environment.
221
+ const nativePaths = getOmpPaths(process.env, { resolveRuntime: false });
222
+ if (fs.existsSync(nativePaths.agentDir) || fs.existsSync(nativePaths.sessions)) {
223
+ return nativePaths;
224
+ }
225
+ return getOmpPaths();
226
+ }
227
+
228
+ function scanSessionFiles(rootDir = getOmpSessionPaths().sessions) {
217
229
  if (!fs.existsSync(rootDir)) {
218
230
  return [];
219
231
  }
@@ -462,20 +474,12 @@ function isOmpCliInstalled() {
462
474
  }
463
475
 
464
476
  let installed = false;
465
- if (isOmpInstalled()) {
477
+ const nativePaths = getOmpSessionPaths();
478
+ if (fs.existsSync(nativePaths.agentDir)) {
466
479
  installed = true;
467
480
  } else {
468
481
  const runtime = resolveOmpRuntime();
469
- if (runtime.installed) {
470
- installed = true;
471
- } else {
472
- try {
473
- execFileSync(getOmpCommand(), ['--version'], { stdio: 'ignore', timeout: 3000 });
474
- installed = true;
475
- } catch {
476
- installed = false;
477
- }
478
- }
482
+ installed = Boolean(runtime && runtime.installed);
479
483
  }
480
484
 
481
485
  ompInstallCache = {
@@ -833,7 +833,8 @@ class PluginsService {
833
833
  cwd: process.cwd(),
834
834
  encoding: 'utf8',
835
835
  stdio: 'pipe',
836
- timeout: 120000
836
+ timeout: 120000,
837
+ windowsHide: true
837
838
  });
838
839
  }
839
840
 
@@ -6,6 +6,8 @@ const {
6
6
  } = require('./snapshot-cache');
7
7
 
8
8
  const PROJECT_SNAPSHOT_TTL_MS = 60 * 1000;
9
+ const PROJECT_SNAPSHOT_WAIT_ON_MISS_MS = process.env.NODE_ENV === 'test' ? 0 : 1500;
10
+ const PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS = process.env.NODE_ENV === 'test' ? 0 : 1500;
9
11
 
10
12
  function projectListKey(source) {
11
13
  return `projects:list:${source}`;
@@ -33,7 +35,10 @@ async function getProjectListSnapshot(source, {
33
35
  fallbackValue,
34
36
  refresh,
35
37
  force,
36
- backgroundOnMiss: !force
38
+ backgroundOnMiss: !force,
39
+ staleWhileForce: true,
40
+ waitOnMissMs: PROJECT_SNAPSHOT_WAIT_ON_MISS_MS,
41
+ waitOnForceMs: PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS
37
42
  });
38
43
  }
39
44
 
@@ -44,6 +49,8 @@ function invalidateProjectSnapshots(source) {
44
49
 
45
50
  module.exports = {
46
51
  PROJECT_SNAPSHOT_TTL_MS,
52
+ PROJECT_SNAPSHOT_WAIT_ON_MISS_MS,
53
+ PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS,
47
54
  projectListKey,
48
55
  projectCountKey,
49
56
  emptyProjectList,
@@ -9,7 +9,9 @@ const {
9
9
 
10
10
  const SESSION_SNAPSHOT_TTL_MS = 60 * 1000;
11
11
  const SESSION_SNAPSHOT_DEFER_MS = process.env.NODE_ENV === 'test' ? 0 : 750;
12
- const SESSION_SNAPSHOT_WORKER_TIMEOUT_MS = 120 * 1000;
12
+ const SESSION_SNAPSHOT_WORKER_TIMEOUT_MS = 180 * 1000;
13
+ const SESSION_SNAPSHOT_WAIT_ON_MISS_MS = process.env.NODE_ENV === 'test' ? 0 : 2500;
14
+ const SESSION_SNAPSHOT_WAIT_ON_FORCE_MS = process.env.NODE_ENV === 'test' ? 0 : 2500;
13
15
 
14
16
  function sessionListKey(source, projectName) {
15
17
  return `sessions:list:${source}:${projectName}`;
@@ -113,6 +115,9 @@ async function getSessionListSnapshot(source, projectName, {
113
115
  refresh,
114
116
  force: process.env.NODE_ENV === 'test' ? true : force,
115
117
  backgroundOnMiss: process.env.NODE_ENV === 'test' ? false : !force,
118
+ staleWhileForce: true,
119
+ waitOnMissMs: SESSION_SNAPSHOT_WAIT_ON_MISS_MS,
120
+ waitOnForceMs: SESSION_SNAPSHOT_WAIT_ON_FORCE_MS,
116
121
  deferMs: SESSION_SNAPSHOT_DEFER_MS
117
122
  });
118
123
  }
@@ -128,6 +133,9 @@ function invalidateSessionSnapshots(source, projectName = null) {
128
133
 
129
134
  module.exports = {
130
135
  SESSION_SNAPSHOT_TTL_MS,
136
+ SESSION_SNAPSHOT_WAIT_ON_MISS_MS,
137
+ SESSION_SNAPSHOT_WAIT_ON_FORCE_MS,
138
+ SESSION_SNAPSHOT_WORKER_TIMEOUT_MS,
131
139
  defaultProjectInfo,
132
140
  emptySessionList,
133
141
  getSessionListSnapshot,
@@ -9,6 +9,7 @@ const DEFAULT_TTL_MS = 60 * 1000;
9
9
  const SNAPSHOT_VERSION = 1;
10
10
  const DEFAULT_BACKGROUND_DELAY_MS = process.env.NODE_ENV === 'test' ? 0 : 750;
11
11
  const DEFAULT_BACKGROUND_GAP_MS = process.env.NODE_ENV === 'test' ? 0 : 100;
12
+ const WAIT_TIMEOUT = Symbol('snapshot-wait-timeout');
12
13
 
13
14
  const snapshots = new Map();
14
15
  const inflight = new Map();
@@ -196,6 +197,28 @@ function isExpired(entry, ttlMs, nowMs) {
196
197
  return nowMs - Number(entry.updatedAtMs || 0) >= ttlMs;
197
198
  }
198
199
 
200
+ async function waitForRefresh(promise, waitMs) {
201
+ const timeoutMs = Number(waitMs) || 0;
202
+ if (timeoutMs <= 0) return { skipped: true };
203
+
204
+ let timer = null;
205
+ const result = await Promise.race([
206
+ promise
207
+ .then(entry => ({ entry }))
208
+ .catch(error => ({ error })),
209
+ new Promise(resolve => {
210
+ timer = setTimeout(() => resolve(WAIT_TIMEOUT), timeoutMs);
211
+ })
212
+ ]);
213
+ if (timer) {
214
+ clearTimeout(timer);
215
+ }
216
+ if (result === WAIT_TIMEOUT) {
217
+ return { timedOut: true };
218
+ }
219
+ return result;
220
+ }
221
+
199
222
  function makeMeta(entry, {
200
223
  key,
201
224
  stale,
@@ -220,7 +243,9 @@ async function getSnapshot(key, {
220
243
  force = false,
221
244
  staleWhileForce = false,
222
245
  nowMs = Date.now(),
223
- deferMs = DEFAULT_BACKGROUND_DELAY_MS
246
+ deferMs = DEFAULT_BACKGROUND_DELAY_MS,
247
+ waitOnMissMs = 0,
248
+ waitOnForceMs = 0
224
249
  } = {}) {
225
250
  if (typeof refresh !== 'function') {
226
251
  const entry = getEntry(key);
@@ -241,7 +266,26 @@ async function getSnapshot(key, {
241
266
  let refreshError = null;
242
267
 
243
268
  if (expired) {
244
- if (!entry && backgroundOnMiss === false) {
269
+ const coldWaitMs = !entry
270
+ ? (force ? (waitOnForceMs || waitOnMissMs) : waitOnMissMs)
271
+ : 0;
272
+
273
+ if (!entry && Number(coldWaitMs) > 0) {
274
+ refreshPromise = refreshSnapshot(key, refresh, { bypassInflight: force });
275
+ const waitResult = await waitForRefresh(refreshPromise, coldWaitMs);
276
+ if (waitResult.entry) {
277
+ entry = waitResult.entry;
278
+ expired = false;
279
+ refreshPromise = null;
280
+ } else if (waitResult.error) {
281
+ refreshError = waitResult.error?.message || String(waitResult.error);
282
+ entry = getEntry(key);
283
+ if (entry) {
284
+ expired = isExpired(entry, ttlMs, nowMs);
285
+ }
286
+ refreshPromise = null;
287
+ }
288
+ } else if (!entry && backgroundOnMiss === false) {
245
289
  try {
246
290
  entry = await refreshSnapshot(key, refresh, { bypassInflight: force });
247
291
  expired = false;
@@ -251,7 +295,7 @@ async function getSnapshot(key, {
251
295
  if (!entry) throw error;
252
296
  }
253
297
  } else {
254
- refreshPromise = refreshSnapshot(key, refresh, { defer: true, deferMs, bypassInflight: force });
298
+ refreshPromise = refreshSnapshot(key, refresh, { defer: !force, deferMs, bypassInflight: force });
255
299
  }
256
300
  }
257
301
 
@@ -1 +0,0 @@
1
- import{r as e,f as a,c as t,o as s,a as l,X as n,s as o,Y as r,N as c,_ as i,G as u,Z as d,p,a0 as v,W as m,a8 as f,a5 as h,w as y,h as g,K as _,F as k,$ as j,a6 as w,R as x,v as C}from"./vue-vendor-B5RqjEjQ.js";import{d as b}from"./vendors-DQA4Hw9v.js";import{_ as $,k as z,l as L,m as N,o as P,p as M,q as D}from"./index-DnDBHGCC.js";import{a9 as I,T,d as E,ad as K,h as U}from"./icons-nGdSKQn2.js";import{N as R,B as q,c as V,a as H,_ as O,q as S,m as A,g as B,x as F,h as G,p as W}from"./naive-ui-CVyvdmg1.js";import"./markdown-BNJfEgCA.js";const X={class:"card-content"},Y={class:"header-row"},Z={class:"project-name"},J={class:"stats-row"},Q=$({__name:"ProjectCard",props:{project:{type:Object,required:!0}},emits:["click","delete"],setup(f,{emit:h}){const y=f,g=h,_=e(!1);function k(){g("click")}function j(){g("delete",y.project)}const w=a(()=>{var e,a;return(null==(e=y.project)?void 0:e.displayName)||(null==(a=y.project)?void 0:a.name)||""}),x=a(()=>{var e,a,t;return(null==(e=y.project)?void 0:e.path)||(null==(a=y.project)?void 0:a.fullPath)||(null==(t=y.project)?void 0:t.name)||""}),C=a(()=>{var e;return(null==(e=y.project)?void 0:e.sessionCount)||0}),b=a(()=>{var e;return null==(e=y.project)?void 0:e.lastUsed}),$=a(()=>{var e;const a=["#18a058","#2080f0","#f0a020","#d03050","#9333ea","#06b6d4"];return a[((null==(e=y.project)?void 0:e.name)||"").split("").reduce((e,a)=>e+a.charCodeAt(0),0)%a.length]});function z(e){if(!e)return"";const a=new Date(e),t=new Date-a;return t<6e4?"刚刚":t<36e5?`${Math.floor(t/6e4)}分钟前`:t<864e5?`${Math.floor(t/36e5)}小时前`:t<6048e5?`${Math.floor(t/864e5)}天前`:a.toLocaleDateString("zh-CN")}return(e,a)=>(s(),t("div",{class:"project-card",onClick:k,onMouseenter:a[0]||(a[0]=e=>_.value=!0),onMouseleave:a[1]||(a[1]=e=>_.value=!1)},[l("div",X,[l("div",Y,[n(c(R),{size:"18",color:$.value},{default:r(()=>[n(c(I))]),_:1},8,["color"]),l("span",Z,i(w.value),1),o(n(c(q),{text:"",type:"error",size:"tiny",class:"delete-btn",onClick:d(j,["stop"])},{icon:r(()=>[n(c(R),{size:"14"},{default:r(()=>[n(c(T))]),_:1})]),_:1},512),[[u,_.value]])]),n(c(V),{depth:"3",class:"project-path"},{default:r(()=>[p(i(x.value),1)]),_:1}),l("div",J,[n(c(H),{size:"tiny",bordered:!1,type:"success"},{default:r(()=>[p(i(C.value)+" 会话 ",1)]),_:1}),b.value?(s(),v(c(V),{key:0,depth:"3",class:"last-used"},{default:r(()=>[p(i(z(b.value)),1)]),_:1})):m("",!0)])])],32))}},[["__scopeId","data-v-3a18e66d"]]),ee={class:"project-list-container"},ae={class:"header"},te={class:"header-text"},se={key:0,class:"loading-container"},le={key:3,class:"projects-grid"},ne={class:"search-hint"},oe={style:{"margin-bottom":"16px"}},re={key:0,style:{"max-height":"60vh","overflow-y":"auto"}},ce={class:"search-result-header"},ie={class:"search-result-title"},ue=$({__name:"ProjectList",setup(o){const u=f(),d=h(),$=z(),T=a(()=>d.meta.channel||"claude"),X=e(""),Y=e([]),Z=e(null),J=e(!1),ue=e(""),de=e(null),pe=e(!1),ve=e(null),me=a(()=>{const e=X.value.toLowerCase();return Y.value.filter(a=>{const t=(a.displayName||"").toLowerCase(),s=(a.fullPath||"").toLowerCase();return t.includes(e)||s.includes(e)})});function fe(e){const a=d.meta.channel||"claude";u.push({name:`${a}-sessions`,params:{projectName:e}})}async function he(){const e=Y.value.map(e=>e.name);await $.saveProjectOrder(e)}function ye(e){const a=e.fullPath||e.name,t=e.sessionCount||0;P.warning({title:"删除项目",content:`确定要删除项目 "${a}" 吗?\n\n当前项目包含 ${t} 个会话,删除后所有会话数据将无法恢复!`,positiveText:"确定删除",negativeText:"取消",onPositiveClick:async()=>{try{await $.deleteProject(e.name),N.success("项目已删除")}catch(a){N.error("删除失败: "+a.message)}}})}async function ge(){try{const a="codex"===(e=T.value||"claude")?"codex":"gemini"===e?"gemini":"opencode"===e?"opencode":"omp"===e?"omp":"claude",t=await L(a);if("manual"===(null==t?void 0:t.method))return void N.warning("自动复制失败,已弹出手动复制框");N.success("基础启动命令已复制到剪贴板")}catch(a){N.error("复制失败: "+a.message)}var e}async function _e(){if(ue.value){pe.value=!0;try{const e=await M(ue.value,35,T.value);de.value=e}catch(e){N.error("搜索失败: "+e.message)}finally{pe.value=!1}}}function ke(e,a){if(!a||!e)return e;const t=new RegExp(`(${a})`,"gi");return e.replace(t,'<mark style="background-color: #ffd700; padding: 2px 4px; border-radius: 2px; font-weight: 600;">$1</mark>')}function je(e){(e.metaKey||e.ctrlKey)&&"k"===e.key&&(e.preventDefault(),J.value=!0,C(()=>{var e;null==(e=ve.value)||e.focus()}))}return y(()=>$.projects,e=>{Y.value=[...e]},{immediate:!0}),y(J,e=>{e||(ue.value="",de.value=null)}),y(T,e=>{$.setChannel(e),$.fetchProjects()},{immediate:!0}),g(()=>{document.addEventListener("keydown",je)}),_(()=>{document.removeEventListener("keydown",je)}),(e,a)=>(s(),t("div",ee,[l("div",ae,[l("div",te,[n(c(O),{style:{margin:"0"}},{default:r(()=>[...a[4]||(a[4]=[p("我的项目",-1)])]),_:1}),n(c(V),{depth:"3"},{default:r(()=>[...a[5]||(a[5]=[p("选择一个项目查看会话,拖拽可调整顺序",-1)])]),_:1})]),n(c(A),null,{default:r(()=>[n(c(q),{type:"primary",size:"medium",onClick:ge},{icon:r(()=>[n(c(R),null,{default:r(()=>[n(c(E))]),_:1})]),default:r(()=>[a[6]||(a[6]=p(" 新建项目 ",-1))]),_:1}),n(c(S),{value:X.value,"onUpdate:value":a[0]||(a[0]=e=>X.value=e),placeholder:"搜索项目...",clearable:"",class:"search-input"},{prefix:r(()=>[n(c(R),null,{default:r(()=>[n(c(K))]),_:1})]),_:1},8,["value"])]),_:1})]),l("div",{class:"content",ref_key:"contentEl",ref:Z},[c($).loading?(s(),t("div",se,[n(c(B),{size:"large"},{description:r(()=>[...a[7]||(a[7]=[p(" 加载项目列表... ",-1)])]),_:1})])):c($).error?(s(),v(c(F),{key:1,type:"error",title:"加载失败",style:{"margin-top":"20px"}},{default:r(()=>[p(i(c($).error),1)]),_:1})):X.value?(s(),t("div",le,[(s(!0),t(k,null,j(me.value,e=>(s(),v(Q,{key:e.name,project:e,onClick:a=>fe(e.name),onDelete:ye},null,8,["project","onClick"]))),128))])):(s(),v(c(b),{key:2,modelValue:Y.value,"onUpdate:modelValue":a[1]||(a[1]=e=>Y.value=e),"item-key":"name",class:"projects-grid","ghost-class":"ghost","chosen-class":"chosen","drag-class":"drag",animation:"200",onEnd:he},{item:r(({element:e})=>[n(Q,{project:e,onClick:a=>fe(e.name),onDelete:ye},null,8,["project","onClick"])]),_:1},8,["modelValue"])),c($).loading||c($).error||0!==c($).projects.length?m("",!0):(s(),v(c(G),{key:4,description:"没有找到项目",style:{"margin-top":"60px"}},{icon:r(()=>[n(c(R),null,{default:r(()=>[n(c(I))]),_:1})]),_:1}))],512),l("div",ne,[n(c(V),{depth:"3",style:{"font-size":"12px"}},{default:r(()=>[...a[8]||(a[8]=[l("kbd",null,"Cmd",-1),p(" / ",-1),l("kbd",null,"Ctrl",-1),p(" + ",-1),l("kbd",null,"K",-1),p(" 全局搜索 ",-1)])]),_:1})]),n(c(W),{show:J.value,"onUpdate:show":a[3]||(a[3]=e=>J.value=e),preset:"card",title:"全局搜索",style:x([{width:"800px","max-width":"90vw"},{marginTop:"80px"}])},{default:r(()=>[l("div",oe,[n(c(S),{ref_key:"globalSearchInputRef",ref:ve,value:ue.value,"onUpdate:value":a[2]||(a[2]=e=>ue.value=e),placeholder:"搜索所有项目的对话内容...",clearable:"",onKeyup:w(_e,["enter"]),disabled:pe.value},{prefix:r(()=>[n(c(R),null,{default:r(()=>[n(c(K))]),_:1})]),suffix:r(()=>[n(c(q),{text:"",onClick:_e,disabled:!ue.value||pe.value,loading:pe.value},{default:r(()=>[...a[9]||(a[9]=[p(" 搜索 ",-1)])]),_:1},8,["disabled","loading"])]),_:1},8,["value","disabled"])]),de.value?(s(),t("div",re,[n(c(F),{type:"info",style:{"margin-bottom":"16px"}},{default:r(()=>[p(' 关键词 "'+i(de.value.keyword)+'" 共找到 '+i(de.value.totalMatches)+" 处匹配 ",1)]),_:1}),(s(!0),t(k,null,j(de.value.sessions,e=>(s(),t("div",{key:`${e.projectName}-${e.sessionId}`,class:"search-result-item"},[l("div",ce,[l("div",ie,[n(c(V),{strong:"",style:{"font-size":"15px","font-weight":"700"}},{default:r(()=>[p(i(e.projectDisplayName),1)]),_:2},1024),n(c(V),{depth:"2",style:{margin:"0 8px"}},{default:r(()=>[...a[10]||(a[10]=[p("·",-1)])]),_:1}),n(c(V),{strong:""},{default:r(()=>[p(i(e.alias?`${e.alias} (${e.sessionId.substring(0,8)})`:e.sessionId.substring(0,8)),1)]),_:2},1024),n(c(H),{size:"small",bordered:!1},{default:r(()=>[p(i(e.matchCount)+" 个匹配",1)]),_:2},1024)]),n(c(q),{size:"small",type:"primary",onClick:a=>async function(e,a){try{const{copyResult:t}=await D(e,a,T.value);"manual"===(null==t?void 0:t.method)?N.warning("自动复制失败,已弹出手动复制框"):N.success("启动命令已复制到剪贴板"),J.value=!1}catch(t){N.error("复制失败: "+t.message)}}(e.projectName,e.sessionId)},{icon:r(()=>[n(c(R),null,{default:r(()=>[n(c(U))]),_:1})]),default:r(()=>[a[11]||(a[11]=p(" 使用对话 ",-1))]),_:1},8,["onClick"])]),(s(!0),t(k,null,j(e.matches,(e,a)=>(s(),t("div",{key:a,class:"search-match"},[n(c(H),{size:"tiny",type:"user"===e.role?"info":"success",bordered:!1},{default:r(()=>[p(i("user"===e.role?"用户":"助手"),1)]),_:2},1032,["type"]),n(c(V),{depth:"3",class:"search-match-text",innerHTML:ke(e.context,de.value.keyword)},null,8,["innerHTML"])]))),128))]))),128)),0===de.value.sessions.length?(s(),v(c(G),{key:0,description:"没有找到匹配的内容"})):m("",!0)])):m("",!0)]),_:1},8,["show"])]))}},[["__scopeId","data-v-f9fdf45c"]]);export{ue as default};
@@ -1 +0,0 @@
1
- .project-card[data-v-3a18e66d]{padding:12px 14px;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1);border-radius:8px;border:1px solid var(--border-primary);box-shadow:0 1px 3px #0000000a;background:var(--bg-primary);position:relative;overflow:hidden}.project-card[data-v-3a18e66d]:before{content:"";position:absolute;top:0;left:0;width:3px;height:100%;background:linear-gradient(180deg,#18a058,#18a05866);opacity:0;transition:opacity .25s ease}.project-card[data-v-3a18e66d]:hover{transform:translateY(-3px);box-shadow:0 8px 20px #18a0581f,0 2px 6px #0000000f;border-color:#18a05866}.project-card[data-v-3a18e66d]:hover:before{opacity:1}.card-content[data-v-3a18e66d]{display:flex;flex-direction:column;gap:6px}.header-row[data-v-3a18e66d]{display:flex;align-items:center;gap:8px}.project-name[data-v-3a18e66d]{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;color:var(--text-primary);letter-spacing:-.2px}.delete-btn[data-v-3a18e66d]{opacity:.7;transition:opacity .2s;padding:2px!important}.delete-btn[data-v-3a18e66d]:hover{opacity:1}.project-path[data-v-3a18e66d]{font-size:11px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.3}.stats-row[data-v-3a18e66d]{display:flex;align-items:center;justify-content:space-between;margin-top:4px;padding-top:8px;border-top:1px dashed var(--border-primary)}.stats-row[data-v-3a18e66d] .n-tag{font-size:11px;font-weight:600;padding:0 6px;height:18px;border-radius:4px}.last-used[data-v-3a18e66d]{font-size:11px;font-weight:500}.project-list-container[data-v-f9fdf45c]{width:100%;height:100%;display:flex;flex-direction:column;box-sizing:border-box}.header[data-v-f9fdf45c]{flex-shrink:0;padding:24px 24px 20px;background:var(--gradient-bg);border-bottom:1px solid var(--border-primary);display:flex;justify-content:space-between;align-items:flex-start;gap:24px;position:relative}.header[data-v-f9fdf45c]:after{content:"";position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(24,160,88,.1),transparent)}.header-text[data-v-f9fdf45c]{flex:1}.header-text[data-v-f9fdf45c] .n-h2{font-size:26px;font-weight:700;background:linear-gradient(135deg,#1f2937,#374151);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:-.5px}[data-theme=dark] .header-text[data-v-f9fdf45c] .n-h2{background:linear-gradient(135deg,#f9fafb,#d1d5db);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.search-input[data-v-f9fdf45c]{width:320px;flex-shrink:0}.search-input[data-v-f9fdf45c] .n-input{border-radius:10px;background:var(--bg-primary);border:1px solid var(--border-primary);transition:all .2s ease}.search-input[data-v-f9fdf45c] .n-input:hover{border-color:var(--border-secondary);box-shadow:var(--shadow-sm)}.search-input[data-v-f9fdf45c] .n-input:focus-within{border-color:#18a058;box-shadow:0 0 0 3px #18a0581a}.content[data-v-f9fdf45c]{flex:1;overflow-y:auto;padding:20px 24px 24px;background:var(--gradient-bg)}.loading-container[data-v-f9fdf45c]{display:flex;justify-content:center;align-items:center;min-height:400px}.projects-grid[data-v-f9fdf45c]{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}.ghost[data-v-f9fdf45c]{opacity:.4}.chosen[data-v-f9fdf45c]{transform:scale(1.05);box-shadow:0 8px 24px #0003!important;cursor:move!important}.drag[data-v-f9fdf45c]{opacity:.8;transform:rotate(2deg);box-shadow:0 12px 32px #00000040!important}.search-hint[data-v-f9fdf45c]{position:fixed;bottom:24px;left:24px;padding:10px 14px;background:#ffffffe6;border:1px solid var(--border-primary);border-radius:8px;box-shadow:0 4px 12px #00000014;z-index:10;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:.8;transition:all .2s ease}[data-theme=dark] .search-hint[data-v-f9fdf45c]{background:#1e293bf2;border:1px solid rgba(71,85,105,.6);box-shadow:0 4px 16px #0006}.search-hint[data-v-f9fdf45c]:hover{opacity:1;transform:translateY(-2px);box-shadow:0 6px 16px #0000001f}[data-theme=dark] .search-hint[data-v-f9fdf45c]:hover{box-shadow:0 6px 20px #00000080}.search-hint kbd[data-v-f9fdf45c]{display:inline-block;padding:4px 8px;font-family:SF Mono,Monaco,monospace;font-size:12px;line-height:1.4;font-weight:600;color:var(--text-secondary);background:var(--bg-primary);border:1px solid var(--border-secondary);border-radius:4px;box-shadow:0 2px 0 var(--border-primary),0 1px 3px #0000001a}[data-theme=dark] .search-hint kbd[data-v-f9fdf45c]{background:#33415599;border:1px solid rgba(71,85,105,.8);box-shadow:0 2px #1e293bcc,0 1px 4px #0006;color:#e2e8f0}.search-result-item[data-v-f9fdf45c]{margin-bottom:16px;padding:12px;border:1px solid var(--border-primary);border-radius:6px;background:var(--bg-elevated)}.search-result-header[data-v-f9fdf45c]{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.search-result-title[data-v-f9fdf45c]{display:flex;align-items:center;gap:8px}.search-match[data-v-f9fdf45c]{display:flex;align-items:flex-start;gap:8px;margin-top:6px;padding:6px;background:var(--bg-secondary);border-radius:4px}.search-match-text[data-v-f9fdf45c]{flex:1;line-height:1.6}