@polderlabs/bizar 4.5.1 → 4.5.2
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.
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +361 -0
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +1 -0
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
- package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js → mobile-BRhoDOUz.js} +1 -2
- package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js.map → mobile-BRhoDOUz.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-O6ANdD4W.js → mobile-lbH6szyX.js} +2 -3
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/src/server/headroom.mjs +37 -35
- package/bizar-dash/src/server/memory-lightrag.mjs +39 -31
- package/bizar-dash/src/server/mods-loader.mjs +13 -5
- package/bizar-dash/src/server/providers-store.mjs +73 -1
- package/bizar-dash/src/server/routes/chat.mjs +38 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +6 -2
- package/bizar-dash/src/server/routes/memory.mjs +12 -4
- package/bizar-dash/src/server/schedules-runner.mjs +4 -4
- package/bizar-dash/src/server/server.mjs +39 -37
- package/bizar-dash/src/server/watcher.mjs +2 -2
- package/bizar-dash/src/web/App.tsx +40 -6
- package/bizar-dash/src/web/components/Toast.tsx +1 -1
- package/bizar-dash/src/web/components/Topbar.tsx +1 -1
- package/bizar-dash/src/web/lib/api.ts +12 -11
- package/bizar-dash/src/web/styles/chat.css +2 -0
- package/bizar-dash/src/web/styles/main.css +52 -23
- package/bizar-dash/src/web/styles/tasks.css +2 -0
- package/bizar-dash/src/web/views/Memory.tsx +4 -3
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
- package/bizar-dash/src/web/views/Overview.tsx +4 -2
- package/bizar-dash/src/web/views/Settings.tsx +2 -1
- package/bizar-dash/src/web/views/Skills.tsx +3 -2
- package/bizar-dash/src/web/views/Tasks.tsx +4 -3
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +8 -6
- package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
- package/cli/artifact.mjs +11 -4
- package/cli/bin.mjs +46 -12
- package/cli/doctor.mjs +5 -3
- package/cli/memory.mjs +65 -21
- package/cli/provision.mjs +8 -2
- package/install.sh +1 -2
- package/package.json +3 -3
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +0 -1
- package/bizar-dash/dist/assets/main-usWhlPWa.js +0 -362
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +0 -1
|
@@ -24,6 +24,7 @@ import { agentsStore } from './agents-store.mjs';
|
|
|
24
24
|
import { tasksStore } from './tasks-store.mjs';
|
|
25
25
|
import { schedulesStore } from './schedules-store.mjs';
|
|
26
26
|
import { providersStore, mcpsStore } from './providers-store.mjs';
|
|
27
|
+
import { readOpencodeJsonCached } from './providers-store.mjs';
|
|
27
28
|
import { homedir } from 'node:os';
|
|
28
29
|
import { startBgPoller, stopBgPoller } from './bg-poller.mjs';
|
|
29
30
|
import { startBgRetryLoop, stopBgRetryLoop } from './bg-retry.mjs';
|
|
@@ -281,8 +282,8 @@ export async function createServer({
|
|
|
281
282
|
if (client.bufferedAmount > WS_BACKPRESSURE_LIMIT_BYTES) {
|
|
282
283
|
try {
|
|
283
284
|
client.terminate();
|
|
284
|
-
} catch {
|
|
285
|
-
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.warn('swallowed in terminate (backpressure):', err.message);
|
|
286
287
|
}
|
|
287
288
|
return false;
|
|
288
289
|
}
|
|
@@ -396,8 +397,8 @@ export async function createServer({
|
|
|
396
397
|
try {
|
|
397
398
|
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
398
399
|
socket.destroy();
|
|
399
|
-
} catch {
|
|
400
|
-
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.warn('swallowed in 403 destroy:', err.message);
|
|
401
402
|
}
|
|
402
403
|
return;
|
|
403
404
|
}
|
|
@@ -405,8 +406,8 @@ export async function createServer({
|
|
|
405
406
|
try {
|
|
406
407
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
407
408
|
socket.destroy();
|
|
408
|
-
} catch {
|
|
409
|
-
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.warn('swallowed in 401 destroy:', err.message);
|
|
410
411
|
}
|
|
411
412
|
return;
|
|
412
413
|
}
|
|
@@ -420,8 +421,8 @@ export async function createServer({
|
|
|
420
421
|
if (client.isAlive === false) {
|
|
421
422
|
try {
|
|
422
423
|
client.terminate();
|
|
423
|
-
} catch {
|
|
424
|
-
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.warn('swallowed in heartbeat terminate:', err.message);
|
|
425
426
|
}
|
|
426
427
|
return;
|
|
427
428
|
}
|
|
@@ -431,8 +432,8 @@ export async function createServer({
|
|
|
431
432
|
} catch {
|
|
432
433
|
try {
|
|
433
434
|
client.terminate();
|
|
434
|
-
} catch {
|
|
435
|
-
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.warn('swallowed in ping-fail terminate:', err.message);
|
|
436
437
|
}
|
|
437
438
|
}
|
|
438
439
|
});
|
|
@@ -577,8 +578,8 @@ export async function createServer({
|
|
|
577
578
|
}
|
|
578
579
|
fileSize = newSize;
|
|
579
580
|
}
|
|
580
|
-
} catch {
|
|
581
|
-
|
|
581
|
+
} catch (err) {
|
|
582
|
+
console.warn('swallowed in sendLogChunk:', err.message);
|
|
582
583
|
}
|
|
583
584
|
}
|
|
584
585
|
|
|
@@ -617,8 +618,8 @@ export async function createServer({
|
|
|
617
618
|
data: buildSnapshotSafe(state, opencodeConfigDir),
|
|
618
619
|
}),
|
|
619
620
|
);
|
|
620
|
-
} catch {
|
|
621
|
-
|
|
621
|
+
} catch (err) {
|
|
622
|
+
console.warn('swallowed in ws snapshot send:', err.message);
|
|
622
623
|
}
|
|
623
624
|
|
|
624
625
|
ws.on('message', (raw) => {
|
|
@@ -678,34 +679,34 @@ export async function createServer({
|
|
|
678
679
|
function close() {
|
|
679
680
|
try {
|
|
680
681
|
stopBgPoller();
|
|
681
|
-
} catch {
|
|
682
|
-
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.warn('swallowed in stopBgPoller:', err.message);
|
|
683
684
|
}
|
|
684
685
|
try {
|
|
685
686
|
stopBgRetryLoop();
|
|
686
|
-
} catch {
|
|
687
|
-
|
|
687
|
+
} catch (err) {
|
|
688
|
+
console.warn('swallowed in stopBgRetryLoop:', err.message);
|
|
688
689
|
}
|
|
689
690
|
try {
|
|
690
691
|
watcher.stop();
|
|
691
|
-
} catch {
|
|
692
|
-
|
|
692
|
+
} catch (err) {
|
|
693
|
+
console.warn('swallowed in watcher.stop:', err.message);
|
|
693
694
|
}
|
|
694
695
|
try {
|
|
695
696
|
wss.clients.forEach((c) => c.terminate());
|
|
696
697
|
wss.close();
|
|
697
|
-
} catch {
|
|
698
|
-
|
|
698
|
+
} catch (err) {
|
|
699
|
+
console.warn('swallowed in wss.close:', err.message);
|
|
699
700
|
}
|
|
700
701
|
try {
|
|
701
702
|
clearInterval(heartbeatInterval);
|
|
702
|
-
} catch {
|
|
703
|
-
|
|
703
|
+
} catch (err) {
|
|
704
|
+
console.warn('swallowed in clearInterval:', err.message);
|
|
704
705
|
}
|
|
705
706
|
try {
|
|
706
707
|
server.close();
|
|
707
|
-
} catch {
|
|
708
|
-
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.warn('swallowed in server.close:', err.message);
|
|
709
710
|
}
|
|
710
711
|
currentBroadcast = () => {};
|
|
711
712
|
}
|
|
@@ -723,14 +724,15 @@ function buildSnapshotSafe(state, opencodeConfigDir) {
|
|
|
723
724
|
|
|
724
725
|
function buildSnapshot(state, opencodeConfigDir) {
|
|
725
726
|
const cfgFile = join(opencodeConfigDir, 'opencode.json');
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
727
|
+
// v5.0.0 — Bug S2: read opencode.json via the 1s-debounced cache in
|
|
728
|
+
// providers-store.mjs. WS clients connect on snapshot delivery; with N
|
|
729
|
+
// clients the readFileSync here was the main per-connection blocker.
|
|
730
|
+
// The cache collapses all reads within a 1-second window to a single
|
|
731
|
+
// disk hit. Writes (settings change, providers-store mutations)
|
|
732
|
+
// invalidate the cache, so the snapshot always reflects the latest
|
|
733
|
+
// state within at most 1s.
|
|
734
|
+
const exists = existsSync(cfgFile);
|
|
735
|
+
const cfg = readOpencodeJsonCached();
|
|
734
736
|
const activeProject = projectsStore.active();
|
|
735
737
|
return {
|
|
736
738
|
overview: state.getOverview(),
|
|
@@ -740,9 +742,9 @@ function buildSnapshot(state, opencodeConfigDir) {
|
|
|
740
742
|
activeProject,
|
|
741
743
|
config: {
|
|
742
744
|
path: cfgFile,
|
|
743
|
-
data: cfg,
|
|
744
|
-
raw: cfg ? JSON.stringify(cfg, null, 2) : '',
|
|
745
|
-
exists
|
|
745
|
+
data: exists ? cfg : null,
|
|
746
|
+
raw: exists && cfg ? JSON.stringify(cfg, null, 2) : '',
|
|
747
|
+
exists,
|
|
746
748
|
},
|
|
747
749
|
settings: readSettings(),
|
|
748
750
|
tasks: activeProject ? tasksStore.loadTasks(activeProject.id) : [],
|
|
@@ -69,8 +69,8 @@ export function createWatcher({ paths, onChange, options = {} }) {
|
|
|
69
69
|
async stop() {
|
|
70
70
|
try {
|
|
71
71
|
await watcher.close();
|
|
72
|
-
} catch {
|
|
73
|
-
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn('swallowed in watcher.close:', err.message);
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
/** Force a synthetic broadcast — useful after a self-mutation. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/App.tsx — root shell. Wires data + contexts + tab routing.
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Component, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { Topbar, TABS } from './components/Topbar';
|
|
5
5
|
import { Sidebar } from './components/Sidebar';
|
|
6
6
|
import { ModalProvider, useModal } from './components/Modal';
|
|
@@ -394,10 +394,6 @@ function Shell() {
|
|
|
394
394
|
const m = msg;
|
|
395
395
|
const file = m.path?.split('/').pop() || m.path || '';
|
|
396
396
|
toast.info(`File changed: ${file}`, 2500);
|
|
397
|
-
api
|
|
398
|
-
.get<Snapshot>('/snapshot')
|
|
399
|
-
.then((s) => setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })))
|
|
400
|
-
.catch(() => undefined);
|
|
401
397
|
} else if (msg.type === 'tasks:change') {
|
|
402
398
|
const m = msg;
|
|
403
399
|
setSnapshot((cur) => {
|
|
@@ -798,7 +794,11 @@ function Shell() {
|
|
|
798
794
|
<p>Loading Bizar…</p>
|
|
799
795
|
</div>
|
|
800
796
|
)}
|
|
801
|
-
{renderedView
|
|
797
|
+
{renderedView && (
|
|
798
|
+
<Suspense fallback={<div className="loading"><Spinner size="lg" /><p>Loading…</p></div>}>
|
|
799
|
+
<ViewErrorBoundary>{renderedView}</ViewErrorBoundary>
|
|
800
|
+
</Suspense>
|
|
801
|
+
)}
|
|
802
802
|
</main>
|
|
803
803
|
</div>
|
|
804
804
|
<SearchModal
|
|
@@ -809,3 +809,37 @@ function Shell() {
|
|
|
809
809
|
</div>
|
|
810
810
|
);
|
|
811
811
|
}
|
|
812
|
+
|
|
813
|
+
// v4.5.x — Catches render-time errors in the active view so a single bad
|
|
814
|
+
// component doesn't bring down the whole dashboard (topbar, sidebar, WS).
|
|
815
|
+
// Falls back to a friendly error card with a Retry button that re-mounts
|
|
816
|
+
// the wrapped subtree by remounting the boundary via React `key` bumps.
|
|
817
|
+
class ViewErrorBoundary extends Component<
|
|
818
|
+
{ children: React.ReactNode },
|
|
819
|
+
{ err: Error | null }
|
|
820
|
+
> {
|
|
821
|
+
state: { err: Error | null } = { err: null };
|
|
822
|
+
static getDerivedStateFromError(err: Error): { err: Error | null } {
|
|
823
|
+
return { err };
|
|
824
|
+
}
|
|
825
|
+
componentDidCatch(err: Error, info: { componentStack?: string }) {
|
|
826
|
+
// eslint-disable-next-line no-console
|
|
827
|
+
console.error('[ViewErrorBoundary] caught render error:', err, info?.componentStack);
|
|
828
|
+
}
|
|
829
|
+
reset = () => this.setState({ err: null });
|
|
830
|
+
override render() {
|
|
831
|
+
if (this.state.err) {
|
|
832
|
+
return (
|
|
833
|
+
<div className="view-error-fallback" role="alert">
|
|
834
|
+
<AlertTriangle size={20} />
|
|
835
|
+
<div className="view-error-body">
|
|
836
|
+
<strong>This view crashed.</strong>
|
|
837
|
+
<pre>{this.state.err.message}</pre>
|
|
838
|
+
<Button variant="secondary" size="sm" onClick={this.reset}>Retry</Button>
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
return this.props.children;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
@@ -134,7 +134,7 @@ function ToastItem({
|
|
|
134
134
|
}) {
|
|
135
135
|
const Icon = ICONS[toast.kind];
|
|
136
136
|
return (
|
|
137
|
-
<div className={`toast toast-${toast.kind}`} role="
|
|
137
|
+
<div className={`toast toast-${toast.kind}`} role="alert" aria-live="assertive" aria-atomic="true">
|
|
138
138
|
<Icon size={16} className="toast-icon" />
|
|
139
139
|
<span className="toast-message">{toast.message}</span>
|
|
140
140
|
<button
|
|
@@ -139,7 +139,7 @@ export function Topbar({
|
|
|
139
139
|
</div>
|
|
140
140
|
</div>
|
|
141
141
|
{showTabs && (
|
|
142
|
-
<nav className="tabs-row" role="tablist">
|
|
142
|
+
<nav className="tabs-row" role="tablist" aria-label="Primary tabs">
|
|
143
143
|
{TABS.map((tab) => {
|
|
144
144
|
const Icon = tab.icon;
|
|
145
145
|
const active = tab.id === activeTab;
|
|
@@ -135,8 +135,8 @@ class ApiClient {
|
|
|
135
135
|
* @example
|
|
136
136
|
* const r = await api.get<ProjectList>('/projects');
|
|
137
137
|
*/
|
|
138
|
-
async get<T>(path: string): Promise<T> {
|
|
139
|
-
return this.req<T>('GET', path);
|
|
138
|
+
async get<T>(path: string, signal?: AbortSignal): Promise<T> {
|
|
139
|
+
return this.req<T>('GET', path, undefined, signal);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
@@ -145,8 +145,8 @@ class ApiClient {
|
|
|
145
145
|
* IMPORTANT: do NOT include the `/api` prefix in `path` — the
|
|
146
146
|
* wrapper adds it. Use the unprefixed form: `api.post('/fs/mkdir', body)`.
|
|
147
147
|
*/
|
|
148
|
-
async post<T>(path: string, body?: unknown): Promise<T> {
|
|
149
|
-
return this.req<T>('POST', path, body);
|
|
148
|
+
async post<T>(path: string, body?: unknown, signal?: AbortSignal): Promise<T> {
|
|
149
|
+
return this.req<T>('POST', path, body, signal);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/**
|
|
@@ -155,8 +155,8 @@ class ApiClient {
|
|
|
155
155
|
* IMPORTANT: do NOT include the `/api` prefix in `path` — the
|
|
156
156
|
* wrapper adds it. Use the unprefixed form: `api.put('/settings', body)`.
|
|
157
157
|
*/
|
|
158
|
-
async put<T>(path: string, body?: unknown): Promise<T> {
|
|
159
|
-
return this.req<T>('PUT', path, body);
|
|
158
|
+
async put<T>(path: string, body?: unknown, signal?: AbortSignal): Promise<T> {
|
|
159
|
+
return this.req<T>('PUT', path, body, signal);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
@@ -165,8 +165,8 @@ class ApiClient {
|
|
|
165
165
|
* IMPORTANT: do NOT include the `/api` prefix in `path` — the
|
|
166
166
|
* wrapper adds it. Use the unprefixed form: `api.patch('/settings', body)`.
|
|
167
167
|
*/
|
|
168
|
-
async patch<T>(path: string, body?: unknown): Promise<T> {
|
|
169
|
-
return this.req<T>('PATCH', path, body);
|
|
168
|
+
async patch<T>(path: string, body?: unknown, signal?: AbortSignal): Promise<T> {
|
|
169
|
+
return this.req<T>('PATCH', path, body, signal);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
/**
|
|
@@ -175,11 +175,11 @@ class ApiClient {
|
|
|
175
175
|
* IMPORTANT: do NOT include the `/api` prefix in `path` — the
|
|
176
176
|
* wrapper adds it. Use the unprefixed form: `api.del('/projects/123')`.
|
|
177
177
|
*/
|
|
178
|
-
async del<T = unknown>(path: string): Promise<T> {
|
|
179
|
-
return this.req<T>('DELETE', path);
|
|
178
|
+
async del<T = unknown>(path: string, signal?: AbortSignal): Promise<T> {
|
|
179
|
+
return this.req<T>('DELETE', path, undefined, signal);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
private async req<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
182
|
+
private async req<T>(method: string, path: string, body?: unknown, signal?: AbortSignal): Promise<T> {
|
|
183
183
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
184
184
|
const tok = this.getToken();
|
|
185
185
|
if (tok) headers.Authorization = `Bearer ${tok}`;
|
|
@@ -187,6 +187,7 @@ class ApiClient {
|
|
|
187
187
|
if (body !== undefined && body !== null) {
|
|
188
188
|
opts.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
189
189
|
}
|
|
190
|
+
if (signal) opts.signal = signal;
|
|
190
191
|
const r = await fetch(this.base + path, opts);
|
|
191
192
|
const ct = r.headers.get('content-type') || '';
|
|
192
193
|
if (ct.includes('application/json')) {
|
|
@@ -1376,6 +1376,8 @@
|
|
|
1376
1376
|
border-radius: var(--radius-lg); padding: 0.875rem 1.125rem;
|
|
1377
1377
|
transition: border-color var(--motion-base) var(--motion-ease);
|
|
1378
1378
|
max-width: 880px; width: 100%; align-self: flex-start;
|
|
1379
|
+
content-visibility: auto;
|
|
1380
|
+
contain-intrinsic-size: 0 80px;
|
|
1379
1381
|
}
|
|
1380
1382
|
.chat-message-user { background: var(--bg-2); }
|
|
1381
1383
|
.chat-message-pinned { border-left: 2px solid var(--accent); }
|
|
@@ -129,10 +129,10 @@ pre,
|
|
|
129
129
|
--accent-glow: rgba(139, 92, 246, 0.18);
|
|
130
130
|
--accent-soft: rgba(139, 92, 246, 0.08);
|
|
131
131
|
|
|
132
|
-
--success:
|
|
133
|
-
--warning:
|
|
134
|
-
--error:
|
|
135
|
-
--info:
|
|
132
|
+
--success: oklch(0.72 0.16 145);
|
|
133
|
+
--warning: oklch(0.78 0.14 70);
|
|
134
|
+
--error: oklch(0.66 0.20 25);
|
|
135
|
+
--info: oklch(0.72 0.13 235);
|
|
136
136
|
--success-soft: rgba(52, 211, 153, 0.15);
|
|
137
137
|
--error-soft: rgba(248, 113, 113, 0.12);
|
|
138
138
|
--warning-soft: rgba(251, 191, 36, 0.15);
|
|
@@ -155,7 +155,7 @@ pre,
|
|
|
155
155
|
'Inter var', 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto,
|
|
156
156
|
sans-serif;
|
|
157
157
|
|
|
158
|
-
/* Spacing scale (8pt) */
|
|
158
|
+
/* Spacing scale (8pt + extra for spacious layouts) */
|
|
159
159
|
--space-1: 4px;
|
|
160
160
|
--space-2: 8px;
|
|
161
161
|
--space-3: 12px;
|
|
@@ -165,6 +165,9 @@ pre,
|
|
|
165
165
|
--space-8: 32px;
|
|
166
166
|
--space-10: 40px;
|
|
167
167
|
--space-12: 48px;
|
|
168
|
+
--space-16: 64px;
|
|
169
|
+
--space-20: 80px;
|
|
170
|
+
--space-24: 96px;
|
|
168
171
|
|
|
169
172
|
/* v4.6.0 — Semantic spacing aliases. These mirror the existing
|
|
170
173
|
--space-* numeric scale so existing rules stay untouched, and
|
|
@@ -179,10 +182,10 @@ pre,
|
|
|
179
182
|
--spacing-xl: var(--space-6); /* 24px — page-level padding */
|
|
180
183
|
|
|
181
184
|
/* Radius */
|
|
182
|
-
--radius-sm:
|
|
185
|
+
--radius-sm: 6px;
|
|
183
186
|
--radius: 8px;
|
|
184
187
|
--radius-md: 10px;
|
|
185
|
-
--radius-lg:
|
|
188
|
+
--radius-lg: 14px;
|
|
186
189
|
--radius-xl: 16px;
|
|
187
190
|
|
|
188
191
|
/* Shadows */
|
|
@@ -197,28 +200,15 @@ pre,
|
|
|
197
200
|
|
|
198
201
|
/* Motion */
|
|
199
202
|
--motion-fast: 120ms;
|
|
200
|
-
--motion-base:
|
|
201
|
-
--motion-slow:
|
|
203
|
+
--motion-base: 200ms;
|
|
204
|
+
--motion-slow: 320ms;
|
|
202
205
|
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
206
|
+
--motion-ease: cubic-bezier(0.2, 0, 0, 1);
|
|
203
207
|
|
|
204
208
|
/* v3.21.x — Semantic and surface tokens for chat UI overhaul */
|
|
205
209
|
--surface-0: var(--bg);
|
|
206
210
|
--surface-1: color-mix(in oklab, var(--bg) 92%, var(--text) 8%);
|
|
207
211
|
--surface-2: color-mix(in oklab, var(--bg) 84%, var(--text) 16%);
|
|
208
|
-
--success: oklch(0.72 0.16 145);
|
|
209
|
-
--warning: oklch(0.78 0.14 70);
|
|
210
|
-
--error: oklch(0.66 0.20 25);
|
|
211
|
-
--info: oklch(0.72 0.13 235);
|
|
212
|
-
--radius-sm: 6px;
|
|
213
|
-
--radius-md: 10px;
|
|
214
|
-
--radius-lg: 14px;
|
|
215
|
-
--motion-fast: 120ms;
|
|
216
|
-
--motion-base: 200ms;
|
|
217
|
-
--motion-slow: 320ms;
|
|
218
|
-
--motion-ease: cubic-bezier(0.2, 0, 0, 1);
|
|
219
|
-
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
|
|
220
|
-
--space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
|
|
221
|
-
--space-12: 48px; --space-16: 64px; --space-20: 80px; --space-24: 96px;
|
|
222
212
|
|
|
223
213
|
/* v3.21.x — Gemini-inspired surface tokens for chat UI overhaul.
|
|
224
214
|
--bg-1, --bg-2 are NEW names that don't conflict with --bg / --bg-elev. */
|
|
@@ -429,6 +419,41 @@ pre,
|
|
|
429
419
|
font-size: 12px;
|
|
430
420
|
}
|
|
431
421
|
|
|
422
|
+
/* v4.5.x — Per-view render-error fallback shown by App's ErrorBoundary.
|
|
423
|
+
Different from .boot-error (which fires before any view mounts). */
|
|
424
|
+
.view-error-fallback {
|
|
425
|
+
max-width: 560px;
|
|
426
|
+
margin: var(--space-12) auto;
|
|
427
|
+
padding: var(--space-6);
|
|
428
|
+
background: var(--bg-elev);
|
|
429
|
+
border: 1px solid var(--error);
|
|
430
|
+
border-radius: var(--radius-lg);
|
|
431
|
+
box-shadow: var(--shadow-2);
|
|
432
|
+
display: flex;
|
|
433
|
+
gap: var(--space-4);
|
|
434
|
+
align-items: flex-start;
|
|
435
|
+
}
|
|
436
|
+
.view-error-fallback > svg {
|
|
437
|
+
color: var(--error);
|
|
438
|
+
flex-shrink: 0;
|
|
439
|
+
margin-top: 2px;
|
|
440
|
+
}
|
|
441
|
+
.view-error-fallback .view-error-body {
|
|
442
|
+
display: flex;
|
|
443
|
+
flex-direction: column;
|
|
444
|
+
gap: var(--space-2);
|
|
445
|
+
min-width: 0;
|
|
446
|
+
}
|
|
447
|
+
.view-error-fallback pre {
|
|
448
|
+
background: var(--bg);
|
|
449
|
+
padding: var(--space-2) var(--space-3);
|
|
450
|
+
border-radius: var(--radius-sm);
|
|
451
|
+
font-size: 12px;
|
|
452
|
+
white-space: pre-wrap;
|
|
453
|
+
word-break: break-word;
|
|
454
|
+
margin: 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
432
457
|
/* v3.6.2 — Token-entry form on boot-error screen */
|
|
433
458
|
.token-entry-form {
|
|
434
459
|
margin: var(--space-4) 0;
|
|
@@ -1438,6 +1463,8 @@ pre,
|
|
|
1438
1463
|
font-family: var(--font-mono);
|
|
1439
1464
|
border-left: 2px solid var(--border);
|
|
1440
1465
|
align-items: baseline;
|
|
1466
|
+
content-visibility: auto;
|
|
1467
|
+
contain-intrinsic-size: 0 80px;
|
|
1441
1468
|
}
|
|
1442
1469
|
|
|
1443
1470
|
.activity-item:hover {
|
|
@@ -3207,6 +3234,8 @@ pre,
|
|
|
3207
3234
|
box-shadow var(--motion-base) var(--ease),
|
|
3208
3235
|
transform var(--motion-fast) var(--ease),
|
|
3209
3236
|
background var(--motion-base) var(--ease);
|
|
3237
|
+
content-visibility: auto;
|
|
3238
|
+
contain-intrinsic-size: 0 80px;
|
|
3210
3239
|
}
|
|
3211
3240
|
.task-card::before {
|
|
3212
3241
|
content: '';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// semantic search) plus a config panel and a high-level overview. Each is a
|
|
8
8
|
// standalone component under views/memory/.
|
|
9
9
|
|
|
10
|
-
import { useCallback, useState } from 'react';
|
|
10
|
+
import React, { useCallback, useState } from 'react';
|
|
11
11
|
import {
|
|
12
12
|
Brain,
|
|
13
13
|
FileText,
|
|
@@ -50,7 +50,7 @@ const SOURCES: Array<{
|
|
|
50
50
|
{ id: 'config', label: 'Config', icon: Sliders },
|
|
51
51
|
];
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
function MemoryInner(_props: Props) {
|
|
54
54
|
const toast = useToast();
|
|
55
55
|
const [active, setActive] = useState<SubPanel>('overview');
|
|
56
56
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
@@ -137,4 +137,5 @@ export function Memory(_props: Props) {
|
|
|
137
137
|
</div>
|
|
138
138
|
</div>
|
|
139
139
|
);
|
|
140
|
-
}
|
|
140
|
+
}
|
|
141
|
+
export const Memory = React.memo(MemoryInner);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// (written by minimax.mjs after every chatCompletion / fetchRemains call).
|
|
9
9
|
// The Usage Analytics tab is only shown when the key is configured.
|
|
10
10
|
|
|
11
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
12
12
|
import {
|
|
13
13
|
Coins,
|
|
14
14
|
RefreshCw,
|
|
@@ -196,7 +196,7 @@ type TimeRange = '24h' | '7d' | '30d' | 'custom';
|
|
|
196
196
|
|
|
197
197
|
// ─── Main component ────────────────────────────────────────────────────
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
function MiniMaxUsageInner({ activeTab, setActiveTab }: Props) {
|
|
200
200
|
// Share state between tabs.
|
|
201
201
|
const [view, setView] = useState<'quota' | 'analytics'>('quota');
|
|
202
202
|
const toast = useToast();
|
|
@@ -921,3 +921,4 @@ function OnboardingWizard({ step, setStep, keyDraft, setKeyDraft, showKey, setSh
|
|
|
921
921
|
</div>
|
|
922
922
|
);
|
|
923
923
|
}
|
|
924
|
+
export const MiniMaxUsage = React.memo(MiniMaxUsageInner);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/views/Overview.tsx — v3.7.0 activity cards + SSE stream.
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Bot,
|
|
5
5
|
CheckSquare,
|
|
@@ -49,7 +49,7 @@ type Props = {
|
|
|
49
49
|
refreshSnapshot: () => Promise<void>;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
function OverviewInner({
|
|
53
53
|
snapshot,
|
|
54
54
|
settings,
|
|
55
55
|
setActiveTab,
|
|
@@ -813,3 +813,5 @@ function ActivityFeedItem({
|
|
|
813
813
|
</div>
|
|
814
814
|
);
|
|
815
815
|
}
|
|
816
|
+
const OverviewMemo = React.memo(OverviewInner);
|
|
817
|
+
export { OverviewMemo as Overview };
|
|
@@ -485,7 +485,7 @@ function UpdatesCard() {
|
|
|
485
485
|
);
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
|
|
488
|
+
function SettingsViewInner({ settings: initial, refreshSnapshot }: Props) {
|
|
489
489
|
const toast = useToast();
|
|
490
490
|
const [settings, setSettings] = useState<Settings>(initial);
|
|
491
491
|
const [dirty, setDirty] = useState(false);
|
|
@@ -1821,3 +1821,4 @@ function ActivityLogCard() {
|
|
|
1821
1821
|
</Card>
|
|
1822
1822
|
);
|
|
1823
1823
|
}
|
|
1824
|
+
export const SettingsView = React.memo(SettingsViewInner);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/views/Skills.tsx — v4.0.0 skills browser: scans all local SKILL.md sources.
|
|
2
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Sparkles,
|
|
5
5
|
Search,
|
|
@@ -58,7 +58,7 @@ const SOURCE_ICON: Record<string, typeof Package> = {
|
|
|
58
58
|
project: Folder,
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
function SkillsInner({ snapshot, refreshSnapshot }: Props) {
|
|
62
62
|
const toast = useToast();
|
|
63
63
|
const modal = useModal();
|
|
64
64
|
const [skills, setSkills] = useState<Skill[]>([]);
|
|
@@ -377,3 +377,4 @@ function highlightMatch(text: string, q: string): React.ReactNode {
|
|
|
377
377
|
</>
|
|
378
378
|
);
|
|
379
379
|
}
|
|
380
|
+
export const Skills = React.memo(SkillsInner);
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// We keep the API contract (statuses: queued/doing/done/blocked/backlog)
|
|
21
21
|
// so the dashboard doesn't break the existing server, tasks store, and
|
|
22
22
|
// tests. The UI maps API statuses to friendlier labels.
|
|
23
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
23
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
24
24
|
import {
|
|
25
25
|
CheckSquare,
|
|
26
26
|
Plus,
|
|
@@ -76,7 +76,7 @@ const COLUMNS: { id: Task['status']; label: string; kind: StatusKind }[] = [
|
|
|
76
76
|
const PRIORITIES = ['low', 'normal', 'high'] as const;
|
|
77
77
|
type Priority = (typeof PRIORITIES)[number];
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
|
|
80
80
|
const toast = useToast();
|
|
81
81
|
const modal = useModal();
|
|
82
82
|
const [tasks, setTasks] = useState<Task[]>(snapshot.tasks || []);
|
|
@@ -771,4 +771,5 @@ function openEditTaskModal(
|
|
|
771
771
|
</div>
|
|
772
772
|
),
|
|
773
773
|
});
|
|
774
|
-
}
|
|
774
|
+
}
|
|
775
|
+
export const Tasks = React.memo(TasksInner);
|
|
@@ -98,14 +98,14 @@ export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Pro
|
|
|
98
98
|
const [data, setData] = useState<MemoryOverviewData | null>(null);
|
|
99
99
|
const [loading, setLoading] = useState(true);
|
|
100
100
|
|
|
101
|
-
const reload = async () => {
|
|
101
|
+
const reload = async (signal?: AbortSignal) => {
|
|
102
102
|
setLoading(true);
|
|
103
103
|
try {
|
|
104
104
|
const [health, status, lightrag, storage] = await Promise.all([
|
|
105
|
-
api.get<HealthResponse>('/memory/health').catch(() => null),
|
|
106
|
-
api.get<MemoryStatus>('/memory/status').catch(() => null),
|
|
107
|
-
api.get<LightragStats>('/memory/lightrag/stats').catch(() => null),
|
|
108
|
-
api.get<StorageStats>('/memory/storage').catch(() => null),
|
|
105
|
+
api.get<HealthResponse>('/memory/health', signal).catch(() => null),
|
|
106
|
+
api.get<MemoryStatus>('/memory/status', signal).catch(() => null),
|
|
107
|
+
api.get<LightragStats>('/memory/lightrag/stats', signal).catch(() => null),
|
|
108
|
+
api.get<StorageStats>('/memory/storage', signal).catch(() => null),
|
|
109
109
|
]);
|
|
110
110
|
setData({
|
|
111
111
|
health: health || {
|
|
@@ -145,7 +145,9 @@ export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Pro
|
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
useEffect(() => {
|
|
148
|
-
|
|
148
|
+
const ctrl = new AbortController();
|
|
149
|
+
void reload(ctrl.signal);
|
|
150
|
+
return () => ctrl.abort();
|
|
149
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
152
|
}, [refreshKey]);
|
|
151
153
|
|