@polderlabs/bizar 5.0.0 → 5.0.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/{icons-CFqu2M-c.js → icons-Bo0iH9EC.js} +166 -121
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
- package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
- package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
- package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js → mobile-DD-FZrTC.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js.map → mobile-DD-FZrTC.js.map} +1 -1
- package/bizar-dash/dist/index.html +4 -4
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.package-lock.json +6 -0
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/package-lock.json +6 -0
- package/bizar-dash/src/server/api.mjs +7 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
- package/bizar-dash/src/server/memory-store.mjs +46 -0
- package/bizar-dash/src/server/providers-store.mjs +11 -4
- package/bizar-dash/src/server/routes/_shared.mjs +2 -2
- package/bizar-dash/src/server/routes/doctor.mjs +71 -0
- package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
- package/bizar-dash/src/server/routes/memory.mjs +3 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
- package/bizar-dash/src/server/routes/schedules.mjs +55 -0
- package/bizar-dash/src/server/serve-info.mjs +172 -0
- package/bizar-dash/src/web/App.tsx +48 -11
- package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
- package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
- package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
- package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
- package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
- package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
- package/bizar-dash/src/web/components/Topbar.tsx +11 -0
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
- package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
- package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
- package/bizar-dash/src/web/lib/types.ts +134 -0
- package/bizar-dash/src/web/styles/chat.css +8 -5
- package/bizar-dash/src/web/styles/main.css +77 -2
- package/bizar-dash/src/web/styles/settings.css +265 -0
- package/bizar-dash/src/web/views/Chat.tsx +15 -1
- package/bizar-dash/src/web/views/Doctor.tsx +317 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
- package/bizar-dash/src/web/views/Schedules.tsx +94 -12
- package/bizar-dash/src/web/views/Settings.tsx +76 -46
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
- package/bizar-dash/tests/autosave.test.tsx +276 -0
- package/bizar-dash/tests/chat-composer.test.tsx +140 -0
- package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
- package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
- package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
- package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
- package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
- package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
- package/bizar-dash/tests/minimax-models.test.mjs +123 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
- package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
- package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
- package/bizar-dash/tests/settings-layout.test.tsx +129 -0
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
- package/bizar-dash/tests/settings-nav.test.tsx +126 -0
- package/cli/commands/dash.mjs +5 -1
- package/cli/commands/minimax.mjs +5 -2
- package/cli/tests/minimax-cli.test.mjs +79 -0
- package/config/agents/frigg.md +1 -1
- package/config/agents/heimdall.md +1 -1
- package/config/agents/mimir.md +1 -1
- package/config/agents/quick.md +1 -1
- package/config/agents/semble-search.md +1 -1
- package/config/agents/vor.md +1 -1
- package/config/opencode.json +28 -21
- package/config/opencode.json.template +8 -1
- package/package.json +1 -1
- package/plugins/bizar/index.ts +77 -0
- package/plugins/bizar/src/compaction.d.mts +48 -0
- package/plugins/bizar/src/compaction.mjs +192 -0
- package/plugins/bizar/tests/compaction.test.ts +264 -0
- package/templates/schedules/daily-backup.json +12 -0
- package/templates/schedules/daily-cleanup.json +12 -0
- package/templates/schedules/hourly-health-check.json +12 -0
- package/templates/schedules/webhook-on-push.json +13 -0
- package/templates/schedules/weekly-digest.json +13 -0
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +0 -1
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js +0 -16
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +0 -1
- package/bizar-dash/dist/assets/main-ZAfGKENE.css +0 -1
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +0 -2
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +0 -1
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
- package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
|
@@ -5,9 +5,17 @@
|
|
|
5
5
|
// in use, a section for usage/cost placeholders (filled in by the
|
|
6
6
|
// MiniMax usage module when present), and rename / delete / export
|
|
7
7
|
// actions wired to the parent.
|
|
8
|
+
//
|
|
9
|
+
// v5.0.0 — bug #4 fix: the panel now renders a structured error
|
|
10
|
+
// section when the opencode session load fails. The chat-thread
|
|
11
|
+
// header (rendered by Chat.tsx) also shows the error inline; the
|
|
12
|
+
// info-panel error makes it impossible to miss when the right
|
|
13
|
+
// sidebar is in view, and exposes the server's `suggestion` and a
|
|
14
|
+
// Retry button so the operator can act without scrolling.
|
|
8
15
|
|
|
9
16
|
import { useEffect, useState } from 'react';
|
|
10
17
|
import {
|
|
18
|
+
AlertTriangle,
|
|
11
19
|
MessageSquare,
|
|
12
20
|
Bot,
|
|
13
21
|
Server,
|
|
@@ -16,6 +24,7 @@ import {
|
|
|
16
24
|
Trash2,
|
|
17
25
|
Download,
|
|
18
26
|
CircleDollarSign,
|
|
27
|
+
RotateCw,
|
|
19
28
|
} from 'lucide-react';
|
|
20
29
|
import type { ChatMessage } from '../../lib/types';
|
|
21
30
|
|
|
@@ -36,6 +45,23 @@ interface Props {
|
|
|
36
45
|
onExport?: () => void;
|
|
37
46
|
/** Per-action busy flags — disable buttons while pending. */
|
|
38
47
|
busy?: { rename?: boolean; delete?: boolean };
|
|
48
|
+
/**
|
|
49
|
+
* v5.0.0 — bug #4: structured error envelope from `loadOpencodeSession`.
|
|
50
|
+
* When present, the panel renders an error section above the Session
|
|
51
|
+
* section so the operator can see what went wrong without scrolling.
|
|
52
|
+
*/
|
|
53
|
+
error?: {
|
|
54
|
+
code: string;
|
|
55
|
+
message: string;
|
|
56
|
+
suggestion?: string | null;
|
|
57
|
+
canRetry?: boolean;
|
|
58
|
+
} | null;
|
|
59
|
+
/**
|
|
60
|
+
* v5.0.0 — bug #4: retry handler invoked when the operator clicks
|
|
61
|
+
* the Retry button on the error section. Wired to
|
|
62
|
+
* `chat.retryOpencodeSession` by Chat.tsx.
|
|
63
|
+
*/
|
|
64
|
+
onRetry?: () => void;
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
interface UsageRow {
|
|
@@ -86,6 +112,8 @@ export function ChatInfoPanel({
|
|
|
86
112
|
onDelete,
|
|
87
113
|
onExport,
|
|
88
114
|
busy,
|
|
115
|
+
error,
|
|
116
|
+
onRetry,
|
|
89
117
|
}: Props) {
|
|
90
118
|
// Best-effort: pull usage summary from the MiniMax usage endpoint
|
|
91
119
|
// when the session is opencode. Falls back silently if the module
|
|
@@ -129,6 +157,42 @@ export function ChatInfoPanel({
|
|
|
129
157
|
|
|
130
158
|
return (
|
|
131
159
|
<aside className="chat-info">
|
|
160
|
+
{error && (
|
|
161
|
+
<div
|
|
162
|
+
className="chat-info-section chat-info-error"
|
|
163
|
+
role="alert"
|
|
164
|
+
aria-live="polite"
|
|
165
|
+
>
|
|
166
|
+
<h4>
|
|
167
|
+
<AlertTriangle
|
|
168
|
+
size={11}
|
|
169
|
+
style={{ marginRight: 4, verticalAlign: -1 }}
|
|
170
|
+
aria-hidden
|
|
171
|
+
/>{' '}
|
|
172
|
+
Couldn't load session
|
|
173
|
+
</h4>
|
|
174
|
+
<p className="chat-info-value chat-ellipsis" title={error.message}>
|
|
175
|
+
{error.message}
|
|
176
|
+
</p>
|
|
177
|
+
<div className="chat-info-mono">
|
|
178
|
+
code · {error.code}
|
|
179
|
+
</div>
|
|
180
|
+
{error.suggestion ? (
|
|
181
|
+
<p className="chat-info-suggestion">{error.suggestion}</p>
|
|
182
|
+
) : null}
|
|
183
|
+
{error.canRetry !== false && onRetry ? (
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
className="btn btn-ghost btn-sm chat-info-retry"
|
|
187
|
+
onClick={onRetry}
|
|
188
|
+
title="Retry loading the session"
|
|
189
|
+
>
|
|
190
|
+
<RotateCw size={12} aria-hidden /> Retry
|
|
191
|
+
</button>
|
|
192
|
+
) : null}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
132
196
|
<div className="chat-info-section">
|
|
133
197
|
<div className="chat-info-section-head">
|
|
134
198
|
<h4>
|
|
@@ -41,7 +41,7 @@ export function Composer({
|
|
|
41
41
|
}: Props) {
|
|
42
42
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
43
43
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
44
|
-
useAutoGrowTextarea(inputRef, text,
|
|
44
|
+
useAutoGrowTextarea(inputRef, text, 160);
|
|
45
45
|
|
|
46
46
|
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
47
47
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
// - bizar (GET /api/chat)
|
|
6
6
|
// - opencode (GET /api/opencode-sessions/:id/messages + SSE)
|
|
7
7
|
//
|
|
8
|
+
// v5.0.0 — bug #4 fix: `loadOpencodeSession` failure path now extracts
|
|
9
|
+
// structured `code`, `cause`, `status`, and `suggestion` from the
|
|
10
|
+
// server's ApiError.data envelope and exposes them via a new
|
|
11
|
+
// `opencodeErrorInfo` field. `ChatInfoPanel` consumes that field to
|
|
12
|
+
// render a structured "Couldn't load session" panel with the server's
|
|
13
|
+
// `suggestion` verbatim, plus a Retry button.
|
|
14
|
+
//
|
|
8
15
|
// Changes from v3.22:
|
|
9
16
|
// * SSE has automatic reconnect-with-backoff (1s → 30s, doubled on
|
|
10
17
|
// each error up to a cap). Closes cleanly on unmount and on
|
|
@@ -59,6 +66,31 @@ export interface ChatBusyState {
|
|
|
59
66
|
delete: boolean;
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Structured error envelope surfaced by `loadOpencodeSession` when
|
|
71
|
+
* the upstream `/api/opencode-sessions/:id/messages` call fails.
|
|
72
|
+
*
|
|
73
|
+
* - `code` mirrors the server's `error` field (`plugin_offline`,
|
|
74
|
+
* `directory_unknown`, `opencode_error`, `bad_request`).
|
|
75
|
+
* - `cause` is the server's structured cause (`network`, `timeout`,
|
|
76
|
+
* `unknown`, or a raw ECONNREFUSED-style string).
|
|
77
|
+
* - `suggestion` is the operator-friendly hint from the server
|
|
78
|
+
* (e.g. "Run `bizar doctor` for diagnostics").
|
|
79
|
+
* - `canRetry` is `true` whenever the failure is transient (network
|
|
80
|
+
* error, upstream 502) — i.e. the user can fix it by trying again
|
|
81
|
+
* after starting the plugin / waiting for opencode to come back.
|
|
82
|
+
* False for hard failures (bad_request).
|
|
83
|
+
*/
|
|
84
|
+
export interface OpencodeErrorInfo {
|
|
85
|
+
type: 'session_load_failed';
|
|
86
|
+
code: string;
|
|
87
|
+
message: string;
|
|
88
|
+
cause?: string;
|
|
89
|
+
status?: number;
|
|
90
|
+
suggestion?: string | null;
|
|
91
|
+
canRetry: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
/** Shape of one opencode SSE envelope after unwrapping. */
|
|
63
95
|
interface OpencodeSseEnvelope {
|
|
64
96
|
type: string;
|
|
@@ -95,6 +127,14 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
95
127
|
const [activeSource, setActiveSource] = useState<'bizar' | 'opencode' | null>(null);
|
|
96
128
|
const [activeOpencodeSessionId, setActiveOpencodeSessionId] = useState<string | null>(null);
|
|
97
129
|
const [opencodeError, setOpencodeError] = useState<string | null>(null);
|
|
130
|
+
const [opencodeSuggestion, setOpencodeSuggestion] = useState<string | null>(null);
|
|
131
|
+
/**
|
|
132
|
+
* Structured error info for the opencode session load path. New in
|
|
133
|
+
* v5.0.0 — `ChatInfoPanel` consumes this to render its own error
|
|
134
|
+
* section (in addition to the chat-thread-header error already
|
|
135
|
+
* rendered by Chat.tsx). `null` when no error.
|
|
136
|
+
*/
|
|
137
|
+
const [opencodeErrorInfo, setOpencodeErrorInfo] = useState<OpencodeErrorInfo | null>(null);
|
|
98
138
|
|
|
99
139
|
// ── v3.22 — session-state, unread, tree per session ────────────────────────
|
|
100
140
|
const [sessionStates, setSessionStates] = useState<Record<string, SessionDisplayState>>({});
|
|
@@ -300,6 +340,8 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
300
340
|
sseAutoReconnectRef.current = true;
|
|
301
341
|
sseReconnectAttemptRef.current = 0;
|
|
302
342
|
setOpencodeError(null);
|
|
343
|
+
setOpencodeSuggestion(null);
|
|
344
|
+
setOpencodeErrorInfo(null);
|
|
303
345
|
|
|
304
346
|
const connect = () => {
|
|
305
347
|
if (!sseAutoReconnectRef.current) return;
|
|
@@ -527,6 +569,8 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
527
569
|
// Successful (re)connect — reset backoff.
|
|
528
570
|
sseReconnectAttemptRef.current = 0;
|
|
529
571
|
setOpencodeError(null);
|
|
572
|
+
setOpencodeSuggestion(null);
|
|
573
|
+
setOpencodeErrorInfo(null);
|
|
530
574
|
};
|
|
531
575
|
};
|
|
532
576
|
|
|
@@ -554,6 +598,8 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
554
598
|
setOpencodeMessages([]);
|
|
555
599
|
setSeenOpencodeMessages(new Set());
|
|
556
600
|
setOpencodeError(null);
|
|
601
|
+
setOpencodeSuggestion(null);
|
|
602
|
+
setOpencodeErrorInfo(null);
|
|
557
603
|
setActiveOpencodeSessionId(null);
|
|
558
604
|
setActiveSource('bizar');
|
|
559
605
|
}, []);
|
|
@@ -576,6 +622,8 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
576
622
|
}
|
|
577
623
|
|
|
578
624
|
setOpencodeError(null);
|
|
625
|
+
setOpencodeSuggestion(null);
|
|
626
|
+
setOpencodeErrorInfo(null);
|
|
579
627
|
setLoading(true);
|
|
580
628
|
|
|
581
629
|
try {
|
|
@@ -585,7 +633,62 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
585
633
|
setOpencodeMessages(data.messages || []);
|
|
586
634
|
} catch (err) {
|
|
587
635
|
const msg = (err as Error).message;
|
|
588
|
-
|
|
636
|
+
// v5.0.0 — bug #4: surface the server's structured error
|
|
637
|
+
// envelope so the UI can render a useful suggestion instead
|
|
638
|
+
// of the raw upstream error string. The server returns
|
|
639
|
+
// `{error, message, cause?, status?, suggestion?}` for every
|
|
640
|
+
// non-2xx response (see serve-info.mjs + routes/opencode-
|
|
641
|
+
// session-detail.mjs).
|
|
642
|
+
let info: OpencodeErrorInfo | null = null;
|
|
643
|
+
if (err instanceof ApiError && err.data && typeof err.data === 'object') {
|
|
644
|
+
const d = err.data as {
|
|
645
|
+
error?: string;
|
|
646
|
+
message?: string;
|
|
647
|
+
cause?: string;
|
|
648
|
+
status?: number;
|
|
649
|
+
suggestion?: string;
|
|
650
|
+
};
|
|
651
|
+
// canRetry: 503/502 are transient (plugin offline /
|
|
652
|
+
// upstream hiccup); the operator can fix the plugin and
|
|
653
|
+
// try again. 400 is a hard bad_request — retrying with the
|
|
654
|
+
// same payload won't help.
|
|
655
|
+
const code = d.error || 'unknown';
|
|
656
|
+
const canRetry =
|
|
657
|
+
code !== 'bad_request' &&
|
|
658
|
+
code !== 'too_many_subscribers' &&
|
|
659
|
+
code !== 'unauthorized';
|
|
660
|
+
info = {
|
|
661
|
+
type: 'session_load_failed',
|
|
662
|
+
code,
|
|
663
|
+
message: d.message || msg,
|
|
664
|
+
cause: d.cause,
|
|
665
|
+
status: d.status,
|
|
666
|
+
suggestion: d.suggestion ?? null,
|
|
667
|
+
canRetry,
|
|
668
|
+
};
|
|
669
|
+
} else if (err instanceof ApiError) {
|
|
670
|
+
info = {
|
|
671
|
+
type: 'session_load_failed',
|
|
672
|
+
code: 'unknown',
|
|
673
|
+
message: msg,
|
|
674
|
+
suggestion: null,
|
|
675
|
+
canRetry: true,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
// Legacy fields — kept so the existing Chat.tsx thread-header
|
|
679
|
+
// error display continues to work unchanged.
|
|
680
|
+
setOpencodeError(
|
|
681
|
+
info
|
|
682
|
+
? `Opencode session load failed (${info.code}): ${info.message}`
|
|
683
|
+
: `Opencode session load failed: ${msg}`,
|
|
684
|
+
);
|
|
685
|
+
setOpencodeSuggestion(info?.suggestion ?? null);
|
|
686
|
+
setOpencodeErrorInfo(info);
|
|
687
|
+
toastRef.current?.error(
|
|
688
|
+
info
|
|
689
|
+
? `Opencode session load failed: ${info.message}`
|
|
690
|
+
: `Opencode session load failed: ${msg}`,
|
|
691
|
+
);
|
|
589
692
|
setOpencodeMessages([]);
|
|
590
693
|
} finally {
|
|
591
694
|
setLoading(false);
|
|
@@ -1039,6 +1142,20 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
1039
1142
|
activeSource,
|
|
1040
1143
|
activeOpencodeSessionId,
|
|
1041
1144
|
opencodeError,
|
|
1145
|
+
opencodeSuggestion,
|
|
1146
|
+
/**
|
|
1147
|
+
* v5.0.0 — bug #4: structured error info consumed by
|
|
1148
|
+
* `ChatInfoPanel`. `null` when no error is active.
|
|
1149
|
+
*/
|
|
1150
|
+
opencodeErrorInfo,
|
|
1151
|
+
/**
|
|
1152
|
+
* v5.0.0 — bug #4: retry helper exposed for `ChatInfoPanel`'s
|
|
1153
|
+
* Retry button. Calls `loadOpencodeSession` on the currently
|
|
1154
|
+
* active opencode session (or no-op if none is active).
|
|
1155
|
+
*/
|
|
1156
|
+
retryOpencodeSession: () => {
|
|
1157
|
+
if (activeOpencodeSessionId) void loadOpencodeSession(activeOpencodeSessionId);
|
|
1158
|
+
},
|
|
1042
1159
|
// ── v3.22 — per-session display state ──────────────────────────────────
|
|
1043
1160
|
sessionStates,
|
|
1044
1161
|
busy,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/web/hooks/useAutosave.ts
|
|
2
|
+
// Debounced autosave hook for settings fields.
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
5
|
+
|
|
6
|
+
export type AutosaveStatus = 'idle' | 'saving' | 'saved' | 'error';
|
|
7
|
+
|
|
8
|
+
export function useAutosave<T>(
|
|
9
|
+
initialValue: T,
|
|
10
|
+
saveFn: (value: T) => Promise<void>,
|
|
11
|
+
{
|
|
12
|
+
delay = 800,
|
|
13
|
+
onSaved,
|
|
14
|
+
onError,
|
|
15
|
+
}: { delay?: number; onSaved?: () => void; onError?: (e: Error) => void } = {},
|
|
16
|
+
) {
|
|
17
|
+
const [value, setValue] = useState<T>(initialValue);
|
|
18
|
+
const [status, setStatus] = useState<AutosaveStatus>('idle');
|
|
19
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
20
|
+
const pendingValueRef = useRef<T | null>(null);
|
|
21
|
+
const lastSavedValue = useRef<T>(initialValue);
|
|
22
|
+
// Tracks whether a debounce timeout is currently pending (set when timeout is scheduled, cleared when it fires)
|
|
23
|
+
const hasPendingDebounceRef = useRef(false);
|
|
24
|
+
|
|
25
|
+
const save = useCallback(
|
|
26
|
+
async (v: T) => {
|
|
27
|
+
setStatus('saving');
|
|
28
|
+
try {
|
|
29
|
+
await saveFn(v);
|
|
30
|
+
lastSavedValue.current = v;
|
|
31
|
+
pendingValueRef.current = null;
|
|
32
|
+
setStatus('saved');
|
|
33
|
+
onSaved?.();
|
|
34
|
+
setTimeout(() => setStatus('idle'), 2000);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setStatus('error');
|
|
37
|
+
onError?.(err as Error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
[saveFn, onSaved, onError],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const setValueAndSave = useCallback(
|
|
44
|
+
(v: T) => {
|
|
45
|
+
setValue(v);
|
|
46
|
+
pendingValueRef.current = v;
|
|
47
|
+
hasPendingDebounceRef.current = true;
|
|
48
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
49
|
+
timeoutRef.current = setTimeout(() => {
|
|
50
|
+
timeoutRef.current = null;
|
|
51
|
+
hasPendingDebounceRef.current = false;
|
|
52
|
+
const toSave = pendingValueRef.current;
|
|
53
|
+
pendingValueRef.current = null;
|
|
54
|
+
if (toSave !== null) save(toSave);
|
|
55
|
+
}, delay);
|
|
56
|
+
},
|
|
57
|
+
[delay, save],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clear any pending debounce without saving.
|
|
62
|
+
* Use this when you want the debounce to handle the save naturally.
|
|
63
|
+
*/
|
|
64
|
+
const clearDebounce = useCallback(() => {
|
|
65
|
+
if (timeoutRef.current) {
|
|
66
|
+
clearTimeout(timeoutRef.current);
|
|
67
|
+
timeoutRef.current = null;
|
|
68
|
+
hasPendingDebounceRef.current = false;
|
|
69
|
+
}
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Flush any pending save immediately (called on unmount or explicit flush).
|
|
74
|
+
*/
|
|
75
|
+
const flush = useCallback(async () => {
|
|
76
|
+
if (timeoutRef.current) {
|
|
77
|
+
clearTimeout(timeoutRef.current);
|
|
78
|
+
timeoutRef.current = null;
|
|
79
|
+
hasPendingDebounceRef.current = false;
|
|
80
|
+
const toSave = pendingValueRef.current;
|
|
81
|
+
pendingValueRef.current = null;
|
|
82
|
+
if (toSave !== null) await save(toSave);
|
|
83
|
+
}
|
|
84
|
+
}, [save]);
|
|
85
|
+
|
|
86
|
+
// Cleanup on unmount: fire pending save immediately
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
return () => {
|
|
89
|
+
if (timeoutRef.current) {
|
|
90
|
+
clearTimeout(timeoutRef.current);
|
|
91
|
+
timeoutRef.current = null;
|
|
92
|
+
const toSave = pendingValueRef.current;
|
|
93
|
+
pendingValueRef.current = null;
|
|
94
|
+
// Fire-and-forget: call saveFn directly to persist without awaiting
|
|
95
|
+
if (toSave !== null) {
|
|
96
|
+
setStatus('saving');
|
|
97
|
+
saveFn(toSave)
|
|
98
|
+
.then(() => { lastSavedValue.current = toSave; setStatus('saved'); onSaved?.(); })
|
|
99
|
+
.catch((err: Error) => { setStatus('error'); onError?.(err); })
|
|
100
|
+
.finally(() => { setTimeout(() => setStatus('idle'), 2000); });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}, [saveFn, onSaved, onError]);
|
|
105
|
+
|
|
106
|
+
return { value, setValue: setValueAndSave, status, save, flush, clearDebounce, hasPendingDebounceRef, pendingValueRef };
|
|
107
|
+
}
|
|
@@ -535,6 +535,140 @@ export type TailscaleStatus = {
|
|
|
535
535
|
settings: { enabled: boolean; port: number; https: boolean; hostname: string };
|
|
536
536
|
};
|
|
537
537
|
|
|
538
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
539
|
+
// v6.0.0 — Doctor page types
|
|
540
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
export type DoctorStatus = 'ok' | 'warn' | 'fail';
|
|
543
|
+
|
|
544
|
+
export type DoctorCheck = {
|
|
545
|
+
name: string;
|
|
546
|
+
status: DoctorStatus;
|
|
547
|
+
message: string;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
export type DoctorChecks = {
|
|
551
|
+
system: DoctorCheck[];
|
|
552
|
+
config: DoctorCheck[];
|
|
553
|
+
services: DoctorCheck[];
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
export type DoctorHealth = {
|
|
557
|
+
ts: string;
|
|
558
|
+
status: DoctorStatus;
|
|
559
|
+
issues: DoctorCheck[];
|
|
560
|
+
groups: DoctorChecks;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
export type DoctorCounts = {
|
|
564
|
+
tasks: number;
|
|
565
|
+
schedules: number;
|
|
566
|
+
mods: number;
|
|
567
|
+
providers: number;
|
|
568
|
+
mcps: number;
|
|
569
|
+
agents: number;
|
|
570
|
+
projects: number;
|
|
571
|
+
activeProject: string | null;
|
|
572
|
+
workspaces: number;
|
|
573
|
+
voiceNotes: number;
|
|
574
|
+
evalRuns: number;
|
|
575
|
+
backups: number;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
export type DoctorService = {
|
|
579
|
+
running: boolean;
|
|
580
|
+
port?: number;
|
|
581
|
+
host?: string;
|
|
582
|
+
pid?: number;
|
|
583
|
+
uptime?: number;
|
|
584
|
+
error?: string;
|
|
585
|
+
baseUrl?: string | null;
|
|
586
|
+
reachable?: boolean;
|
|
587
|
+
path?: string | null;
|
|
588
|
+
logPath?: string;
|
|
589
|
+
logExists?: boolean;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
export type DoctorServices = {
|
|
593
|
+
dashboard: DoctorService;
|
|
594
|
+
headroom: DoctorService;
|
|
595
|
+
lightrag: DoctorService;
|
|
596
|
+
opencode: DoctorService;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
export type DoctorRecentError = {
|
|
600
|
+
line: string;
|
|
601
|
+
ts: string | null;
|
|
602
|
+
tsMs: number | null;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
export type DoctorOpencode = {
|
|
606
|
+
configExists: boolean;
|
|
607
|
+
configPath: string;
|
|
608
|
+
agentsDir: string;
|
|
609
|
+
agentFiles: string[];
|
|
610
|
+
dashboardConnected: boolean;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
export type DoctorDisk = {
|
|
614
|
+
path: string;
|
|
615
|
+
exists: boolean;
|
|
616
|
+
sizeBytes: number;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
export type DoctorSnapshot = {
|
|
620
|
+
timestamp: string;
|
|
621
|
+
bizarVersion: string;
|
|
622
|
+
nodeVersion: string;
|
|
623
|
+
platform: string;
|
|
624
|
+
arch: string;
|
|
625
|
+
uptime: number;
|
|
626
|
+
memory: NodeJS.MemoryUsage;
|
|
627
|
+
disk: DoctorDisk;
|
|
628
|
+
services: DoctorServices;
|
|
629
|
+
counts: DoctorCounts;
|
|
630
|
+
recentErrors: DoctorRecentError[];
|
|
631
|
+
configHealth: DoctorCheck[];
|
|
632
|
+
opencode: DoctorOpencode;
|
|
633
|
+
checks: DoctorChecks;
|
|
634
|
+
health: { status: DoctorStatus; issues: DoctorCheck[] };
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* The categories the Doctor page renders. Order is the render order.
|
|
639
|
+
* Each maps 1:1 to either a `DoctorChecks` group or a derived slice.
|
|
640
|
+
*/
|
|
641
|
+
export type DoctorCategory =
|
|
642
|
+
| 'system'
|
|
643
|
+
| 'services'
|
|
644
|
+
| 'counts'
|
|
645
|
+
| 'errors'
|
|
646
|
+
| 'actions';
|
|
647
|
+
|
|
648
|
+
export type DoctorCheckOrInfo =
|
|
649
|
+
| DoctorCheck
|
|
650
|
+
| { kind: 'count'; label: string; value: number | string };
|
|
651
|
+
|
|
652
|
+
export type DoctorPanelProps = {
|
|
653
|
+
category: DoctorCategory;
|
|
654
|
+
/** Title shown in the card header. Optional — derived from category if omitted. */
|
|
655
|
+
title?: string;
|
|
656
|
+
/** Lucide icon for the header. Optional — derived from category if omitted. */
|
|
657
|
+
icon?: React.ComponentType<{ size?: number; 'aria-hidden'?: boolean }>;
|
|
658
|
+
/** Per-line entries. Each carries an implicit `ok | warn | fail` status pill. */
|
|
659
|
+
checks: DoctorCheck[];
|
|
660
|
+
/**
|
|
661
|
+
* Plain key/value rows that aren't checks (e.g. counts). Rendered
|
|
662
|
+
* as a compact two-column list below the check rows when present.
|
|
663
|
+
*/
|
|
664
|
+
info?: Array<{ label: string; value: string | number }>;
|
|
665
|
+
/** Meta line shown beneath the title. */
|
|
666
|
+
meta?: string;
|
|
667
|
+
/** Optional custom body — bypasses the default check-list render. */
|
|
668
|
+
children?: React.ReactNode;
|
|
669
|
+
className?: string;
|
|
670
|
+
};
|
|
671
|
+
|
|
538
672
|
export type SearchResult = {
|
|
539
673
|
type: string;
|
|
540
674
|
score: number;
|
|
@@ -892,13 +892,15 @@
|
|
|
892
892
|
============================================================ */
|
|
893
893
|
.chat-composer-wrap {
|
|
894
894
|
position: relative;
|
|
895
|
-
padding:
|
|
895
|
+
padding: 8px 16px 8px;
|
|
896
896
|
border-top: 1px solid var(--border);
|
|
897
897
|
background: var(--bg);
|
|
898
898
|
}
|
|
899
899
|
.chat-composer-pill {
|
|
900
|
-
display: flex; align-items:
|
|
900
|
+
display: flex; align-items: flex-end; gap: var(--space-2);
|
|
901
901
|
width: 100%;
|
|
902
|
+
min-height: 48px;
|
|
903
|
+
max-height: 200px;
|
|
902
904
|
padding: 8px 8px 8px 16px;
|
|
903
905
|
background: var(--bg-1);
|
|
904
906
|
border: 1px solid var(--border);
|
|
@@ -1480,9 +1482,9 @@
|
|
|
1480
1482
|
.chat-composer-textarea {
|
|
1481
1483
|
flex: 1; min-width: 0; width: 100%; resize: none;
|
|
1482
1484
|
background: transparent; border: none; outline: none;
|
|
1483
|
-
color: var(--text); font: inherit; font-size:
|
|
1484
|
-
line-height: 1.
|
|
1485
|
-
max-height:
|
|
1485
|
+
color: var(--text); font: inherit; font-size: 14px;
|
|
1486
|
+
line-height: 1.4; padding: 8px 12px;
|
|
1487
|
+
min-height: 24px; max-height: 160px; overflow-y: auto;
|
|
1486
1488
|
}
|
|
1487
1489
|
.chat-composer-textarea::placeholder { color: var(--text-muted); }
|
|
1488
1490
|
.chat-attach-btn {
|
|
@@ -1494,6 +1496,7 @@
|
|
|
1494
1496
|
.chat-attach-btn:hover { background: var(--bg-2); color: var(--text); }
|
|
1495
1497
|
.chat-send-btn {
|
|
1496
1498
|
flex-shrink: 0; display: flex; align-items: center; justify-content: center;
|
|
1499
|
+
align-self: flex-end;
|
|
1497
1500
|
width: 32px; height: 32px; padding: 0; background: var(--accent);
|
|
1498
1501
|
color: white; border: none; border-radius: var(--radius-sm); cursor: pointer;
|
|
1499
1502
|
}
|
|
@@ -349,11 +349,25 @@ pre,
|
|
|
349
349
|
width: 100%;
|
|
350
350
|
display: flex;
|
|
351
351
|
flex-direction: column;
|
|
352
|
-
gap: var(--space-
|
|
352
|
+
gap: var(--space-6);
|
|
353
|
+
padding: var(--space-6) var(--space-6) 0;
|
|
353
354
|
animation: view-in var(--motion-slow) var(--ease);
|
|
354
355
|
min-height: 0;
|
|
355
356
|
}
|
|
356
357
|
|
|
358
|
+
/* v4.x.x — Responsive page-level padding for .view */
|
|
359
|
+
@media (max-width: 1000px) {
|
|
360
|
+
.view {
|
|
361
|
+
padding: var(--space-4) var(--space-4) 0;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@media (max-width: 600px) {
|
|
366
|
+
.view {
|
|
367
|
+
padding: var(--space-3) var(--space-3) 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
357
371
|
@keyframes view-in {
|
|
358
372
|
from {
|
|
359
373
|
opacity: 0;
|
|
@@ -727,6 +741,35 @@ pre,
|
|
|
727
741
|
flex-wrap: wrap;
|
|
728
742
|
}
|
|
729
743
|
|
|
744
|
+
/* v4.x.x — Page-level header spacing. Used by views that render a
|
|
745
|
+
title row above the content grid. Adds breathing room below the
|
|
746
|
+
view title and a bottom border to visually separate from content. */
|
|
747
|
+
.page-header {
|
|
748
|
+
display: flex;
|
|
749
|
+
align-items: center;
|
|
750
|
+
justify-content: space-between;
|
|
751
|
+
gap: var(--space-4);
|
|
752
|
+
flex-wrap: wrap;
|
|
753
|
+
margin-bottom: var(--space-6);
|
|
754
|
+
padding-bottom: var(--space-4);
|
|
755
|
+
border-bottom: 1px solid var(--border);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.page-header h1,
|
|
759
|
+
.page-header h2 {
|
|
760
|
+
margin: 0;
|
|
761
|
+
font-weight: 600;
|
|
762
|
+
font-size: 22px;
|
|
763
|
+
letter-spacing: -0.01em;
|
|
764
|
+
color: var(--text-strong);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* v4.x.x — Extra breathing room between a view-header and the content
|
|
768
|
+
below it when the header has no bottom border. */
|
|
769
|
+
.view-header {
|
|
770
|
+
margin-bottom: var(--space-6);
|
|
771
|
+
}
|
|
772
|
+
|
|
730
773
|
/* ─── Cards ────────────────────────────────────────────────────────── */
|
|
731
774
|
|
|
732
775
|
.card {
|
|
@@ -782,6 +825,16 @@ pre,
|
|
|
782
825
|
color: var(--text-dim);
|
|
783
826
|
}
|
|
784
827
|
|
|
828
|
+
/* v4.x.x — Card body used when a card is split into header + body */
|
|
829
|
+
.card-body {
|
|
830
|
+
padding: var(--space-4) var(--space-5);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/* v4.x.x — Extra top margin between sibling cards */
|
|
834
|
+
.card + .card {
|
|
835
|
+
margin-top: var(--space-4);
|
|
836
|
+
}
|
|
837
|
+
|
|
785
838
|
/* ─── Buttons ──────────────────────────────────────────────────────── */
|
|
786
839
|
|
|
787
840
|
.btn {
|
|
@@ -7391,7 +7444,7 @@ select:focus-visible,
|
|
|
7391
7444
|
display: flex;
|
|
7392
7445
|
flex-direction: column;
|
|
7393
7446
|
gap: var(--space-4);
|
|
7394
|
-
padding: var(--space-
|
|
7447
|
+
padding: var(--space-6) var(--space-6) 0;
|
|
7395
7448
|
max-width: 1100px;
|
|
7396
7449
|
margin: 0 auto;
|
|
7397
7450
|
}
|
|
@@ -8990,3 +9043,25 @@ select:focus-visible,
|
|
|
8990
9043
|
gap: 0.5rem;
|
|
8991
9044
|
margin-top: 1rem;
|
|
8992
9045
|
}
|
|
9046
|
+
|
|
9047
|
+
/* v4.9.0 — Settings mode visual indicators */
|
|
9048
|
+
|
|
9049
|
+
.settings-mode-banner {
|
|
9050
|
+
display: flex;
|
|
9051
|
+
align-items: center;
|
|
9052
|
+
justify-content: space-between;
|
|
9053
|
+
padding: 12px 20px;
|
|
9054
|
+
background: linear-gradient(to right, rgba(139, 92, 246, 0.1), transparent);
|
|
9055
|
+
border: 1px solid var(--accent, #8b5cf6);
|
|
9056
|
+
border-radius: 8px;
|
|
9057
|
+
margin-bottom: 24px;
|
|
9058
|
+
font-size: 14px;
|
|
9059
|
+
color: var(--text);
|
|
9060
|
+
}
|
|
9061
|
+
|
|
9062
|
+
.settings-mode-indicator {
|
|
9063
|
+
display: inline-flex;
|
|
9064
|
+
align-items: center;
|
|
9065
|
+
color: var(--accent, #8b5cf6);
|
|
9066
|
+
margin-left: 6px;
|
|
9067
|
+
}
|