codexmate 0.1.24 → 0.1.27

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 (50) hide show
  1. package/cli/session-usage.js +10 -4
  2. package/cli.js +737 -200
  3. package/lib/cli-sessions.js +1 -1
  4. package/package.json +3 -3
  5. package/plugins/prompt-templates/computed.mjs +1 -1
  6. package/plugins/prompt-templates/index.mjs +3 -3
  7. package/plugins/prompt-templates/manifest.mjs +5 -3
  8. package/plugins/prompt-templates/methods.mjs +9 -2
  9. package/plugins/registry.mjs +5 -2
  10. package/plugins/text-tools/computed.mjs +23 -0
  11. package/plugins/text-tools/index.mjs +14 -0
  12. package/plugins/text-tools/manifest.mjs +20 -0
  13. package/plugins/text-tools/methods.mjs +24 -0
  14. package/plugins/text-tools/overview.mjs +12 -0
  15. package/plugins/text-tools/ownership.mjs +7 -0
  16. package/tools/dev/launch.js +147 -0
  17. package/web-ui/app.js +15 -5
  18. package/web-ui/index.html +1 -2
  19. package/web-ui/logic.markdown-editor.mjs +5 -57
  20. package/web-ui/logic.prompts-token-count.mjs +49 -0
  21. package/web-ui/logic.sessions.mjs +30 -15
  22. package/web-ui/modules/app.computed.dashboard.mjs +1 -0
  23. package/web-ui/modules/app.computed.prompts.mjs +18 -0
  24. package/web-ui/modules/app.computed.session.mjs +3 -1
  25. package/web-ui/modules/app.methods.navigation.mjs +79 -3
  26. package/web-ui/modules/app.methods.prompts-editor.mjs +6 -64
  27. package/web-ui/modules/app.methods.session-actions.mjs +10 -4
  28. package/web-ui/modules/app.methods.session-browser.mjs +34 -45
  29. package/web-ui/modules/app.methods.session-timeline.mjs +1 -0
  30. package/web-ui/modules/app.methods.web-ui-preferences.mjs +7 -1
  31. package/web-ui/modules/i18n/locales/en.mjs +20 -1
  32. package/web-ui/modules/i18n/locales/ja.mjs +20 -1
  33. package/web-ui/modules/i18n/locales/vi.mjs +20 -1
  34. package/web-ui/modules/i18n/locales/zh-tw.mjs +20 -1
  35. package/web-ui/modules/i18n/locales/zh.mjs +20 -1
  36. package/web-ui/modules/plugins.computed.mjs +8 -2
  37. package/web-ui/modules/plugins.methods.mjs +8 -2
  38. package/web-ui/partials/index/layout-header.html +23 -6
  39. package/web-ui/partials/index/panel-plugins.html +45 -5
  40. package/web-ui/partials/index/panel-prompts.html +16 -17
  41. package/web-ui/partials/index/panel-sessions.html +19 -4
  42. package/web-ui/res/web-ui-render.precompiled.js +193 -93
  43. package/web-ui/session-helpers.mjs +1 -2
  44. package/web-ui/source-bundle.cjs +1 -0
  45. package/web-ui/styles/base-theme.css +24 -0
  46. package/web-ui/styles/modals-core.css +48 -0
  47. package/web-ui/styles/prompts-editor.css +3 -66
  48. package/web-ui/styles/sessions-list.css +1 -1
  49. package/web-ui/styles/sessions-preview.css +56 -7
  50. package/web-ui/styles/sessions-toolbar-trash.css +1 -7
@@ -29,14 +29,14 @@ function shouldUseFastSessionBrowseLimit(options = {}) {
29
29
 
30
30
  export function isSessionQueryEnabled(source) {
31
31
  const normalized = normalizeSessionSource(source, '');
32
- return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi' || normalized === 'all';
32
+ return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi' || normalized === 'opencode' || normalized === 'all';
33
33
  }
34
34
 
35
35
  export function normalizeSessionSource(source, fallback = 'all') {
36
36
  const normalized = typeof source === 'string'
37
37
  ? source.trim().toLowerCase()
38
38
  : '';
39
- if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi' || normalized === 'all') {
39
+ if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi' || normalized === 'opencode' || normalized === 'all') {
40
40
  return normalized;
41
41
  }
42
42
  return fallback;
@@ -57,6 +57,18 @@ function isConcreteSessionModelName(value) {
57
57
  return normalized.toLowerCase() !== '<synthetic>';
58
58
  }
59
59
 
60
+ function getSessionSourceLabel(source) {
61
+ if (source === 'claude') return 'Claude Code';
62
+ if (source === 'gemini') return 'Gemini CLI';
63
+ if (source === 'codebuddy') return 'CodeBuddy Code';
64
+ if (source === 'workbuddy') return 'WorkBuddy AI';
65
+ if (source === 'pi') return 'Pi';
66
+ if (source === 'opencode') return 'OpenCode';
67
+ return 'Codex';
68
+ }
69
+
70
+ const SESSION_USAGE_SOURCE_ORDER = ['codex', 'claude', 'gemini', 'codebuddy', 'workbuddy', 'pi', 'opencode'];
71
+
60
72
  function collectSessionModelNames(session) {
61
73
  if (!session || typeof session !== 'object') {
62
74
  return [];
@@ -209,7 +221,7 @@ export function buildUsageHeatmap(sessions = [], options = {}) {
209
221
  for (const session of list) {
210
222
  if (!session || typeof session !== 'object') continue;
211
223
  const source = normalizeSessionSource(session.source, '');
212
- if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
224
+ if (!SESSION_USAGE_SOURCE_ORDER.includes(source)) continue;
213
225
  const updatedAtMs = Date.parse(session.updatedAt || '');
214
226
  if (!Number.isFinite(updatedAtMs)) continue;
215
227
  normalized.push({
@@ -309,7 +321,7 @@ export function buildUsageHourlyHeatmap(sessions = [], options = {}) {
309
321
  for (const session of list) {
310
322
  if (!session || typeof session !== 'object') continue;
311
323
  const source = normalizeSessionSource(session.source, '');
312
- if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
324
+ if (!SESSION_USAGE_SOURCE_ORDER.includes(source)) continue;
313
325
  const updatedAtMs = Date.parse(session.updatedAt || '');
314
326
  if (!Number.isFinite(updatedAtMs)) continue;
315
327
  const dayStart = toUtcDayStartMs(updatedAtMs);
@@ -402,7 +414,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
402
414
  for (const [sessionIndex, session] of list.entries()) {
403
415
  if (!session || typeof session !== 'object') continue;
404
416
  const source = normalizeSessionSource(session.source, '');
405
- if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
417
+ if (!SESSION_USAGE_SOURCE_ORDER.includes(source)) continue;
406
418
  const updatedAtMs = Date.parse(session.updatedAt || '');
407
419
  if (!Number.isFinite(updatedAtMs)) continue;
408
420
  const createdAtMs = Date.parse(session.createdAt || '');
@@ -437,6 +449,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
437
449
  return ns.updatedAtMs >= rangeStartMs && ns.updatedAtMs <= rangeEndMs;
438
450
  });
439
451
 
452
+ const sourceSessionTotals = Object.fromEntries(SESSION_USAGE_SOURCE_ORDER.map((source) => [source, 0]));
440
453
  let codexTotal = 0;
441
454
  let claudeTotal = 0;
442
455
  let messageTotal = 0;
@@ -449,8 +462,8 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
449
462
  const modelMap = new Map();
450
463
  const missingModelProviderMap = new Map();
451
464
  const missingModelSessionMap = new Map();
452
- const sourceMessageTotals = { codex: 0, claude: 0 };
453
- const missingModelSourceTotals = { codex: 0, claude: 0 };
465
+ const sourceMessageTotals = Object.fromEntries(SESSION_USAGE_SOURCE_ORDER.map((source) => [source, 0]));
466
+ const missingModelSourceTotals = Object.fromEntries(SESSION_USAGE_SOURCE_ORDER.map((source) => [source, 0]));
454
467
  let missingModelSessions = 0;
455
468
  let providerOnlySessions = 0;
456
469
  const hourCounts = Array.from({ length: 24 }, (_, hour) => ({
@@ -485,10 +498,11 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
485
498
  : 0;
486
499
  bucket.totalSessions += 1;
487
500
  bucket.totalMessages += messageCount;
501
+ sourceSessionTotals[source] = (sourceSessionTotals[source] || 0) + 1;
488
502
  if (source === 'codex') {
489
503
  bucket.codex += 1;
490
504
  codexTotal += 1;
491
- } else {
505
+ } else if (source === 'claude') {
492
506
  bucket.claude += 1;
493
507
  claudeTotal += 1;
494
508
  }
@@ -519,7 +533,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
519
533
  });
520
534
  }
521
535
 
522
- const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
536
+ const sourceLabel = getSessionSourceLabel(source);
523
537
  const normalizedTitle = typeof session.title === 'string' && session.title.trim()
524
538
  ? session.title.trim()
525
539
  : (typeof session.sessionId === 'string' && session.sessionId.trim() ? session.sessionId.trim() : '未命名会话');
@@ -564,11 +578,12 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
564
578
  topSessionsByMessages.push({ ...sessionEntry });
565
579
  }
566
580
 
567
- const totalSessions = codexTotal + claudeTotal;
568
- const sourceShare = [
569
- { key: 'codex', label: 'Codex', value: codexTotal },
570
- { key: 'claude', label: 'Claude', value: claudeTotal }
571
- ].map((item) => ({
581
+ const totalSessions = SESSION_USAGE_SOURCE_ORDER.reduce((sum, source) => sum + (sourceSessionTotals[source] || 0), 0);
582
+ const sourceShare = SESSION_USAGE_SOURCE_ORDER.map((source) => ({
583
+ key: source,
584
+ label: getSessionSourceLabel(source),
585
+ value: sourceSessionTotals[source] || 0
586
+ })).map((item) => ({
572
587
  ...item,
573
588
  percent: totalSessions > 0 ? Math.round((item.value / totalSessions) * 100) : 0,
574
589
  messageTotal: sourceMessageTotals[item.key] || 0,
@@ -591,7 +606,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
591
606
  .map(([modelId, meta]) => {
592
607
  const sourceLabels = [...meta.sources]
593
608
  .sort((a, b) => a.localeCompare(b, 'en-US'))
594
- .map((source) => (source === 'codex' ? 'Codex' : 'Claude Code'));
609
+ .map((source) => getSessionSourceLabel(source));
595
610
  return {
596
611
  key: modelId,
597
612
  model: modelId,
@@ -196,6 +196,7 @@ export function createDashboardComputed() {
196
196
  if (this.sessionFilterSource === 'claude') return this.t('dashboard.sessionSource.claude');
197
197
  if (this.sessionFilterSource === 'gemini') return this.t('dashboard.sessionSource.gemini');
198
198
  if (this.sessionFilterSource === 'codebuddy') return this.t('dashboard.sessionSource.codebuddy');
199
+ if (this.sessionFilterSource === 'workbuddy') return this.t('dashboard.sessionSource.workbuddy');
199
200
  return this.t('dashboard.sessionSource.all');
200
201
  },
201
202
  inspectorSessionPathLabel() {
@@ -1,5 +1,23 @@
1
+ import { estimatePromptTokenCount, buildPromptTokenDiffStats } from '../logic.prompts-token-count.mjs';
2
+
1
3
  export function createPromptsComputed() {
2
4
  return {
5
+ agentsTokenCount() {
6
+ return estimatePromptTokenCount(this.agentsContent);
7
+ },
8
+
9
+ sysPromptTokenCount() {
10
+ return estimatePromptTokenCount(this.sysPromptContent);
11
+ },
12
+
13
+ agentsDiffTokenStats() {
14
+ return buildPromptTokenDiffStats(this.agentsOriginalContent, this.agentsContent);
15
+ },
16
+
17
+ sysPromptDiffTokenStats() {
18
+ return buildPromptTokenDiffStats(this.sysPromptOriginalContent, this.sysPromptContent);
19
+ },
20
+
3
21
  promptsContextHint() {
4
22
  if (!this.agentsLoading && !this.agentsDiffVisible && this.hasAgentsContentChanged()) {
5
23
  return { text: this.t('modal.agents.unsaved.detectedHint'), warn: true };
@@ -323,7 +323,9 @@ export function createSessionComputed() {
323
323
  { value: "claude", label: this.t("sessions.source.claudeCode") },
324
324
  { value: "gemini", label: this.t("sessions.source.gemini") },
325
325
  { value: "codebuddy", label: this.t("sessions.source.codebuddy") },
326
- { value: "pi", label: this.t("sessions.source.pi") }
326
+ { value: "workbuddy", label: this.t("sessions.source.workbuddy") },
327
+ { value: "pi", label: this.t("sessions.source.pi") },
328
+ { value: "opencode", label: this.t("sessions.source.opencode") }
327
329
  ];
328
330
  },
329
331
  activeSessionExportKey() {
@@ -110,12 +110,17 @@
110
110
  const settingsTab = typeof vm.settingsTab === 'string' ? vm.settingsTab.trim().toLowerCase() : 'general';
111
111
  const skillsTargetApp = typeof vm.skillsTargetApp === 'string' && (vm.skillsTargetApp === 'codex' || vm.skillsTargetApp === 'claude' || vm.skillsTargetApp === 'pi') ? vm.skillsTargetApp : 'codex';
112
112
  const promptTemplatesMode = typeof vm.promptTemplatesMode === 'string' && (vm.promptTemplatesMode === 'compose' || vm.promptTemplatesMode === 'manage') ? vm.promptTemplatesMode : 'compose';
113
+ const pluginsActiveIdSource = resolvedOverrides && typeof resolvedOverrides.pluginsActiveId === 'string'
114
+ ? resolvedOverrides.pluginsActiveId
115
+ : vm.pluginsActiveId;
116
+ const pluginsActiveId = typeof pluginsActiveIdSource === 'string' ? pluginsActiveIdSource.trim() : '';
113
117
  const snapshot = {
114
118
  settingsTab: settingsTab === 'data' ? 'data' : 'general',
115
119
  mainTab: resolveSelectableMainTab(mainTab),
116
120
  configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex',
117
121
  skillsTargetApp,
118
- promptTemplatesMode
122
+ promptTemplatesMode,
123
+ pluginsActiveId: pluginsActiveId || 'prompt-templates'
119
124
  };
120
125
  if (typeof vm.persistWebUiPreferences === 'function') {
121
126
  vm.persistWebUiPreferences({ navigation: snapshot });
@@ -192,6 +197,7 @@
192
197
  intent: '',
193
198
  pendingTarget: '',
194
199
  pendingConfigMode: '',
200
+ pendingPluginsId: '',
195
201
  ticket: 0
196
202
  };
197
203
  return this.__mainTabSwitchState;
@@ -242,7 +248,7 @@
242
248
  if (!normalizedKind || !normalizedValue) {
243
249
  return;
244
250
  }
245
- const expectedIntent = normalizedKind === 'config' ? 'config' : normalizedValue;
251
+ const expectedIntent = normalizedKind === 'config' || normalizedKind === 'plugins' ? normalizedKind : normalizedValue;
246
252
  this.cancelTouchNavIntentReset();
247
253
  const token = (Number(this.__touchNavIntentResetToken) || 0) + 1;
248
254
  this.__touchNavIntentResetToken = token;
@@ -259,21 +265,26 @@
259
265
  this.clearMainTabSwitchIntent(expectedIntent);
260
266
  }, 1000);
261
267
  },
262
- applyImmediateNavIntent(tab, configMode = '') {
268
+ applyImmediateNavIntent(tab, configMode = '', pluginsId = '') {
263
269
  if (typeof document === 'undefined') return;
264
270
  const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
265
271
  if (!normalizedTab) return;
266
272
  const normalizedMode = typeof configMode === 'string' ? configMode.trim().toLowerCase() : '';
273
+ const normalizedPluginId = typeof pluginsId === 'string' ? pluginsId.trim().toLowerCase() : '';
267
274
  const domState = this.ensureImmediateNavDomState();
268
275
  const nodes = Array.isArray(domState.navNodes) ? domState.navNodes : [];
269
276
  for (const node of nodes) {
270
277
  if (!node || !node.classList) continue;
271
278
  const nodeTab = String(node.getAttribute('data-main-tab') || '').trim().toLowerCase();
272
279
  const nodeMode = String(node.getAttribute('data-config-mode') || '').trim().toLowerCase();
280
+ const nodePluginId = String(node.getAttribute('data-plugins-id') || '').trim().toLowerCase();
273
281
  let shouldActivate = nodeTab === normalizedTab;
274
282
  if (shouldActivate && normalizedTab === 'config') {
275
283
  shouldActivate = nodeMode ? nodeMode === normalizedMode : false;
276
284
  }
285
+ if (shouldActivate && normalizedTab === 'plugins') {
286
+ shouldActivate = nodePluginId ? nodePluginId === normalizedPluginId : !normalizedPluginId;
287
+ }
277
288
  node.classList.toggle('nav-intent-active', !!shouldActivate);
278
289
  node.classList.toggle('nav-intent-inactive', !shouldActivate);
279
290
  }
@@ -395,6 +406,67 @@
395
406
  if (this.consumePointerNavCommit('config', normalizedMode)) return;
396
407
  this.switchConfigMode(normalizedMode);
397
408
  },
409
+
410
+ switchPluginsTab(pluginId) {
411
+ const normalizedId = typeof pluginId === 'string' ? pluginId.trim().toLowerCase() : '';
412
+ if (!normalizedId) return;
413
+ if (typeof this.selectPlugin === 'function') {
414
+ this.selectPlugin(normalizedId);
415
+ } else {
416
+ this.pluginsActiveId = normalizedId;
417
+ }
418
+ this.switchMainTab('plugins');
419
+ },
420
+
421
+ onPluginsTabPointerDown(pluginId) {
422
+ const event = arguments.length > 1 ? arguments[1] : null;
423
+ if (event && typeof event.button === 'number' && event.button !== 0) {
424
+ return;
425
+ }
426
+ const normalizedId = typeof pluginId === 'string' ? pluginId.trim().toLowerCase() : '';
427
+ if (!normalizedId) return;
428
+ persistNavState(this, { mainTab: 'plugins', pluginsActiveId: normalizedId });
429
+ this.setMainTabSwitchIntent('plugins');
430
+ if (typeof this.ensureMainTabSwitchState === 'function') {
431
+ this.ensureMainTabSwitchState().pendingPluginsId = normalizedId;
432
+ }
433
+ this.applyImmediateNavIntent('plugins', '', normalizedId);
434
+ const shouldHideSessionPanel = this.mainTab === 'sessions';
435
+ this.setSessionPanelFastHidden(shouldHideSessionPanel);
436
+ const pointerType = event && typeof event.pointerType === 'string'
437
+ ? event.pointerType.trim().toLowerCase()
438
+ : '';
439
+ if (pointerType === 'touch') {
440
+ this.scheduleTouchNavIntentReset('plugins', normalizedId);
441
+ return;
442
+ }
443
+ this.recordPointerNavCommit('plugins', normalizedId);
444
+ this.switchPluginsTab(normalizedId);
445
+ },
446
+
447
+ onPluginsTabClick(pluginId) {
448
+ const normalizedId = typeof pluginId === 'string' ? pluginId.trim().toLowerCase() : '';
449
+ if (!normalizedId) return;
450
+ if (this.consumePointerNavCommit('plugins', normalizedId)) return;
451
+ this.switchPluginsTab(normalizedId);
452
+ },
453
+
454
+ isPluginsIdNavActive(pluginId) {
455
+ const normalizedId = typeof pluginId === 'string' ? pluginId.trim().toLowerCase() : '';
456
+ if (!normalizedId) return false;
457
+ if (!this.isMainTabNavActive('plugins')) return false;
458
+ const state = this.ensureMainTabSwitchState();
459
+ const pendingId = typeof state.pendingPluginsId === 'string'
460
+ ? state.pendingPluginsId.trim().toLowerCase()
461
+ : '';
462
+ if (state.intent === 'plugins' && pendingId) {
463
+ return pendingId === normalizedId;
464
+ }
465
+ const currentId = typeof this.pluginsActiveId === 'string'
466
+ ? this.pluginsActiveId.trim().toLowerCase()
467
+ : '';
468
+ return currentId === normalizedId;
469
+ },
398
470
  clearMainTabSwitchIntent(expectedTab = '') {
399
471
  const state = this.ensureMainTabSwitchState();
400
472
  if (expectedTab && state.intent && state.intent !== expectedTab) {
@@ -404,6 +476,7 @@
404
476
  state.intent = '';
405
477
  state.pendingTarget = '';
406
478
  state.pendingConfigMode = '';
479
+ state.pendingPluginsId = '';
407
480
  this.clearImmediateNavIntent();
408
481
  this.setSessionPanelFastHidden(false);
409
482
  },
@@ -463,6 +536,9 @@
463
536
  if (targetTab !== 'config') {
464
537
  switchState.pendingConfigMode = '';
465
538
  }
539
+ if (targetTab !== 'plugins') {
540
+ switchState.pendingPluginsId = '';
541
+ }
466
542
  if (targetTab === previousTab) {
467
543
  switchState.ticket += 1;
468
544
  switchState.pendingTarget = '';
@@ -1,25 +1,20 @@
1
1
  /**
2
- * Prompts-panel Markdown editor methods: overlay highlight sync,
3
- * mobile view switching, and the live split preview (marked + DOMPurify,
4
- * debounced). One implementation is shared by the agentsContent editor
5
- * and the sysPromptContent editor.
2
+ * Prompts-panel Markdown editor methods: mobile view switching and the
3
+ * live split preview (marked + DOMPurify, debounced). One implementation
4
+ * is shared by the agentsContent editor and the sysPromptContent editor.
6
5
  *
7
6
  * State contract (declared in app.js data()):
8
7
  * promptsPreviewEnabled, promptsPreviewCollapsed, promptsMobileView,
9
- * promptsPreviewLibsMissing, agentsHighlightHtml, sysPromptHighlightHtml,
10
- * agentsPreviewHtml, sysPromptPreviewHtml, agentsChangeAxis, sysChangeAxis,
8
+ * promptsPreviewLibsMissing, agentsPreviewHtml, sysPromptPreviewHtml,
9
+ * agentsChangeAxis, sysChangeAxis,
11
10
  * promptsPathReferencePaths, promptsPathReferenceLoading,
12
11
  * promptsPathReferenceCacheKey (path-reference dropdown)
13
12
  * Template contract:
14
13
  * refs promptsAgentsTextarea / promptsSysTextarea on the two textareas,
15
- * refs promptsAgentsHighlight / promptsSysHighlight on the backdrop <pre>,
16
14
  * refs promptsAgentsDiffView / promptsSysDiffView on the two diff row containers.
17
15
  */
18
16
 
19
- import {
20
- buildMarkdownPreviewHtml,
21
- highlightMarkdownText
22
- } from '../logic.markdown-editor.mjs';
17
+ import { buildMarkdownPreviewHtml } from '../logic.markdown-editor.mjs';
23
18
  import {
24
19
  buildPromptPathReferenceOptions as buildPromptPathReferenceOptionList,
25
20
  buildPromptPathReferenceSentence,
@@ -29,7 +24,6 @@ import {
29
24
  buildPromptsDiffAxis
30
25
  } from '../logic.prompts-change-axis.mjs';
31
26
 
32
- const PROMPTS_HIGHLIGHT_DEBOUNCE_MS = 150;
33
27
  const PROMPTS_PREVIEW_DEBOUNCE_MS = 300;
34
28
  const PROMPTS_AXIS_DEBOUNCE_MS = 300;
35
29
 
@@ -37,7 +31,6 @@ const PROMPTS_EDITOR_META = {
37
31
  agents: {
38
32
  contentField: 'agentsContent',
39
33
  originalField: 'agentsOriginalContent',
40
- highlightField: 'agentsHighlightHtml',
41
34
  previewField: 'agentsPreviewHtml',
42
35
  axisField: 'agentsChangeAxis',
43
36
  diffLinesField: 'agentsDiffLines',
@@ -48,13 +41,11 @@ const PROMPTS_EDITOR_META = {
48
41
  diffLoadingField: 'agentsDiffLoading',
49
42
  prepareDiffMethod: 'prepareAgentsDiff',
50
43
  textareaRef: 'promptsAgentsTextarea',
51
- highlightRef: 'promptsAgentsHighlight',
52
44
  diffViewRef: 'promptsAgentsDiffView'
53
45
  },
54
46
  sys: {
55
47
  contentField: 'sysPromptContent',
56
48
  originalField: 'sysPromptOriginalContent',
57
- highlightField: 'sysPromptHighlightHtml',
58
49
  previewField: 'sysPromptPreviewHtml',
59
50
  axisField: 'sysChangeAxis',
60
51
  diffLinesField: 'sysPromptDiffLines',
@@ -65,7 +56,6 @@ const PROMPTS_EDITOR_META = {
65
56
  diffLoadingField: 'sysPromptDiffLoading',
66
57
  prepareDiffMethod: 'prepareSysPromptDiff',
67
58
  textareaRef: 'promptsSysTextarea',
68
- highlightRef: 'promptsSysHighlight',
69
59
  diffViewRef: 'promptsSysDiffView'
70
60
  }
71
61
  };
@@ -74,13 +64,6 @@ function emptyPromptsChangeAxis() {
74
64
  return { mode: 'edit', ticks: [], truncated: false, totalLines: 0, added: 0, removed: 0 };
75
65
  }
76
66
 
77
- function resolvePromptsHljs() {
78
- if (typeof window !== 'undefined' && window.hljs) {
79
- return window.hljs;
80
- }
81
- return null;
82
- }
83
-
84
67
  function resolvePromptsPreviewLibs() {
85
68
  if (typeof window === 'undefined') {
86
69
  return { marked: null, purify: null };
@@ -97,46 +80,6 @@ export function createPromptsEditorMethods(options = {}) {
97
80
  },
98
81
 
99
82
 
100
- refreshPromptsHighlight(key) {
101
- const meta = this.promptsEditorMeta(key);
102
- if (!meta) {
103
- return;
104
- }
105
- this[meta.highlightField] = highlightMarkdownText(this[meta.contentField], resolvePromptsHljs());
106
- this.syncPromptsOverlayScroll(key);
107
- },
108
-
109
- schedulePromptsHighlight(key) {
110
- const meta = this.promptsEditorMeta(key);
111
- if (!meta) {
112
- return;
113
- }
114
- if (!this._promptsHighlightTimers) {
115
- this._promptsHighlightTimers = {};
116
- }
117
- if (this._promptsHighlightTimers[key]) {
118
- clearTimeout(this._promptsHighlightTimers[key]);
119
- }
120
- this._promptsHighlightTimers[key] = setTimeout(() => {
121
- this._promptsHighlightTimers[key] = null;
122
- this.refreshPromptsHighlight(key);
123
- }, PROMPTS_HIGHLIGHT_DEBOUNCE_MS);
124
- },
125
-
126
- syncPromptsOverlayScroll(key) {
127
- const meta = this.promptsEditorMeta(key);
128
- if (!meta || typeof this.$refs !== 'object' || !this.$refs) {
129
- return;
130
- }
131
- const textarea = this.$refs[meta.textareaRef];
132
- const backdrop = this.$refs[meta.highlightRef];
133
- if (!textarea || !backdrop) {
134
- return;
135
- }
136
- backdrop.scrollTop = textarea.scrollTop;
137
- backdrop.scrollLeft = textarea.scrollLeft;
138
- },
139
-
140
83
  refreshPromptsPreview(key) {
141
84
  const meta = this.promptsEditorMeta(key);
142
85
  if (!meta) {
@@ -174,7 +117,6 @@ export function createPromptsEditorMethods(options = {}) {
174
117
  },
175
118
 
176
119
  schedulePromptsEditorRefresh(key) {
177
- this.schedulePromptsHighlight(key);
178
120
  this.schedulePromptsPreview(key);
179
121
  this.schedulePromptsChangeAxis(key);
180
122
  },
@@ -28,8 +28,8 @@ export function createSessionActionMethods(options = {}) {
28
28
  let error = '';
29
29
  if (!source) {
30
30
  error = '缺少 source 参数';
31
- } else if (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi') {
32
- error = 'source 仅支持 codex、claude、gemini、codebuddy 或 pi';
31
+ } else if (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'workbuddy' && source !== 'pi' && source !== 'opencode') {
32
+ error = 'source 仅支持 codex、claude、gemini、codebuddy、workbuddy、pi 或 opencode';
33
33
  }
34
34
  if (!sessionId && !filePath) {
35
35
  error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
@@ -75,7 +75,13 @@ export function createSessionActionMethods(options = {}) {
75
75
  ? 'Gemini CLI'
76
76
  : (context.params.source === 'pi'
77
77
  ? 'Pi'
78
- : (context.params.source === 'codebuddy' ? 'CodeBuddy Code' : context.params.source))));
78
+ : (context.params.source === 'opencode'
79
+ ? 'OpenCode'
80
+ : (context.params.source === 'codebuddy'
81
+ ? 'CodeBuddy Code'
82
+ : (context.params.source === 'workbuddy'
83
+ ? 'WorkBuddy AI'
84
+ : context.params.source))))));
79
85
  this.activeSession = {
80
86
  source: context.params.source,
81
87
  sourceLabel,
@@ -104,7 +110,7 @@ export function createSessionActionMethods(options = {}) {
104
110
  buildSessionStandaloneUrl(session) {
105
111
  if (!session) return '';
106
112
  const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
107
- if (!source || (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi')) return '';
113
+ if (!source || (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'workbuddy' && source !== 'pi' && source !== 'opencode')) return '';
108
114
  const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
109
115
  const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
110
116
  if (!sessionId && !filePath) return '';
@@ -2,11 +2,11 @@ import {
2
2
  buildSessionFilterCacheState,
3
3
  isSessionQueryEnabled,
4
4
  normalizeSessionMessageRole,
5
- normalizeSessionPathFilter
5
+ normalizeSessionPathFilter,
6
+ normalizeSessionSource
6
7
  } from '../logic.mjs';
7
8
  import {
8
9
  applySessionsFilterUrlState,
9
- buildSessionsFilterShareUrl,
10
10
  normalizeSessionRoleFilter,
11
11
  normalizeSessionTimePreset,
12
12
  readSessionsFilterUrlState,
@@ -118,9 +118,7 @@ export function createSessionBrowserMethods(options = {}) {
118
118
  },
119
119
 
120
120
  syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
121
- const targetSource = source === 'claude'
122
- ? 'claude'
123
- : (source === 'gemini' ? 'gemini' : (source === 'pi' ? 'pi' : (source === 'all' ? 'all' : 'codex')));
121
+ const targetSource = normalizeSessionSource(source, 'all');
124
122
  const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
125
123
  ? this.sessionPathOptionsMap[targetSource]
126
124
  : [];
@@ -135,9 +133,7 @@ export function createSessionBrowserMethods(options = {}) {
135
133
  },
136
134
 
137
135
  refreshSessionPathOptions(source) {
138
- const targetSource = source === 'claude'
139
- ? 'claude'
140
- : (source === 'gemini' ? 'gemini' : (source === 'pi' ? 'pi' : (source === 'all' ? 'all' : 'codex')));
136
+ const targetSource = normalizeSessionSource(source, 'all');
141
137
  const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
142
138
  ? [...this.sessionPathOptionsMap[targetSource]]
143
139
  : [];
@@ -159,9 +155,7 @@ export function createSessionBrowserMethods(options = {}) {
159
155
  },
160
156
 
161
157
  async loadSessionPathOptions(options = {}) {
162
- const source = options.source === 'claude'
163
- ? 'claude'
164
- : (options.source === 'gemini' ? 'gemini' : (options.source === 'pi' ? 'pi' : (options.source === 'all' ? 'all' : 'codex')));
158
+ const source = normalizeSessionSource(options.source, 'all');
165
159
  const forceRefresh = !!options.forceRefresh;
166
160
  const loaded = !!this.sessionPathOptionsLoadedMap[source];
167
161
  if (!forceRefresh && loaded) {
@@ -454,17 +448,28 @@ export function createSessionBrowserMethods(options = {}) {
454
448
  this.loadSessions();
455
449
  },
456
450
 
457
- highlightQueryText(text) {
458
- if (typeof text !== 'string' || !text) return text;
459
- var tokens = this.queryTokens;
460
- if (!tokens || tokens.length === 0) return text;
461
- var escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
462
- for (var i = 0; i < tokens.length; i++) {
463
- var token = tokens[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
464
- var re = new RegExp('(' + token + ')', 'gi');
465
- escaped = escaped.replace(re, '<mark>$1</mark>');
451
+ getSessionPreviewScrollContainer() {
452
+ return this.sessionPreviewScrollEl || (this.$refs && this.$refs.sessionPreviewScroll) || null;
453
+ },
454
+
455
+ scrollSessionPreviewToTop() {
456
+ const scrollEl = this.getSessionPreviewScrollContainer();
457
+ if (!scrollEl) return;
458
+ if (typeof scrollEl.scrollTo === 'function') {
459
+ scrollEl.scrollTo({ top: 0, behavior: 'smooth' });
460
+ } else {
461
+ scrollEl.scrollTop = 0;
462
+ }
463
+ },
464
+
465
+ scrollSessionPreviewToBottom() {
466
+ const scrollEl = this.getSessionPreviewScrollContainer();
467
+ if (!scrollEl) return;
468
+ if (typeof scrollEl.scrollTo === 'function') {
469
+ scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior: 'smooth' });
470
+ } else {
471
+ scrollEl.scrollTop = scrollEl.scrollHeight;
466
472
  }
467
- return escaped;
468
473
  },
469
474
 
470
475
  async onSessionSourceChange(event) {
@@ -513,9 +518,13 @@ export function createSessionBrowserMethods(options = {}) {
513
518
  ? this.t('sessions.source.gemini')
514
519
  : (this.sessionFilterSource === 'codebuddy'
515
520
  ? this.t('sessions.source.codebuddy')
516
- : (this.sessionFilterSource === 'pi'
517
- ? this.t('sessions.source.pi')
518
- : this.sessionFilterSource))));
521
+ : (this.sessionFilterSource === 'workbuddy'
522
+ ? this.t('sessions.source.workbuddy')
523
+ : (this.sessionFilterSource === 'pi'
524
+ ? this.t('sessions.source.pi')
525
+ : (this.sessionFilterSource === 'opencode'
526
+ ? this.t('sessions.source.opencode')
527
+ : this.sessionFilterSource))))));
519
528
  chips.push({ key: 'source', title: this.t('sessions.filters.source'), value: label });
520
529
  }
521
530
  if (this.sessionPathFilter) {
@@ -567,27 +576,6 @@ export function createSessionBrowserMethods(options = {}) {
567
576
  await this.onSessionSourceChange();
568
577
  },
569
578
 
570
- async copySessionsFilterShareUrl() {
571
- const url = buildSessionsFilterShareUrl(this);
572
- if (!url) {
573
- this.showMessage(typeof this.t === 'function' ? this.t('sessions.filters.urlBuildFail') : 'Failed to build link', 'error');
574
- return;
575
- }
576
- try {
577
- if (navigator.clipboard && window.isSecureContext) {
578
- await navigator.clipboard.writeText(url);
579
- this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
580
- return;
581
- }
582
- } catch (_) {}
583
- const ok = typeof this.fallbackCopyText === 'function' ? this.fallbackCopyText(url) : false;
584
- if (ok) {
585
- this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
586
- return;
587
- }
588
- this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.fail') : 'Copy failed', 'error');
589
- },
590
-
591
579
  normalizeSessionMessage(message) {
592
580
  const fallback = {
593
581
  role: 'assistant',
@@ -934,6 +922,7 @@ export function createSessionBrowserMethods(options = {}) {
934
922
  return;
935
923
  }
936
924
  this.activeSession = session;
925
+ this.sessionPreviewScrollAtTop = true;
937
926
  emitSessionLoadNativeDialog(this, 'selectSession:activate', `sessionId=${session.sessionId || ''}`);
938
927
  if (typeof this.expandVisibleSessionList === 'function') {
939
928
  this.expandVisibleSessionList(0, { ensureActive: true });
@@ -262,6 +262,7 @@ export function createSessionTimelineMethods() {
262
262
  ) return;
263
263
  const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
264
264
  if (!scrollEl) return;
265
+ this.sessionPreviewScrollAtTop = Number(scrollEl.scrollTop || 0) <= 8;
265
266
 
266
267
  if (
267
268
  this.canLoadMoreSessionMessages