@polderlabs/bizar 5.3.0 → 5.4.0
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/EnvVarsSection-DqvB8ZcJ.js +4 -0
- package/bizar-dash/dist/assets/EnvVarsSection-DqvB8ZcJ.js.map +1 -0
- package/bizar-dash/dist/assets/MobileChat-DRXP2ERQ.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-OidrxXR9.js.map → MobileChat-DRXP2ERQ.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js.map +1 -0
- package/bizar-dash/dist/assets/Toast-CcZVVN4i.js +1 -0
- package/bizar-dash/dist/assets/Toast-CcZVVN4i.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Bd8piSb2.js → icons-OMEGx5KA.js} +66 -61
- package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +1 -0
- package/bizar-dash/dist/assets/main-CMAGZouj.js +16 -0
- package/bizar-dash/dist/assets/main-CMAGZouj.js.map +1 -0
- package/bizar-dash/dist/assets/{main-3G6mOhiK.css → main-O2tZVdHm.css} +1 -1
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +1 -0
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-CsZQAswA.css → mobile-layout-D6V-7WnA.css} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +1 -0
- package/bizar-dash/dist/assets/useSlashCommands-iyoW95UB.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-iyoW95UB.js.map +1 -0
- package/bizar-dash/dist/index.html +8 -6
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/auth.mjs +14 -1
- package/bizar-dash/src/server/routes/users.mjs +18 -0
- package/bizar-dash/src/web/MobileApp.tsx +26 -19
- package/bizar-dash/src/web/components/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/components/MobileDrawer.tsx +68 -0
- package/bizar-dash/src/web/components/MobileHeader.tsx +50 -0
- package/bizar-dash/src/web/components/MobileLayout.tsx +63 -0
- package/bizar-dash/src/web/components/TaskCard.tsx +69 -0
- package/bizar-dash/src/web/components/TaskCreateSheet.tsx +76 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/mobile/MobileDoctor.tsx +80 -0
- package/bizar-dash/src/web/mobile/MobileEval.tsx +167 -0
- package/bizar-dash/src/web/mobile/MobileMarketplace.tsx +106 -0
- package/bizar-dash/src/web/mobile/MobileMemory.tsx +108 -0
- package/bizar-dash/src/web/mobile/MobileOverview.tsx +213 -0
- package/bizar-dash/src/web/mobile/MobilePlugins.tsx +71 -0
- package/bizar-dash/src/web/mobile/MobileSettings.tsx +235 -180
- package/bizar-dash/src/web/mobile/MobileTasks.tsx +94 -0
- package/bizar-dash/src/web/mobile.tsx +1 -0
- package/bizar-dash/src/web/styles/main.css +388 -0
- package/bizar-dash/src/web/styles/memory.css +45 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +231 -0
- package/bizar-dash/src/web/styles/mobile.css +180 -0
- package/bizar-dash/src/web/styles/settings.css +110 -0
- package/bizar-dash/src/web/styles/tasks.css +305 -0
- package/bizar-dash/tests/mobile-chat.test.tsx +82 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +296 -0
- package/bizar-dash/tests/mobile-misc.test.tsx +321 -0
- package/bizar-dash/tests/mobile-overview.test.tsx +214 -0
- package/bizar-dash/tests/mobile-settings.test.tsx +181 -0
- package/bizar-dash/tests/mobile-tasks.test.tsx +162 -0
- package/cli/commands/dash.mjs +5 -0
- package/cli/commands/tailscale.mjs +8 -1
- package/package.json +1 -1
- package/bizar-dash/dist/assets/MobileChat-OidrxXR9.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-B91gVFii.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-B91gVFii.js.map +0 -1
- package/bizar-dash/dist/assets/icons-Bd8piSb2.js.map +0 -1
- package/bizar-dash/dist/assets/main-6YUZY968.js +0 -19
- package/bizar-dash/dist/assets/main-6YUZY968.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-CSsp30lr.js +0 -2
- package/bizar-dash/dist/assets/mobile-CSsp30lr.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-DtwK7DOV.js +0 -1
- package/bizar-dash/dist/assets/mobile-DtwK7DOV.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-DEkxEvOO.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-DEkxEvOO.js.map +0 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/mobile-overview.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* v5.4 — Component tests for the mobile-optimized overview (hero + stats +
|
|
5
|
+
* recent activity + quick actions).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import { render, screen } from '@testing-library/react';
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { MobileOverview } from '../src/web/mobile/MobileOverview';
|
|
12
|
+
import type { Snapshot } from '../src/web/lib/types';
|
|
13
|
+
|
|
14
|
+
// ─── Mocks ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
vi.mock('../src/web/lib/api', () => ({
|
|
17
|
+
api: {
|
|
18
|
+
post: vi.fn().mockResolvedValue({ ok: true }),
|
|
19
|
+
get: vi.fn().mockResolvedValue({}),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Stub lucide icons — keep them tiny testids so we can assert presence cheaply.
|
|
24
|
+
vi.mock('lucide-react', async (importOriginal) => {
|
|
25
|
+
const actual = await importOriginal() as Record<string, unknown>;
|
|
26
|
+
const make = (name: string) => {
|
|
27
|
+
const Cmp = (props: { size?: number }) => <svg data-testid={`icon-${name}`} data-size={props.size} />;
|
|
28
|
+
Cmp.displayName = name;
|
|
29
|
+
return Cmp;
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
CheckSquare: make('CheckSquare'),
|
|
34
|
+
Clock: make('Clock'),
|
|
35
|
+
Bot: make('Bot'),
|
|
36
|
+
Coins: make('Coins'),
|
|
37
|
+
Plus: make('Plus'),
|
|
38
|
+
MessageSquare: make('MessageSquare'),
|
|
39
|
+
Stethoscope: make('Stethoscope'),
|
|
40
|
+
Send: make('Send'),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ─── Test data ──────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const BASE_SNAPSHOT: Snapshot = {
|
|
47
|
+
overview: {
|
|
48
|
+
counts: { agents: 0, plans: 0, projects: 0, sessions: 0 },
|
|
49
|
+
recentActivity: [],
|
|
50
|
+
versions: { node: '', platform: '', projectRoot: '', bizarRoot: '' },
|
|
51
|
+
generatedAt: '',
|
|
52
|
+
},
|
|
53
|
+
agents: [],
|
|
54
|
+
artifacts: [],
|
|
55
|
+
projects: [],
|
|
56
|
+
activeProject: null,
|
|
57
|
+
config: { path: '', data: null, raw: '', exists: false },
|
|
58
|
+
settings: {
|
|
59
|
+
path: '',
|
|
60
|
+
data: {
|
|
61
|
+
theme: { mode: 'dark', accent: '#8b5cf6', success: '#3fb950', warning: '#f0883e', error: '#f85149', info: '#58a6ff', fontFamily: 'sans-serif', fontSize: 14, compactMode: false, animations: true },
|
|
62
|
+
ui: { layout: 'topnav', showHeader: true, showStatusBar: true, defaultTab: 'overview' },
|
|
63
|
+
defaultAgent: 'odin',
|
|
64
|
+
defaultModel: '',
|
|
65
|
+
notifications: { onAgentComplete: false, onPlanApproval: false },
|
|
66
|
+
dashboard: { autoLaunchWeb: false },
|
|
67
|
+
service: { enabled: false, autostart: false },
|
|
68
|
+
about: { version: '5.4.0', homepage: '', license: 'MIT' },
|
|
69
|
+
agents: { maxParallel: 6, stuckThresholdMs: 600000, autoRestart: false },
|
|
70
|
+
personalization: { displayName: '', role: '', team: '', aboutMe: '', preferences: '' },
|
|
71
|
+
workflow: { artifactsEnabled: true, agentsDecideAutonomously: false, chatAutonomous: false },
|
|
72
|
+
headroom: { enabled: false, autoInstall: true, port: 8787, host: '127.0.0.1', outputShaper: false, telemetry: false, budget: 0, backend: 'anthropic', autoStart: true, autoWrap: true, routeAllProviders: true },
|
|
73
|
+
},
|
|
74
|
+
exists: true,
|
|
75
|
+
},
|
|
76
|
+
tasks: [],
|
|
77
|
+
mods: [],
|
|
78
|
+
schedules: [],
|
|
79
|
+
providers: [],
|
|
80
|
+
mcps: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
describe('MobileOverview', () => {
|
|
86
|
+
const refreshSnapshot = vi.fn().mockResolvedValue(undefined);
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('renders hero card with input', () => {
|
|
93
|
+
render(<MobileOverview snapshot={BASE_SNAPSHOT} refreshSnapshot={refreshSnapshot} />);
|
|
94
|
+
|
|
95
|
+
expect(screen.getByRole('heading', { name: /what do you want to do/i })).toBeInTheDocument();
|
|
96
|
+
const textarea = screen.getByPlaceholderText(/implement user authentication/i);
|
|
97
|
+
expect(textarea).toBeInTheDocument();
|
|
98
|
+
expect(textarea.tagName).toBe('TEXTAREA');
|
|
99
|
+
expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('disables submit button when textarea is empty', () => {
|
|
103
|
+
render(<MobileOverview snapshot={BASE_SNAPSHOT} refreshSnapshot={refreshSnapshot} />);
|
|
104
|
+
|
|
105
|
+
const submit = screen.getByRole('button', { name: /submit/i });
|
|
106
|
+
expect(submit).toBeDisabled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('enables submit button when textarea has text', async () => {
|
|
110
|
+
const user = userEvent.setup();
|
|
111
|
+
render(<MobileOverview snapshot={BASE_SNAPSHOT} refreshSnapshot={refreshSnapshot} />);
|
|
112
|
+
|
|
113
|
+
await user.type(screen.getByPlaceholderText(/implement user authentication/i), 'fix the bug');
|
|
114
|
+
expect(screen.getByRole('button', { name: /submit/i })).not.toBeDisabled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('renders 4 stat cards', () => {
|
|
118
|
+
render(<MobileOverview snapshot={BASE_SNAPSHOT} refreshSnapshot={refreshSnapshot} />);
|
|
119
|
+
|
|
120
|
+
expect(screen.getByText('Tasks')).toBeInTheDocument();
|
|
121
|
+
expect(screen.getByText('Schedules')).toBeInTheDocument();
|
|
122
|
+
expect(screen.getByText('Active Agents')).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByText('API Tokens')).toBeInTheDocument();
|
|
124
|
+
|
|
125
|
+
// Each stat label sits on a card with the matching modifier class.
|
|
126
|
+
expect(document.querySelectorAll('.mobile-stat-card.is-blue')).toHaveLength(1);
|
|
127
|
+
expect(document.querySelectorAll('.mobile-stat-card.is-purple')).toHaveLength(1);
|
|
128
|
+
expect(document.querySelectorAll('.mobile-stat-card.is-green')).toHaveLength(1);
|
|
129
|
+
expect(document.querySelectorAll('.mobile-stat-card.is-yellow')).toHaveLength(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('reflects task count and active task subtext from snapshot', () => {
|
|
133
|
+
const snap: Snapshot = {
|
|
134
|
+
...BASE_SNAPSHOT,
|
|
135
|
+
tasks: [
|
|
136
|
+
{ ...BASE_SNAPSHOT.tasks[0], id: 't1', title: 'a', description: '', status: 'doing', tags: [], priority: 'normal', createdAt: '', updatedAt: '' },
|
|
137
|
+
{ ...BASE_SNAPSHOT.tasks[0], id: 't2', title: 'b', description: '', status: 'done', tags: [], priority: 'normal', createdAt: '', updatedAt: '' },
|
|
138
|
+
{ ...BASE_SNAPSHOT.tasks[0], id: 't3', title: 'c', description: '', status: 'queued', tags: [], priority: 'normal', createdAt: '', updatedAt: '' },
|
|
139
|
+
] as Snapshot['tasks'],
|
|
140
|
+
schedules: [
|
|
141
|
+
{ id: 's1', name: 'nightly', cron: '0 0 * * *', enabled: true, lastRun: null, nextRun: null, target: 'agent', agentName: 'odin', createdAt: '', updatedAt: '' },
|
|
142
|
+
] as Snapshot['schedules'],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
render(<MobileOverview snapshot={snap} refreshSnapshot={refreshSnapshot} />);
|
|
146
|
+
|
|
147
|
+
// Tasks stat — first mobile-stat-value inside the blue card.
|
|
148
|
+
const blueCard = document.querySelector('.mobile-stat-card.is-blue')!;
|
|
149
|
+
expect(blueCard.querySelector('.mobile-stat-value')?.textContent).toBe('3');
|
|
150
|
+
expect(blueCard.querySelector('.mobile-stat-subtext')?.textContent).toBe('1 active');
|
|
151
|
+
|
|
152
|
+
// Schedules stat.
|
|
153
|
+
const purpleCard = document.querySelector('.mobile-stat-card.is-purple')!;
|
|
154
|
+
expect(purpleCard.querySelector('.mobile-stat-value')?.textContent).toBe('1');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('shows recent activity when snapshot has items', () => {
|
|
158
|
+
const snap: Snapshot = {
|
|
159
|
+
...BASE_SNAPSHOT,
|
|
160
|
+
overview: {
|
|
161
|
+
...BASE_SNAPSHOT.overview,
|
|
162
|
+
recentActivity: [
|
|
163
|
+
{ ts: '2026-07-05T12:34:00.000Z', kind: 'task', title: 'hello' },
|
|
164
|
+
{ ts: '2026-07-05T12:35:00.000Z', kind: 'agent', name: 'odin' },
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
render(<MobileOverview snapshot={snap} refreshSnapshot={refreshSnapshot} />);
|
|
170
|
+
|
|
171
|
+
expect(screen.getByRole('heading', { name: /recent activity/i })).toBeInTheDocument();
|
|
172
|
+
const items = document.querySelectorAll('.mobile-activity-item');
|
|
173
|
+
expect(items).toHaveLength(2);
|
|
174
|
+
expect(screen.queryByText(/no recent activity/i)).not.toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('shows empty state when there is no recent activity', () => {
|
|
178
|
+
render(<MobileOverview snapshot={BASE_SNAPSHOT} refreshSnapshot={refreshSnapshot} />);
|
|
179
|
+
|
|
180
|
+
expect(screen.getByText(/no recent activity/i)).toBeInTheDocument();
|
|
181
|
+
expect(document.querySelector('.mobile-activity-list')).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('renders quick action buttons', () => {
|
|
185
|
+
render(<MobileOverview snapshot={BASE_SNAPSHOT} refreshSnapshot={refreshSnapshot} />);
|
|
186
|
+
|
|
187
|
+
expect(screen.getByRole('button', { name: /new task/i })).toBeInTheDocument();
|
|
188
|
+
expect(screen.getByRole('button', { name: /new chat/i })).toBeInTheDocument();
|
|
189
|
+
expect(screen.getByRole('button', { name: /run doctor/i })).toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('clamps recent activity to 8 items', () => {
|
|
193
|
+
const many: { ts: string; kind: string; title: string }[] = [];
|
|
194
|
+
for (let i = 0; i < 12; i++) {
|
|
195
|
+
many.push({ ts: `2026-07-05T12:${String(i).padStart(2, '0')}:00.000Z`, kind: 'task', title: `t${i}` });
|
|
196
|
+
}
|
|
197
|
+
const snap: Snapshot = {
|
|
198
|
+
...BASE_SNAPSHOT,
|
|
199
|
+
overview: { ...BASE_SNAPSHOT.overview, recentActivity: many },
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
render(<MobileOverview snapshot={snap} refreshSnapshot={refreshSnapshot} />);
|
|
203
|
+
|
|
204
|
+
expect(document.querySelectorAll('.mobile-activity-item')).toHaveLength(8);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('handles null snapshot without crashing', () => {
|
|
208
|
+
render(<MobileOverview snapshot={null} refreshSnapshot={refreshSnapshot} />);
|
|
209
|
+
|
|
210
|
+
expect(screen.getByRole('heading', { name: /what do you want to do/i })).toBeInTheDocument();
|
|
211
|
+
expect(screen.getByText(/no recent activity/i)).toBeInTheDocument();
|
|
212
|
+
expect(screen.getByText('Tasks')).toBeInTheDocument();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/mobile-settings.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* v5.4 — Component tests for the mobile settings accordion view.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { render, screen } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { MobileSettings } from '../src/web/mobile/MobileSettings';
|
|
11
|
+
import type { Settings, Snapshot } from '../src/web/lib/types';
|
|
12
|
+
|
|
13
|
+
// ─── Mock sub-components that make API calls ────────────────────────────────
|
|
14
|
+
|
|
15
|
+
vi.mock('../src/web/views/settings/GeneralSection', () => ({
|
|
16
|
+
GeneralSection: () => <div data-testid="general-section">GeneralSection</div>,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('../src/web/views/settings/EnvVarsSection', () => ({
|
|
20
|
+
EnvVarsSection: () => <div data-testid="env-vars-section">EnvVarsSection</div>,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('../src/web/views/settings/MemorySection', () => ({
|
|
24
|
+
MemorySection: () => <div data-testid="memory-section">MemorySection</div>,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock('../src/web/views/settings/SystemLlmSection', () => ({
|
|
28
|
+
SystemLlmSection: () => <div data-testid="system-llm-section">SystemLlmSection</div>,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('../src/web/views/settings/UpdatesSection', () => ({
|
|
32
|
+
UpdatesSection: () => <div data-testid="updates-section">UpdatesSection</div>,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock('../src/web/views/settings/HeadroomSection', () => ({
|
|
36
|
+
HeadroomSection: () => <div data-testid="headroom-section">HeadroomSection</div>,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('../src/web/components/TailscaleSettings', () => ({
|
|
40
|
+
TailscaleSettings: () => <div data-testid="tailscale-section">TailscaleSettings</div>,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('../src/web/views/settings/ActivitySection', () => ({
|
|
44
|
+
ActivitySection: () => <div data-testid="about-section">AboutSection</div>,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// ─── Test data ─────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const DEFAULT_SETTINGS: Settings = {
|
|
50
|
+
theme: {
|
|
51
|
+
mode: 'dark',
|
|
52
|
+
accent: '#8b5cf6',
|
|
53
|
+
success: '#3fb950',
|
|
54
|
+
warning: '#f0883e',
|
|
55
|
+
error: '#f85149',
|
|
56
|
+
info: '#58a6ff',
|
|
57
|
+
fontFamily: 'sans-serif',
|
|
58
|
+
fontSize: 14,
|
|
59
|
+
compactMode: false,
|
|
60
|
+
animations: true,
|
|
61
|
+
},
|
|
62
|
+
ui: {
|
|
63
|
+
layout: 'topnav',
|
|
64
|
+
showHeader: true,
|
|
65
|
+
showStatusBar: true,
|
|
66
|
+
defaultTab: 'overview',
|
|
67
|
+
},
|
|
68
|
+
defaultAgent: 'odin',
|
|
69
|
+
defaultModel: '',
|
|
70
|
+
notifications: { onAgentComplete: false, onPlanApproval: false },
|
|
71
|
+
dashboard: { autoLaunchWeb: false },
|
|
72
|
+
service: { enabled: false, autostart: false },
|
|
73
|
+
about: { version: '5.4.0', homepage: 'https://github.com/DrB0rk/BizarHarness', license: 'MIT' },
|
|
74
|
+
agents: { maxParallel: 6, stuckThresholdMs: 600000, autoRestart: false },
|
|
75
|
+
personalization: { displayName: '', role: '', team: '', aboutMe: '', preferences: '' },
|
|
76
|
+
workflow: { artifactsEnabled: true, agentsDecideAutonomously: false, chatAutonomous: false },
|
|
77
|
+
headroom: { enabled: false, autoInstall: true, port: 8787, host: '127.0.0.1', outputShaper: false, telemetry: false, budget: 0, backend: 'anthropic', autoStart: true, autoWrap: true, routeAllProviders: true },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const SNAPSHOT_WITH_PROVIDERS: Snapshot = {
|
|
81
|
+
agents: [],
|
|
82
|
+
artifacts: [],
|
|
83
|
+
projects: [],
|
|
84
|
+
activeProject: null,
|
|
85
|
+
config: { path: '', data: null, raw: '', exists: false },
|
|
86
|
+
settings: { path: '', data: DEFAULT_SETTINGS, exists: true },
|
|
87
|
+
tasks: [],
|
|
88
|
+
mods: [],
|
|
89
|
+
schedules: [],
|
|
90
|
+
providers: [
|
|
91
|
+
{ id: 'p1', name: 'OpenAI', enabled: true, kind: 'openai', models: [] },
|
|
92
|
+
{ id: 'p2', name: 'Anthropic', enabled: true, kind: 'anthropic', models: [] },
|
|
93
|
+
],
|
|
94
|
+
mcps: [],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe('MobileSettings', () => {
|
|
100
|
+
const onSettingsChange = vi.fn();
|
|
101
|
+
const autoSave = vi.fn();
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders all 9 settings sections', () => {
|
|
108
|
+
render(
|
|
109
|
+
<MobileSettings
|
|
110
|
+
settings={DEFAULT_SETTINGS}
|
|
111
|
+
onSettingsChange={onSettingsChange}
|
|
112
|
+
autoSave={autoSave}
|
|
113
|
+
snapshot={SNAPSHOT_WITH_PROVIDERS}
|
|
114
|
+
/>,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(screen.getByText('General')).toBeInTheDocument();
|
|
118
|
+
expect(screen.getByText('AI Providers')).toBeInTheDocument();
|
|
119
|
+
expect(screen.getByText('Environment Variables')).toBeInTheDocument();
|
|
120
|
+
expect(screen.getByText('Memory Vault')).toBeInTheDocument();
|
|
121
|
+
expect(screen.getByText('System LLM')).toBeInTheDocument();
|
|
122
|
+
expect(screen.getByText('Updates')).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByText('Headroom')).toBeInTheDocument();
|
|
124
|
+
expect(screen.getByText('Tailscale')).toBeInTheDocument();
|
|
125
|
+
expect(screen.getByText('About')).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('filters sections by search query', async () => {
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
render(
|
|
131
|
+
<MobileSettings
|
|
132
|
+
settings={DEFAULT_SETTINGS}
|
|
133
|
+
onSettingsChange={onSettingsChange}
|
|
134
|
+
autoSave={autoSave}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const searchInput = screen.getByRole('textbox', { name: /search settings/i });
|
|
139
|
+
await user.type(searchInput, 'provider');
|
|
140
|
+
|
|
141
|
+
// Only "AI Providers" matches
|
|
142
|
+
expect(screen.getByText('AI Providers')).toBeInTheDocument();
|
|
143
|
+
expect(screen.queryByText('General')).not.toBeInTheDocument();
|
|
144
|
+
expect(screen.queryByText('Memory Vault')).not.toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('expands/collapses section on click', async () => {
|
|
148
|
+
const user = userEvent.setup();
|
|
149
|
+
render(
|
|
150
|
+
<MobileSettings
|
|
151
|
+
settings={DEFAULT_SETTINGS}
|
|
152
|
+
onSettingsChange={onSettingsChange}
|
|
153
|
+
autoSave={autoSave}
|
|
154
|
+
/>,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Initially nothing is expanded
|
|
158
|
+
expect(screen.queryByTestId('general-section')).not.toBeInTheDocument();
|
|
159
|
+
|
|
160
|
+
// Click General — expands it
|
|
161
|
+
await user.click(screen.getByText('General'));
|
|
162
|
+
expect(screen.getByTestId('general-section')).toBeInTheDocument();
|
|
163
|
+
|
|
164
|
+
// Click again — collapses
|
|
165
|
+
await user.click(screen.getByText('General'));
|
|
166
|
+
expect(screen.queryByTestId('general-section')).not.toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('shows "2 configured" for AI Providers when snapshot has 2 providers', () => {
|
|
170
|
+
render(
|
|
171
|
+
<MobileSettings
|
|
172
|
+
settings={DEFAULT_SETTINGS}
|
|
173
|
+
onSettingsChange={onSettingsChange}
|
|
174
|
+
autoSave={autoSave}
|
|
175
|
+
snapshot={SNAPSHOT_WITH_PROVIDERS}
|
|
176
|
+
/>,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(screen.getByText('2 configured')).toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/mobile-tasks.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* v5.4 — Mobile tasks component tests.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { render, screen, act, fireEvent, waitFor } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { MobileTasks } from '../src/web/mobile/MobileTasks';
|
|
11
|
+
import { TaskCard } from '../src/web/components/TaskCard';
|
|
12
|
+
import type { Snapshot, Task } from '../src/web/lib/types';
|
|
13
|
+
|
|
14
|
+
const mockTask = (overrides: Partial<Task> = {}): Task => ({
|
|
15
|
+
id: 'task-1',
|
|
16
|
+
title: 'Test task',
|
|
17
|
+
description: 'A test task description',
|
|
18
|
+
status: 'queued',
|
|
19
|
+
tags: [],
|
|
20
|
+
priority: 'normal',
|
|
21
|
+
assignee: null,
|
|
22
|
+
parent: null,
|
|
23
|
+
dependencies: [],
|
|
24
|
+
timeSpent: undefined,
|
|
25
|
+
recurring: null,
|
|
26
|
+
attachments: [],
|
|
27
|
+
comments: [],
|
|
28
|
+
activity: [],
|
|
29
|
+
archived: false,
|
|
30
|
+
workedBy: null,
|
|
31
|
+
dueDate: null,
|
|
32
|
+
subtasks: [],
|
|
33
|
+
metadata: null,
|
|
34
|
+
progress: undefined,
|
|
35
|
+
currentStep: null,
|
|
36
|
+
progressAgent: null,
|
|
37
|
+
progressHistory: [],
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
updatedAt: new Date().toISOString(),
|
|
40
|
+
completedAt: null,
|
|
41
|
+
_timerStart: undefined,
|
|
42
|
+
...overrides,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const mockSnapshot = (tasks: Task[] = []): Snapshot => ({
|
|
46
|
+
overview: {
|
|
47
|
+
counts: { agents: 0, plans: 0, projects: 0, sessions: 0, activeProject: null },
|
|
48
|
+
recentActivity: [],
|
|
49
|
+
versions: { node: '', platform: '', projectRoot: '', bizarRoot: '' },
|
|
50
|
+
generatedAt: new Date().toISOString(),
|
|
51
|
+
},
|
|
52
|
+
agents: [],
|
|
53
|
+
artifacts: [],
|
|
54
|
+
projects: [],
|
|
55
|
+
activeProject: null,
|
|
56
|
+
config: { path: '', data: {}, raw: '', exists: false },
|
|
57
|
+
settings: { path: '', data: { theme: { mode: 'dark', accent: '#8b5cf6', success: '#3fb950', warning: '#d29922', error: '#f85149', info: '#58a6ff', fontFamily: '', fontSize: 14, compactMode: false, animations: true }, ui: { layout: 'topnav', showHeader: true, showStatusBar: true, defaultTab: '' }, defaultAgent: 'odin', defaultModel: '', notifications: { onAgentComplete: false, onPlanApproval: false }, dashboard: { autoLaunchWeb: false }, service: { enabled: false, autostart: false }, about: { version: '', homepage: '', license: '' }, agents: { maxParallel: 1, stuckThresholdMs: 0, autoRestart: false }, personalization: { displayName: '', role: '', team: '', aboutMe: '', preferences: '' }, workflow: { artifactsEnabled: false, agentsDecideAutonomously: false, chatAutonomous: false } }, exists: false },
|
|
58
|
+
tasks,
|
|
59
|
+
mods: [],
|
|
60
|
+
schedules: [],
|
|
61
|
+
providers: [],
|
|
62
|
+
mcps: [],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('MobileTasks', () => {
|
|
66
|
+
const refreshSnapshot = vi.fn().mockResolvedValue(undefined);
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.clearAllMocks();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders filter tabs with counts', () => {
|
|
73
|
+
const tasks = [
|
|
74
|
+
mockTask({ id: '1', status: 'queued' }),
|
|
75
|
+
mockTask({ id: '2', status: 'doing' }),
|
|
76
|
+
mockTask({ id: '3', status: 'done' }),
|
|
77
|
+
mockTask({ id: '4', status: 'failed' }),
|
|
78
|
+
];
|
|
79
|
+
render(<MobileTasks snapshot={mockSnapshot(tasks)} refreshSnapshot={refreshSnapshot} />);
|
|
80
|
+
|
|
81
|
+
expect(screen.getByRole('button', { name: /All 4/i })).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByRole('button', { name: /Todo 1/i })).toBeInTheDocument();
|
|
83
|
+
expect(screen.getByRole('button', { name: /Doing 1/i })).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByRole('button', { name: /Done 1/i })).toBeInTheDocument();
|
|
85
|
+
expect(screen.getByRole('button', { name: /Failed 1/i })).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('filters by search query', async () => {
|
|
89
|
+
const tasks = [
|
|
90
|
+
mockTask({ id: '1', title: 'Build the thing' }),
|
|
91
|
+
mockTask({ id: '2', title: 'Fix the bug' }),
|
|
92
|
+
];
|
|
93
|
+
render(<MobileTasks snapshot={mockSnapshot(tasks)} refreshSnapshot={refreshSnapshot} />);
|
|
94
|
+
|
|
95
|
+
// Both visible initially
|
|
96
|
+
expect(screen.getByText('Build the thing')).toBeInTheDocument();
|
|
97
|
+
expect(screen.getByText('Fix the bug')).toBeInTheDocument();
|
|
98
|
+
|
|
99
|
+
// Type search query using fireEvent (more reliable in tests)
|
|
100
|
+
const input = screen.getByPlaceholderText('Search tasks...');
|
|
101
|
+
fireEvent.change(input, { target: { value: 'build' } });
|
|
102
|
+
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(screen.getByText('Build the thing')).toBeInTheDocument();
|
|
105
|
+
expect(screen.queryByText('Fix the bug')).not.toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('opens create sheet on FAB click', async () => {
|
|
110
|
+
render(<MobileTasks snapshot={mockSnapshot([])} refreshSnapshot={refreshSnapshot} />);
|
|
111
|
+
|
|
112
|
+
const fab = screen.getByRole('button', { name: /Create new task/i });
|
|
113
|
+
fireEvent.click(fab);
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('shows empty state when no tasks', () => {
|
|
121
|
+
render(<MobileTasks snapshot={mockSnapshot([])} refreshSnapshot={refreshSnapshot} />);
|
|
122
|
+
expect(screen.getByText(/No tasks/i)).toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('TaskCard', () => {
|
|
127
|
+
it('renders title and status', () => {
|
|
128
|
+
const task = mockTask({ title: 'My task title', status: 'doing' });
|
|
129
|
+
render(<TaskCard task={task} />);
|
|
130
|
+
expect(screen.getByText('My task title')).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('expands on click to show details', async () => {
|
|
134
|
+
const task = mockTask({ title: 'Expandable task', description: 'Task description here', status: 'queued' });
|
|
135
|
+
render(<TaskCard task={task} />);
|
|
136
|
+
|
|
137
|
+
// Details not visible initially
|
|
138
|
+
expect(screen.queryByText('Task description here')).not.toBeInTheDocument();
|
|
139
|
+
|
|
140
|
+
// Click to expand
|
|
141
|
+
fireEvent.click(screen.getByText('Expandable task'));
|
|
142
|
+
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(screen.getByText('Task description here')).toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
expect(screen.getByRole('button', { name: /Start/i })).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('color-coded by status', () => {
|
|
150
|
+
const doingTask = mockTask({ id: 'doing-task', status: 'doing' });
|
|
151
|
+
const { rerender } = render(<TaskCard task={doingTask} />);
|
|
152
|
+
|
|
153
|
+
const card = screen.getByText('Test task').closest('.task-card-mobile');
|
|
154
|
+
expect(card).toHaveClass('is-doing');
|
|
155
|
+
|
|
156
|
+
const doneTask = mockTask({ id: 'done-task', status: 'done' });
|
|
157
|
+
rerender(<TaskCard task={doneTask} />);
|
|
158
|
+
|
|
159
|
+
const doneCard = screen.getByText('Test task').closest('.task-card-mobile');
|
|
160
|
+
expect(doneCard).toHaveClass('is-done');
|
|
161
|
+
});
|
|
162
|
+
});
|
package/cli/commands/dash.mjs
CHANGED
|
@@ -138,6 +138,11 @@ export async function runDash(dashArgs) {
|
|
|
138
138
|
const serveResult = await setupTailscaleServe({ dashboardPort: usePort });
|
|
139
139
|
if (serveResult.ok) {
|
|
140
140
|
console.log(chalk.green(` ✓ Tailscale serve: ${serveResult.url}`));
|
|
141
|
+
// v5.3.1 — Auto-trust Tailscale clients so they don't need
|
|
142
|
+
// a bearer token. Tailscale users are already authenticated
|
|
143
|
+
// on the tailnet before they reach the dashboard.
|
|
144
|
+
process.env.BIZAR_DASHBOARD_TRUST_TAILSCALE = '1';
|
|
145
|
+
console.log(chalk.dim(' ✓ Tailscale clients auto-trusted (BIZAR_DASHBOARD_TRUST_TAILSCALE=1)'));
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
} catch (err) {
|
|
@@ -83,9 +83,16 @@ export async function setupTailscaleServe({ dashboardPort = 4097, path = '/', ht
|
|
|
83
83
|
} catch { /* ignore — might not have existing config */ }
|
|
84
84
|
|
|
85
85
|
// Set up new serve config
|
|
86
|
+
// v5.3.1 — Use --set-x-forwarded-for=trailing so the dashboard sees
|
|
87
|
+
// the original client IP (a 100.x Tailscale address) in X-Forwarded-For.
|
|
88
|
+
// Without this, the dashboard can't tell that the request came from
|
|
89
|
+
// a trusted tailnet client and demands a bearer token.
|
|
86
90
|
try {
|
|
87
91
|
execFileSync('tailscale', [
|
|
88
|
-
'serve', '--bg',
|
|
92
|
+
'serve', '--bg',
|
|
93
|
+
`--https=${https}`,
|
|
94
|
+
`--set-path=${path}`,
|
|
95
|
+
'--set-x-forwarded-for=trailing',
|
|
89
96
|
`http://localhost:${dashboardPort}`,
|
|
90
97
|
], { encoding: 'utf8', timeout: 30000 });
|
|
91
98
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness. v4 ships as a single npm package bundling the dashboard server, opencode plugin, and typed SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{j as n,r as t}from"./react-vendor-Dn4wqh4Z.js";import{C as N,u as T,a as k,b as R,c as E,d as F}from"./useSlashCommands-DEkxEvOO.js";import"./icons-Bd8piSb2.js";import"./mobile-CSsp30lr.js";import"./markdown-C6mXtQxD.js";import"./vendor-CeHGtduv.js";function $(o){const{activeSource:c}=o;return n.jsx("div",{className:`chat-composer-wrap chat-composer-source-${c??"none"} legacy`,children:n.jsx("div",{className:"chat-composer-pill",children:n.jsx(N,{...o})})})}function A({snapshot:o,settings:c,setActiveTab:g,initialTaskId:j,onClearTaskId:z}){const i=T(),s=k(o,c,j??"");t.useEffect(()=>{s.setToast({error:e=>i.error(e),success:e=>i.success(e),info:e=>i.info(e),warning:e=>i.warning(e)})},[s,i]);const[r,l]=t.useState(""),[f,x]=t.useState(c.defaultAgent||"odin"),[p,C]=t.useState(c.defaultModel||""),[h,v]=t.useState([]),u=t.useRef(null),{suggestions:y,setQuery:m}=R(o);t.useEffect(()=>{m(r)},[r,m]);const P=()=>{var e;return(e=u.current)==null?void 0:e.click()},w=e=>{const d=e.target.files;if(!d)return;const S=[];for(let a=0;a<d.length;a++)S.push(d[a].name);v(a=>{const O=S.filter(M=>!a.includes(M));return[...a,...O]}),u.current&&(u.current.value="")},I=async()=>{const e=r.trim();if(!e)return;l(""),m(""),(await s.onSend(e,f,p,h)).ok&&s.jumpToLatest()};return n.jsxs("div",{className:"chat-shell",children:[n.jsx(E,{activeProject:o.activeProject,sessionCount:s.sessions.length,sessionsOpen:!1,infoOpen:!1,onToggleSessions:()=>{},onToggleInfo:()=>{},onOpenOverview:()=>g==null?void 0:g("overview")}),n.jsx("div",{className:"chat-body",children:n.jsxs("main",{className:"chat-main",children:[n.jsx(F,{messages:s.activeSource==="opencode"?s.opencodeMessages:s.bizarMessages,loading:s.loading,activeProject:o.activeProject,sessionId:s.activeSource==="opencode"?s.activeOpencodeSessionId??s.sessionId:s.sessionId,pinned:s.pinned,activeSource:s.activeSource,onPickSuggestion:e=>l(e),onCopy:e=>s.copyMessage(e),onDelete:()=>{},onTogglePin:s.togglePin,onRegenerate:s.onRegenerate}),n.jsx($,{agent:f,setAgent:x,model:p,setModel:C,text:r,setText:l,sending:s.sending,activeSource:s.activeSource,onSend:I,attachments:h,setAttachments:v,suggestions:y,onPickSuggestion:e=>l(`${e.split(" ")[0]} `),agents:o.agents||[],onAttach:P,sessionsOpen:!1,infoOpen:!1}),n.jsx("input",{ref:u,type:"file",multiple:!0,style:{display:"none"},onChange:w})]})})]})}export{A as MobileChat};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as o,j as e}from"./react-vendor-Dn4wqh4Z.js";import{d,a as f,g as N}from"./mobile-CSsp30lr.js";import{aw as y,ax as k,ay as S}from"./icons-Bd8piSb2.js";const w=[{id:"dark",label:"Dark",Icon:y},{id:"light",label:"Light",Icon:k},{id:"system",label:"System",Icon:S}],C=[{name:"Purple",accent:"#8b5cf6"},{name:"Blue",accent:"#3b82f6"},{name:"Green",accent:"#10b981"},{name:"Orange",accent:"#f97316"},{name:"Red",accent:"#ef4444"},{name:"Pink",accent:"#ec4899"},{name:"Cyan",accent:"#06b6d4"},{name:"Mono",accent:"#6b7280"}];function D({settings:n,snapshot:a,onRefresh:u}){var g,x,j;const[t,m]=o.useState(n),[v,r]=o.useState(!1),[b,h]=o.useState(!1);o.useEffect(()=>{m(n),r(!1),n.theme&&d(n.theme)},[n]);const i=s=>{m(l=>{const c={...l,theme:{...l.theme,...s}};return N(c.theme),d(c.theme),c}),r(!0)},p=async()=>{h(!0);try{const s=await f.put("/settings",t);m(s.data),r(!1),N(s.data.theme),d(s.data.theme),u()}catch{}finally{h(!1)}};return e.jsxs("div",{className:"mobile-view",children:[v&&e.jsxs("div",{className:"mobile-settings-save-bar",children:[e.jsx("span",{children:"Unsaved changes"}),e.jsx("button",{type:"button",className:"mobile-btn",disabled:b,onClick:p,children:b?"Saving…":"Save"})]}),e.jsxs("section",{className:"mobile-section",children:[e.jsx("h3",{className:"mobile-section-title",children:"Appearance"}),e.jsxs("div",{className:"mobile-card",children:[e.jsxs("div",{className:"mobile-setting-row",children:[e.jsx("span",{className:"mobile-setting-label",children:"Theme"}),e.jsx("div",{className:"mobile-theme-btns",children:w.map(({id:s,label:l,Icon:c})=>e.jsxs("button",{type:"button",className:`mobile-theme-btn ${t.theme.mode===s?"active":""}`,onClick:()=>i({mode:s}),children:[e.jsx(c,{size:14})," ",l]},s))})]}),e.jsxs("div",{className:"mobile-setting-row",style:{flexDirection:"column",alignItems:"flex-start",gap:8},children:[e.jsx("span",{className:"mobile-setting-label",children:"Accent color"}),e.jsx("div",{className:"mobile-accent-swatches",children:C.map(s=>e.jsx("button",{type:"button",className:`mobile-accent-swatch ${t.theme.accent===s.accent?"active":""}`,style:{background:s.accent},onClick:()=>i({accent:s.accent}),title:s.name},s.name))})]}),e.jsxs("div",{className:"mobile-setting-row",style:{flexDirection:"column",alignItems:"flex-start",gap:8},children:[e.jsxs("span",{className:"mobile-setting-label",children:["Font size: ",t.theme.fontSize,"px"]}),e.jsx("input",{type:"range",min:12,max:20,value:t.theme.fontSize,onChange:s=>i({fontSize:Number(s.target.value)}),style:{width:"100%"}})]}),e.jsxs("div",{className:"mobile-setting-row",children:[e.jsx("span",{className:"mobile-setting-label",children:"Compact mode"}),e.jsxs("label",{className:"mobile-toggle",children:[e.jsx("input",{type:"checkbox",checked:t.theme.compactMode,onChange:s=>i({compactMode:s.target.checked})}),e.jsx("span",{className:"mobile-toggle-slider"})]})]}),e.jsxs("div",{className:"mobile-setting-row",children:[e.jsx("span",{className:"mobile-setting-label",children:"Animations"}),e.jsxs("label",{className:"mobile-toggle",children:[e.jsx("input",{type:"checkbox",checked:t.theme.animations,onChange:s=>i({animations:s.target.checked})}),e.jsx("span",{className:"mobile-toggle-slider"})]})]})]})]}),e.jsxs("section",{className:"mobile-section",children:[e.jsx("h3",{className:"mobile-section-title",children:"Chat Defaults"}),e.jsxs("div",{className:"mobile-card",children:[e.jsx("label",{className:"mobile-field-label",children:"Default Agent"}),e.jsx("select",{className:"mobile-input",value:t.defaultAgent||"odin",onChange:s=>m(l=>({...l,defaultAgent:s.target.value})),children:((a==null?void 0:a.agents)||[]).map(s=>e.jsxs("option",{value:s.name,children:["@",s.name]},s.name))})]})]}),e.jsxs("section",{className:"mobile-section",children:[e.jsx("h3",{className:"mobile-section-title",children:"About"}),e.jsxs("div",{className:"mobile-card",children:[e.jsxs("div",{className:"mobile-setting-row",children:[e.jsx("span",{className:"mobile-setting-label",children:"Version"}),e.jsx("span",{className:"mobile-setting-value mono",children:((g=t.about)==null?void 0:g.version)||"—"})]}),e.jsxs("div",{className:"mobile-setting-row",children:[e.jsx("span",{className:"mobile-setting-label",children:"Agents"}),e.jsx("span",{className:"mobile-setting-value",children:((x=a==null?void 0:a.agents)==null?void 0:x.length)||0})]}),e.jsxs("div",{className:"mobile-setting-row",children:[e.jsx("span",{className:"mobile-setting-label",children:"Tasks"}),e.jsx("span",{className:"mobile-setting-value",children:((j=a==null?void 0:a.tasks)==null?void 0:j.length)||0})]})]})]}),e.jsx("div",{className:"mobile-view-footer",children:e.jsx("a",{href:"/?desktop=1",className:"mobile-btn mobile-btn-secondary",children:"Switch to Desktop"})})]})}export{D as MobileSettings};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"MobileSettings-B91gVFii.js","sources":["../../src/web/mobile/MobileSettings.tsx"],"sourcesContent":["// src/web/mobile/MobileSettings.tsx — minimal mobile settings (no companion app, no agent config).\n// Drops: Companion App (qrcode.react chunk), Tailscale Serve, Agent Behavior, Notifications toggles.\nimport { useEffect, useState } from 'react';\nimport { Sun, Moon, Monitor } from 'lucide-react';\nimport { api } from '../lib/api';\nimport { applyTheme, applyThemeTokens, type Settings, type Snapshot, type ThemeName } from '../lib/types';\n\ntype Props = {\n settings: Settings;\n snapshot: Snapshot | null;\n onRefresh: () => Promise<void>;\n};\n\nconst THEMES: { id: ThemeName; label: string; Icon: typeof Sun }[] = [\n { id: 'dark', label: 'Dark', Icon: Moon },\n { id: 'light', label: 'Light', Icon: Sun },\n { id: 'system', label: 'System', Icon: Monitor },\n];\n\nconst PRESET_ACCENTS = [\n { name: 'Purple', accent: '#8b5cf6' },\n { name: 'Blue', accent: '#3b82f6' },\n { name: 'Green', accent: '#10b981' },\n { name: 'Orange', accent: '#f97316' },\n { name: 'Red', accent: '#ef4444' },\n { name: 'Pink', accent: '#ec4899' },\n { name: 'Cyan', accent: '#06b6d4' },\n { name: 'Mono', accent: '#6b7280' },\n];\n\nexport function MobileSettings({ settings: initial, snapshot, onRefresh }: Props) {\n const [settings, setSettings] = useState<Settings>(initial);\n const [dirty, setDirty] = useState(false);\n const [saving, setSaving] = useState(false);\n\n useEffect(() => {\n setSettings(initial);\n setDirty(false);\n if (initial.theme) applyThemeTokens(initial.theme);\n }, [initial]);\n\n const patchTheme = (patch: Partial<Settings['theme']>) => {\n setSettings((cur) => {\n const next = { ...cur, theme: { ...cur.theme, ...patch } };\n applyTheme(next.theme);\n applyThemeTokens(next.theme);\n return next;\n });\n setDirty(true);\n };\n\n const onSave = async () => {\n setSaving(true);\n try {\n const r = await api.put<{ data: Settings }>('/settings', settings);\n setSettings(r.data);\n setDirty(false);\n applyTheme(r.data.theme);\n applyThemeTokens(r.data.theme);\n onRefresh();\n } catch {\n // best-effort\n } finally {\n setSaving(false);\n }\n };\n\n return (\n <div className=\"mobile-view\">\n {dirty && (\n <div className=\"mobile-settings-save-bar\">\n <span>Unsaved changes</span>\n <button type=\"button\" className=\"mobile-btn\" disabled={saving} onClick={onSave}>\n {saving ? 'Saving…' : 'Save'}\n </button>\n </div>\n )}\n\n {/* Theme */}\n <section className=\"mobile-section\">\n <h3 className=\"mobile-section-title\">Appearance</h3>\n <div className=\"mobile-card\">\n <div className=\"mobile-setting-row\">\n <span className=\"mobile-setting-label\">Theme</span>\n <div className=\"mobile-theme-btns\">\n {THEMES.map(({ id, label, Icon }) => (\n <button\n key={id}\n type=\"button\"\n className={`mobile-theme-btn ${settings.theme.mode === id ? 'active' : ''}`}\n onClick={() => patchTheme({ mode: id })}\n >\n <Icon size={14} /> {label}\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"mobile-setting-row\" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 8 }}>\n <span className=\"mobile-setting-label\">Accent color</span>\n <div className=\"mobile-accent-swatches\">\n {PRESET_ACCENTS.map((p) => (\n <button\n key={p.name}\n type=\"button\"\n className={`mobile-accent-swatch ${settings.theme.accent === p.accent ? 'active' : ''}`}\n style={{ background: p.accent }}\n onClick={() => patchTheme({ accent: p.accent })}\n title={p.name}\n />\n ))}\n </div>\n </div>\n\n <div className=\"mobile-setting-row\" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 8 }}>\n <span className=\"mobile-setting-label\">Font size: {settings.theme.fontSize}px</span>\n <input\n type=\"range\"\n min={12}\n max={20}\n value={settings.theme.fontSize}\n onChange={(e) => patchTheme({ fontSize: Number(e.target.value) })}\n style={{ width: '100%' }}\n />\n </div>\n\n <div className=\"mobile-setting-row\">\n <span className=\"mobile-setting-label\">Compact mode</span>\n <label className=\"mobile-toggle\">\n <input\n type=\"checkbox\"\n checked={settings.theme.compactMode}\n onChange={(e) => patchTheme({ compactMode: e.target.checked })}\n />\n <span className=\"mobile-toggle-slider\" />\n </label>\n </div>\n <div className=\"mobile-setting-row\">\n <span className=\"mobile-setting-label\">Animations</span>\n <label className=\"mobile-toggle\">\n <input\n type=\"checkbox\"\n checked={settings.theme.animations}\n onChange={(e) => patchTheme({ animations: e.target.checked })}\n />\n <span className=\"mobile-toggle-slider\" />\n </label>\n </div>\n </div>\n </section>\n\n {/* Chat defaults */}\n <section className=\"mobile-section\">\n <h3 className=\"mobile-section-title\">Chat Defaults</h3>\n <div className=\"mobile-card\">\n <label className=\"mobile-field-label\">Default Agent</label>\n <select\n className=\"mobile-input\"\n value={settings.defaultAgent || 'odin'}\n onChange={(e) => setSettings((cur) => ({ ...cur, defaultAgent: e.target.value }))}\n >\n {(snapshot?.agents || []).map((a) => (\n <option key={a.name} value={a.name}>@{a.name}</option>\n ))}\n </select>\n </div>\n </section>\n\n {/* About */}\n <section className=\"mobile-section\">\n <h3 className=\"mobile-section-title\">About</h3>\n <div className=\"mobile-card\">\n <div className=\"mobile-setting-row\">\n <span className=\"mobile-setting-label\">Version</span>\n <span className=\"mobile-setting-value mono\">{settings.about?.version || '—'}</span>\n </div>\n <div className=\"mobile-setting-row\">\n <span className=\"mobile-setting-label\">Agents</span>\n <span className=\"mobile-setting-value\">{snapshot?.agents?.length || 0}</span>\n </div>\n <div className=\"mobile-setting-row\">\n <span className=\"mobile-setting-label\">Tasks</span>\n <span className=\"mobile-setting-value\">{snapshot?.tasks?.length || 0}</span>\n </div>\n </div>\n </section>\n\n {/* Switch to desktop */}\n <div className=\"mobile-view-footer\">\n <a href=\"/?desktop=1\" className=\"mobile-btn mobile-btn-secondary\">\n Switch to Desktop\n </a>\n </div>\n </div>\n );\n}\n"],"names":["THEMES","Moon","Sun","Monitor","PRESET_ACCENTS","MobileSettings","initial","snapshot","onRefresh","settings","setSettings","useState","dirty","setDirty","saving","setSaving","useEffect","applyThemeTokens","patchTheme","patch","cur","next","applyTheme","onSave","r","api","jsxs","jsx","id","label","Icon","p","e","a","_a","_b","_c"],"mappings":"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"}
|