codexmate 0.0.34 → 0.0.37

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 (34) hide show
  1. package/README.md +14 -5
  2. package/README.zh.md +14 -5
  3. package/cli.js +74 -61
  4. package/package.json +2 -1
  5. package/web-ui/app.js +32 -2
  6. package/web-ui/index.html +1 -1
  7. package/web-ui/logic.sessions.mjs +6 -5
  8. package/web-ui/modules/app.computed.dashboard.mjs +4 -0
  9. package/web-ui/modules/app.computed.session.mjs +147 -6
  10. package/web-ui/modules/app.methods.claude-config.mjs +4 -0
  11. package/web-ui/modules/app.methods.navigation.mjs +32 -16
  12. package/web-ui/modules/app.methods.session-browser.mjs +7 -0
  13. package/web-ui/modules/app.methods.session-trash.mjs +30 -0
  14. package/web-ui/modules/i18n.dict.mjs +5 -0
  15. package/web-ui/modules/sessions-filters-url.mjs +65 -12
  16. package/web-ui/modules/skills.methods.mjs +31 -0
  17. package/web-ui/partials/index/layout-header.html +20 -11
  18. package/web-ui/partials/index/panel-config-claude.html +5 -3
  19. package/web-ui/partials/index/panel-config-codex.html +1 -1
  20. package/web-ui/partials/index/panel-market.html +76 -149
  21. package/web-ui/partials/index/panel-sessions.html +2 -2
  22. package/web-ui/partials/index/panel-settings.html +4 -2
  23. package/web-ui/partials/index/panel-usage.html +111 -68
  24. package/web-ui/res/vue.runtime.global.prod.js +7 -0
  25. package/web-ui/res/web-ui-render.precompiled.js +7269 -0
  26. package/web-ui/session-helpers.mjs +15 -4
  27. package/web-ui/source-bundle.cjs +73 -1
  28. package/web-ui/styles/base-theme.css +10 -0
  29. package/web-ui/styles/layout-shell.css +65 -27
  30. package/web-ui/styles/navigation-panels.css +8 -0
  31. package/web-ui/styles/responsive.css +50 -9
  32. package/web-ui/styles/sessions-usage.css +501 -336
  33. package/web-ui/styles/skills-market.css +294 -0
  34. package/web-ui/styles/titles-cards.css +14 -0
@@ -1,4 +1,4 @@
1
- import { buildSessionListParams } from './logic.mjs';
1
+ import { buildSessionListParams, formatSessionTimelineTimestamp } from './logic.mjs';
2
2
 
3
3
  function clearSessionTimelineRefs(vm) {
4
4
  if (typeof vm.clearSessionTimelineRefs === 'function') {
@@ -90,7 +90,7 @@ export function switchMainTab(tab) {
90
90
  emitSessionLoadDebug(this, 'switchMainTab:start', `from=${previousTab}\nto=${nextTab}`);
91
91
  this.mainTab = nextTab;
92
92
 
93
- if (leavingSessions) {
93
+ if (leavingSessions && this.preserveSessionRenderOnTabLeave !== true) {
94
94
  const teardown = () => {
95
95
  if (this.mainTab === 'sessions') return;
96
96
  if (typeof this.finalizeSessionTabTeardown === 'function') {
@@ -156,7 +156,13 @@ export function switchMainTab(tab) {
156
156
  if (nextTab !== 'orchestration' && typeof this.stopTaskOrchestrationPolling === 'function') {
157
157
  this.stopTaskOrchestrationPolling();
158
158
  }
159
- if (nextTab === 'sessions') {
159
+ if (
160
+ nextTab === 'sessions'
161
+ && (
162
+ !this.sessionListRenderEnabled
163
+ || !this.sessionPreviewRenderEnabled
164
+ )
165
+ ) {
160
166
  this.prepareSessionTabRender();
161
167
  }
162
168
  const shouldLoadTrashListOnSettingsEnter = nextTab === 'settings'
@@ -259,7 +265,12 @@ export async function loadSessions(api, options = {}) {
259
265
  clearSessionTimelineRefs(this);
260
266
  } else {
261
267
  loadSucceeded = true;
262
- this.sessionsList = Array.isArray(res.sessions) ? res.sessions : [];
268
+ const rawSessions = Array.isArray(res.sessions) ? res.sessions : [];
269
+ this.sessionsList = rawSessions.filter(s => s && typeof s === 'object');
270
+ for (const session of this.sessionsList) {
271
+ const rawUpdatedAt = typeof session.updatedAt === 'string' ? session.updatedAt : '';
272
+ session.updatedAtLabel = formatSessionTimelineTimestamp(rawUpdatedAt);
273
+ }
263
274
  emitSessionLoadDebug(this, 'loadSessions:response', `sessions=${this.sessionsList.length}`);
264
275
  if (typeof this.primeSessionListRender === 'function') {
265
276
  this.primeSessionListRender();
@@ -7,6 +7,11 @@ const JS_IMPORT_RE = /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?['"](\.[^'"]+)[
7
7
  const JS_EXPORT_FROM_RE = /(?:^|\n)\s*export\s+\*\s+from\s+['"](\.[^'"]+)['"]\s*;?/g;
8
8
  const JS_RELATIVE_IMPORT_STATEMENT_RE = /(^|\n)([ \t]*)import\s+([\s\S]*?)\s+from\s+['"](\.[^'"]+)['"]\s*;?[ \t]*/g;
9
9
  const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/;
10
+ const VOID_HTML_TAGS = new Set([
11
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
12
+ 'link', 'meta', 'param', 'source', 'track', 'wbr'
13
+ ]);
14
+ const PRECOMPILED_RENDER_PATH = path.join(__dirname, 'res', 'web-ui-render.precompiled.js');
10
15
 
11
16
  function stripBom(content) {
12
17
  return content.replace(/^\uFEFF/, '');
@@ -58,6 +63,69 @@ function bundleCssFile(filePath, stack = []) {
58
63
  });
59
64
  }
60
65
 
66
+ function extractElementInnerHtmlById(html, id) {
67
+ const escapedId = String(id || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
68
+ const startRe = new RegExp(`<([A-Za-z][\\w:-]*)(?=[^>]*\\bid=["']${escapedId}["'])[^>]*>`, 'i');
69
+ const startMatch = startRe.exec(html);
70
+ if (!startMatch) {
71
+ throw new Error(`Unable to find #${id} in bundled Web UI HTML`);
72
+ }
73
+ const tagName = startMatch[1].toLowerCase();
74
+ const openEnd = startMatch.index + startMatch[0].length;
75
+ const tagRe = new RegExp(`</?${tagName}\\b[^>]*>`, 'ig');
76
+ tagRe.lastIndex = openEnd;
77
+ let depth = 1;
78
+ let match = tagRe.exec(html);
79
+ while (match) {
80
+ const rawTag = match[0];
81
+ const isClosing = rawTag.startsWith('</');
82
+ const isSelfClosing = rawTag.endsWith('/>') || VOID_HTML_TAGS.has(tagName);
83
+ if (isClosing) {
84
+ depth -= 1;
85
+ } else if (!isSelfClosing) {
86
+ depth += 1;
87
+ }
88
+ if (depth === 0) {
89
+ return html.slice(openEnd, match.index);
90
+ }
91
+ match = tagRe.exec(html);
92
+ }
93
+ throw new Error(`Unable to find closing </${tagName}> for #${id} in bundled Web UI HTML`);
94
+ }
95
+
96
+ function compileWebUiTemplateToRenderScript(htmlPath = path.join(__dirname, 'index.html')) {
97
+ let compile;
98
+ try {
99
+ ({ compile } = require('@vue/compiler-dom'));
100
+ } catch (e) {
101
+ throw new Error(`Unable to compile Web UI template: @vue/compiler-dom is required (${e.message})`);
102
+ }
103
+ const html = bundleHtmlFile(htmlPath);
104
+ const appTemplate = extractElementInnerHtmlById(html, 'app');
105
+ const result = compile(appTemplate, { mode: 'function', prefixIdentifiers: true });
106
+ return [
107
+ 'window.__CODEXMATE_WEB_UI_RENDER__ = (() => {',
108
+ result.code.trimEnd(),
109
+ '})();',
110
+ ''
111
+ ].join('\n');
112
+ }
113
+
114
+ function readPrecompiledWebUiRenderScript(entryPath = PRECOMPILED_RENDER_PATH) {
115
+ const resolvedPath = path.isAbsolute(entryPath)
116
+ ? entryPath
117
+ : path.resolve(__dirname, entryPath);
118
+ try {
119
+ const source = readUtf8Text(resolvedPath).trimEnd();
120
+ return source ? `${source}\n` : '';
121
+ } catch (error) {
122
+ if (error && error.code === 'ENOENT') {
123
+ return '';
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+
61
129
  function resolveJavaScriptDependencies(filePath) {
62
130
  const source = readUtf8Text(filePath);
63
131
  const dependencies = [];
@@ -212,7 +280,8 @@ function readBundledWebUiScript(entryPath = path.join(__dirname, 'app.js')) {
212
280
  }
213
281
 
214
282
  function readExecutableBundledWebUiScript(entryPath = path.join(__dirname, 'app.js')) {
215
- return bundleExecutableJavaScriptFile(entryPath, { preserveExports: false });
283
+ const renderScript = readPrecompiledWebUiRenderScript() || compileWebUiTemplateToRenderScript();
284
+ return `${renderScript}\n${bundleExecutableJavaScriptFile(entryPath, { preserveExports: false })}`;
216
285
  }
217
286
 
218
287
  function readExecutableBundledJavaScriptModule(entryPath) {
@@ -224,6 +293,9 @@ function readExecutableBundledJavaScriptModule(entryPath) {
224
293
 
225
294
  module.exports = {
226
295
  collectJavaScriptFiles,
296
+ compileWebUiTemplateToRenderScript,
297
+ extractElementInnerHtmlById,
298
+ readPrecompiledWebUiRenderScript,
227
299
  readUtf8Text,
228
300
  readBundledWebUiHtml,
229
301
  readBundledWebUiCss,
@@ -26,6 +26,16 @@
26
26
  --color-success: #4B8B6A;
27
27
  --color-error: #C44536;
28
28
 
29
+ /* Delta 指示色 */
30
+ --color-delta-up: #4caf50;
31
+ --color-delta-down: #f44336;
32
+
33
+ /* 热力图色阶 */
34
+ --color-heatmap-1: rgba(200, 121, 99, 0.2);
35
+ --color-heatmap-2: rgba(200, 121, 99, 0.4);
36
+ --color-heatmap-3: rgba(200, 121, 99, 0.6);
37
+ --color-heatmap-4: rgba(200, 121, 99, 0.85);
38
+
29
39
  --bg-warm-gradient:
30
40
  radial-gradient(circle at 14% 8%, rgba(255, 219, 196, 0.5) 0%, rgba(255, 219, 196, 0) 32%),
31
41
  radial-gradient(circle at 88% 0%, rgba(252, 239, 207, 0.58) 0%, rgba(252, 239, 207, 0) 30%),
@@ -415,6 +415,14 @@ body::after {
415
415
  word-break: break-word;
416
416
  }
417
417
 
418
+ .side-item-ghost {
419
+ opacity: 0;
420
+ pointer-events: none;
421
+ user-select: none;
422
+ cursor: default;
423
+ flex: 0 0 auto;
424
+ }
425
+
418
426
  @media (min-width: 721px) {
419
427
  body:not(.force-compact) #app > .top-tabs {
420
428
  display: none;
@@ -425,58 +433,88 @@ body::after {
425
433
  display: flex;
426
434
  flex-direction: column;
427
435
  align-items: flex-start;
428
- gap: 10px;
429
436
  margin-bottom: 0;
430
- padding: 14px 10px 16px;
431
- border-bottom: 1px solid rgba(137, 111, 94, 0.10);
437
+ padding: 16px 12px 20px;
438
+ border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
439
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 100%);
432
440
  }
433
441
 
434
442
  .brand-head {
435
443
  display: flex;
436
444
  align-items: center;
437
- gap: 12px;
445
+ gap: 10px;
438
446
  }
439
447
 
440
448
  .brand-logo {
441
- width: 38px;
442
- height: 38px;
443
- border-radius: 13px;
449
+ width: 28px;
450
+ height: 28px;
451
+ border-radius: 6px;
444
452
  object-fit: cover;
445
453
  flex-shrink: 0;
446
- box-shadow:
447
- 0 8px 20px rgba(92, 68, 52, 0.10),
448
- 0 0 0 1px rgba(200, 121, 99, 0.08);
454
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5);
455
+ transition: width 0.24s var(--ease-smooth), height 0.24s var(--ease-smooth), box-shadow 0.24s var(--ease-smooth);
456
+ position: relative;
457
+ }
458
+
459
+ .brand-logo::after {
460
+ content: "";
461
+ position: absolute;
462
+ inset: -4px;
463
+ background: radial-gradient(circle, rgba(0, 122, 255, 0.12), transparent 70%);
464
+ opacity: 0;
465
+ transition: opacity 0.3s var(--ease-smooth);
466
+ pointer-events: none;
467
+ }
468
+
469
+ .brand-block:hover .brand-logo {
470
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
471
+ }
472
+
473
+ .brand-block:hover .brand-logo::after {
474
+ opacity: 1;
449
475
  }
450
476
 
451
477
  .brand-copy {
452
478
  display: flex;
453
479
  flex-direction: column;
454
- gap: 4px;
480
+ gap: 0;
455
481
  min-width: 0;
456
482
  }
457
483
 
458
484
  .brand-kicker {
459
- font-size: 18px;
460
- line-height: 1.15;
461
- font-family: var(--font-family-display);
462
- color: var(--color-text-primary);
463
- letter-spacing: -0.02em;
464
- font-weight: 700;
485
+ font-size: 15px;
486
+ line-height: 1.2;
487
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;
488
+ color: #1d1d1f;
489
+ letter-spacing: -0.01em;
490
+ font-weight: 500;
491
+ }
492
+
493
+ @supports (-webkit-background-clip: text) or (background-clip: text) {
494
+ .brand-kicker {
495
+ background: linear-gradient(180deg, #1d1d1f 0%, #2d2d2f 100%);
496
+ -webkit-background-clip: text;
497
+ -webkit-text-fill-color: transparent;
498
+ background-clip: text;
499
+ }
465
500
  }
466
501
 
467
502
  .brand-version {
468
- font-size: 11px;
469
- font-weight: 500;
470
- color: var(--color-text-muted);
471
- vertical-align: middle;
472
- margin-left: 4px;
503
+ font-size: 13px;
504
+ font-weight: 400;
505
+ color: #8e8e93;
506
+ vertical-align: baseline;
507
+ -webkit-text-fill-color: #8e8e93;
473
508
  }
474
509
 
475
- .brand-subtitle {
476
- font-size: 12px;
477
- line-height: 1.45;
478
- color: var(--color-text-secondary);
479
- max-width: 22ch;
510
+ .brand-version-fade-enter-active,
511
+ .brand-version-fade-leave-active {
512
+ transition: opacity 0.2s var(--ease-smooth);
513
+ }
514
+
515
+ .brand-version-fade-enter-from,
516
+ .brand-version-fade-leave-to {
517
+ opacity: 0;
480
518
  }
481
519
 
482
520
  .github-badge {
@@ -38,10 +38,18 @@
38
38
  margin: 0 0 12px;
39
39
  }
40
40
 
41
+ .status-strip-placeholder {
42
+ visibility: hidden;
43
+ pointer-events: none;
44
+ user-select: none;
45
+ }
46
+
41
47
  /* Give the status strip a bit more breathing room under the sticky header. */
42
48
  .main-panel-topbar .status-strip {
43
49
  margin-top: 10px;
44
50
  margin-bottom: 16px;
51
+ min-height: 36px;
52
+ align-items: center;
45
53
  }
46
54
 
47
55
  .lang-toggle {
@@ -85,11 +85,21 @@ textarea:focus-visible {
85
85
  :root {
86
86
  --side-rail-width: min(188px, 46vw);
87
87
  }
88
+
89
+ .container {
90
+ display: flex;
91
+ flex-direction: column;
92
+ height: 100vh;
93
+ min-height: 100vh;
94
+ overflow: hidden;
95
+ }
96
+
88
97
  .app-shell {
89
98
  grid-template-columns: 1fr;
90
- min-height: auto;
99
+ flex: 1 1 auto;
100
+ min-height: 0;
91
101
  height: auto;
92
- overflow: visible;
102
+ overflow: hidden;
93
103
  }
94
104
 
95
105
  .side-rail {
@@ -102,26 +112,34 @@ textarea:focus-visible {
102
112
 
103
113
  .top-tabs {
104
114
  display: flex !important;
115
+ flex: 0 0 auto;
105
116
  flex-wrap: nowrap;
106
117
  overflow-x: auto;
107
118
  overflow-y: hidden;
108
119
  padding-bottom: 2px;
109
120
  margin-left: -2px;
110
121
  margin-right: -2px;
122
+ background: rgba(255, 248, 241, 0.96);
123
+ position: relative;
124
+ z-index: 30;
111
125
  }
112
126
 
113
127
  .main-panel {
114
128
  padding: 0 12px 16px;
115
- height: auto;
116
- overflow-y: visible;
129
+ height: 100%;
130
+ min-height: 0;
131
+ overflow-y: auto;
132
+ -webkit-overflow-scrolling: touch;
117
133
  }
118
134
 
119
135
  .main-panel-topbar {
120
- position: static;
136
+ position: sticky;
137
+ top: 0;
138
+ z-index: 20;
121
139
  margin: 0 0 10px;
122
- padding: 0;
123
- background: transparent;
124
- backdrop-filter: none;
140
+ padding: 0 0 8px;
141
+ background: rgba(255, 248, 241, 0.96);
142
+ backdrop-filter: blur(14px) saturate(130%);
125
143
  }
126
144
 
127
145
  .panel-header-refined {
@@ -155,10 +173,29 @@ textarea:focus-visible {
155
173
  }
156
174
 
157
175
  .status-strip {
158
- grid-template-columns: 1fr;
159
176
  gap: 12px;
160
177
  }
161
178
 
179
+ .main-panel-topbar .status-strip {
180
+ display: flex;
181
+ flex-wrap: nowrap;
182
+ gap: 8px;
183
+ overflow-x: auto;
184
+ overflow-y: hidden;
185
+ padding-bottom: 2px;
186
+ scrollbar-width: none;
187
+ -webkit-overflow-scrolling: touch;
188
+ }
189
+
190
+ .main-panel-topbar .status-strip::-webkit-scrollbar {
191
+ display: none;
192
+ }
193
+
194
+ .main-panel-topbar .status-chip {
195
+ flex: 0 0 auto;
196
+ min-width: max-content;
197
+ }
198
+
162
199
  .market-grid {
163
200
  grid-template-columns: 1fr;
164
201
  }
@@ -212,6 +249,10 @@ textarea:focus-visible {
212
249
  min-width: 100%;
213
250
  }
214
251
 
252
+ .main-panel-topbar .status-chip {
253
+ min-width: max-content;
254
+ }
255
+
215
256
  .btn-add,
216
257
  .btn-tool,
217
258
  .card-action-btn,