codexmate 0.1.25 → 0.1.28

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/cli/config-bootstrap.js +2 -9
  2. package/cli/session-usage.js +10 -4
  3. package/cli.js +772 -225
  4. package/lib/cli-sessions.js +1 -1
  5. package/package.json +3 -3
  6. package/tools/dev/launch.js +147 -0
  7. package/web-ui/app.js +17 -3
  8. package/web-ui/index.html +12 -1
  9. package/web-ui/logic.prompts-token-count.mjs +49 -0
  10. package/web-ui/logic.sessions.mjs +30 -15
  11. package/web-ui/modules/app.computed.dashboard.mjs +1 -0
  12. package/web-ui/modules/app.computed.prompts.mjs +18 -0
  13. package/web-ui/modules/app.computed.session.mjs +3 -1
  14. package/web-ui/modules/app.methods.index.mjs +2 -0
  15. package/web-ui/modules/app.methods.session-actions.mjs +10 -4
  16. package/web-ui/modules/app.methods.session-browser.mjs +34 -45
  17. package/web-ui/modules/app.methods.session-timeline.mjs +1 -0
  18. package/web-ui/modules/app.methods.theme.mjs +38 -0
  19. package/web-ui/modules/app.methods.web-ui-preferences.mjs +9 -0
  20. package/web-ui/modules/i18n/locales/en.mjs +15 -2
  21. package/web-ui/modules/i18n/locales/ja.mjs +15 -2
  22. package/web-ui/modules/i18n/locales/vi.mjs +15 -2
  23. package/web-ui/modules/i18n/locales/zh-tw.mjs +15 -2
  24. package/web-ui/modules/i18n/locales/zh.mjs +15 -2
  25. package/web-ui/partials/index/layout-header.html +4 -2
  26. package/web-ui/partials/index/panel-prompts.html +14 -0
  27. package/web-ui/partials/index/panel-sessions.html +21 -6
  28. package/web-ui/partials/index/panel-settings.html +24 -0
  29. package/web-ui/res/web-ui-render.precompiled.js +115 -23
  30. package/web-ui/session-helpers.mjs +1 -1
  31. package/web-ui/styles/base-theme.css +25 -0
  32. package/web-ui/styles/controls-forms.css +1 -1
  33. package/web-ui/styles/modals-core.css +48 -0
  34. package/web-ui/styles/sessions-list.css +1 -1
  35. package/web-ui/styles/sessions-preview.css +56 -7
  36. package/web-ui/styles/sessions-toolbar-trash.css +2 -8
  37. package/web-ui/styles/sessions-usage.css +1 -1
  38. package/web-ui/styles/settings-panel.css +2 -2
  39. package/web-ui/styles/theme-dark-tokens.css +83 -0
  40. package/web-ui/styles/theme-dark.css +1355 -0
  41. package/web-ui/styles.css +1 -0
@@ -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
@@ -0,0 +1,38 @@
1
+ const THEME_STORAGE_CACHE_KEY = 'codexmateThemeCache';
2
+
3
+ export function createThemeMethods() {
4
+ return {
5
+ normalizeTheme(value) {
6
+ return value === 'dark' ? 'dark' : 'light';
7
+ },
8
+ applyDocumentTheme() {
9
+ try {
10
+ if (typeof document === 'undefined' || !document.documentElement) return;
11
+ const next = this.normalizeTheme(this.theme);
12
+ if (next === 'dark') {
13
+ document.documentElement.setAttribute('data-theme', 'dark');
14
+ } else {
15
+ document.documentElement.removeAttribute('data-theme');
16
+ }
17
+ // Render hint only: lets the inline head script skip the light
18
+ // flash before server preferences arrive. Source of truth is
19
+ // the server-side webUi preference store.
20
+ const storage = typeof globalThis !== 'undefined' ? globalThis.localStorage : null;
21
+ if (storage && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function') {
22
+ if (next === 'dark') storage.setItem(THEME_STORAGE_CACHE_KEY, 'dark');
23
+ else storage.removeItem(THEME_STORAGE_CACHE_KEY);
24
+ }
25
+ } catch (_) {}
26
+ },
27
+ setTheme(theme) {
28
+ const next = this.normalizeTheme(theme);
29
+ if (next !== this.theme) {
30
+ this.theme = next;
31
+ this.applyDocumentTheme();
32
+ }
33
+ if (typeof this.persistWebUiPreferences === 'function') {
34
+ this.persistWebUiPreferences({ theme: next });
35
+ }
36
+ }
37
+ };
38
+ }
@@ -352,6 +352,9 @@ export function createWebUiPreferencesMethods(options = {}) {
352
352
  ? source.projectClaudeMdPath
353
353
  : (typeof this.projectClaudeMdPath === 'string' ? this.projectClaudeMdPath : ''),
354
354
  sidebarCollapsed: normalizeBoolean(hasOwn(source, 'sidebarCollapsed') ? source.sidebarCollapsed : this.sidebarCollapsed, false),
355
+ theme: typeof this.normalizeTheme === 'function'
356
+ ? this.normalizeTheme(hasOwn(source, 'theme') ? source.theme : this.theme)
357
+ : 'light',
355
358
  configModeVisibility: typeof this.normalizeConfigModeVisibility === 'function'
356
359
  ? this.normalizeConfigModeVisibility(hasOwn(source, 'configModeVisibility') ? source.configModeVisibility : this.configModeVisibility)
357
360
  : { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true },
@@ -423,6 +426,12 @@ export function createWebUiPreferencesMethods(options = {}) {
423
426
  if (hasOwn(source, 'sidebarCollapsed')) {
424
427
  this.sidebarCollapsed = normalizeBoolean(source.sidebarCollapsed, false);
425
428
  }
429
+ if (typeof source.theme === 'string') {
430
+ this.theme = typeof this.normalizeTheme === 'function'
431
+ ? this.normalizeTheme(source.theme)
432
+ : 'light';
433
+ if (typeof this.applyDocumentTheme === 'function') this.applyDocumentTheme();
434
+ }
426
435
  if (hasOwn(source, 'configModeVisibility') && typeof this.normalizeConfigModeVisibility === 'function') {
427
436
  this.configModeVisibility = this.normalizeConfigModeVisibility(source.configModeVisibility);
428
437
  }
@@ -254,6 +254,8 @@ const en = Object.freeze({
254
254
  'prompts.editor.previewEmpty': 'Nothing to preview yet',
255
255
  'prompts.editor.changeAxis': 'Change navigation axis',
256
256
  'prompts.editor.changeAxisTick': 'Line {line} (+{added} / -{removed})',
257
+ 'prompts.tokenCount.hint': '~{count} tokens',
258
+ 'prompts.tokenCount.diff': 'Tokens: {before} before → {current} now',
257
259
  'prompts.presets.title': 'Presets',
258
260
  'prompts.presets.hint': 'Manage presets.',
259
261
  'prompts.presets.namePlaceholder': 'Preset name',
@@ -350,6 +352,8 @@ const en = Object.freeze({
350
352
  'dashboard.sessionSource.claude': 'Claude Code',
351
353
  'dashboard.sessionSource.gemini': 'Gemini CLI',
352
354
  'dashboard.sessionSource.codebuddy': 'CodeBuddy Code',
355
+
356
+ 'dashboard.sessionSource.workbuddy': 'WorkBuddy AI',
353
357
  'dashboard.sessionSource.all': 'All',
354
358
  'dashboard.sessionPath.all': 'All paths',
355
359
  'dashboard.sessionQuery.unsupported': 'Unsupported source',
@@ -682,7 +686,10 @@ const en = Object.freeze({
682
686
  'sessions.source.claudeCode': 'Claude Code',
683
687
  'sessions.source.gemini': 'Gemini CLI',
684
688
  'sessions.source.codebuddy': 'CodeBuddy Code',
689
+
690
+ 'sessions.source.workbuddy': 'WorkBuddy AI',
685
691
  'sessions.source.pi': 'Pi',
692
+ 'sessions.source.opencode': 'OpenCode',
686
693
  'sessions.loadingList': 'Loading sessions...',
687
694
  'sessions.empty': 'No sessions found',
688
695
  'sessions.unknownTime': 'unknown time',
@@ -694,10 +701,12 @@ const en = Object.freeze({
694
701
  'sessions.unpin': 'Unpin',
695
702
  'sessions.copyResume': 'Copy resume command',
696
703
  'sessions.preview.refresh': 'Refresh content',
704
+ 'sessions.preview.scrollTop': 'Back to top',
705
+ 'sessions.preview.scrollBottom': 'Jump to bottom',
697
706
  'sessions.preview.loading': 'Loading...',
698
707
  'sessions.batch.select': 'Batch delete',
699
708
  'sessions.batch.exit': 'Exit selection',
700
- 'sessions.batch.selectAll': 'Select all',
709
+ 'sessions.batch.selectAllVisibleN': 'Select {count} visible',
701
710
  'sessions.batch.unselectAll': 'Clear selection',
702
711
  'sessions.batch.deleteSelected': 'Delete selected',
703
712
  'sessions.batch.deleteSelectedN': 'Delete selected ({count})',
@@ -784,7 +793,6 @@ const en = Object.freeze({
784
793
  'sessions.sort.hot': 'Sort: hot',
785
794
  'sessions.sort.hotBadge': 'Hot',
786
795
  'sessions.filters.copyLink': 'Copy filter link',
787
- 'sessions.filters.urlBuildFail': 'Failed to build link',
788
796
  'sessions.filters.source': 'Source',
789
797
  'sessions.filters.path': 'Path',
790
798
  'sessions.filters.keyword': 'Keyword',
@@ -1125,6 +1133,11 @@ const en = Object.freeze({
1125
1133
  'settings.language.meta': 'Choose the Web UI display language',
1126
1134
  'settings.language.label': 'Interface language',
1127
1135
  'settings.language.hint': 'Language preference is saved in this browser.',
1136
+ 'settings.theme.title': 'Appearance',
1137
+ 'settings.theme.meta': 'Choose the Web UI color theme',
1138
+ 'settings.theme.light': 'Light',
1139
+ 'settings.theme.dark': 'Dark',
1140
+ 'settings.theme.hint': 'Theme preference is saved automatically and persists across reloads.',
1128
1141
  'settings.sharePrefix.title': 'Share command prefix',
1129
1142
  'settings.sharePrefix.meta': 'Used as the prefix for “Copy share command” in the Web UI',
1130
1143
  'settings.sharePrefix.label': 'Prefix',
@@ -255,6 +255,8 @@ const ja = Object.freeze({
255
255
  'prompts.editor.previewEmpty': 'プレビューする内容がありません',
256
256
  'prompts.editor.changeAxis': '変更ナビゲーション軸',
257
257
  'prompts.editor.changeAxisTick': '{line} 行目(+{added} / -{removed})',
258
+ 'prompts.tokenCount.hint': '約 {count} トークン',
259
+ 'prompts.tokenCount.diff': 'トークン:変更前 {before} → 現在 {current}',
258
260
  'prompts.presets.title': 'プリセット',
259
261
  'prompts.presets.hint': 'プリセットを管理します。',
260
262
  'prompts.presets.namePlaceholder': 'プリセット名',
@@ -352,6 +354,8 @@ const ja = Object.freeze({
352
354
  'dashboard.sessionSource.claude': 'Claude Code',
353
355
  'dashboard.sessionSource.gemini': 'Gemini CLI',
354
356
  'dashboard.sessionSource.codebuddy': 'CodeBuddy Code',
357
+
358
+ 'dashboard.sessionSource.workbuddy': 'WorkBuddy AI',
355
359
  'dashboard.sessionSource.all': 'すべて',
356
360
  'dashboard.sessionPath.all': 'すべてのパス',
357
361
  'dashboard.sessionQuery.unsupported': '現在のソースは非対応',
@@ -684,7 +688,10 @@ const ja = Object.freeze({
684
688
  'sessions.source.claudeCode': 'Claude Code',
685
689
  'sessions.source.gemini': 'Gemini',
686
690
  'sessions.source.codebuddy': 'CodeBuddy',
691
+
692
+ 'sessions.source.workbuddy': 'WorkBuddy AI',
687
693
  'sessions.source.pi': 'Pi',
694
+ 'sessions.source.opencode': 'OpenCode',
688
695
  'sessions.loadingList': 'セッション一覧を読み込み中...',
689
696
  'sessions.empty': 'セッションがありません',
690
697
  'sessions.unknownTime': '不明な時間',
@@ -696,10 +703,12 @@ const ja = Object.freeze({
696
703
  'sessions.unpin': 'ピン留め解除',
697
704
  'sessions.copyResume': '再開コマンドをコピー',
698
705
  'sessions.preview.refresh': '更新',
706
+ 'sessions.preview.scrollTop': '先頭へ',
707
+ 'sessions.preview.scrollBottom': '末尾へ',
699
708
  'sessions.preview.loading': 'プレビュー読み込み中...',
700
709
  'sessions.batch.select': '一括削除',
701
710
  'sessions.batch.exit': '選択を終了',
702
- 'sessions.batch.selectAll': 'すべて選択',
711
+ 'sessions.batch.selectAllVisibleN': '表示中の {count} 件を選択',
703
712
  'sessions.batch.unselectAll': '選択を解除',
704
713
  'sessions.batch.deleteSelected': '選択を削除',
705
714
  'sessions.batch.deleteSelectedN': '選択を削除({count})',
@@ -787,7 +796,6 @@ const ja = Object.freeze({
787
796
  'sessions.sort.hot': '話題順',
788
797
  'sessions.sort.hotBadge': '話題',
789
798
  'sessions.filters.copyLink': 'リンクをコピー',
790
- 'sessions.filters.urlBuildFail': 'リンク生成失敗',
791
799
  'sessions.filters.source': 'ソース',
792
800
  'sessions.filters.path': 'パス',
793
801
  'sessions.filters.keyword': 'キーワード',
@@ -1128,6 +1136,11 @@ const ja = Object.freeze({
1128
1136
  'settings.language.meta': 'Web UI の表示言語を選択',
1129
1137
  'settings.language.label': 'インターフェース言語',
1130
1138
  'settings.language.hint': '言語設定はこのブラウザに保存されます。',
1139
+ 'settings.theme.title': '外観テーマ',
1140
+ 'settings.theme.meta': 'Web UI のカラーテーマを選択します',
1141
+ 'settings.theme.light': 'ライト',
1142
+ 'settings.theme.dark': 'ダーク',
1143
+ 'settings.theme.hint': 'テーマ設定は自動保存され、再読み込み後も保持されます。',
1131
1144
  'settings.sharePrefix.title': '共有コマンドプレフィックス',
1132
1145
  'settings.sharePrefix.meta': 'Web UI の「共有コマンドをコピー」のプレフィックスに影響',
1133
1146
  'settings.sharePrefix.label': 'プレフィックス',
@@ -283,6 +283,8 @@ const vi = Object.freeze({
283
283
  'prompts.editor.previewEmpty': 'Chưa có nội dung để xem trước',
284
284
  'prompts.editor.changeAxis': 'Trục điều hướng thay đổi',
285
285
  'prompts.editor.changeAxisTick': 'Dòng {line} (+{added} / -{removed})',
286
+ 'prompts.tokenCount.hint': '~{count} token',
287
+ 'prompts.tokenCount.diff': 'Token: trước {before} → hiện tại {current}',
286
288
  'prompts.presets.title': 'Preset',
287
289
  'prompts.presets.hint': 'Quản lý preset.',
288
290
  'prompts.presets.namePlaceholder': 'Tên preset',
@@ -395,6 +397,11 @@ const vi = Object.freeze({
395
397
  'settings.language.meta': 'Chọn ngôn ngữ hiển thị cho Web UI',
396
398
  'settings.language.label': 'Ngôn ngữ giao diện',
397
399
  'settings.language.hint': 'Tùy chọn ngôn ngữ được lưu trong trình duyệt này.',
400
+ 'settings.theme.title': 'Chủ đề giao diện',
401
+ 'settings.theme.meta': 'Chọn chủ đề màu cho Web UI',
402
+ 'settings.theme.light': 'Sáng',
403
+ 'settings.theme.dark': 'Tối',
404
+ 'settings.theme.hint': 'Tùy chọn chủ đề được tự động lưu và giữ nguyên sau khi tải lại.',
398
405
 
399
406
  'settings.webhook.title': 'Webhook',
400
407
  'settings.webhook.meta': 'Gửi thông báo sự kiện tới dịch vụ bên ngoài',
@@ -633,6 +640,8 @@ const vi = Object.freeze({
633
640
  'dashboard.sessionSource.claude': 'Claude Code',
634
641
  'dashboard.sessionSource.gemini': 'Gemini CLI',
635
642
  'dashboard.sessionSource.codebuddy': 'CodeBuddy Code',
643
+
644
+ 'dashboard.sessionSource.workbuddy': 'WorkBuddy AI',
636
645
  'dashboard.sessionSource.all': 'Tất cả',
637
646
  'dashboard.sessionPath.all': 'Tất cả đường dẫn',
638
647
  'dashboard.sessionQuery.unsupported': 'Nguồn không được hỗ trợ',
@@ -859,7 +868,10 @@ const vi = Object.freeze({
859
868
  'sessions.source.claudeCode': 'Claude Code',
860
869
  'sessions.source.gemini': 'Gemini CLI',
861
870
  'sessions.source.codebuddy': 'CodeBuddy Code',
871
+
872
+ 'sessions.source.workbuddy': 'WorkBuddy AI',
862
873
  'sessions.source.pi': 'Pi',
874
+ 'sessions.source.opencode': 'OpenCode',
863
875
  'sessions.loadingList': 'Đang tải phiên làm việc...',
864
876
  'sessions.empty': 'Không tìm thấy phiên nào',
865
877
  'sessions.unknownTime': 'thời gian không rõ',
@@ -869,10 +881,12 @@ const vi = Object.freeze({
869
881
  'sessions.unpin': 'Bỏ ghim',
870
882
  'sessions.copyResume': 'Sao chép lệnh tiếp tục',
871
883
  'sessions.preview.refresh': 'Làm mới nội dung',
884
+ 'sessions.preview.scrollTop': 'Về đầu',
885
+ 'sessions.preview.scrollBottom': 'Xuống cuối',
872
886
  'sessions.preview.loading': 'Đang tải...',
873
887
  'sessions.batch.select': 'Xóa hàng loạt',
874
888
  'sessions.batch.exit': 'Thoát chọn',
875
- 'sessions.batch.selectAll': 'Chọn tất cả',
889
+ 'sessions.batch.selectAllVisibleN': 'Chọn {count} mục hiển thị',
876
890
  'sessions.batch.unselectAll': 'Bỏ chọn tất cả',
877
891
  'sessions.batch.deleteSelected': 'Xóa đã chọn',
878
892
  'sessions.batch.deleteSelectedN': 'Xóa đã chọn ({count})',
@@ -958,7 +972,6 @@ const vi = Object.freeze({
958
972
  'sessions.sort.hot': 'Sắp xếp: hot',
959
973
  'sessions.sort.hotBadge': 'Hot',
960
974
  'sessions.filters.copyLink': 'Sao chép link bộ lọc',
961
- 'sessions.filters.urlBuildFail': 'Tạo link thất bại',
962
975
  'sessions.filters.source': 'Nguồn',
963
976
  'sessions.filters.path': 'Đường dẫn',
964
977
  'sessions.filters.keyword': 'Từ khóa',
@@ -254,6 +254,8 @@ const zhTw = Object.freeze({
254
254
  'prompts.editor.previewEmpty': '尚無內容可預覽',
255
255
  'prompts.editor.changeAxis': '改動導覽軸',
256
256
  'prompts.editor.changeAxisTick': '第 {line} 行(+{added} / -{removed})',
257
+ 'prompts.tokenCount.hint': '約 {count} tokens',
258
+ 'prompts.tokenCount.diff': 'Tokens:修改前 {before} → 目前 {current}',
257
259
  'prompts.presets.title': '預設池',
258
260
  'prompts.presets.hint': '管理預設。',
259
261
  'prompts.presets.namePlaceholder': '預設名稱',
@@ -350,6 +352,8 @@ const zhTw = Object.freeze({
350
352
  'dashboard.sessionSource.claude': 'Claude Code',
351
353
  'dashboard.sessionSource.gemini': 'Gemini CLI',
352
354
  'dashboard.sessionSource.codebuddy': 'CodeBuddy Code',
355
+
356
+ 'dashboard.sessionSource.workbuddy': 'WorkBuddy AI',
353
357
  'dashboard.sessionSource.all': '全部',
354
358
  'dashboard.sessionPath.all': '全部路徑',
355
359
  'dashboard.sessionQuery.unsupported': '目前來源不支持',
@@ -682,7 +686,10 @@ const zhTw = Object.freeze({
682
686
  'sessions.source.claudeCode': 'Claude Code',
683
687
  'sessions.source.gemini': 'Gemini CLI',
684
688
  'sessions.source.codebuddy': 'CodeBuddy Code',
689
+
690
+ 'sessions.source.workbuddy': 'WorkBuddy AI',
685
691
  'sessions.source.pi': 'Pi',
692
+ 'sessions.source.opencode': 'OpenCode',
686
693
  'sessions.loadingList': '會話載入中...',
687
694
  'sessions.empty': '暫無可用會話記錄',
688
695
  'sessions.unknownTime': '未知時間',
@@ -693,10 +700,12 @@ const zhTw = Object.freeze({
693
700
  'sessions.unpin': '取消置頂',
694
701
  'sessions.copyResume': '複製恢復命令',
695
702
  'sessions.preview.refresh': '重新整理內容',
703
+ 'sessions.preview.scrollTop': '回到頂部',
704
+ 'sessions.preview.scrollBottom': '跳到底部',
696
705
  'sessions.preview.loading': '載入中...',
697
706
  'sessions.batch.select': '批次刪除',
698
707
  'sessions.batch.exit': '退出選擇',
699
- 'sessions.batch.selectAll': '全選',
708
+ 'sessions.batch.selectAllVisibleN': '全選可見 {count} 項',
700
709
  'sessions.batch.unselectAll': '取消全選',
701
710
  'sessions.batch.deleteSelected': '刪除選中',
702
711
  'sessions.batch.deleteSelectedN': '刪除選中({count})',
@@ -783,7 +792,6 @@ const zhTw = Object.freeze({
783
792
  'sessions.sort.hot': '按熱度',
784
793
  'sessions.sort.hotBadge': '熱',
785
794
  'sessions.filters.copyLink': '複製篩選鏈接',
786
- 'sessions.filters.urlBuildFail': '無法生成鏈接',
787
795
  'sessions.filters.source': '來源',
788
796
  'sessions.filters.path': '路徑',
789
797
  'sessions.filters.keyword': '關鍵詞',
@@ -1124,6 +1132,11 @@ const zhTw = Object.freeze({
1124
1132
  'settings.language.meta': '選擇 Web UI 的顯示語言',
1125
1133
  'settings.language.label': '界面語言',
1126
1134
  'settings.language.hint': '語言偏好會保存在目前瀏覽器。',
1135
+ 'settings.theme.title': '外觀主題',
1136
+ 'settings.theme.meta': '選擇 Web UI 的配色主題',
1137
+ 'settings.theme.light': '淺色',
1138
+ 'settings.theme.dark': '深色',
1139
+ 'settings.theme.hint': '主題偏好會自動儲存,重新整理後保持不變。',
1127
1140
  'settings.sharePrefix.title': '分享命令前綴',
1128
1141
  'settings.sharePrefix.meta': '影響 Web UI 裡“複製分享命令”的前綴',
1129
1142
  'settings.sharePrefix.label': '前綴',
@@ -254,6 +254,8 @@ const zh = Object.freeze({
254
254
  'prompts.editor.previewEmpty': '暂无内容可预览',
255
255
  'prompts.editor.changeAxis': '改动导航轴',
256
256
  'prompts.editor.changeAxisTick': '第 {line} 行(+{added} / -{removed})',
257
+ 'prompts.tokenCount.hint': '约 {count} tokens',
258
+ 'prompts.tokenCount.diff': 'Tokens:修改前 {before} → 当前 {current}',
257
259
  'prompts.presets.title': '预设池',
258
260
  'prompts.presets.hint': '管理预设。',
259
261
  'prompts.presets.namePlaceholder': '预设名称',
@@ -350,6 +352,8 @@ const zh = Object.freeze({
350
352
  'dashboard.sessionSource.claude': 'Claude Code',
351
353
  'dashboard.sessionSource.gemini': 'Gemini CLI',
352
354
  'dashboard.sessionSource.codebuddy': 'CodeBuddy Code',
355
+
356
+ 'dashboard.sessionSource.workbuddy': 'WorkBuddy AI',
353
357
  'dashboard.sessionSource.all': '全部',
354
358
  'dashboard.sessionPath.all': '全部路径',
355
359
  'dashboard.sessionQuery.unsupported': '当前来源不支持',
@@ -682,7 +686,10 @@ const zh = Object.freeze({
682
686
  'sessions.source.claudeCode': 'Claude Code',
683
687
  'sessions.source.gemini': 'Gemini CLI',
684
688
  'sessions.source.codebuddy': 'CodeBuddy Code',
689
+
690
+ 'sessions.source.workbuddy': 'WorkBuddy AI',
685
691
  'sessions.source.pi': 'Pi',
692
+ 'sessions.source.opencode': 'OpenCode',
686
693
  'sessions.loadingList': '会话加载中...',
687
694
  'sessions.empty': '暂无可用会话记录',
688
695
  'sessions.unknownTime': '未知时间',
@@ -693,10 +700,12 @@ const zh = Object.freeze({
693
700
  'sessions.unpin': '取消置顶',
694
701
  'sessions.copyResume': '复制恢复命令',
695
702
  'sessions.preview.refresh': '刷新内容',
703
+ 'sessions.preview.scrollTop': '回到顶部',
704
+ 'sessions.preview.scrollBottom': '跳到底部',
696
705
  'sessions.preview.loading': '加载中...',
697
706
  'sessions.batch.select': '批量删除',
698
707
  'sessions.batch.exit': '退出选择',
699
- 'sessions.batch.selectAll': '全选',
708
+ 'sessions.batch.selectAllVisibleN': '全选可见 {count} 项',
700
709
  'sessions.batch.unselectAll': '取消全选',
701
710
  'sessions.batch.deleteSelected': '删除选中',
702
711
  'sessions.batch.deleteSelectedN': '删除选中({count})',
@@ -783,7 +792,6 @@ const zh = Object.freeze({
783
792
  'sessions.sort.hot': '按热度',
784
793
  'sessions.sort.hotBadge': '热',
785
794
  'sessions.filters.copyLink': '复制筛选链接',
786
- 'sessions.filters.urlBuildFail': '无法生成链接',
787
795
  'sessions.filters.source': '来源',
788
796
  'sessions.filters.path': '路径',
789
797
  'sessions.filters.keyword': '关键词',
@@ -1124,6 +1132,11 @@ const zh = Object.freeze({
1124
1132
  'settings.language.meta': '选择 Web UI 的显示语言',
1125
1133
  'settings.language.label': '界面语言',
1126
1134
  'settings.language.hint': '语言偏好会保存在当前浏览器。',
1135
+ 'settings.theme.title': '外观主题',
1136
+ 'settings.theme.meta': '选择 Web UI 的配色主题',
1137
+ 'settings.theme.light': '浅色',
1138
+ 'settings.theme.dark': '深色',
1139
+ 'settings.theme.hint': '主题偏好会自动保存,刷新后保持不变。',
1127
1140
  'settings.sharePrefix.title': '分享命令前缀',
1128
1141
  'settings.sharePrefix.meta': '影响 Web UI 里“复制分享命令”的前缀',
1129
1142
  'settings.sharePrefix.label': '前缀',
@@ -289,7 +289,7 @@
289
289
  <div class="side-item-title">{{ t('side.sessions.browser') }}</div>
290
290
  <div class="side-item-meta">
291
291
  <span>{{ t('side.sessions.browser.meta') }}</span>
292
- <span>{{ t('sessions.sourceLabel', { value: (sessionFilterSource === 'all' ? t('sessions.source.all') : (sessionFilterSource === 'claude' ? 'Claude Code' : (sessionFilterSource === 'gemini' ? 'Gemini CLI' : (sessionFilterSource === 'codebuddy' ? 'CodeBuddy Code' : 'Codex')))) }) }}</span>
292
+ <span>{{ t('sessions.sourceLabel', { value: (sessionFilterSource === 'all' ? t('sessions.source.all') : (sessionFilterSource === 'claude' ? 'Claude Code' : (sessionFilterSource === 'gemini' ? 'Gemini CLI' : (sessionFilterSource === 'codebuddy' ? 'CodeBuddy Code' : (sessionFilterSource === 'workbuddy' ? 'WorkBuddy AI' : 'Codex'))))) }) }}</span>
293
293
  </div>
294
294
  </button>
295
295
  <button
@@ -527,7 +527,9 @@
527
527
  ? t('sessions.source.gemini')
528
528
  : (sessionFilterSource === 'codebuddy'
529
529
  ? t('sessions.source.codebuddy')
530
- : t('sessions.source.codex'))))) }}
530
+ : (sessionFilterSource === 'workbuddy'
531
+ ? t('sessions.source.workbuddy')
532
+ : t('sessions.source.codex')))))) }}
531
533
  </span>
532
534
  </div>
533
535
  <div class="status-chip">
@@ -42,6 +42,7 @@
42
42
  <span v-if="agentsPath">
43
43
  ({{ agentsExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }})
44
44
  </span>
45
+ <span v-if="agentsPath" class="prompts-token-hint">· {{ t('prompts.tokenCount.hint', { count: agentsTokenCount }) }}</span>
45
46
  </div>
46
47
  <div class="prompts-editor-actions">
47
48
  <div class="prompts-editor-group prompts-editor-group--pathref">
@@ -182,6 +183,12 @@
182
183
 
183
184
  <div class="form-group">
184
185
  <div v-if="agentsDiffVisible">
186
+ <div
187
+ v-if="!agentsDiffLoading && !agentsDiffError"
188
+ class="agents-diff-token-summary">
189
+ <span class="agents-diff-token-label">{{ t('prompts.tokenCount.diff', { before: agentsDiffTokenStats.before, current: agentsDiffTokenStats.current }) }}</span>
190
+ <span :class="['agents-diff-token-delta', 'agents-diff-token-delta--' + agentsDiffTokenStats.deltaKind]">{{ agentsDiffTokenStats.deltaLabel }}</span>
191
+ </div>
185
192
  <div
186
193
  v-if="!agentsDiffLoading && !agentsDiffError && !agentsDiffTruncated && (agentsDiffStats.added || agentsDiffStats.removed)"
187
194
  class="agents-diff-summary">
@@ -274,6 +281,7 @@
274
281
  <span v-if="sysPromptPath">
275
282
  ({{ sysPromptExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }})
276
283
  </span>
284
+ <span v-if="sysPromptPath" class="prompts-token-hint">· {{ t('prompts.tokenCount.hint', { count: sysPromptTokenCount }) }}</span>
277
285
  </div>
278
286
  <div class="prompts-editor-actions">
279
287
  <div class="prompts-editor-group prompts-editor-group--pathref">
@@ -335,6 +343,12 @@
335
343
  </details>
336
344
  <div class="form-group">
337
345
  <div v-if="sysPromptDiffVisible">
346
+ <div
347
+ v-if="!sysPromptDiffLoading && !sysPromptDiffError"
348
+ class="agents-diff-token-summary">
349
+ <span class="agents-diff-token-label">{{ t('prompts.tokenCount.diff', { before: sysPromptDiffTokenStats.before, current: sysPromptDiffTokenStats.current }) }}</span>
350
+ <span :class="['agents-diff-token-delta', 'agents-diff-token-delta--' + sysPromptDiffTokenStats.deltaKind]">{{ sysPromptDiffTokenStats.deltaLabel }}</span>
351
+ </div>
338
352
  <div
339
353
  v-if="!sysPromptDiffLoading && !sysPromptDiffError && !sysPromptDiffTruncated && (sysPromptDiffStats.added || sysPromptDiffStats.removed)"
340
354
  class="agents-diff-summary">