@corenel/web-ui 0.5.4 → 0.5.5

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.
@@ -1,37 +1,65 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /* The Corenel options drawer — Settings (persona/policy/display/budget) and
3
- * Capabilities (built-in tools, web-search key, skills, MCP servers) — extracted
4
- * from the editor AgentPanel so every agent surface (editor, /agent, /craft) can
5
- * render the same drawer. Persona/policy selection is CONTROLLED (the host owns
6
- * the selected id and persists it); everything else — persona/policy LISTS,
7
- * skills, transcript-view, live-edits — is harness/web-ui-backed and lives here.
2
+ /* The Corenel composer popout — ONE pane answering one question: what does THIS
3
+ * chat run with.
8
4
  *
9
- * Four host couplings come in as props instead of direct imports, so this module
10
- * never touches @corenel/tools-web or src/: MCP/web-key management
11
- * (`connections`, omitted = those sections hidden, no fake forms), the per-run
12
- * budget form (`budgetSection`), the Monaco PolicyEditor modal (`policyEditor`,
13
- * a render prop — Monaco stays host-side), and a per-tool provenance badge
14
- * (`toolBadge`, e.g. the editor's "sidecar" tag). */
5
+ * WHAT THIS REPLACED, and why none of it survived intact:
6
+ *
7
+ * Two tabs. "Settings" held persona/policy/display/budget; "Capabilities"
8
+ * held a read-only row for every tool, each with an allow/ask/deny badge --
9
+ * and the POLICY that decided those badges lived on the other tab. You read
10
+ * the consequence in one place and changed the cause in another. With
11
+ * Microsoft 365 connected the list was 80+ rows, none of them actionable,
12
+ * with the actual controls (a key form, an add-server form, skill install)
13
+ * buried underneath the whole scroll.
14
+ *
15
+ * Worse, the add-server form POSTed to a CLOUD list while Settings wrote
16
+ * daemon.json on the connected machine. Two stores, two front doors, the same
17
+ * word for both, and neither mentioning the other.
18
+ *
19
+ * WHAT IT IS NOW: sources, not tools. One row per builtin namespace, per
20
+ * connected MCP server, per installed skill -- about a dozen instead of a
21
+ * hundred -- each switchable off FOR THIS CONVERSATION and expandable to see
22
+ * the tools it brings. Policy sits directly above them, so cause and effect are
23
+ * finally in the same pane.
24
+ *
25
+ * MANAGEMENT LIVES IN SETTINGS AND NOWHERE ELSE. Adding a server, storing a
26
+ * key, installing a skill: all moved, so daemon.json has exactly one writer.
27
+ * This drawer reads and scopes; it never mutates what exists.
28
+ *
29
+ * Host couplings still arrive as props so this module never imports the host:
30
+ * `connections` (read-only MCP listing; omitted = those rows hidden), the
31
+ * per-run budget form (`budgetSection`), the Monaco PolicyEditor
32
+ * (`policyEditor`, a render prop), and `toolBadge` for per-tool provenance.
33
+ */
15
34
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
16
35
  import { allTools } from '@corenel/harness/tools/builtins';
17
36
  import { resolveNamespace, toolAddress, toolMatchesAnyGlob, NAMESPACE_LABELS } from '@corenel/harness/tools/namespaces';
37
+ import { parseMcpWireName } from '@corenel/protocol';
18
38
  import { PermissionService, STANDARD_POLICY, listPolicies, getPolicy } from '@corenel/harness/guardrail';
19
39
  import { loadPersonas, createPersona, deletePersona } from '@corenel/harness/prompts/config';
40
+ import { listSkills, subscribeSkills } from '@corenel/harness/skills';
41
+ import { isSourceEnabled, setSourceEnabled, subscribeSessionCapabilities } from '@corenel/tools-web/sessionCapabilities';
20
42
  import { useBrand } from '@corenel/ui-kit/host';
21
- import { getEditorControls } from '@corenel/ui-kit/theme';
22
- import { IcPlus, IcTrash, IcX, IcKey, IcCheck, IcSliders, IcZap, IcSearch, IcEdit } from '@corenel/ui-kit/icons';
43
+ import { IcPlus, IcTrash, IcX, IcCheck, IcEdit, IcChevronRight } from '@corenel/ui-kit/icons';
23
44
  import { Section, Row, Toggle, MenuBtn } from '@corenel/ui-kit/FieldControls';
24
- import { useTranscriptView, TranscriptViewToggle } from '../transcriptView';
25
- import { useLiveEditsSetting } from '../liveEdit';
26
- import { SkillsSection } from './SkillsSection';
45
+ import { sessionSearchableTools } from '@corenel/tools-web/sessionTools';
46
+ import { loadedToolNames, removeLoadedTool, subscribeLoadedTools } from '@corenel/tools-web/sessionLoadedTools';
47
+ /* The source list is the primary control here, and it is worked down with a
48
+ * thumb as often as a mouse. `Toggle` carries its own 44px hit area; the row
49
+ * beside it -- whose left two thirds opens the source -- has to match, or half
50
+ * of every row stays a mouse-sized target on a device with no mouse.
51
+ *
52
+ * A module const on `(pointer: coarse)`, which is what this package already
53
+ * does: primary-pointer coarseness does not change at runtime (Composer.tsx). */
54
+ const COARSE_POINTER = typeof matchMedia !== 'undefined' && matchMedia('(pointer: coarse)').matches;
27
55
  const BUILTIN_BLURB = {
28
56
  standard: 'Reads run automatically; writes ask first.',
29
57
  'read-only': 'Inspection only — writes are refused.',
30
58
  autonomous: 'Everything runs without prompts.',
31
59
  };
32
60
  const policyBlurb = (p) => BUILTIN_BLURB[p.name] || p.description || '';
33
- /** Effective per-tool status under a policy, for the tool list badges. Matches by
34
- * namespace address (fs:*) and bare name. */
61
+ /** Effective per-tool status under a policy, shown on an EXPANDED source's tool
62
+ * list. Matches by namespace address (fs:*) and bare name. */
35
63
  function toolStatus(policy, tool) {
36
64
  const deny = policy.tools?.deny ?? [];
37
65
  const allow = policy.tools?.allow ?? [];
@@ -40,15 +68,12 @@ function toolStatus(policy, tool) {
40
68
  if (allow.length && !toolMatchesAnyGlob(tool, allow))
41
69
  return 'deny';
42
70
  return new PermissionService({ policy }).resolveLevel({
43
- name: tool.name, address: toolAddress(tool), mutates: tool.mutates, permission: tool.permission,
71
+ name: tool.name, address: toolAddress(tool), mutates: tool.mutates, permission: tool.permission, roomSafe: tool.roomSafe,
44
72
  });
45
73
  }
46
- /** Discovered MCP tools — mount-load + refresh-on-`refreshKey` (the drawer bumps
47
- * this when it opens or after a server/key mutation). Also consumed by
48
- * AgentChat's slash menu, which is why this lives outside the drawer's render.
49
- * The first fetch (refreshKey's initial value) reads the cache; every later bump
50
- * forces a re-list, mirroring the old AgentPanel's mount-load + drawer-open
51
- * refresh + post-mutation `listMcpTools(true)` calls. */
74
+ /** MCP tools for the drawer — mount-load plus a forced refresh each time the
75
+ * drawer opens. Exported because AgentChat builds its "/" command menu from
76
+ * the same list. */
52
77
  export function useMcpTools(connections, refreshKey) {
53
78
  const [tools, setTools] = useState([]);
54
79
  const firstRef = React.useRef(true);
@@ -65,21 +90,27 @@ export function useMcpTools(connections, refreshKey) {
65
90
  }
66
91
  export function OptionsDrawer(props) {
67
92
  const { name: brandName } = useBrand();
68
- const { open, onClose, personaName, onPersona, policyName, onPolicy, hideIdentity, budgetSection, settingsExtras, connections, toolBadge, policyEditor, registryTick, capabilities, onConnectionsChanged, } = props;
69
- const [drawerTab, setDrawerTab] = useState('settings');
70
- const [toolQuery, setToolQuery] = useState('');
71
- useEffect(() => { if (open) {
72
- setToolQuery('');
73
- setDrawerTab('settings');
74
- } }, [open]);
75
- const [transcriptView, setTranscriptView] = useTranscriptView();
76
- const [liveEdits, setLiveEdits] = useLiveEditsSetting();
93
+ const { open, onClose, sessionId, personaName, onPersona, policyName, onPolicy, hideIdentity, budgetSection, settingsExtras, connections, toolBadge, policyEditor, registryTick, capabilities, } = props;
94
+ const [expanded, setExpanded] = useState(null);
95
+ useEffect(() => { if (open)
96
+ setExpanded(null); }, [open]);
77
97
  // Persona/policy LISTS stay internal — only the selected ids are the host's.
78
98
  const [personas, setPersonas] = useState([]);
79
99
  const [personaMenuOpen, setPersonaMenuOpen] = useState(false);
80
100
  const [newPersona, setNewPersona] = useState(null);
81
- const reloadPersonas = useCallback(() => { loadPersonas().then(setPersonas); }, []);
101
+ /* CAUGHT, because "not yet" is the ordinary case at mount. Personas live in
102
+ * the config workspace, which lives on the sidecar, and the sidecar is still
103
+ * dialling when this drawer first renders -- `channelFor` throws "that
104
+ * machine is not connected", correctly, and with no catch that became an
105
+ * unhandled rejection on every single app start (twice, under StrictMode). */
106
+ const reloadPersonas = useCallback(() => {
107
+ loadPersonas().then(setPersonas).catch(() => setPersonas([]));
108
+ }, []);
82
109
  useEffect(() => { reloadPersonas(); }, [reloadPersonas]);
110
+ /* And read again when the picker is OPENED -- otherwise catching above trades
111
+ * a console error for a permanently empty list. */
112
+ useEffect(() => { if (personaMenuOpen)
113
+ reloadPersonas(); }, [personaMenuOpen, reloadPersonas]);
83
114
  const personaLabel = personas.find((p) => p.id === personaName)?.name || personaName;
84
115
  const pickPersona = (id) => { onPersona(id); setPersonaMenuOpen(false); };
85
116
  const addPersona = async () => {
@@ -101,103 +132,100 @@ export function OptionsDrawer(props) {
101
132
  };
102
133
  const [policyMenuOpen, setPolicyMenuOpen] = useState(false);
103
134
  const [policyEditorOpen, setPolicyEditorOpen] = useState(false);
104
- const [policyListVersion, setPolicyListVersion] = useState(0); // bump to re-resolve after edits
135
+ const [policyListVersion, setPolicyListVersion] = useState(0);
105
136
  const selectPolicy = useCallback((name) => { onPolicy(name); setPolicyMenuOpen(false); }, [onPolicy]);
106
137
  // eslint-disable-next-line react-hooks/exhaustive-deps
107
138
  const activePolicy = useMemo(() => getPolicy(policyName) ?? STANDARD_POLICY, [policyName, policyListVersion]);
108
- // Connections (web-search key + MCP servers) — omitted entirely when
109
- // `connections` is not supplied (no fake forms for a surface that can't act on them).
110
- const [toolKeys, setToolKeys] = useState({});
111
139
  const [mcpServers, setMcpServers] = useState([]);
112
- const [connAuthNeeded, setConnAuthNeeded] = useState(false);
113
- const [connBusy, setConnBusy] = useState(false);
114
140
  const [connErr, setConnErr] = useState(null);
115
- const [serverForm, setServerForm] = useState(null);
116
- const [webKeyEdit, setWebKeyEdit] = useState(null); // input value while editing, else null
117
- const loadConnections = useCallback(() => {
118
- if (!connections)
141
+ useEffect(() => {
142
+ if (!open || !connections)
119
143
  return;
120
144
  setConnErr(null);
121
- Promise.all([connections.listToolKeys(), connections.listMcpServers()])
122
- .then(([keys, servers]) => { setToolKeys(keys); setMcpServers(servers); setConnAuthNeeded(false); })
123
- .catch((e) => { if (String(e?.message) === 'AUTH_REQUIRED')
124
- setConnAuthNeeded(true);
125
- else
126
- setConnErr(String(e?.message || e)); });
127
- }, [connections]);
128
- useEffect(() => { if (open && connections)
129
- loadConnections(); }, [open, connections, loadConnections]);
130
- // MCP tools — mount-load + refresh whenever the drawer opens or a mutation
131
- // below bumps the key (matching the old `listMcpTools(true)` refresh calls).
132
- const [mcpRefresh, setMcpRefresh] = useState(0);
133
- useEffect(() => { if (open)
134
- setMcpRefresh((k) => k + 1); }, [open]);
135
- const mcpTools = useMcpTools(connections, mcpRefresh);
136
- const refreshMcp = useCallback(() => { setMcpRefresh((k) => k + 1); loadConnections(); onConnectionsChanged?.(); }, [loadConnections, onConnectionsChanged]);
137
- const runConn = async (fn) => {
138
- setConnBusy(true);
139
- setConnErr(null);
140
- try {
141
- await fn();
142
- }
143
- catch (e) {
144
- setConnErr(String(e?.message || e));
145
- }
146
- finally {
147
- setConnBusy(false);
148
- }
149
- };
150
- const addServer = () => {
151
- if (!connections)
152
- return;
153
- return runConn(async () => {
154
- if (!serverForm)
155
- return;
156
- const label = serverForm.label.trim();
157
- const url = serverForm.url.trim();
158
- if (!label || !url) {
159
- setConnErr('Label and URL are required.');
160
- return;
161
- }
162
- await connections.addMcpServer({ label, url, apiKey: serverForm.apiKey.trim() || undefined });
163
- setServerForm(null);
164
- refreshMcp();
165
- });
166
- };
167
- const removeServer = (id) => { if (!connections)
168
- return; return runConn(async () => { await connections.removeMcpServer(id); refreshMcp(); }); };
169
- const saveWebKey = () => {
170
- if (!connections)
171
- return;
172
- return runConn(async () => {
173
- const k = (webKeyEdit ?? '').trim();
174
- if (k.length < 8) {
175
- setConnErr('Enter a valid Tavily key.');
176
- return;
177
- }
178
- await connections.setToolKey('tavily', k);
179
- setWebKeyEdit(null);
180
- loadConnections();
145
+ connections.listMcpServers()
146
+ .then(setMcpServers)
147
+ .catch((e) => {
148
+ // A sidecar that is not connected is the ordinary case, not an error
149
+ // worth shouting about -- the source list simply has no servers in it.
150
+ const msg = String(e?.message || e);
151
+ if (msg !== 'AUTH_REQUIRED')
152
+ setConnErr(msg);
153
+ setMcpServers([]);
181
154
  });
182
- };
183
- const clearWebKey = () => { if (!connections)
184
- return; return runConn(async () => { await connections.clearToolKey('tavily'); loadConnections(); }); };
155
+ }, [open, connections]);
156
+ const [skills, setSkills] = useState([]);
157
+ useEffect(() => {
158
+ const reload = () => { listSkills().then(setSkills).catch(() => setSkills([])); };
159
+ reload();
160
+ return subscribeSkills(reload);
161
+ }, []);
162
+ // Re-render when an override changes so the switches and the run agree
163
+ // without the drawer being closed and reopened.
164
+ const [capTick, setCapTick] = useState(0);
165
+ useEffect(() => subscribeSessionCapabilities(() => setCapTick((t) => t + 1)), []);
166
+ useEffect(() => subscribeLoadedTools(() => setCapTick((t) => t + 1)), []);
185
167
  const toolsFn = capabilities?.tools ?? allTools;
186
- // Built-in tools grouped by UI category, filtered — memoized so tab-count and
187
- // list rendering share the same pass; registryTick forces a recompute when
188
- // the host's registry changes underneath (sidecar connect, etc.).
189
- const toolGroups = useMemo(() => {
190
- const needle = toolQuery.trim().toLowerCase();
191
- const matches = (name, desc) => !needle || name.toLowerCase().includes(needle) || desc.toLowerCase().includes(needle);
192
- return NAMESPACE_LABELS
193
- .map((c) => ({ key: c.ns, label: c.label, items: toolsFn().filter((t) => resolveNamespace(t) === c.ns && matches(t.name, t.description)) }))
194
- .filter((g) => g.items.length > 0);
168
+ /* THE SOURCE LIST — builtin namespaces, then connected servers, then skills.
169
+ * Built in one pass so the row count and the expanded detail cannot disagree. */
170
+ const sources = useMemo(() => {
171
+ const registry = toolsFn();
172
+ /* ONE SOURCE OF TRUTH: the tool registry the engine actually runs with.
173
+ *
174
+ * The Connected rows used to come from a CLOUD server list while Settings
175
+ * wrote daemon.json on the machine -- two stores, the same word for both.
176
+ * Every MCP tool in the registry already carries its server in its wire
177
+ * name, so grouping them here shows exactly the servers whose tools a run
178
+ * can reach, and needs no second list to drift from. */
179
+ const mcpByServer = new Map();
180
+ for (const t of registry) {
181
+ if (resolveNamespace(t) !== 'mcp')
182
+ continue;
183
+ const parsed = parseMcpWireName(t.name);
184
+ const server = parsed?.server ?? 'mcp';
185
+ (mcpByServer.get(server) ?? mcpByServer.set(server, []).get(server))
186
+ .push({ id: t.name, name: parsed?.tool ?? t.name, description: t.description });
187
+ }
188
+ const builtin = NAMESPACE_LABELS
189
+ // `mcp` becomes one row PER SERVER below; `skill` is not a tool namespace.
190
+ .filter((c) => c.ns !== 'mcp' && c.ns !== 'skill')
191
+ .map((c) => ({
192
+ id: `ns:${c.ns}`,
193
+ label: c.label,
194
+ tools: registry
195
+ .filter((t) => resolveNamespace(t) === c.ns)
196
+ .map((t) => ({ id: t.name, name: t.name, description: t.description, status: toolStatus(activePolicy, t), badge: toolBadge?.(t.name) })),
197
+ }))
198
+ .filter((r) => r.tools.length > 0);
199
+ const labelFor = (id) => mcpServers.find((s2) => s2.id === id)?.label ?? id;
200
+ const connected = [...mcpByServer.entries()].map(([server, tools]) => ({
201
+ id: `mcp:${server}`,
202
+ label: labelFor(server),
203
+ sub: mcpServers.find((s2) => s2.id === server)?.url,
204
+ tools,
205
+ }));
206
+ const skillRows = skills.map((s2) => ({ id: `skill:${s2.name}`, label: s2.name, tools: [] }));
207
+ return [
208
+ { group: 'Built-in', rows: builtin },
209
+ { group: 'Connected', rows: connected },
210
+ { group: 'Skills', rows: skillRows },
211
+ ].filter((g) => g.rows.length > 0);
195
212
  // eslint-disable-next-line react-hooks/exhaustive-deps
196
- }, [toolQuery, toolsFn, registryTick]);
213
+ }, [toolsFn, activePolicy, mcpServers, skills, registryTick, toolBadge]);
214
+ /** How many of a source's tools this conversation still sends. */
215
+ const sentCount = (r) => r.tools.filter((t) => isSourceEnabled(sessionId, `tool:${t.id}`)).length;
216
+ const sourceCount = sources.reduce((n, g) => n + g.rows.length, 0);
217
+ /* LOADED IN THIS CHAT. MCP tools reach the model only once `tool_search`
218
+ * finds them or one is picked from "/", so a connected server no longer means
219
+ * its schemas ride every request -- and the old "over the 128-tool limit"
220
+ * warning has nothing left to warn about. What this conversation DOES carry
221
+ * is listed instead, each removable. Filtered to tools still enabled here,
222
+ * the same rule the run applies, so a switched-off server's tools do not
223
+ * linger in the list. */
224
+ const searchable = new Map(sessionSearchableTools(sessionId, toolsFn()).map((t) => [t.name, t]));
225
+ const loadedHere = loadedToolNames(sessionId).filter((n) => searchable.has(n));
197
226
  if (!open) {
198
- // The PolicyEditor modal is independent of the drawer's open state (matches
199
- // the old AgentPanel, where the modal lived at the panel root, not inside
200
- // the drawer's conditional block) — it can stay open after the drawer closes.
227
+ // The PolicyEditor modal is independent of the drawer's open state — it can
228
+ // stay open after the drawer closes.
201
229
  return _jsx(_Fragment, { children: policyEditorOpen && policyEditor?.({
202
230
  initialName: policyName,
203
231
  onClose: () => setPolicyEditorOpen(false),
@@ -205,101 +233,67 @@ export function OptionsDrawer(props) {
205
233
  onPolicy(name); },
206
234
  }) });
207
235
  }
208
- const needle = toolQuery.trim().toLowerCase();
209
- const matches = (name, desc) => !needle || name.toLowerCase().includes(needle) || desc.toLowerCase().includes(needle);
210
- const ec = getEditorControls();
211
- const toggleDisplay = ec === 'icons' ? 'icons' : ec === 'labels' ? 'labels' : 'both';
212
- // Discovered MCP tools (+ per-server errors) keyed by serverId, so each
213
- // configured server can show its own tools beneath it.
214
- const mcpByServer = new Map();
215
- const mcpErrByServer = new Map();
216
- for (const t of mcpTools) {
217
- if (t.error) {
218
- mcpErrByServer.set(t.serverId, t.error);
219
- continue;
220
- }
221
- (mcpByServer.get(t.serverId) ?? mcpByServer.set(t.serverId, []).get(t.serverId)).push(t);
222
- }
223
- const serverShown = (s) => {
224
- if (!needle)
225
- return true;
226
- if (s.label.toLowerCase().includes(needle) || s.url.toLowerCase().includes(needle))
227
- return true;
228
- return (mcpByServer.get(s.id) || []).some((t) => matches(t.name || '', t.description || ''));
229
- };
230
- const shownServers = mcpServers.filter(serverShown);
231
- const showSkills = !needle || 'skills'.includes(needle);
232
- const showWeb = !!connections && (!needle || 'web search tavily key api'.includes(needle));
233
- const showMcp = !!connections && (!needle || 'mcp server'.includes(needle) || shownServers.length > 0);
234
- const nothing = toolGroups.length === 0 && !showSkills && !showWeb && !showMcp;
235
- return (_jsxs(_Fragment, { children: [_jsx("div", { style: S.drawerBackdrop, onClick: onClose }), _jsxs("aside", { style: S.drawer, role: "dialog", "aria-label": `${brandName} options`, children: [_jsxs("div", { style: S.drawerHead, children: [_jsx("span", { style: S.drawerTitle, children: brandName }), _jsx("button", { style: S.iconBtn, onClick: onClose, "aria-label": "Close options", children: _jsx(IcX, { size: 16 }) })] }), _jsxs("div", { style: S.drawerTabs, role: "tablist", "aria-label": `${brandName} settings sections`, children: [_jsxs("button", { role: "tab", "aria-selected": drawerTab === 'settings', style: drawerTabStyle(drawerTab === 'settings'), onClick: () => setDrawerTab('settings'), children: [_jsx(IcSliders, { size: 13 }), " Settings"] }), _jsxs("button", { role: "tab", "aria-selected": drawerTab === 'capabilities', style: drawerTabStyle(drawerTab === 'capabilities'), onClick: () => setDrawerTab('capabilities'), children: [_jsx(IcZap, { size: 13 }), " Capabilities", _jsx("span", { style: S.drawerTabCount, children: toolsFn().length + mcpTools.length })] })] }), drawerTab === 'settings' ? (_jsxs("div", { style: S.settingsScroll, children: [!hideIdentity && (_jsxs(Section, { label: "Behavior", children: [_jsx(Row, { label: "Persona", children: _jsx(MenuBtn, { value: personaLabel, open: personaMenuOpen, onClick: () => { setPersonaMenuOpen((o) => !o); setPolicyMenuOpen(false); } }) }), personaMenuOpen && (_jsxs("div", { style: S.ddPanel, children: [personas.map((p) => (_jsxs("div", { style: S.personaRow, children: [_jsxs("button", { style: personaPickStyle(p.id === personaName), onClick: () => pickPersona(p.id), children: [_jsxs("span", { style: S.personaPickName, children: [p.name, p.id === personaName && _jsx(IcCheck, { size: 11 })] }), p.description && _jsx("span", { style: S.personaDesc, children: p.description })] }), _jsx("button", { style: S.menuDel, onClick: () => removePersona(p.id), title: "Delete persona", children: _jsx(IcTrash, { size: 12 }) })] }, p.id))), newPersona ? (_jsxs("div", { style: S.personaForm, children: [_jsx("input", { style: S.personaInput, placeholder: "persona name", value: newPersona.name, onChange: (e) => setNewPersona((n) => ({ ...n, name: e.target.value })) }), _jsx("textarea", { style: S.personaTextarea, placeholder: "System prompt \u2014 the voice/role\u2026", value: newPersona.system, onChange: (e) => setNewPersona((n) => ({ ...n, system: e.target.value })) }), _jsxs("div", { style: S.personaFormActions, children: [_jsx("button", { style: S.personaGhost, onClick: () => setNewPersona(null), children: "Cancel" }), _jsxs("button", { style: S.personaSave, onClick: addPersona, children: [_jsx(IcCheck, { size: 12 }), " Save"] })] })] })) : (_jsxs("button", { style: S.menuItem, onClick: () => setNewPersona({ name: '', system: '' }), children: [_jsx(IcPlus, { size: 13 }), " New persona"] }))] })), _jsx(Row, { label: "Policy", children: _jsx(MenuBtn, { value: policyName, open: policyMenuOpen, onClick: () => { setPolicyMenuOpen((o) => !o); setPersonaMenuOpen(false); }, title: policyBlurb(activePolicy) }) }), policyMenuOpen && (_jsxs("div", { style: S.ddPanel, children: [listPolicies().map((e) => (_jsxs("button", { style: policyOptStyle(e.name === policyName), onClick: () => selectPolicy(e.name), children: [_jsxs("span", { style: S.personaPickName, children: [e.name, !e.builtin && _jsx("span", { style: S.policyTag, children: "custom" }), e.name === policyName && _jsx(IcCheck, { size: 11 })] }), _jsx("span", { style: S.personaDesc, children: policyBlurb(e.policy) })] }, e.name))), _jsxs("button", { style: S.policyEditBtn, onClick: () => { setPolicyMenuOpen(false); setPolicyEditorOpen(true); }, children: [_jsx(IcEdit, { size: 12 }), " Edit policies\u2026"] })] }))] })), _jsxs(Section, { label: "Display", children: [_jsx(Row, { label: "Transcript detail", children: _jsx(TranscriptViewToggle, { view: transcriptView, onChange: setTranscriptView, display: toggleDisplay }) }), _jsx(Row, { label: "Show live edits", tip: "Review AI edits inline in the editor (Accept/Reject there); the chat diff collapses. Off: review in chat.", children: _jsx(Toggle, { ariaLabel: "Show live edits", checked: liveEdits, onChange: setLiveEdits }) })] }), budgetSection && (_jsx(Section, { label: "Budget", meta: "this chat", children: budgetSection })), settingsExtras] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: S.toolsScroll, children: [_jsxs("div", { style: S.toolsSearch, children: [_jsx(IcSearch, { size: 13 }), _jsx("input", { style: S.toolsSearchInput, placeholder: "Filter tools, skills, MCP\u2026", value: toolQuery, onChange: (e) => setToolQuery(e.target.value), onKeyDown: (e) => { if (e.key === 'Escape') {
236
- e.preventDefault();
237
- onClose();
238
- } } }), toolQuery && _jsx("button", { style: S.toolsClear, onClick: () => setToolQuery(''), "aria-label": "Clear filter", children: _jsx(IcX, { size: 12 }) })] }), connErr && _jsx("div", { style: S.connError, children: connErr }), toolGroups.map((g) => (_jsx(Section, { label: g.label, meta: g.items.length, children: g.items.map((t) => {
239
- const st = toolStatus(activePolicy, t);
240
- const badge = toolBadge?.(t.name);
241
- return (_jsxs("div", { style: S.toolRow, title: t.description, children: [_jsxs("span", { style: S.toolMain, children: [_jsxs("span", { style: S.toolNm, children: [t.name, badge && _jsx("span", { style: S.toolSidecar, children: badge })] }), _jsx("span", { style: S.toolDesc, children: t.description })] }), _jsx("span", { style: st === 'deny' ? S.toolDenied : st === 'ask' ? S.toolAsks : S.toolAuto, children: st })] }, t.name));
242
- }) }, g.key))), showWeb && (_jsx(Section, { label: "Web search key", meta: "Tavily", children: connAuthNeeded ? (_jsx("div", { style: S.connHint, children: "Sign in to manage your web-search key." })) : webKeyEdit != null ? (_jsxs("div", { style: S.connForm, children: [_jsx("input", { style: S.connInput, type: "password", placeholder: "Tavily API key (tvly-\u2026)", value: webKeyEdit, onChange: (e) => setWebKeyEdit(e.target.value), autoFocus: true }), _jsxs("div", { style: S.connFormRow, children: [_jsx("button", { style: S.connGhost, onClick: () => setWebKeyEdit(null), disabled: connBusy, children: "Cancel" }), _jsxs("button", { style: S.connSave, onClick: saveWebKey, disabled: connBusy, children: [_jsx(IcCheck, { size: 12 }), " Save key"] })] })] })) : (_jsxs("div", { style: S.connRow, children: [_jsxs("span", { style: S.connMain, children: [_jsxs("span", { style: S.connName, children: [_jsx(IcKey, { size: 12 }), " ", toolKeys.tavily?.hasKey ? 'Your key is set' : toolKeys.tavily?.paidFallback ? 'Using the Prompd-provided key' : 'No key set'] }), _jsx("span", { style: S.connSub, children: toolKeys.tavily?.hasKey ? 'web_search uses your Tavily key' : toolKeys.tavily?.paidFallback ? 'add your own key to use your own Tavily quota' : 'web_search is unavailable until a key is added' })] }), _jsx("button", { style: S.connBtn, onClick: () => setWebKeyEdit(''), disabled: connBusy, children: toolKeys.tavily?.hasKey ? 'Change' : 'Add key' }), toolKeys.tavily?.hasKey && _jsx("button", { style: S.connBtnDanger, onClick: clearWebKey, disabled: connBusy, title: "Remove your key", children: _jsx(IcTrash, { size: 12 }) })] })) })), showSkills && _jsx(SkillsSection, {}), showMcp && (_jsx(Section, { label: "MCP servers", meta: connAuthNeeded ? undefined : _jsxs("button", { style: S.catAdd, onClick: () => setServerForm({ label: '', url: '', apiKey: '' }), disabled: connBusy, children: [_jsx(IcPlus, { size: 12 }), " Add"] }), children: connAuthNeeded ? (_jsx("div", { style: S.connHint, children: "Sign in to add and manage MCP servers." })) : (_jsxs(_Fragment, { children: [serverForm && (_jsxs("div", { style: S.connForm, children: [_jsx("input", { style: S.connInput, placeholder: "Label (e.g. GitHub)", value: serverForm.label, onChange: (e) => setServerForm((f) => ({ ...f, label: e.target.value })), autoFocus: true }), _jsx("input", { style: S.connInput, placeholder: "https://server.example.com/mcp", value: serverForm.url, onChange: (e) => setServerForm((f) => ({ ...f, url: e.target.value })) }), _jsx("input", { style: S.connInput, type: "password", placeholder: "Bearer key (optional)", value: serverForm.apiKey, onChange: (e) => setServerForm((f) => ({ ...f, apiKey: e.target.value })) }), _jsxs("div", { style: S.connFormRow, children: [_jsx("button", { style: S.connGhost, onClick: () => setServerForm(null), disabled: connBusy, children: "Cancel" }), _jsxs("button", { style: S.connSave, onClick: addServer, disabled: connBusy, children: [_jsx(IcCheck, { size: 12 }), " Add server"] })] })] })), shownServers.length === 0 && !serverForm && _jsx("div", { style: S.connHint, children: "No MCP servers yet. Add one to give the agent remote tools." }), shownServers.map((s) => {
243
- const stools = (mcpByServer.get(s.id) || []).filter((t) => matches(t.name || '', t.description || ''));
244
- const err = mcpErrByServer.get(s.id);
245
- return (_jsxs("div", { style: S.serverBlock, children: [_jsxs("div", { style: S.serverHead, children: [_jsxs("span", { style: S.connMain, children: [_jsxs("span", { style: S.connName, children: [s.label, s.hasKey && _jsx(IcKey, { size: 11 })] }), _jsx("span", { style: S.connSub, children: s.url })] }), _jsx("span", { style: S.serverCount, children: err ? 'error' : `${stools.length} tool${stools.length === 1 ? '' : 's'}` }), _jsx("button", { style: S.connBtnDanger, onClick: () => removeServer(s.id), disabled: connBusy, title: "Remove server", children: _jsx(IcTrash, { size: 12 }) })] }), err && _jsx("div", { style: S.serverErr, children: err }), stools.map((t, i) => (_jsxs("div", { style: S.toolRow, title: t.description, children: [_jsxs("span", { style: S.toolMain, children: [_jsx("span", { style: S.toolNm, children: t.name }), t.description && _jsx("span", { style: S.toolDesc, children: t.description })] }), _jsx("span", { style: S.toolAuto, children: "mcp" })] }, `${s.id}-${t.name || i}`)))] }, s.id));
246
- })] })) })), nothing && _jsxs("div", { style: S.toolsEmpty, children: ["No matches for \u201C", toolQuery.trim(), "\u201D."] })] }), _jsxs("div", { style: S.toolsNote, children: ["Status reflects the \u201C", policyName, "\u201D policy: \u201Callow\u201D runs, \u201Cask\u201D needs approval, \u201Cdeny\u201D is blocked."] })] }))] }), policyEditorOpen && policyEditor?.({
236
+ return (_jsxs(_Fragment, { children: [_jsx("div", { style: S.drawerBackdrop, onClick: onClose }), _jsxs("aside", { style: S.drawer, role: "dialog", "aria-label": `${brandName} options`, children: [_jsxs("div", { style: S.drawerHead, children: [_jsx("span", { style: S.drawerTitle, children: "This chat" }), _jsx("button", { style: S.iconBtn, onClick: onClose, "aria-label": "Close options", children: _jsx(IcX, { size: 16 }) })] }), _jsxs("div", { style: S.settingsScroll, children: [!hideIdentity && (_jsxs(Section, { label: "Behavior", children: [_jsx(Row, { label: "Persona", children: _jsx(MenuBtn, { value: personaLabel, open: personaMenuOpen, onClick: () => { setPersonaMenuOpen((o) => !o); setPolicyMenuOpen(false); } }) }), personaMenuOpen && (_jsxs("div", { style: S.ddPanel, children: [personas.map((p) => (_jsxs("div", { style: S.personaRow, children: [_jsxs("button", { style: personaPickStyle(p.id === personaName), onClick: () => pickPersona(p.id), children: [_jsxs("span", { style: S.personaPickName, children: [p.name, p.id === personaName && _jsx(IcCheck, { size: 11 })] }), p.description && _jsx("span", { style: S.personaDesc, children: p.description })] }), _jsx("button", { style: S.menuDel, onClick: () => removePersona(p.id), title: "Delete persona", children: _jsx(IcTrash, { size: 12 }) })] }, p.id))), newPersona ? (_jsxs("div", { style: S.personaForm, children: [_jsx("input", { style: S.personaInput, placeholder: "persona name", value: newPersona.name, onChange: (e) => setNewPersona((n) => ({ ...n, name: e.target.value })) }), _jsx("textarea", { style: S.personaTextarea, placeholder: "System prompt \u2014 the voice/role\u2026", value: newPersona.system, onChange: (e) => setNewPersona((n) => ({ ...n, system: e.target.value })) }), _jsxs("div", { style: S.personaFormActions, children: [_jsx("button", { style: S.personaGhost, onClick: () => setNewPersona(null), children: "Cancel" }), _jsxs("button", { style: S.personaSave, onClick: addPersona, children: [_jsx(IcCheck, { size: 12 }), " Save"] })] })] })) : (_jsxs("button", { style: S.menuItem, onClick: () => setNewPersona({ name: '', system: '' }), children: [_jsx(IcPlus, { size: 13 }), " New persona"] }))] })), _jsx(Row, { label: "Policy", children: _jsx(MenuBtn, { value: policyName, open: policyMenuOpen, onClick: () => { setPolicyMenuOpen((o) => !o); setPersonaMenuOpen(false); }, title: policyBlurb(activePolicy) }) }), policyMenuOpen && (_jsxs("div", { style: S.ddPanel, children: [listPolicies().map((e) => (_jsxs("button", { style: policyOptStyle(e.name === policyName), onClick: () => selectPolicy(e.name), children: [_jsxs("span", { style: S.personaPickName, children: [e.name, !e.builtin && _jsx("span", { style: S.policyTag, children: "custom" }), e.name === policyName && _jsx(IcCheck, { size: 11 })] }), _jsx("span", { style: S.personaDesc, children: policyBlurb(e.policy) })] }, e.name))), _jsxs("button", { style: S.policyEditBtn, onClick: () => { setPolicyMenuOpen(false); setPolicyEditorOpen(true); }, children: [_jsx(IcEdit, { size: 12 }), " Edit policies\u2026"] })] }))] })), budgetSection && (_jsx(Section, { label: "Budget", meta: "this chat", children: budgetSection })), connErr && _jsx("div", { style: S.connError, children: connErr }), loadedHere.length > 0 && (_jsx(Section, { label: "Loaded in this chat", meta: loadedHere.length, children: loadedHere.map((n) => {
237
+ const parsed = parseMcpWireName(n);
238
+ return (_jsxs("div", { style: S.loadedRow, "data-loaded-tool": n, children: [_jsx("span", { style: S.toolNm, title: searchable.get(n)?.description, children: parsed ? `${parsed.server} · ${parsed.tool}` : n }), _jsx("button", { style: S.iconBtn, onClick: () => removeLoadedTool(sessionId, n), "aria-label": `Stop sending ${n}`, title: "Stop sending this tool (it stays searchable)", children: _jsx(IcX, { size: 13 }) })] }, n));
239
+ }) })), sources.map((g) => (_jsx(Section, { label: g.group === 'Built-in' ? `Sources · ${g.group}` : g.group, meta: g.group === 'Built-in' ? sourceCount : undefined, children: g.rows.map((r) => {
240
+ const on = isSourceEnabled(sessionId, r.id);
241
+ const isOpen = expanded === r.id;
242
+ return (_jsxs("div", { "data-source-row": r.id, children: [_jsxs("div", { style: S.sourceRow, children: [_jsxs("button", { style: S.sourceMain, onClick: () => setExpanded(isOpen ? null : r.id), "aria-expanded": isOpen, title: r.sub, children: [_jsx("span", { style: { ...S.sourceChevron, transform: isOpen ? 'rotate(90deg)' : 'none' }, children: _jsx(IcChevronRight, { size: 12 }) }), _jsxs("span", { style: S.sourceText, children: [_jsx("span", { style: on ? S.sourceName : S.sourceNameOff, children: r.label }), _jsx("span", { style: S.sourceSub, children: r.error
243
+ ? 'error'
244
+ : !r.tools.length
245
+ ? 'prompt only'
246
+ /* "N of M" only once they differ. Saying "2 tools"
247
+ * while sending one is the untraceable kind of
248
+ * copy this codebase keeps finding. */
249
+ : sentCount(r) === r.tools.length
250
+ ? `${r.tools.length} tool${r.tools.length === 1 ? '' : 's'}`
251
+ : `${sentCount(r)} of ${r.tools.length} tools` })] })] }), _jsx(Toggle, { ariaLabel: `${r.label} in this chat`, checked: on, onChange: (v) => setSourceEnabled(sessionId, r.id, v) })] }), r.error && isOpen && _jsx("div", { style: S.serverErr, children: r.error }), isOpen && r.tools.map((t) => {
252
+ /* A tool is only sent when BOTH its source and it are on.
253
+ * Rendering the conjunction rather than its own flag is
254
+ * what keeps the switch honest while the source is off:
255
+ * a control reading "on" for something being withheld
256
+ * would be a claim the run does not honour. */
257
+ const toolOn = on && isSourceEnabled(sessionId, `tool:${t.id}`);
258
+ return (_jsxs("div", { style: S.toolRow, title: t.description, children: [_jsxs("span", { style: S.toolMain, children: [_jsxs("span", { style: toolOn ? S.toolNm : S.toolNmOff, children: [t.name, t.badge && _jsx("span", { style: S.toolSidecar, children: t.badge })] }), t.description && _jsx("span", { style: S.toolDesc, children: t.description })] }), t.status && (_jsx("span", { style: !toolOn ? S.toolDenied : t.status === 'deny' ? S.toolDenied : t.status === 'ask' ? S.toolAsks : S.toolAuto, children: toolOn ? t.status : 'off' })), _jsx(Toggle, { ariaLabel: `${t.name} in this chat`, checked: toolOn,
259
+ /* Disabled with the source off: the only honest way
260
+ * to turn this one back on is to turn the source on,
261
+ * and a switch that appeared to work while changing
262
+ * nothing visible is worse than one that cannot. */
263
+ disabled: !on, onChange: (v) => setSourceEnabled(sessionId, `tool:${t.id}`, v) })] }, t.id));
264
+ })] }, r.id));
265
+ }) }, g.group))), settingsExtras] }, capTick), _jsx("div", { style: S.toolsNote, children: "Switches apply to this conversation only. Manage sources in Settings \u2192 Tools." })] }), policyEditorOpen && policyEditor?.({
247
266
  initialName: policyName,
248
267
  onClose: () => setPolicyEditorOpen(false),
249
268
  onSaved: (name) => { setPolicyListVersion((v) => v + 1); if (name)
250
269
  onPolicy(name); },
251
270
  })] }));
252
271
  }
253
- // Style keyed by an "active"/state flag — kept out of S (a flat style map) and
254
- // typed as their own helpers.
255
- const drawerTabStyle = (active) => ({ display: 'inline-flex', alignItems: 'center', gap: 6, height: 30, padding: '0 11px', border: 0, borderRadius: 'var(--radius-sm)', background: active ? 'var(--accent-soft)' : 'transparent', color: active ? 'var(--accent)' : 'var(--text-secondary)', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' });
272
+ // Style keyed by an "active"/state flag — kept out of S (a flat style map).
256
273
  const policyOptStyle = (active) => ({ display: 'flex', flexDirection: 'column', gap: 1, textAlign: 'left', padding: '7px 9px', border: 0, borderRadius: 'var(--radius-sm)', background: active ? 'var(--accent-soft)' : 'transparent', color: active ? 'var(--accent)' : 'var(--text-primary)', cursor: 'pointer', fontFamily: 'inherit' });
257
274
  const personaPickStyle = (active) => ({ flex: 1, display: 'flex', flexDirection: 'column', gap: 1, textAlign: 'left', padding: '7px 9px', border: 0, borderRadius: 'var(--radius-sm)', background: active ? 'var(--accent-soft)' : 'transparent', color: active ? 'var(--accent)' : 'var(--text-primary)', cursor: 'pointer', fontFamily: 'inherit' });
258
275
  const S = {
259
276
  iconBtn: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, border: 0, borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'var(--text-tertiary)', cursor: 'pointer' },
260
- // Options drawer — scoped to the panel (absolute, not viewport-fixed) so it
261
- // slides over the chat, not the whole app.
262
277
  drawerBackdrop: { position: 'absolute', inset: 0, background: 'color-mix(in srgb, var(--surface-0, #000) 40%, transparent)', zIndex: 40 },
263
278
  drawer: { position: 'absolute', top: 0, right: 0, bottom: 0, width: 340, maxWidth: '92%', display: 'flex', flexDirection: 'column', background: 'var(--surface-1)', borderLeft: '1px solid var(--border-default)', boxShadow: 'var(--shadow-4)', zIndex: 41 },
264
279
  drawerHead: { display: 'flex', alignItems: 'center', height: 48, padding: '0 10px 0 14px', borderBottom: '1px solid var(--border-subtle)', flexShrink: 0 },
265
280
  drawerTitle: { flex: 1, fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' },
266
- // Drawer section tabs — a clean segmented row, monochrome with an accent-soft
267
- // pill on the active tab (gradients stay reserved for the brand mark).
268
- drawerTabs: { display: 'flex', gap: 4, padding: '6px 8px', borderBottom: '1px solid var(--border-subtle)', flexShrink: 0 },
269
- drawerTabCount: { fontSize: 10.5, fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', color: 'currentColor', opacity: 0.7 },
270
- // Right-aligned action button used inside a drawerRow (icon + short label).
271
- rowBtn: { display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, padding: '0 11px', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-2)', color: 'var(--text-secondary)', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer', flexShrink: 0 },
272
281
  ddPanel: { margin: '0 9px 6px', padding: 4, border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', background: 'var(--surface-2)', display: 'flex', flexDirection: 'column', gap: 2 },
273
282
  policyTag: { marginLeft: 6, padding: '0 5px', borderRadius: 'var(--radius-pill, 999px)', background: 'var(--surface-3)', color: 'var(--text-tertiary)', fontSize: 9.5, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4, verticalAlign: 'middle' },
274
283
  policyEditBtn: { display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 2, padding: '7px 9px', border: 0, borderTop: '1px solid var(--border-subtle)', borderRadius: 0, background: 'transparent', color: 'var(--accent)', fontSize: 12, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer', textAlign: 'left' },
275
284
  menuItem: { display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '8px 9px', border: 0, borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'var(--text-primary)', cursor: 'pointer', fontSize: 13, fontWeight: 500 },
276
285
  menuDel: { display: 'inline-flex', width: 26, height: 26, alignItems: 'center', justifyContent: 'center', border: 0, borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'var(--text-disabled)', cursor: 'pointer' },
277
- toolsScroll: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '4px 6px 6px' },
278
- settingsScroll: { flex: 1, overflowY: 'auto', padding: '4px 12px 12px', display: 'flex', flexDirection: 'column' },
279
- textAction: { border: 0, background: 'transparent', color: 'var(--accent)', fontSize: 11.5, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer', padding: 0 },
280
- toolMain: { display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 0 },
281
- toolDesc: { fontSize: 11, lineHeight: 1.35, color: 'var(--text-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
282
- toolsEmpty: { padding: '20px 9px', fontSize: 12.5, color: 'var(--text-tertiary)', textAlign: 'center' },
283
- toolsSearch: { display: 'flex', alignItems: 'center', gap: 7, height: 28, padding: '0 9px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-2)', color: 'var(--text-tertiary)' },
284
- toolsSearchInput: { flex: 1, minWidth: 0, border: 0, outline: 'none', background: 'transparent', color: 'var(--text-primary)', fontSize: 12.5, fontFamily: 'inherit' },
285
- toolsClear: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16, border: 0, borderRadius: 999, background: 'transparent', color: 'var(--text-tertiary)', cursor: 'pointer', padding: 0, flexShrink: 0 },
286
- catAdd: { display: 'inline-flex', alignItems: 'center', gap: 4, height: 22, padding: '0 8px', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-pill, 999px)', background: 'var(--surface-2)', color: 'var(--text-secondary)', fontSize: 11, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer', textTransform: 'none', letterSpacing: 0 },
286
+ settingsScroll: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '4px 12px 12px', display: 'flex', flexDirection: 'column' },
287
+ // A source row: a wide click target that expands, plus the per-chat switch.
288
+ sourceRow: { display: 'flex', alignItems: 'center', gap: 8, minHeight: COARSE_POINTER ? 44 : 32, padding: '0 2px' },
289
+ sourceMain: { flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 7, minHeight: COARSE_POINTER ? 44 : 28, padding: '4px 2px', border: 0, background: 'transparent', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' },
290
+ sourceChevron: { display: 'inline-flex', alignItems: 'center', color: 'var(--text-disabled)', transition: 'transform 120ms ease', flexShrink: 0 },
291
+ sourceText: { display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 },
292
+ sourceName: { fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
293
+ sourceNameOff: { fontSize: 13, fontWeight: 600, color: 'var(--text-disabled)', textDecoration: 'line-through', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
294
+ sourceSub: { fontSize: 10.5, color: 'var(--text-tertiary)', fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums' },
295
+ loadedRow: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, padding: '2px 3px 2px 9px', minWidth: 0 },
287
296
  connError: { margin: '4px 3px 6px', padding: '7px 9px', borderRadius: 'var(--radius-sm)', background: 'var(--danger-soft)', color: 'var(--danger)', fontSize: 12, lineHeight: 1.4 },
288
- connHint: { padding: '4px 9px 8px', fontSize: 12, lineHeight: 1.45, color: 'var(--text-tertiary)' },
289
- connRow: { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 9px 6px' },
290
- connMain: { display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 0 },
291
- connName: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' },
292
- connSub: { fontSize: 11, color: 'var(--text-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
293
- connBtn: { flexShrink: 0, height: 26, padding: '0 10px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-2)', color: 'var(--text-secondary)', fontSize: 12, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' },
294
- connBtnDanger: { flexShrink: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 26, height: 26, border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'var(--text-tertiary)', cursor: 'pointer', padding: 0 },
295
- connForm: { display: 'flex', flexDirection: 'column', gap: 6, padding: '2px 9px 8px' },
296
- connInput: { height: 'var(--control-h)', padding: '0 var(--space-3)', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-2)', color: 'var(--text-primary)', fontSize: 'var(--fs-body-sm)', fontFamily: 'inherit', outline: 'none' },
297
- connFormRow: { display: 'flex', justifyContent: 'flex-end', gap: 6 },
298
- connGhost: { height: 28, padding: '0 10px', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-sm)', background: 'transparent', color: 'var(--text-tertiary)', fontSize: 12, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' },
299
- connSave: { display: 'inline-flex', alignItems: 'center', gap: 5, height: 28, padding: '0 12px', border: 0, borderRadius: 'var(--radius-sm)', background: 'var(--grad-brand)', color: 'var(--text-on-accent, #fff)', fontSize: 12, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' },
300
- serverBlock: { border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', padding: 6, margin: '0 3px 6px' },
301
- serverHead: { display: 'flex', alignItems: 'center', gap: 8, padding: '2px 4px 4px' },
302
- serverCount: { flexShrink: 0, fontSize: 10.5, color: 'var(--text-disabled)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em' },
303
297
  serverErr: { padding: '2px 6px 6px', fontSize: 11.5, lineHeight: 1.4, color: 'var(--danger)' },
304
298
  personaRow: { display: 'flex', alignItems: 'stretch', gap: 4 },
305
299
  personaPickName: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, fontWeight: 600, fontFamily: 'var(--font-mono)' },
@@ -310,12 +304,15 @@ const S = {
310
304
  personaFormActions: { display: 'flex', justifyContent: 'flex-end', gap: 6 },
311
305
  personaGhost: { height: 28, padding: '0 12px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-sm)', background: 'var(--surface-1)', color: 'var(--text-secondary)', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' },
312
306
  personaSave: { display: 'inline-flex', alignItems: 'center', gap: 5, height: 28, padding: '0 12px', border: 0, borderRadius: 'var(--radius-sm)', background: 'var(--grad-brand)', color: 'var(--text-on-accent, #fff)', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' },
313
- toolRow: { display: 'flex', alignItems: 'center', gap: 8, minHeight: 28, padding: '0 9px', borderRadius: 'var(--radius-sm)', color: 'var(--text-secondary)' },
314
- toolNm: { fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
307
+ toolRow: { display: 'flex', alignItems: 'center', gap: 8, minHeight: 26, padding: '0 9px 0 21px', borderRadius: 'var(--radius-sm)', color: 'var(--text-secondary)' },
308
+ toolMain: { display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 0 },
309
+ toolNm: { fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
310
+ toolNmOff: { fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--text-disabled)', textDecoration: 'line-through', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
311
+ toolDesc: { fontSize: 11, lineHeight: 1.35, color: 'var(--text-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
315
312
  toolSidecar: { marginLeft: 6, padding: '0 5px', borderRadius: 'var(--radius-sm)', background: 'var(--accent-soft)', color: 'var(--accent)', fontFamily: 'var(--font-sans)', fontSize: 9.5, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', verticalAlign: 'middle' },
316
313
  toolAuto: { marginLeft: 'auto', flexShrink: 0, alignSelf: 'center', fontSize: 10.5, color: 'var(--text-disabled)', textTransform: 'uppercase', letterSpacing: '0.06em' },
317
314
  toolAsks: { marginLeft: 'auto', flexShrink: 0, alignSelf: 'center', fontSize: 10.5, color: 'var(--warning)', textTransform: 'uppercase', letterSpacing: '0.06em' },
318
315
  toolDenied: { marginLeft: 'auto', flexShrink: 0, alignSelf: 'center', fontSize: 10.5, color: 'var(--danger)', textTransform: 'uppercase', letterSpacing: '0.06em' },
319
- toolsNote: { padding: '8px 10px', fontSize: 11.5, lineHeight: 1.45, color: 'var(--text-tertiary)', borderTop: '1px solid var(--border-subtle)', flexShrink: 0, background: 'var(--surface-1)' },
316
+ toolsNote: { padding: '8px 12px', fontSize: 11.5, lineHeight: 1.45, color: 'var(--text-tertiary)', borderTop: '1px solid var(--border-subtle)', flexShrink: 0, background: 'var(--surface-1)' },
320
317
  };
321
318
  //# sourceMappingURL=OptionsDrawer.js.map