codexmate 0.0.34 → 0.0.36

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 (32) hide show
  1. package/cli.js +74 -61
  2. package/package.json +2 -1
  3. package/web-ui/app.js +33 -2
  4. package/web-ui/index.html +1 -1
  5. package/web-ui/logic.sessions.mjs +6 -5
  6. package/web-ui/modules/app.computed.dashboard.mjs +4 -0
  7. package/web-ui/modules/app.computed.session.mjs +147 -6
  8. package/web-ui/modules/app.methods.claude-config.mjs +4 -0
  9. package/web-ui/modules/app.methods.navigation.mjs +32 -16
  10. package/web-ui/modules/app.methods.session-browser.mjs +7 -0
  11. package/web-ui/modules/app.methods.session-trash.mjs +30 -0
  12. package/web-ui/modules/i18n.dict.mjs +5 -0
  13. package/web-ui/modules/sessions-filters-url.mjs +65 -12
  14. package/web-ui/modules/skills.methods.mjs +31 -0
  15. package/web-ui/partials/index/layout-header.html +17 -12
  16. package/web-ui/partials/index/panel-config-claude.html +5 -3
  17. package/web-ui/partials/index/panel-config-codex.html +1 -1
  18. package/web-ui/partials/index/panel-market.html +76 -149
  19. package/web-ui/partials/index/panel-sessions.html +2 -2
  20. package/web-ui/partials/index/panel-settings.html +4 -2
  21. package/web-ui/partials/index/panel-usage.html +115 -68
  22. package/web-ui/res/vue.runtime.global.prod.js +7 -0
  23. package/web-ui/res/web-ui-render.precompiled.js +7274 -0
  24. package/web-ui/session-helpers.mjs +15 -4
  25. package/web-ui/source-bundle.cjs +73 -1
  26. package/web-ui/styles/base-theme.css +10 -0
  27. package/web-ui/styles/layout-shell.css +66 -27
  28. package/web-ui/styles/navigation-panels.css +8 -0
  29. package/web-ui/styles/responsive.css +50 -9
  30. package/web-ui/styles/sessions-usage.css +336 -319
  31. package/web-ui/styles/skills-market.css +294 -0
  32. 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%),
@@ -425,58 +425,97 @@ body::after {
425
425
  display: flex;
426
426
  flex-direction: column;
427
427
  align-items: flex-start;
428
- gap: 10px;
429
428
  margin-bottom: 0;
430
- padding: 14px 10px 16px;
431
- border-bottom: 1px solid rgba(137, 111, 94, 0.10);
429
+ padding: 16px 12px 20px;
430
+ border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
431
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 100%);
432
+ }
433
+
434
+ .brand-block:focus-visible {
435
+ outline: 2px solid rgba(0, 122, 255, 0.5);
436
+ outline-offset: -2px;
437
+ border-radius: 4px;
438
+ }
439
+
440
+ .brand-block:focus:not(:focus-visible) {
441
+ outline: none;
432
442
  }
433
443
 
434
444
  .brand-head {
435
445
  display: flex;
436
446
  align-items: center;
437
- gap: 12px;
447
+ gap: 10px;
438
448
  }
439
449
 
440
450
  .brand-logo {
441
- width: 38px;
442
- height: 38px;
443
- border-radius: 13px;
451
+ width: 28px;
452
+ height: 28px;
453
+ border-radius: 6px;
444
454
  object-fit: cover;
445
455
  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);
456
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5);
457
+ transition: width 0.24s var(--ease-smooth), height 0.24s var(--ease-smooth), box-shadow 0.24s var(--ease-smooth);
458
+ position: relative;
459
+ }
460
+
461
+ .brand-logo::after {
462
+ content: "";
463
+ position: absolute;
464
+ inset: -4px;
465
+ background: radial-gradient(circle, rgba(0, 122, 255, 0.12), transparent 70%);
466
+ opacity: 0;
467
+ transition: opacity 0.3s var(--ease-smooth);
468
+ pointer-events: none;
469
+ }
470
+
471
+ .brand-block:hover .brand-logo {
472
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
473
+ }
474
+
475
+ .brand-block:hover .brand-logo::after {
476
+ opacity: 1;
449
477
  }
450
478
 
451
479
  .brand-copy {
452
480
  display: flex;
453
481
  flex-direction: column;
454
- gap: 4px;
482
+ gap: 0;
455
483
  min-width: 0;
456
484
  }
457
485
 
458
486
  .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;
487
+ font-size: 13px;
488
+ line-height: 1.2;
489
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;
490
+ color: #1d1d1f;
491
+ letter-spacing: -0.01em;
492
+ font-weight: 500;
465
493
  }
466
494
 
467
- .brand-version {
468
- font-size: 11px;
469
- font-weight: 500;
470
- color: var(--color-text-muted);
471
- vertical-align: middle;
472
- margin-left: 4px;
495
+ @supports (-webkit-background-clip: text) or (background-clip: text) {
496
+ .brand-kicker {
497
+ background: linear-gradient(180deg, #1d1d1f 0%, #2d2d2f 100%);
498
+ -webkit-background-clip: text;
499
+ -webkit-text-fill-color: transparent;
500
+ background-clip: text;
501
+ }
473
502
  }
474
503
 
475
- .brand-subtitle {
504
+ .brand-version {
476
505
  font-size: 12px;
477
- line-height: 1.45;
478
- color: var(--color-text-secondary);
479
- max-width: 22ch;
506
+ font-weight: 400;
507
+ color: #8e8e93;
508
+ vertical-align: baseline;
509
+ }
510
+
511
+ .brand-version-fade-enter-active,
512
+ .brand-version-fade-leave-active {
513
+ transition: opacity 0.2s var(--ease-smooth);
514
+ }
515
+
516
+ .brand-version-fade-enter-from,
517
+ .brand-version-fade-leave-to {
518
+ opacity: 0;
480
519
  }
481
520
 
482
521
  .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,