claude-code-workflow 6.3.25 → 6.3.26

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 (66) hide show
  1. package/.claude/commands/issue/discover-by-prompt.md +764 -0
  2. package/ccw/dist/core/routes/help-routes.d.ts.map +1 -1
  3. package/ccw/dist/core/routes/help-routes.js +43 -7
  4. package/ccw/dist/core/routes/help-routes.js.map +1 -1
  5. package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -1
  6. package/ccw/dist/core/routes/litellm-api-routes.js +31 -5
  7. package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -1
  8. package/ccw/dist/core/routes/memory-routes.d.ts.map +1 -1
  9. package/ccw/dist/core/routes/memory-routes.js +73 -0
  10. package/ccw/dist/core/routes/memory-routes.js.map +1 -1
  11. package/ccw/dist/core/routes/status-routes.d.ts.map +1 -1
  12. package/ccw/dist/core/routes/status-routes.js +36 -4
  13. package/ccw/dist/core/routes/status-routes.js.map +1 -1
  14. package/ccw/dist/core/server.d.ts.map +1 -1
  15. package/ccw/dist/core/server.js +58 -0
  16. package/ccw/dist/core/server.js.map +1 -1
  17. package/ccw/dist/core/services/api-key-tester.d.ts.map +1 -1
  18. package/ccw/dist/core/services/api-key-tester.js +8 -3
  19. package/ccw/dist/core/services/api-key-tester.js.map +1 -1
  20. package/ccw/dist/tools/claude-cli-tools.d.ts +7 -0
  21. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
  22. package/ccw/dist/tools/claude-cli-tools.js +11 -1
  23. package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
  24. package/ccw/dist/tools/cli-executor-core.d.ts +11 -0
  25. package/ccw/dist/tools/cli-executor-core.d.ts.map +1 -1
  26. package/ccw/dist/tools/cli-executor-core.js +89 -2
  27. package/ccw/dist/tools/cli-executor-core.js.map +1 -1
  28. package/ccw/dist/tools/codex-lens.d.ts +2 -1
  29. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  30. package/ccw/dist/tools/codex-lens.js +51 -8
  31. package/ccw/dist/tools/codex-lens.js.map +1 -1
  32. package/ccw/dist/tools/index.d.ts.map +1 -1
  33. package/ccw/dist/tools/index.js +2 -0
  34. package/ccw/dist/tools/index.js.map +1 -1
  35. package/ccw/dist/tools/litellm-client.d.ts +6 -0
  36. package/ccw/dist/tools/litellm-client.d.ts.map +1 -1
  37. package/ccw/dist/tools/litellm-client.js +22 -1
  38. package/ccw/dist/tools/litellm-client.js.map +1 -1
  39. package/ccw/dist/tools/litellm-executor.js +2 -2
  40. package/ccw/dist/tools/litellm-executor.js.map +1 -1
  41. package/ccw/dist/tools/memory-update-queue.d.ts +172 -0
  42. package/ccw/dist/tools/memory-update-queue.d.ts.map +1 -0
  43. package/ccw/dist/tools/memory-update-queue.js +431 -0
  44. package/ccw/dist/tools/memory-update-queue.js.map +1 -0
  45. package/ccw/src/core/routes/help-routes.ts +46 -7
  46. package/ccw/src/core/routes/litellm-api-routes.ts +35 -4
  47. package/ccw/src/core/routes/memory-routes.ts +84 -0
  48. package/ccw/src/core/routes/status-routes.ts +39 -4
  49. package/ccw/src/core/server.ts +62 -0
  50. package/ccw/src/core/services/api-key-tester.ts +9 -3
  51. package/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css +45 -0
  52. package/ccw/src/templates/dashboard-js/components/cli-status.js +36 -5
  53. package/ccw/src/templates/dashboard-js/components/hook-manager.js +42 -81
  54. package/ccw/src/templates/dashboard-js/components/mcp-manager.js +170 -28
  55. package/ccw/src/templates/dashboard-js/components/notifications.js +14 -4
  56. package/ccw/src/templates/dashboard-js/i18n.js +26 -0
  57. package/ccw/src/templates/dashboard-js/views/cli-manager.js +72 -2
  58. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +11 -1
  59. package/ccw/src/tools/claude-cli-tools.ts +17 -1
  60. package/ccw/src/tools/cli-executor-core.ts +103 -2
  61. package/ccw/src/tools/codex-lens.ts +63 -8
  62. package/ccw/src/tools/index.ts +2 -0
  63. package/ccw/src/tools/litellm-client.ts +25 -3
  64. package/ccw/src/tools/litellm-executor.ts +2 -2
  65. package/ccw/src/tools/memory-update-queue.js +499 -0
  66. package/package.json +1 -1
@@ -7,6 +7,29 @@ import { join } from 'path';
7
7
  import { homedir } from 'os';
8
8
  import type { RouteContext } from './types.js';
9
9
 
10
+ /**
11
+ * Get the ccw-help index directory path (pure function)
12
+ * Priority: project path (.claude/skills/ccw-help/index) > user path (~/.claude/skills/ccw-help/index)
13
+ * @param projectPath - The project path to check first
14
+ */
15
+ function getIndexDir(projectPath: string | null): string | null {
16
+ // Try project path first
17
+ if (projectPath) {
18
+ const projectIndexDir = join(projectPath, '.claude', 'skills', 'ccw-help', 'index');
19
+ if (existsSync(projectIndexDir)) {
20
+ return projectIndexDir;
21
+ }
22
+ }
23
+
24
+ // Fall back to user path
25
+ const userIndexDir = join(homedir(), '.claude', 'skills', 'ccw-help', 'index');
26
+ if (existsSync(userIndexDir)) {
27
+ return userIndexDir;
28
+ }
29
+
30
+ return null;
31
+ }
32
+
10
33
  // ========== In-Memory Cache ==========
11
34
  interface CacheEntry {
12
35
  data: any;
@@ -61,14 +84,15 @@ let watchersInitialized = false;
61
84
 
62
85
  /**
63
86
  * Initialize file watchers for JSON indexes
87
+ * @param projectPath - The project path to resolve index directory
64
88
  */
65
- function initializeFileWatchers(): void {
89
+ function initializeFileWatchers(projectPath: string | null): void {
66
90
  if (watchersInitialized) return;
67
91
 
68
- const indexDir = join(homedir(), '.claude', 'skills', 'command-guide', 'index');
92
+ const indexDir = getIndexDir(projectPath);
69
93
 
70
- if (!existsSync(indexDir)) {
71
- console.warn(`Command guide index directory not found: ${indexDir}`);
94
+ if (!indexDir) {
95
+ console.warn(`ccw-help index directory not found in project or user paths`);
72
96
  return;
73
97
  }
74
98
 
@@ -152,15 +176,20 @@ function groupCommandsByCategory(commands: any[]): any {
152
176
  * @returns true if route was handled, false otherwise
153
177
  */
154
178
  export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
155
- const { pathname, url, req, res } = ctx;
179
+ const { pathname, url, req, res, initialPath } = ctx;
156
180
 
157
181
  // Initialize file watchers on first request
158
- initializeFileWatchers();
182
+ initializeFileWatchers(initialPath);
159
183
 
160
- const indexDir = join(homedir(), '.claude', 'skills', 'command-guide', 'index');
184
+ const indexDir = getIndexDir(initialPath);
161
185
 
162
186
  // API: Get all commands with optional search
163
187
  if (pathname === '/api/help/commands') {
188
+ if (!indexDir) {
189
+ res.writeHead(404, { 'Content-Type': 'application/json' });
190
+ res.end(JSON.stringify({ error: 'ccw-help index directory not found' }));
191
+ return true;
192
+ }
164
193
  const searchQuery = url.searchParams.get('q') || '';
165
194
  const filePath = join(indexDir, 'all-commands.json');
166
195
 
@@ -191,6 +220,11 @@ export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
191
220
 
192
221
  // API: Get workflow command relationships
193
222
  if (pathname === '/api/help/workflows') {
223
+ if (!indexDir) {
224
+ res.writeHead(404, { 'Content-Type': 'application/json' });
225
+ res.end(JSON.stringify({ error: 'ccw-help index directory not found' }));
226
+ return true;
227
+ }
194
228
  const filePath = join(indexDir, 'command-relationships.json');
195
229
  const relationships = getCachedData('command-relationships', filePath);
196
230
 
@@ -207,6 +241,11 @@ export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
207
241
 
208
242
  // API: Get commands by category
209
243
  if (pathname === '/api/help/commands/by-category') {
244
+ if (!indexDir) {
245
+ res.writeHead(404, { 'Content-Type': 'application/json' });
246
+ res.end(JSON.stringify({ error: 'ccw-help index directory not found' }));
247
+ return true;
248
+ }
210
249
  const filePath = join(indexDir, 'by-category.json');
211
250
  const byCategory = getCachedData('by-category', filePath);
212
251
 
@@ -334,12 +334,43 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
334
334
  return true;
335
335
  }
336
336
 
337
- // Test connection using litellm client
338
- const client = getLiteLLMClient();
339
- const available = await client.isAvailable();
337
+ // Get the API key to test (prefer first key from apiKeys array, fall back to default apiKey)
338
+ let apiKeyValue: string | null = null;
339
+ if (provider.apiKeys && provider.apiKeys.length > 0) {
340
+ apiKeyValue = provider.apiKeys[0].key;
341
+ } else if (provider.apiKey) {
342
+ apiKeyValue = provider.apiKey;
343
+ }
344
+
345
+ if (!apiKeyValue) {
346
+ res.writeHead(200, { 'Content-Type': 'application/json' });
347
+ res.end(JSON.stringify({ success: false, error: 'No API key configured for this provider' }));
348
+ return true;
349
+ }
350
+
351
+ // Resolve environment variables in the API key
352
+ const { resolveEnvVar } = await import('../../config/litellm-api-config-manager.js');
353
+ const resolvedKey = resolveEnvVar(apiKeyValue);
354
+
355
+ if (!resolvedKey) {
356
+ res.writeHead(200, { 'Content-Type': 'application/json' });
357
+ res.end(JSON.stringify({ success: false, error: 'API key is empty or environment variable not set' }));
358
+ return true;
359
+ }
360
+
361
+ // Determine API base URL
362
+ const apiBase = provider.apiBase || getDefaultApiBase(provider.type);
363
+
364
+ // Test the API key connection
365
+ const testResult = await testApiKeyConnection(provider.type, apiBase, resolvedKey);
340
366
 
341
367
  res.writeHead(200, { 'Content-Type': 'application/json' });
342
- res.end(JSON.stringify({ success: available, provider: provider.type }));
368
+ res.end(JSON.stringify({
369
+ success: testResult.valid,
370
+ provider: provider.type,
371
+ latencyMs: testResult.latencyMs,
372
+ error: testResult.error,
373
+ }));
343
374
  } catch (err) {
344
375
  res.writeHead(500, { 'Content-Type': 'application/json' });
345
376
  res.end(JSON.stringify({ success: false, error: (err as Error).message }));
@@ -1256,5 +1256,89 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
1256
1256
  return true;
1257
1257
  }
1258
1258
 
1259
+ // API: Memory Queue - Add path to queue
1260
+ if (pathname === '/api/memory/queue/add' && req.method === 'POST') {
1261
+ handlePostRequest(req, res, async (body) => {
1262
+ const { path: modulePath, tool = 'gemini', strategy = 'single-layer' } = body;
1263
+
1264
+ if (!modulePath) {
1265
+ return { error: 'path is required', status: 400 };
1266
+ }
1267
+
1268
+ try {
1269
+ const { memoryQueueTool } = await import('../../tools/memory-update-queue.js');
1270
+ const result = await memoryQueueTool.execute({
1271
+ action: 'add',
1272
+ path: modulePath,
1273
+ tool,
1274
+ strategy
1275
+ }) as { queueSize?: number; willFlush?: boolean; flushed?: boolean };
1276
+
1277
+ // Broadcast queue update event
1278
+ broadcastToClients({
1279
+ type: 'MEMORY_QUEUE_UPDATED',
1280
+ payload: {
1281
+ action: 'add',
1282
+ path: modulePath,
1283
+ queueSize: result.queueSize || 0,
1284
+ willFlush: result.willFlush || false,
1285
+ flushed: result.flushed || false,
1286
+ timestamp: new Date().toISOString()
1287
+ }
1288
+ });
1289
+
1290
+ return { success: true, ...result };
1291
+ } catch (error: unknown) {
1292
+ return { error: (error as Error).message, status: 500 };
1293
+ }
1294
+ });
1295
+ return true;
1296
+ }
1297
+
1298
+ // API: Memory Queue - Get queue status
1299
+ if (pathname === '/api/memory/queue/status' && req.method === 'GET') {
1300
+ try {
1301
+ const { memoryQueueTool } = await import('../../tools/memory-update-queue.js');
1302
+ const result = await memoryQueueTool.execute({ action: 'status' }) as Record<string, unknown>;
1303
+
1304
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1305
+ res.end(JSON.stringify({ success: true, ...result }));
1306
+ } catch (error: unknown) {
1307
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1308
+ res.end(JSON.stringify({ error: (error as Error).message }));
1309
+ }
1310
+ return true;
1311
+ }
1312
+
1313
+ // API: Memory Queue - Flush queue immediately
1314
+ if (pathname === '/api/memory/queue/flush' && req.method === 'POST') {
1315
+ handlePostRequest(req, res, async () => {
1316
+ try {
1317
+ const { memoryQueueTool } = await import('../../tools/memory-update-queue.js');
1318
+ const result = await memoryQueueTool.execute({ action: 'flush' }) as {
1319
+ processed?: number;
1320
+ success?: boolean;
1321
+ errors?: unknown[];
1322
+ };
1323
+
1324
+ // Broadcast queue flushed event
1325
+ broadcastToClients({
1326
+ type: 'MEMORY_QUEUE_FLUSHED',
1327
+ payload: {
1328
+ processed: result.processed || 0,
1329
+ success: result.success || false,
1330
+ errors: result.errors?.length || 0,
1331
+ timestamp: new Date().toISOString()
1332
+ }
1333
+ });
1334
+
1335
+ return { success: true, ...result };
1336
+ } catch (error: unknown) {
1337
+ return { error: (error as Error).message, status: 500 };
1338
+ }
1339
+ });
1340
+ return true;
1341
+ }
1342
+
1259
1343
  return false;
1260
1344
  }
@@ -9,6 +9,15 @@ import { getCliToolsStatus } from '../../tools/cli-executor.js';
9
9
  import { checkVenvStatus, checkSemanticStatus } from '../../tools/codex-lens.js';
10
10
  import type { RouteContext } from './types.js';
11
11
 
12
+ // Performance logging helper
13
+ const PERF_LOG_ENABLED = process.env.CCW_PERF_LOG === '1' || true; // Enable by default for debugging
14
+ function perfLog(label: string, startTime: number, extra?: Record<string, unknown>): void {
15
+ if (!PERF_LOG_ENABLED) return;
16
+ const duration = Date.now() - startTime;
17
+ const extraStr = extra ? ` | ${JSON.stringify(extra)}` : '';
18
+ console.log(`[PERF][Status] ${label}: ${duration}ms${extraStr}`);
19
+ }
20
+
12
21
  /**
13
22
  * Check CCW installation status
14
23
  * Verifies that required workflow files are installed in user's home directory
@@ -62,16 +71,39 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
62
71
 
63
72
  // API: Aggregated Status (all statuses in one call)
64
73
  if (pathname === '/api/status/all') {
74
+ const totalStart = Date.now();
75
+ console.log('[PERF][Status] === /api/status/all START ===');
76
+
65
77
  try {
66
78
  // Check CCW installation status (sync, fast)
79
+ const ccwStart = Date.now();
67
80
  const ccwInstallStatus = checkCcwInstallStatus();
81
+ perfLog('checkCcwInstallStatus', ccwStart);
82
+
83
+ // Execute all status checks in parallel with individual timing
84
+ const cliStart = Date.now();
85
+ const codexStart = Date.now();
86
+ const semanticStart = Date.now();
68
87
 
69
- // Execute all status checks in parallel
70
88
  const [cliStatus, codexLensStatus, semanticStatus] = await Promise.all([
71
- getCliToolsStatus(),
72
- checkVenvStatus(),
89
+ getCliToolsStatus().then(result => {
90
+ perfLog('getCliToolsStatus', cliStart, { toolCount: Object.keys(result).length });
91
+ return result;
92
+ }),
93
+ checkVenvStatus().then(result => {
94
+ perfLog('checkVenvStatus', codexStart, { ready: result.ready });
95
+ return result;
96
+ }),
73
97
  // Always check semantic status (will return available: false if CodexLens not ready)
74
- checkSemanticStatus().catch(() => ({ available: false, backend: null }))
98
+ checkSemanticStatus()
99
+ .then(result => {
100
+ perfLog('checkSemanticStatus', semanticStart, { available: result.available });
101
+ return result;
102
+ })
103
+ .catch(() => {
104
+ perfLog('checkSemanticStatus (error)', semanticStart);
105
+ return { available: false, backend: null };
106
+ })
75
107
  ]);
76
108
 
77
109
  const response = {
@@ -82,10 +114,13 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
82
114
  timestamp: new Date().toISOString()
83
115
  };
84
116
 
117
+ perfLog('=== /api/status/all TOTAL ===', totalStart);
118
+
85
119
  res.writeHead(200, { 'Content-Type': 'application/json' });
86
120
  res.end(JSON.stringify(response));
87
121
  return true;
88
122
  } catch (error) {
123
+ perfLog('=== /api/status/all ERROR ===', totalStart);
89
124
  console.error('[Status Routes] Error fetching aggregated status:', error);
90
125
  res.writeHead(500, { 'Content-Type': 'application/json' });
91
126
  res.end(JSON.stringify({ error: (error as Error).message }));
@@ -42,6 +42,10 @@ import { randomBytes } from 'crypto';
42
42
  // Import health check service
43
43
  import { getHealthCheckService } from './services/health-check-service.js';
44
44
 
45
+ // Import status check functions for warmup
46
+ import { checkSemanticStatus, checkVenvStatus } from '../tools/codex-lens.js';
47
+ import { getCliToolsStatus } from '../tools/cli-executor.js';
48
+
45
49
  import type { ServerConfig } from '../types/config.js';
46
50
  import type { PostRequestHandler } from './routes/types.js';
47
51
 
@@ -290,6 +294,56 @@ function setCsrfCookie(res: http.ServerResponse, token: string, maxAgeSeconds: n
290
294
  appendSetCookie(res, attributes.join('; '));
291
295
  }
292
296
 
297
+ /**
298
+ * Warmup function to pre-populate caches on server startup
299
+ * This runs asynchronously and non-blocking after the server starts
300
+ */
301
+ async function warmupCaches(initialPath: string): Promise<void> {
302
+ console.log('[WARMUP] Starting cache warmup...');
303
+ const startTime = Date.now();
304
+
305
+ // Run all warmup tasks in parallel for faster startup
306
+ const warmupTasks = [
307
+ // Warmup semantic status cache (Python process startup - can be slow first time)
308
+ (async () => {
309
+ const taskStart = Date.now();
310
+ try {
311
+ const semanticStatus = await checkSemanticStatus();
312
+ console.log(`[WARMUP] Semantic status: ${semanticStatus.available ? 'available' : 'not available'} (${Date.now() - taskStart}ms)`);
313
+ } catch (err) {
314
+ console.warn(`[WARMUP] Semantic status check failed: ${(err as Error).message}`);
315
+ }
316
+ })(),
317
+
318
+ // Warmup venv status cache
319
+ (async () => {
320
+ const taskStart = Date.now();
321
+ try {
322
+ const venvStatus = await checkVenvStatus();
323
+ console.log(`[WARMUP] Venv status: ${venvStatus.ready ? 'ready' : 'not ready'} (${Date.now() - taskStart}ms)`);
324
+ } catch (err) {
325
+ console.warn(`[WARMUP] Venv status check failed: ${(err as Error).message}`);
326
+ }
327
+ })(),
328
+
329
+ // Warmup CLI tools status cache
330
+ (async () => {
331
+ const taskStart = Date.now();
332
+ try {
333
+ const cliStatus = await getCliToolsStatus();
334
+ const availableCount = Object.values(cliStatus).filter(s => s.available).length;
335
+ const totalCount = Object.keys(cliStatus).length;
336
+ console.log(`[WARMUP] CLI tools status: ${availableCount}/${totalCount} available (${Date.now() - taskStart}ms)`);
337
+ } catch (err) {
338
+ console.warn(`[WARMUP] CLI tools status check failed: ${(err as Error).message}`);
339
+ }
340
+ })()
341
+ ];
342
+
343
+ await Promise.allSettled(warmupTasks);
344
+ console.log(`[WARMUP] Cache warmup complete (${Date.now() - startTime}ms total)`);
345
+ }
346
+
293
347
  /**
294
348
  * Generate dashboard HTML with embedded CSS and JS
295
349
  */
@@ -650,6 +704,14 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
650
704
  console.warn('[Server] Failed to start health check service:', err);
651
705
  }
652
706
 
707
+ // Start cache warmup asynchronously (non-blocking)
708
+ // Uses setImmediate to not delay server startup response
709
+ setImmediate(() => {
710
+ warmupCaches(initialPath).catch((err) => {
711
+ console.warn('[WARMUP] Cache warmup failed:', err);
712
+ });
713
+ });
714
+
653
715
  resolve(server);
654
716
  });
655
717
  server.on('error', reject);
@@ -72,6 +72,10 @@ export async function testApiKeyConnection(
72
72
  return { valid: false, error: urlValidation.error };
73
73
  }
74
74
 
75
+ // Normalize apiBase: remove trailing slashes to prevent URL construction issues
76
+ // e.g., "https://api.openai.com/v1/" -> "https://api.openai.com/v1"
77
+ const normalizedApiBase = apiBase.replace(/\/+$/, '');
78
+
75
79
  const controller = new AbortController();
76
80
  const timeoutId = setTimeout(() => controller.abort(), timeout);
77
81
  const startTime = Date.now();
@@ -80,7 +84,7 @@ export async function testApiKeyConnection(
80
84
  if (providerType === 'anthropic') {
81
85
  // Anthropic format: Use /v1/models endpoint (no cost, no model dependency)
82
86
  // This validates the API key without making a billable request
83
- const response = await fetch(`${apiBase}/models`, {
87
+ const response = await fetch(`${normalizedApiBase}/models`, {
84
88
  method: 'GET',
85
89
  headers: {
86
90
  'x-api-key': apiKey,
@@ -114,8 +118,10 @@ export async function testApiKeyConnection(
114
118
 
115
119
  return { valid: false, error: errorMessage };
116
120
  } else {
117
- // OpenAI-compatible format: GET /v1/models
118
- const modelsUrl = apiBase.endsWith('/v1') ? `${apiBase}/models` : `${apiBase}/v1/models`;
121
+ // OpenAI-compatible format: GET /v{N}/models
122
+ // Detect if URL already ends with a version pattern like /v1, /v2, /v4, etc.
123
+ const hasVersionSuffix = /\/v\d+$/.test(normalizedApiBase);
124
+ const modelsUrl = hasVersionSuffix ? `${normalizedApiBase}/models` : `${normalizedApiBase}/v1/models`;
119
125
  const response = await fetch(modelsUrl, {
120
126
  method: 'GET',
121
127
  headers: {
@@ -304,6 +304,51 @@
304
304
  margin-top: 0;
305
305
  }
306
306
 
307
+ /* Environment File Input Group */
308
+ .env-file-input-group {
309
+ display: flex;
310
+ flex-direction: column;
311
+ gap: 0.5rem;
312
+ }
313
+
314
+ .env-file-input-row {
315
+ display: flex;
316
+ gap: 0.5rem;
317
+ align-items: center;
318
+ }
319
+
320
+ .env-file-input-row .tool-config-input {
321
+ flex: 1;
322
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
323
+ font-size: 0.8125rem;
324
+ margin-top: 0;
325
+ }
326
+
327
+ .env-file-input-row .btn-sm {
328
+ flex-shrink: 0;
329
+ display: inline-flex;
330
+ align-items: center;
331
+ gap: 0.25rem;
332
+ padding: 0.5rem 0.75rem;
333
+ font-size: 0.8125rem;
334
+ white-space: nowrap;
335
+ }
336
+
337
+ .env-file-hint {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 0.375rem;
341
+ font-size: 0.75rem;
342
+ color: hsl(var(--muted-foreground));
343
+ margin: 0;
344
+ padding: 0;
345
+ }
346
+
347
+ .env-file-hint i {
348
+ flex-shrink: 0;
349
+ opacity: 0.7;
350
+ }
351
+
307
352
  .btn-ghost.text-destructive:hover {
308
353
  background: hsl(var(--destructive) / 0.1);
309
354
  }
@@ -33,11 +33,14 @@ function initCliStatus() {
33
33
  * Load all statuses using aggregated endpoint (single API call)
34
34
  */
35
35
  async function loadAllStatuses() {
36
+ const totalStart = performance.now();
37
+ console.log('[PERF][Frontend] loadAllStatuses START');
38
+
36
39
  // 1. 尝试从缓存获取(预加载的数据)
37
40
  if (window.cacheManager) {
38
41
  const cached = window.cacheManager.get('all-status');
39
42
  if (cached) {
40
- console.log('[CLI Status] Loaded all statuses from cache');
43
+ console.log(`[PERF][Frontend] Cache hit: ${(performance.now() - totalStart).toFixed(1)}ms`);
41
44
  // 应用缓存数据
42
45
  cliToolStatus = cached.cli || {};
43
46
  codexLensStatus = cached.codexLens || { ready: false };
@@ -45,25 +48,32 @@ async function loadAllStatuses() {
45
48
  ccwInstallStatus = cached.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
46
49
 
47
50
  // Load CLI tools config, API endpoints, and CLI Settings(这些有自己的缓存)
51
+ const configStart = performance.now();
48
52
  await Promise.all([
49
53
  loadCliToolsConfig(),
50
54
  loadApiEndpoints(),
51
55
  loadCliSettingsEndpoints()
52
56
  ]);
57
+ console.log(`[PERF][Frontend] Config/Endpoints load: ${(performance.now() - configStart).toFixed(1)}ms`);
53
58
 
54
59
  // Update badges
55
60
  updateCliBadge();
56
61
  updateCodexLensBadge();
57
62
  updateCcwInstallBadge();
63
+
64
+ console.log(`[PERF][Frontend] loadAllStatuses TOTAL (cached): ${(performance.now() - totalStart).toFixed(1)}ms`);
58
65
  return cached;
59
66
  }
60
67
  }
61
68
 
62
69
  // 2. 缓存未命中,从服务器获取
63
70
  try {
71
+ const fetchStart = performance.now();
72
+ console.log('[PERF][Frontend] Fetching /api/status/all...');
64
73
  const response = await fetch('/api/status/all');
65
74
  if (!response.ok) throw new Error('Failed to load status');
66
75
  const data = await response.json();
76
+ console.log(`[PERF][Frontend] /api/status/all fetch: ${(performance.now() - fetchStart).toFixed(1)}ms`);
67
77
 
68
78
  // 存入缓存
69
79
  if (window.cacheManager) {
@@ -77,10 +87,11 @@ async function loadAllStatuses() {
77
87
  ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
78
88
 
79
89
  // Load CLI tools config, API endpoints, and CLI Settings
80
- await Promise.all([
81
- loadCliToolsConfig(),
82
- loadApiEndpoints(),
83
- loadCliSettingsEndpoints()
90
+ const configStart = performance.now();
91
+ const [configResult, endpointsResult, settingsResult] = await Promise.all([
92
+ loadCliToolsConfig().then(r => { console.log(`[PERF][Frontend] loadCliToolsConfig: ${(performance.now() - configStart).toFixed(1)}ms`); return r; }),
93
+ loadApiEndpoints().then(r => { console.log(`[PERF][Frontend] loadApiEndpoints: ${(performance.now() - configStart).toFixed(1)}ms`); return r; }),
94
+ loadCliSettingsEndpoints().then(r => { console.log(`[PERF][Frontend] loadCliSettingsEndpoints: ${(performance.now() - configStart).toFixed(1)}ms`); return r; })
84
95
  ]);
85
96
 
86
97
  // Update badges
@@ -88,9 +99,11 @@ async function loadAllStatuses() {
88
99
  updateCodexLensBadge();
89
100
  updateCcwInstallBadge();
90
101
 
102
+ console.log(`[PERF][Frontend] loadAllStatuses TOTAL: ${(performance.now() - totalStart).toFixed(1)}ms`);
91
103
  return data;
92
104
  } catch (err) {
93
105
  console.error('Failed to load aggregated status:', err);
106
+ console.log(`[PERF][Frontend] loadAllStatuses ERROR after: ${(performance.now() - totalStart).toFixed(1)}ms`);
94
107
  // Fallback to individual calls if aggregated endpoint fails
95
108
  return await loadAllStatusesFallback();
96
109
  }
@@ -1034,6 +1047,15 @@ async function startCodexLensInstall() {
1034
1047
  progressBar.style.width = '100%';
1035
1048
  statusText.textContent = 'Installation complete!';
1036
1049
 
1050
+ // 清理缓存以确保刷新后获取最新状态
1051
+ if (window.cacheManager) {
1052
+ window.cacheManager.invalidate('all-status');
1053
+ window.cacheManager.invalidate('dashboard-init');
1054
+ }
1055
+ if (typeof window.invalidateCodexLensCache === 'function') {
1056
+ window.invalidateCodexLensCache();
1057
+ }
1058
+
1037
1059
  setTimeout(() => {
1038
1060
  closeCodexLensInstallWizard();
1039
1061
  showRefreshToast('CodexLens installed successfully!', 'success');
@@ -1184,6 +1206,15 @@ async function startCodexLensUninstall() {
1184
1206
  progressBar.style.width = '100%';
1185
1207
  statusText.textContent = 'Uninstallation complete!';
1186
1208
 
1209
+ // 清理缓存以确保刷新后获取最新状态
1210
+ if (window.cacheManager) {
1211
+ window.cacheManager.invalidate('all-status');
1212
+ window.cacheManager.invalidate('dashboard-init');
1213
+ }
1214
+ if (typeof window.invalidateCodexLensCache === 'function') {
1215
+ window.invalidateCodexLensCache();
1216
+ }
1217
+
1187
1218
  setTimeout(() => {
1188
1219
  closeCodexLensUninstallWizard();
1189
1220
  showRefreshToast('CodexLens uninstalled successfully!', 'success');