codexmate 0.1.28 → 0.1.29

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 (41) hide show
  1. package/README.md +53 -44
  2. package/README.vi.md +45 -36
  3. package/README.zh.md +51 -42
  4. package/cli/session-usage.js +2 -2
  5. package/cli.js +369 -15
  6. package/package.json +1 -1
  7. package/web-ui/app.js +3 -0
  8. package/web-ui/index.html +3 -2
  9. package/web-ui/logic.sessions.mjs +6 -4
  10. package/web-ui/modules/app.computed.session.mjs +52 -33
  11. package/web-ui/modules/app.methods.agents.mjs +36 -0
  12. package/web-ui/modules/app.methods.codex-config.mjs +3 -1
  13. package/web-ui/modules/app.methods.providers.mjs +12 -4
  14. package/web-ui/modules/app.methods.session-actions.mjs +10 -8
  15. package/web-ui/modules/app.methods.session-browser.mjs +3 -1
  16. package/web-ui/modules/app.methods.session-trash.mjs +16 -16
  17. package/web-ui/modules/app.methods.theme.mjs +5 -4
  18. package/web-ui/modules/i18n/locales/en.mjs +74 -1
  19. package/web-ui/modules/i18n/locales/ja.mjs +74 -1
  20. package/web-ui/modules/i18n/locales/vi.mjs +74 -1
  21. package/web-ui/modules/i18n/locales/zh-tw.mjs +74 -1
  22. package/web-ui/modules/i18n/locales/zh.mjs +74 -1
  23. package/web-ui/modules/i18n.mjs +7 -0
  24. package/web-ui/partials/index/modal-config-template-agents.html +1 -1
  25. package/web-ui/partials/index/modal-health-check.html +1 -1
  26. package/web-ui/partials/index/modal-openclaw-config.html +50 -50
  27. package/web-ui/partials/index/panel-dashboard.html +1 -1
  28. package/web-ui/partials/index/panel-prompts.html +2 -2
  29. package/web-ui/partials/index/panel-sessions.html +45 -28
  30. package/web-ui/partials/index/panel-settings.html +6 -0
  31. package/web-ui/res/web-ui-render.precompiled.js +219 -156
  32. package/web-ui/session-helpers.mjs +3 -0
  33. package/web-ui/styles/base-theme.css +2 -2
  34. package/web-ui/styles/bridge-pool.css +1 -1
  35. package/web-ui/styles/layout-shell.css +0 -1
  36. package/web-ui/styles/sessions-preview.css +77 -0
  37. package/web-ui/styles/theme-dark-tokens.css +2 -2
  38. package/web-ui/styles/theme-dark.css +2 -2
  39. package/web-ui/styles/theme-ocean-tokens.css +88 -0
  40. package/web-ui/styles/theme-ocean.css +920 -0
  41. package/web-ui/styles.css +1 -0
package/cli.js CHANGED
@@ -7,6 +7,7 @@ const toml = require('@iarna/toml');
7
7
  const JSON5 = require('json5');
8
8
  const zipLib = require('zip-lib');
9
9
  const yauzl = require('yauzl');
10
+ const zlib = require('node:zlib');
10
11
  const { exec, execSync, execFileSync, spawn, spawnSync } = require('child_process');
11
12
  const http = require('http');
12
13
  const https = require('https');
@@ -264,6 +265,20 @@ const PI_SESSIONS_DIR = path.join(PI_DIR, 'agent', 'sessions');
264
265
  const OPENCODE_DATA_DIR = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'opencode');
265
266
  const OPENCODE_DB_VIRTUAL_ROOT = path.join(OPENCODE_DATA_DIR, 'sessions');
266
267
  const OPENCODE_DB_READ_LIMIT = 2000;
268
+ function resolveZedDataDir() {
269
+ if (process.env.ZED_DATA_DIR) return process.env.ZED_DATA_DIR;
270
+ if (process.platform === 'win32') {
271
+ return process.env.LOCALAPPDATA
272
+ ? path.join(process.env.LOCALAPPDATA, 'Zed')
273
+ : path.join(os.homedir(), 'AppData', 'Local', 'Zed');
274
+ }
275
+ if (process.platform === 'darwin') return path.join(os.homedir(), '.zed');
276
+ return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'zed');
277
+ }
278
+ const ZED_DATA_DIR = resolveZedDataDir();
279
+ const ZED_THREADS_DB_FILE = path.join(ZED_DATA_DIR, 'threads', 'threads.db');
280
+ const ZED_DB_VIRTUAL_ROOT = path.join(ZED_DATA_DIR, 'threads', 'sessions');
281
+ const ZED_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
267
282
  const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
268
283
  const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
269
284
  const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
@@ -772,7 +787,8 @@ let g_sessionFileLookupCache = {
772
787
  gemini: new Map(),
773
788
  codebuddy: new Map(),
774
789
  workbuddy: new Map(),
775
- pi: new Map()
790
+ pi: new Map(),
791
+ zed: new Map()
776
792
  };
777
793
  let g_exactMessageCountCache = new Map();
778
794
  let g_modelsCache = new Map();
@@ -1041,7 +1057,8 @@ function normalizeSidebarCollapsedPreference(value) {
1041
1057
  }
1042
1058
 
1043
1059
  function normalizeThemePreference(value) {
1044
- return value === 'dark' ? 'dark' : 'light';
1060
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
1061
+ return normalized === 'dark' || normalized === 'ocean' ? normalized : 'light';
1045
1062
  }
1046
1063
 
1047
1064
  function normalizeSessionFilterSourcePreference(value) {
@@ -2417,6 +2434,260 @@ function listOpencodeSessions(limit, options = {}) {
2417
2434
  }, []);
2418
2435
  }
2419
2436
 
2437
+
2438
+ function isZedThreadsDbAvailable() {
2439
+ const Database = getNodeSqliteDatabaseSync();
2440
+ return !!(Database && ZED_THREADS_DB_FILE && fs.existsSync(ZED_THREADS_DB_FILE));
2441
+ }
2442
+
2443
+ function withZedThreadsDb(callback, fallback = null) {
2444
+ const Database = getNodeSqliteDatabaseSync();
2445
+ if (!Database || !ZED_THREADS_DB_FILE || !fs.existsSync(ZED_THREADS_DB_FILE)) {
2446
+ return fallback;
2447
+ }
2448
+ let db;
2449
+ try {
2450
+ db = new Database(ZED_THREADS_DB_FILE, { readOnly: true });
2451
+ return callback(db);
2452
+ } catch (_) {
2453
+ return fallback;
2454
+ } finally {
2455
+ if (db) {
2456
+ try { db.close(); } catch (_) { }
2457
+ }
2458
+ }
2459
+ }
2460
+
2461
+ function buildZedVirtualSessionPath(sessionId) {
2462
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2463
+ if (!id) return '';
2464
+ return path.join(ZED_DB_VIRTUAL_ROOT, `${id}.zed.jsonl`);
2465
+ }
2466
+
2467
+ function parseZedVirtualSessionId(filePath) {
2468
+ const raw = typeof filePath === 'string' ? filePath.trim() : '';
2469
+ if (!raw) return '';
2470
+ const base = path.basename(raw, path.extname(raw));
2471
+ return ZED_SESSION_ID_RE.test(base) ? base.toLowerCase() : '';
2472
+ }
2473
+
2474
+ /* Zed 会话文档存于 threads.db 的 threads.data 列(zstd 压缩 JSON)。
2475
+ * 版本 0.3.0 结构:{ title, messages: [{User:{content:[{Text}]}} | {Agent:{content:[{Text}|{Thinking:{text}}], tool_results, reasoning_details}}],
2476
+ * model: {provider, model}, cumulative_token_usage, initial_project_snapshot } */
2477
+ function parseZedThreadDocument(row) {
2478
+ if (!row || typeof row !== 'object') return null;
2479
+ const dataType = typeof row.data_type === 'string' ? row.data_type.trim().toLowerCase() : '';
2480
+ let text = '';
2481
+ try {
2482
+ if (dataType === 'zstd') {
2483
+ text = zlib.zstdDecompressSync(Buffer.from(row.data)).toString('utf8');
2484
+ } else if (!dataType || dataType === 'json' || dataType === 'utf8' || dataType === 'text') {
2485
+ text = Buffer.from(row.data).toString('utf8');
2486
+ } else {
2487
+ return null;
2488
+ }
2489
+ const parsed = JSON.parse(text);
2490
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
2491
+ } catch (_) {
2492
+ return null;
2493
+ }
2494
+ }
2495
+
2496
+ function extractZedTextFromContentBlocks(blocks) {
2497
+ if (!Array.isArray(blocks)) return '';
2498
+ const parts = [];
2499
+ for (const block of blocks) {
2500
+ if (!block || typeof block !== 'object') continue;
2501
+ if (typeof block.Text === 'string' && block.Text.trim()) {
2502
+ parts.push(block.Text.trim());
2503
+ } else if (block.Thinking && typeof block.Thinking === 'object'
2504
+ && typeof block.Thinking.text === 'string' && block.Thinking.text.trim()) {
2505
+ parts.push(block.Thinking.text.trim());
2506
+ }
2507
+ }
2508
+ return parts.join('\n').trim();
2509
+ }
2510
+
2511
+ function readZedThreadModelInfo(doc) {
2512
+ const model = doc && doc.model && typeof doc.model === 'object' ? doc.model : null;
2513
+ return {
2514
+ model: model && typeof model.model === 'string' ? model.model.trim() : '',
2515
+ provider: model && typeof model.provider === 'string' && model.provider.trim() ? model.provider.trim() : 'zed'
2516
+ };
2517
+ }
2518
+
2519
+ function normalizeZedThreadMessage(entry, modelInfo) {
2520
+ if (!entry || typeof entry !== 'object') return null;
2521
+ if (entry.User && typeof entry.User === 'object') {
2522
+ const text = extractZedTextFromContentBlocks(entry.User.content);
2523
+ if (!text) return null;
2524
+ return { role: 'user', text, timestamp: '', model: modelInfo.model, provider: modelInfo.provider };
2525
+ }
2526
+ if (entry.Agent && typeof entry.Agent === 'object') {
2527
+ const text = extractZedTextFromContentBlocks(entry.Agent.content);
2528
+ if (!text) return null;
2529
+ return { role: 'assistant', text, timestamp: '', model: modelInfo.model, provider: modelInfo.provider };
2530
+ }
2531
+ return null;
2532
+ }
2533
+
2534
+ function countZedThreadMessages(doc, modelInfo) {
2535
+ const rawMessages = doc && Array.isArray(doc.messages) ? doc.messages : [];
2536
+ return rawMessages.map(entry => normalizeZedThreadMessage(entry, modelInfo)).filter(Boolean).length;
2537
+ }
2538
+
2539
+ function readZedThreadUsage(doc) {
2540
+ const usage = doc && doc.cumulative_token_usage && typeof doc.cumulative_token_usage === 'object'
2541
+ ? doc.cumulative_token_usage
2542
+ : {};
2543
+ const inputTokens = readNonNegativeInteger(usage.input_tokens) || 0;
2544
+ const cachedInputTokens = readNonNegativeInteger(usage.cache_read_input_tokens) || 0;
2545
+ const cacheCreationInputTokens = readNonNegativeInteger(usage.cache_creation_input_tokens) || 0;
2546
+ const outputTokens = readNonNegativeInteger(usage.output_tokens) || 0;
2547
+ const reasoningOutputTokens = readNonNegativeInteger(usage.reasoning_output_tokens) || 0;
2548
+ return {
2549
+ inputTokens,
2550
+ cachedInputTokens,
2551
+ cacheCreationInputTokens,
2552
+ outputTokens,
2553
+ reasoningOutputTokens,
2554
+ totalTokens: inputTokens + outputTokens + reasoningOutputTokens + cachedInputTokens + cacheCreationInputTokens
2555
+ };
2556
+ }
2557
+
2558
+ function readZedThreadCwd(row, doc) {
2559
+ const raw = typeof row.folder_paths === 'string' ? row.folder_paths.trim() : '';
2560
+ if (raw && !raw.startsWith('[')) {
2561
+ return raw;
2562
+ }
2563
+ if (raw.startsWith('[')) {
2564
+ try {
2565
+ const parsed = JSON.parse(raw);
2566
+ const first = Array.isArray(parsed) ? parsed.find(item => typeof item === 'string' && item.trim()) : null;
2567
+ if (first) return first;
2568
+ } catch (_) { }
2569
+ }
2570
+ const snapshot = doc && doc.initial_project_snapshot && Array.isArray(doc.initial_project_snapshot.worktree_snapshots)
2571
+ ? doc.initial_project_snapshot.worktree_snapshots.find(item => item && typeof item.worktree_path === 'string' && item.worktree_path.trim())
2572
+ : null;
2573
+ return snapshot ? snapshot.worktree_path : '';
2574
+ }
2575
+
2576
+ function queryZedSessionMessages(sessionId, options = {}) {
2577
+ const id = typeof sessionId === 'string' ? sessionId.trim().toLowerCase() : '';
2578
+ if (!id) return null;
2579
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, DEFAULT_SESSION_DETAIL_MESSAGES);
2580
+ return withZedThreadsDb((db) => {
2581
+ const row = db.prepare('select data, data_type, folder_paths, updated_at from threads where id = ?').get(id) || null;
2582
+ const doc = parseZedThreadDocument(row);
2583
+ const modelInfo = readZedThreadModelInfo(doc);
2584
+ const all = (doc && Array.isArray(doc.messages) ? doc.messages : [])
2585
+ .map(entry => normalizeZedThreadMessage(entry, modelInfo))
2586
+ .filter(Boolean);
2587
+ const totalMessages = all.length;
2588
+ const limit = maxMessages === Infinity
2589
+ ? totalMessages
2590
+ : Math.min(totalMessages, Math.max(1, Math.floor(maxMessages)));
2591
+ const messages = limit < totalMessages ? all.slice(totalMessages - limit) : all;
2592
+ return {
2593
+ sessionId: id,
2594
+ cwd: doc ? readZedThreadCwd(row, doc) : '',
2595
+ updatedAt: row ? toIsoTime(row.updated_at, '') : '',
2596
+ messages,
2597
+ totalMessages,
2598
+ clipped: maxMessages !== Infinity && totalMessages > messages.length,
2599
+ truncated: maxMessages !== Infinity && totalMessages > messages.length
2600
+ };
2601
+ }, null);
2602
+ }
2603
+
2604
+ function countZedMessages(sessionId) {
2605
+ const id = typeof sessionId === 'string' ? sessionId.trim().toLowerCase() : '';
2606
+ if (!id) return null;
2607
+ return withZedThreadsDb((db) => {
2608
+ const row = db.prepare('select data, data_type from threads where id = ?').get(id) || null;
2609
+ const doc = parseZedThreadDocument(row);
2610
+ if (!doc) return null;
2611
+ return countZedThreadMessages(doc, readZedThreadModelInfo(doc));
2612
+ }, null);
2613
+ }
2614
+
2615
+ function parseZedSessionSummaryRow(row) {
2616
+ if (!row || typeof row !== 'object') return null;
2617
+ const sessionId = typeof row.id === 'string' ? row.id.trim().toLowerCase() : '';
2618
+ if (!sessionId) return null;
2619
+ const doc = parseZedThreadDocument(row);
2620
+ if (!doc) return null;
2621
+ const modelInfo = readZedThreadModelInfo(doc);
2622
+ const usage = readZedThreadUsage(doc);
2623
+ let title = typeof row.summary === 'string' ? row.summary.trim() : '';
2624
+ if (!title && typeof doc.title === 'string') title = doc.title.trim();
2625
+ if (!title) {
2626
+ const firstUser = (Array.isArray(doc.messages) ? doc.messages : [])
2627
+ .map(entry => normalizeZedThreadMessage(entry, modelInfo))
2628
+ .find(item => item && item.role === 'user' && item.text);
2629
+ if (firstUser) title = truncateText(firstUser.text, 120);
2630
+ }
2631
+ if (!title) title = sessionId;
2632
+ return {
2633
+ source: 'zed',
2634
+ sourceLabel: 'Zed',
2635
+ provider: modelInfo.provider,
2636
+ model: modelInfo.model,
2637
+ models: modelInfo.model ? [modelInfo.model] : [],
2638
+ sessionId,
2639
+ title,
2640
+ cwd: readZedThreadCwd(row, doc),
2641
+ createdAt: toIsoTime(row.created_at, ''),
2642
+ updatedAt: toIsoTime(row.updated_at, ''),
2643
+ messageCount: countZedThreadMessages(doc, modelInfo),
2644
+ totalTokens: usage.totalTokens,
2645
+ contextWindow: 0,
2646
+ inputTokens: usage.inputTokens,
2647
+ cachedInputTokens: usage.cachedInputTokens,
2648
+ cacheCreationInputTokens: usage.cacheCreationInputTokens,
2649
+ outputTokens: usage.outputTokens,
2650
+ reasoningOutputTokens: usage.reasoningOutputTokens,
2651
+ __messageCountExact: true,
2652
+ filePath: buildZedVirtualSessionPath(sessionId),
2653
+ keywords: [],
2654
+ capabilities: { code: true }
2655
+ };
2656
+ }
2657
+
2658
+ function parseZedSessionSummary(sessionId) {
2659
+ const id = typeof sessionId === 'string' ? sessionId.trim().toLowerCase() : '';
2660
+ if (!id) return null;
2661
+ return withZedThreadsDb((db) => {
2662
+ const row = db.prepare(`
2663
+ select id, summary, folder_paths, created_at, updated_at, data, data_type
2664
+ from threads where id = ?
2665
+ `).get(id) || null;
2666
+ return parseZedSessionSummaryRow(row);
2667
+ }, null);
2668
+ }
2669
+
2670
+ function listZedSessions(limit, options = {}) {
2671
+ if (!isZedThreadsDbAvailable()) return [];
2672
+ const scanFactor = Number.isFinite(Number(options.scanFactor)) ? Math.max(1, Number(options.scanFactor)) : SESSION_SCAN_FACTOR;
2673
+ const targetCount = Number.isFinite(Number(options.targetCount)) ? Math.max(1, Math.floor(Number(options.targetCount))) : Math.max(1, Math.floor(limit * scanFactor));
2674
+ const scanCount = Number.isFinite(Number(options.scanCount)) ? Math.max(targetCount, Math.floor(Number(options.scanCount))) : Math.max(targetCount, Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR));
2675
+ return withZedThreadsDb((db) => {
2676
+ const rows = db.prepare(`
2677
+ select id, summary, folder_paths, created_at, updated_at, data, data_type
2678
+ from threads
2679
+ order by updated_at desc
2680
+ limit ?
2681
+ `).all(Math.min(OPENCODE_DB_READ_LIMIT, Math.max(scanCount, targetCount)));
2682
+ const sessions = [];
2683
+ for (const row of rows) {
2684
+ const summary = parseZedSessionSummaryRow(row);
2685
+ if (summary) sessions.push(summary);
2686
+ if (sessions.length >= targetCount) break;
2687
+ }
2688
+ return mergeAndLimitSessions(sessions, limit);
2689
+ }, []);
2690
+ }
2420
2691
  function getCodexmateDerivedSessionsRoot(target) {
2421
2692
  if (target === 'claude') {
2422
2693
  return CODEXMATE_DERIVED_CLAUDE_DIR;
@@ -4689,7 +4960,7 @@ async function hydrateSessionItemsExactMessageCount(items) {
4689
4960
 
4690
4961
  function normalizeSessionSourceName(value, fallback = '') {
4691
4962
  const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
4692
- if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode') {
4963
+ if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode' || source === 'zed') {
4693
4964
  return source;
4694
4965
  }
4695
4966
  return fallback;
@@ -4707,6 +4978,7 @@ function getSessionSourceLabel(source) {
4707
4978
  if (source === 'workbuddy') return 'WorkBuddy AI';
4708
4979
  if (source === 'pi') return 'Pi';
4709
4980
  if (source === 'opencode') return 'OpenCode';
4981
+ if (source === 'zed') return 'Zed';
4710
4982
  return 'Codex';
4711
4983
  }
4712
4984
 
@@ -4717,6 +4989,7 @@ function getSessionSourceProvider(source) {
4717
4989
  if (source === 'workbuddy') return 'workbuddy';
4718
4990
  if (source === 'pi') return 'pi';
4719
4991
  if (source === 'opencode') return 'opencode';
4992
+ if (source === 'zed') return 'zed';
4720
4993
  return 'codex';
4721
4994
  }
4722
4995
 
@@ -4746,6 +5019,9 @@ async function readSessionMessageCounts(params = {}) {
4746
5019
  if (source === 'opencode') {
4747
5020
  const opencodeSessionId = item.sessionId || parseOpencodeVirtualSessionId(filePath);
4748
5021
  exactMessageCount = countOpencodeMessages(opencodeSessionId);
5022
+ } else if (source === 'zed') {
5023
+ const zedSessionId = item.sessionId || parseZedVirtualSessionId(filePath);
5024
+ exactMessageCount = countZedMessages(zedSessionId);
4749
5025
  } else {
4750
5026
  if (!filePath || !fs.existsSync(filePath)) {
4751
5027
  return { key };
@@ -4896,6 +5172,7 @@ function createSessionQueryScanState(tokens, options = {}) {
4896
5172
  count: 0,
4897
5173
  snippets: [],
4898
5174
  positions: [],
5175
+ complete: true,
4899
5176
  leadingSystem: roleFilter !== 'system'
4900
5177
  };
4901
5178
  }
@@ -4934,11 +5211,13 @@ function consumeSessionQueryMessage(state, message) {
4934
5211
  timestamp: typeof message.timestamp === 'string' ? message.timestamp : ''
4935
5212
  });
4936
5213
  }
5214
+ if (state.count >= state.maxMatches) state.complete = false;
4937
5215
  return state.count >= state.maxMatches;
4938
5216
  }
4939
5217
 
4940
5218
  function buildSessionQueryScanResult(state) {
4941
5219
  return {
5220
+ complete: !!(state && state.complete),
4942
5221
  hit: !!(state && state.count > 0),
4943
5222
  count: state && Number.isFinite(state.count) ? state.count : 0,
4944
5223
  snippets: state && Array.isArray(state.snippets) ? state.snippets : [],
@@ -4983,6 +5262,7 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4983
5262
  const state = createSessionQueryScanState(tokens, options);
4984
5263
  if (session.source === 'opencode') {
4985
5264
  const extracted = queryOpencodeSessionMessages(session.sessionId || parseOpencodeVirtualSessionId(filePath), { maxMessages: Infinity });
5265
+ state.complete = !!extracted && !extracted.truncated;
4986
5266
  const messages = extracted && Array.isArray(extracted.messages) ? extracted.messages : [];
4987
5267
  for (let i = 0; i < messages.length; i += 1) {
4988
5268
  const message = { ...messages[i], recordLineIndex: i };
@@ -4990,6 +5270,16 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4990
5270
  }
4991
5271
  return buildSessionQueryScanResult(state);
4992
5272
  }
5273
+ if (session.source === 'zed') {
5274
+ const zedExtracted = queryZedSessionMessages(session.sessionId || parseZedVirtualSessionId(filePath), { maxMessages: Infinity });
5275
+ state.complete = !!zedExtracted && !zedExtracted.truncated;
5276
+ const zedMessages = zedExtracted && Array.isArray(zedExtracted.messages) ? zedExtracted.messages : [];
5277
+ for (let i = 0; i < zedMessages.length; i += 1) {
5278
+ const message = { ...zedMessages[i], recordLineIndex: i };
5279
+ if (consumeSessionQueryMessage(state, message)) break;
5280
+ }
5281
+ return buildSessionQueryScanResult(state);
5282
+ }
4993
5283
  if (session.source === 'gemini') {
4994
5284
  if (state.roleFilter !== 'all' || state.positionsLimit > 0) {
4995
5285
  let json;
@@ -4998,6 +5288,7 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4998
5288
  } catch (_) {
4999
5289
  json = null;
5000
5290
  }
5291
+ state.complete = !!json;
5001
5292
  const rawMessages = json && Array.isArray(json.messages) ? json.messages : [];
5002
5293
  for (const entry of rawMessages) {
5003
5294
  if (!entry || typeof entry !== 'object') continue;
@@ -5025,12 +5316,14 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
5025
5316
  const buf = Buffer.alloc(targetBytes);
5026
5317
  const bytes = fs.readSync(fd, buf, 0, targetBytes, 0);
5027
5318
  fs.closeSync(fd);
5319
+ state.complete = bytes >= stat.size;
5028
5320
  text = bytes > 0 ? buf.slice(0, bytes).toString('utf-8') : '';
5029
5321
  } catch (_) {
5030
5322
  try {
5031
5323
  text = fs.readFileSync(filePath, 'utf-8');
5032
5324
  } catch (_) {
5033
5325
  text = '';
5326
+ state.complete = false;
5034
5327
  }
5035
5328
  }
5036
5329
 
@@ -5055,6 +5348,7 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
5055
5348
  const currentLineIndex = lineIndex;
5056
5349
  lineIndex += 1;
5057
5350
  if (maxBytes > 0 && bytesRead >= maxBytes) {
5351
+ state.complete = false;
5058
5352
  break;
5059
5353
  }
5060
5354
 
@@ -5084,6 +5378,7 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
5084
5378
 
5085
5379
  return buildSessionQueryScanResult(state);
5086
5380
  } catch (e) {
5381
+ state.complete = false;
5087
5382
  return scanSessionContentForQueryInRecords(readJsonlRecords(filePath), session.source, state);
5088
5383
  } finally {
5089
5384
  if (rl) {
@@ -5098,7 +5393,9 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
5098
5393
  async function applySessionQueryFilter(sessions, options = {}) {
5099
5394
  const tokens = Array.isArray(options.tokens) ? options.tokens : [];
5100
5395
  if (tokens.length === 0) {
5101
- return sessions;
5396
+ const result = sessions.slice();
5397
+ Object.defineProperty(result, 'searchInfo', { value: null });
5398
+ return result;
5102
5399
  }
5103
5400
 
5104
5401
  const mode = normalizeQueryMode(options.queryMode);
@@ -5112,10 +5409,12 @@ async function applySessionQueryFilter(sessions, options = {}) {
5112
5409
  : 0;
5113
5410
 
5114
5411
  let scanned = 0;
5412
+ let complete = true;
5115
5413
  const results = [];
5116
5414
 
5117
5415
  for (const session of sessions) {
5118
5416
  if (scope === 'content' && scanned >= contentScanLimit) {
5417
+ complete = false;
5119
5418
  break;
5120
5419
  }
5121
5420
 
@@ -5135,6 +5434,10 @@ async function applySessionQueryFilter(sessions, options = {}) {
5135
5434
  snippetLimit: 2
5136
5435
  });
5137
5436
  contentHit = contentInfo.hit;
5437
+ // 命中已完成候选的存在性判定,无需穷尽匹配或消息。
5438
+ if (!contentHit && !contentInfo.complete) complete = false;
5439
+ } else if (shouldScanContent) {
5440
+ complete = false;
5138
5441
  }
5139
5442
 
5140
5443
  const hit = scope === 'summary'
@@ -5156,6 +5459,9 @@ async function applySessionQueryFilter(sessions, options = {}) {
5156
5459
  results.push(session);
5157
5460
  }
5158
5461
 
5462
+ Object.defineProperty(results, 'searchInfo', {
5463
+ value: { scanned, candidates: sessions.length, complete }
5464
+ });
5159
5465
  return results;
5160
5466
  }
5161
5467
  function collectRecentJsonlFiles(rootDir, options = {}) {
@@ -5381,7 +5687,7 @@ function getSessionInventoryCache(cacheKey, forceRefresh = false) {
5381
5687
  }
5382
5688
 
5383
5689
  function registerSessionFileLookupEntries(source, sessions = []) {
5384
- const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode'
5690
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode' || source === 'zed'
5385
5691
  ? source
5386
5692
  : 'codex';
5387
5693
  const store = g_sessionFileLookupCache[normalizedSource];
@@ -5422,7 +5728,7 @@ function setSessionInventoryCache(cacheKey, source, value) {
5422
5728
  }
5423
5729
 
5424
5730
  function listSessionInventoryBySource(source, limit, scanOptions = {}, options = {}) {
5425
- const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode'
5731
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode' || source === 'zed'
5426
5732
  ? source
5427
5733
  : 'codex';
5428
5734
  const forceRefresh = !!options.forceRefresh;
@@ -5444,7 +5750,9 @@ function listSessionInventoryBySource(source, limit, scanOptions = {}, options =
5444
5750
  ? listPiSessions(limit, scanOptions)
5445
5751
  : (normalizedSource === 'opencode'
5446
5752
  ? listOpencodeSessions(limit, scanOptions)
5447
- : listCodexSessions(limit, scanOptions))))));
5753
+ : (normalizedSource === 'zed'
5754
+ ? listZedSessions(limit, scanOptions)
5755
+ : listCodexSessions(limit, scanOptions)))))));
5448
5756
  setSessionInventoryCache(cacheKey, normalizedSource, sessions);
5449
5757
  return sessions;
5450
5758
  }
@@ -5459,7 +5767,8 @@ function invalidateSessionListCache() {
5459
5767
  codebuddy: new Map(),
5460
5768
  workbuddy: new Map(),
5461
5769
  pi: new Map(),
5462
- opencode: new Map()
5770
+ opencode: new Map(),
5771
+ zed: new Map()
5463
5772
  };
5464
5773
  }
5465
5774
 
@@ -7325,6 +7634,9 @@ async function listAllSessions(params = {}) {
7325
7634
  if (source === 'all' || source === 'opencode') {
7326
7635
  sessions = sessions.concat(listSessionInventoryBySource('opencode', limit, scanOptions, { forceRefresh }));
7327
7636
  }
7637
+ if (source === 'all' || source === 'zed') {
7638
+ sessions = sessions.concat(listSessionInventoryBySource('zed', limit, scanOptions, { forceRefresh }));
7639
+ }
7328
7640
 
7329
7641
  if (hasPathFilter) {
7330
7642
  sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
@@ -7341,7 +7653,9 @@ async function listAllSessions(params = {}) {
7341
7653
  contentScanBytes: params.contentScanBytes
7342
7654
  });
7343
7655
  }
7656
+ const searchInfo = hasQuery ? result.searchInfo : null;
7344
7657
  result = mergeAndLimitSessions(result, limit);
7658
+ Object.defineProperty(result, 'searchInfo', { value: searchInfo });
7345
7659
  setSessionListCache(cacheKey, result);
7346
7660
  return result;
7347
7661
  }
@@ -7374,6 +7688,7 @@ async function listAllSessionsData(params = {}) {
7374
7688
  delete normalized.__messageCountExact;
7375
7689
  return normalized;
7376
7690
  });
7691
+ Object.defineProperty(result, 'searchInfo', { value: sessions.searchInfo || null });
7377
7692
  if (!hasQuery) {
7378
7693
  setSessionListCache(cacheKey, result);
7379
7694
  }
@@ -7385,7 +7700,7 @@ async function listSessionBrowse(params = {}) {
7385
7700
  ...params,
7386
7701
  browseLightweight: true
7387
7702
  });
7388
- return Array.isArray(sessions)
7703
+ const result = Array.isArray(sessions)
7389
7704
  ? sessions.map((item) => {
7390
7705
  if (!item || typeof item !== 'object' || Array.isArray(item)) {
7391
7706
  return item;
@@ -7395,6 +7710,8 @@ async function listSessionBrowse(params = {}) {
7395
7710
  return normalized;
7396
7711
  })
7397
7712
  : [];
7713
+ Object.defineProperty(result, 'searchInfo', { value: sessions.searchInfo || null });
7714
+ return result;
7398
7715
  }
7399
7716
 
7400
7717
  async function listSessionUsage(params = {}) {
@@ -7466,6 +7783,9 @@ function listSessionPaths(params = {}) {
7466
7783
  if (validSource === 'all' || validSource === 'opencode') {
7467
7784
  sessions = sessions.concat(listSessionInventoryBySource('opencode', gatherLimit, scanOptions, { forceRefresh }));
7468
7785
  }
7786
+ if (validSource === 'all' || validSource === 'zed') {
7787
+ sessions = sessions.concat(listSessionInventoryBySource('zed', gatherLimit, scanOptions, { forceRefresh }));
7788
+ }
7469
7789
 
7470
7790
  const dedupedPaths = [];
7471
7791
  const seen = new Set();
@@ -7492,6 +7812,12 @@ function listSessionPaths(params = {}) {
7492
7812
 
7493
7813
  function resolveSessionFilePath(source, filePath, sessionId) {
7494
7814
  const normalizedSource = normalizeSessionSourceName(source, 'codex');
7815
+ if (normalizedSource === 'zed') {
7816
+ const zedId = typeof sessionId === 'string' && sessionId.trim()
7817
+ ? sessionId.trim()
7818
+ : parseZedVirtualSessionId(filePath);
7819
+ return zedId && parseZedSessionSummary(zedId) ? buildZedVirtualSessionPath(zedId) : '';
7820
+ }
7495
7821
  if (normalizedSource === 'opencode') {
7496
7822
  const id = typeof sessionId === 'string' && sessionId.trim()
7497
7823
  ? sessionId.trim()
@@ -8072,7 +8398,7 @@ function buildSessionSummaryFallback(source, filePath, sessionId = '') {
8072
8398
  contextWindow: 0,
8073
8399
  filePath,
8074
8400
  keywords: [],
8075
- capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'opencode' ? { code: true } : {}
8401
+ capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'opencode' || source === 'zed' ? { code: true } : {}
8076
8402
  };
8077
8403
  }
8078
8404
 
@@ -8659,6 +8985,9 @@ async function trashSessionData(params = {}) {
8659
8985
  if (source === 'opencode') {
8660
8986
  return { error: 'OpenCode DB-backed sessions cannot be deleted from CodexMate' };
8661
8987
  }
8988
+ if (source === 'zed') {
8989
+ return { error: 'Zed DB-backed sessions cannot be deleted from CodexMate' };
8990
+ }
8662
8991
 
8663
8992
  const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
8664
8993
  if (!filePath) {
@@ -8771,6 +9100,9 @@ async function deleteSessionData(params = {}) {
8771
9100
  if (source === 'opencode') {
8772
9101
  return { error: 'OpenCode DB-backed sessions cannot be deleted from CodexMate' };
8773
9102
  }
9103
+ if (source === 'zed') {
9104
+ return { error: 'Zed DB-backed sessions cannot be deleted from CodexMate' };
9105
+ }
8774
9106
 
8775
9107
  const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
8776
9108
  if (!filePath) {
@@ -9470,6 +9802,13 @@ async function readSessionDetail(params = {}) {
9470
9802
  if (!extracted) {
9471
9803
  return { error: 'Failed to parse session file' };
9472
9804
  }
9805
+ }
9806
+ if (source === 'zed') {
9807
+ const zedSessionId = params.sessionId || parseZedVirtualSessionId(filePath);
9808
+ extracted = queryZedSessionMessages(zedSessionId, { maxMessages: messageLimit });
9809
+ if (!extracted) {
9810
+ return { error: 'Failed to parse session file' };
9811
+ }
9473
9812
  } else if (source === 'gemini') {
9474
9813
  let json;
9475
9814
  try {
@@ -9604,6 +9943,13 @@ async function readSessionPlain(params = {}) {
9604
9943
  if (!extracted) {
9605
9944
  return { error: 'Failed to parse session file' };
9606
9945
  }
9946
+ }
9947
+ if (source === 'zed') {
9948
+ const zedSessionId = params.sessionId || parseZedVirtualSessionId(filePath);
9949
+ extracted = queryZedSessionMessages(zedSessionId, { maxMessages });
9950
+ if (!extracted) {
9951
+ return { error: 'Failed to parse session file' };
9952
+ }
9607
9953
  } else if (source === 'gemini') {
9608
9954
  let json;
9609
9955
  try {
@@ -9688,6 +10034,12 @@ async function exportSessionData(params = {}) {
9688
10034
  if (!extracted) {
9689
10035
  return { error: 'Failed to parse session file' };
9690
10036
  }
10037
+ } else if (source === 'zed') {
10038
+ const zedSessionId = params.sessionId || parseZedVirtualSessionId(filePath);
10039
+ extracted = queryZedSessionMessages(zedSessionId, { maxMessages });
10040
+ if (!extracted) {
10041
+ return { error: 'Failed to parse session file' };
10042
+ }
9691
10043
  } else if (source === 'gemini') {
9692
10044
  let json;
9693
10045
  try {
@@ -12929,7 +13281,7 @@ function resolveExportOutputPath(outputPath, defaultFileName) {
12929
13281
  }
12930
13282
 
12931
13283
  function printExportSessionUsage() {
12932
- console.log('\n用法: codexmate export-session --source <codex|claude|gemini|codebuddy|workbuddy|pi|opencode> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
13284
+ console.log('\n用法: codexmate export-session --source <codex|claude|gemini|codebuddy|workbuddy|pi|opencode|zed> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
12933
13285
  console.log('\n示例:');
12934
13286
  console.log(' codexmate export-session --source codex --session-id 123456');
12935
13287
  console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
@@ -13002,7 +13354,7 @@ function parseExportSessionArgs(args = []) {
13002
13354
 
13003
13355
  const normalizedSource = options.source.trim().toLowerCase();
13004
13356
  if (normalizedSource && !isSessionSourceOrAll(normalizedSource)) {
13005
- errors.push('参数 --source 仅支持 codex、claude、gemini、codebuddy、workbuddy、pi 或 opencode');
13357
+ errors.push('参数 --source 仅支持 codex、claude、gemini、codebuddy、workbuddy、pi、opencode 或 zed');
13006
13358
  }
13007
13359
  options.source = normalizedSource;
13008
13360
 
@@ -14433,9 +14785,11 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
14433
14785
  if (source && !isSessionSourceOrAll(source)) {
14434
14786
  result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
14435
14787
  } else {
14788
+ const sessions = await listSessionBrowse(params);
14436
14789
  result = {
14437
- sessions: await listSessionBrowse(params),
14438
- source: source || 'all'
14790
+ sessions,
14791
+ source: source || 'all',
14792
+ searchInfo: sessions.searchInfo
14439
14793
  };
14440
14794
  }
14441
14795
  }
@@ -17374,7 +17728,7 @@ function printMainHelp() {
17374
17728
  console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
17375
17729
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
17376
17730
  console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
17377
- console.log(' codexmate export-session --source <codex|claude|gemini|codebuddy|workbuddy|pi|opencode> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
17731
+ console.log(' codexmate export-session --source <codex|claude|gemini|codebuddy|workbuddy|pi|opencode|zed> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
17378
17732
  console.log(' codexmate convert-session --from <codex|claude> --to <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
17379
17733
  console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
17380
17734
  console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');