@steerable/agent-shell-web 0.6.15
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/LICENSE +91 -0
- package/README.md +20 -0
- package/package.json +69 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/src/AppShell.tsx +24 -0
- package/src/assets/logo-generic.png +0 -0
- package/src/brand.test.ts +22 -0
- package/src/brand.ts +48 -0
- package/src/components/AgentSidebar.test.tsx +127 -0
- package/src/components/AgentSidebar.tsx +1010 -0
- package/src/components/ChatHeader.test.tsx +230 -0
- package/src/components/ChatHeader.tsx +436 -0
- package/src/components/DockPanelHeader.tsx +69 -0
- package/src/components/LocalLlmSettingsModal.test.tsx +372 -0
- package/src/components/LocalLlmSettingsModal.tsx +72 -0
- package/src/components/TerminalPanel.tsx +68 -0
- package/src/components/TerminalView.tsx +228 -0
- package/src/components/chat/ApprovalModal.test.tsx +83 -0
- package/src/components/chat/ApprovalModal.tsx +229 -0
- package/src/components/chat/AskUserModalHost.test.tsx +176 -0
- package/src/components/chat/AskUserModalHost.tsx +61 -0
- package/src/components/chat/AskUserQuestionMenu.tsx +621 -0
- package/src/components/chat/AssistantMessage.test.tsx +68 -0
- package/src/components/chat/AssistantMessage.tsx +469 -0
- package/src/components/chat/ChatInput.test.tsx +609 -0
- package/src/components/chat/ChatInput.tsx +2068 -0
- package/src/components/chat/ChatProjectBadge.tsx +362 -0
- package/src/components/chat/EmptyChat.tsx +18 -0
- package/src/components/chat/ExecPolicyPicker.test.tsx +26 -0
- package/src/components/chat/ExecPolicyPicker.tsx +120 -0
- package/src/components/chat/ExecutedActionsCard.tsx +216 -0
- package/src/components/chat/InterruptedTurnCard.tsx +53 -0
- package/src/components/chat/LocalChatPanel.tsx +408 -0
- package/src/components/chat/Markdown.test.tsx +170 -0
- package/src/components/chat/Markdown.tsx +432 -0
- package/src/components/chat/MessageList.tsx +332 -0
- package/src/components/chat/ModelPicker.test.tsx +149 -0
- package/src/components/chat/ModelPicker.tsx +289 -0
- package/src/components/chat/OrchestrationChildrenCard.tsx +40 -0
- package/src/components/chat/PlanTodoList.tsx +121 -0
- package/src/components/chat/ProjectTrustBanner.tsx +91 -0
- package/src/components/chat/SessionTreeModal.test.tsx +161 -0
- package/src/components/chat/SessionTreeModal.tsx +221 -0
- package/src/components/chat/StreamingStatus.tsx +95 -0
- package/src/components/chat/TaskOutcomeCards.test.tsx +97 -0
- package/src/components/chat/TaskOutcomeCards.tsx +128 -0
- package/src/components/chat/TaskPanelModal.test.tsx +198 -0
- package/src/components/chat/TaskPanelModal.tsx +310 -0
- package/src/components/chat/TaskProcessPanel.test.tsx +111 -0
- package/src/components/chat/TaskProcessPanel.tsx +208 -0
- package/src/components/chat/TurnProcessGroup.test.tsx +161 -0
- package/src/components/chat/TurnProcessGroup.tsx +193 -0
- package/src/components/chat/UnifiedDiff.tsx +31 -0
- package/src/components/chat/UserMessage.tsx +155 -0
- package/src/components/chat/elapsed.test.ts +42 -0
- package/src/components/chat/elapsed.ts +45 -0
- package/src/components/chat/executed-actions-model.test.ts +134 -0
- package/src/components/chat/executed-actions-model.ts +143 -0
- package/src/components/chat/orchestration-children-model.test.ts +115 -0
- package/src/components/chat/orchestration-children-model.ts +104 -0
- package/src/components/chat/timestamp.ts +24 -0
- package/src/components/chat/turn-timeline.test.ts +97 -0
- package/src/components/chat/turn-timeline.ts +132 -0
- package/src/components/chat/useChatTasks.test.ts +136 -0
- package/src/components/chat/useChatTasks.ts +134 -0
- package/src/components/chat/useCopy.ts +62 -0
- package/src/components/settings/AgentCapabilityFields.tsx +343 -0
- package/src/components/settings/AgentsSettingsPanel.test.tsx +229 -0
- package/src/components/settings/AgentsSettingsPanel.tsx +412 -0
- package/src/components/settings/DiagnoseSettingsPanel.tsx +119 -0
- package/src/components/settings/InsightsSettingsPanel.test.tsx +132 -0
- package/src/components/settings/InsightsSettingsPanel.tsx +309 -0
- package/src/components/settings/LlmSettingsPanel.tsx +875 -0
- package/src/components/settings/McpSettingsPanel.tsx +466 -0
- package/src/components/settings/ModelCapabilityChips.tsx +46 -0
- package/src/components/settings/ModelIdCombobox.test.tsx +88 -0
- package/src/components/settings/ModelIdCombobox.tsx +185 -0
- package/src/components/settings/SecuritySettingsPanel.test.tsx +122 -0
- package/src/components/settings/SecuritySettingsPanel.tsx +169 -0
- package/src/components/settings/SettingsSaveButton.tsx +35 -0
- package/src/components/settings/SkillsSettingsPanel.test.tsx +62 -0
- package/src/components/settings/SkillsSettingsPanel.tsx +334 -0
- package/src/components/settings/TelemetrySettingsPanel.tsx +173 -0
- package/src/components/settings/UsagePanel.tsx +196 -0
- package/src/components/settings/WebSearchSettingsPanel.test.tsx +90 -0
- package/src/components/settings/WebSearchSettingsPanel.tsx +183 -0
- package/src/components/settings/compat-flags-model.test.ts +71 -0
- package/src/components/settings/compat-flags-model.ts +57 -0
- package/src/components/settings/llm-vendors.test.ts +171 -0
- package/src/components/settings/llm-vendors.ts +275 -0
- package/src/components/settings/model-capabilities.test.ts +88 -0
- package/src/components/settings/model-capabilities.ts +123 -0
- package/src/components/settings/preset-choice-model.test.ts +96 -0
- package/src/components/settings/preset-choice-model.ts +75 -0
- package/src/fixtures/browser-dev-data.json +315 -0
- package/src/hooks/useChatsAndAgents.ts +182 -0
- package/src/layouts/AgentLayout.tsx +574 -0
- package/src/lib/attachments.ts +78 -0
- package/src/lib/browser-dev-electron-mock.ts +640 -0
- package/src/lib/chat-title.ts +33 -0
- package/src/lib/chat-transport.ts +346 -0
- package/src/lib/date-groups.ts +34 -0
- package/src/lib/demo-flag.ts +15 -0
- package/src/lib/electron-bridge.ts +270 -0
- package/src/lib/exec-policy.ts +25 -0
- package/src/lib/http-bridge.ts +293 -0
- package/src/lib/insights.ts +17 -0
- package/src/lib/local-api.ts +714 -0
- package/src/lib/pending-first-message.test.ts +17 -0
- package/src/lib/pending-first-message.ts +36 -0
- package/src/lib/slash-sources.ts +191 -0
- package/src/main.tsx +103 -0
- package/src/packs/registry.ts +64 -0
- package/src/pages/AgentPage.tsx +1226 -0
- package/src/pages/ChatPanelPreviewPage.tsx +264 -0
- package/src/pages/SettingsPage.test.tsx +102 -0
- package/src/pages/SettingsPage.tsx +197 -0
- package/src/styles/index.css +156 -0
- package/src/vite-env.d.ts +1 -0
- package/vite.base.ts +94 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BS 模式的宿主 bridge:与 Electron preload(src/preload.ts)暴露的
|
|
3
|
+
* `window.electron` 同一接口(ElectronBridge),但传输换成 HTTP + SSE:
|
|
4
|
+
*
|
|
5
|
+
* localBackend.request → fetch /api/v2/*
|
|
6
|
+
* localBackend.startStream → fetch(ReadableStream 逐 chunk 喂 onEvent)
|
|
7
|
+
* steerChat / approval / terminal → /host/* 端点
|
|
8
|
+
* 所有宿主 → 浏览器事件 → 单条 EventSource(/api/v2/events) 按 channel 分发
|
|
9
|
+
*
|
|
10
|
+
* 场景包的 invoke 命名空间不进本实现(3.1):包 web 模块在 BS 下自带
|
|
11
|
+
* fetch 传输直连自己的 /host/<pack>/* 端点;包广播事件经下面的通用
|
|
12
|
+
* onPackEvent(= subscribeChannel)订阅。
|
|
13
|
+
*
|
|
14
|
+
* server 在 index.html 里注入 `window.__DEEPPATH_BS__`(platform/brand),
|
|
15
|
+
* getElectronBridge() 据此在浏览器里选中本实现。
|
|
16
|
+
*/
|
|
17
|
+
import type {
|
|
18
|
+
ApprovalPromptRequest,
|
|
19
|
+
AskUserPromptRequest,
|
|
20
|
+
ElectronBridge,
|
|
21
|
+
LocalBackendRequestInput,
|
|
22
|
+
LocalBackendStreamEvent,
|
|
23
|
+
TerminalSession,
|
|
24
|
+
TerminalSpawnOptions,
|
|
25
|
+
} from './electron-bridge';
|
|
26
|
+
|
|
27
|
+
interface BsBootstrap {
|
|
28
|
+
platform: NodeJS.Platform;
|
|
29
|
+
flavor: string;
|
|
30
|
+
brandName: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bootstrap(): BsBootstrap {
|
|
34
|
+
const b = typeof window !== 'undefined' ? window.__DEEPPATH_BS__ : undefined;
|
|
35
|
+
return typeof b === 'object' && b
|
|
36
|
+
? b
|
|
37
|
+
: { platform: 'linux', flavor: 'generic', brandName: '' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function http<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
41
|
+
const res = await fetch(path, {
|
|
42
|
+
method,
|
|
43
|
+
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
|
|
44
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
45
|
+
});
|
|
46
|
+
const data = (await res.json().catch(() => null)) as T & { detail?: string };
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const err = new Error(
|
|
49
|
+
(data as { detail?: string } | null)?.detail || `Request failed (${res.status})`,
|
|
50
|
+
) as Error & { status?: number };
|
|
51
|
+
err.status = res.status;
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// 事件总线:一条 EventSource 承载所有 channel(channel 名与 IPC 通道一致)。
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
type ChannelListener = (payload: never) => void;
|
|
61
|
+
const channelListeners = new Map<string, Set<ChannelListener>>();
|
|
62
|
+
let eventSource: EventSource | null = null;
|
|
63
|
+
|
|
64
|
+
function ensureEventSource(): EventSource {
|
|
65
|
+
if (eventSource) return eventSource;
|
|
66
|
+
eventSource = new EventSource('/api/v2/events');
|
|
67
|
+
return eventSource;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function subscribeChannel<T>(channel: string, callback: (payload: T) => void): () => void {
|
|
71
|
+
let set = channelListeners.get(channel);
|
|
72
|
+
if (!set) {
|
|
73
|
+
set = new Set();
|
|
74
|
+
channelListeners.set(channel, set);
|
|
75
|
+
ensureEventSource().addEventListener(channel, (event) => {
|
|
76
|
+
let payload: unknown = null;
|
|
77
|
+
try {
|
|
78
|
+
payload = JSON.parse((event as MessageEvent).data);
|
|
79
|
+
} catch {
|
|
80
|
+
/* 非 JSON 帧(注释/心跳)忽略 */
|
|
81
|
+
}
|
|
82
|
+
const listeners = channelListeners.get(channel);
|
|
83
|
+
if (!listeners) return;
|
|
84
|
+
for (const fn of Array.from(listeners)) {
|
|
85
|
+
try {
|
|
86
|
+
(fn as (p: unknown) => void)(payload);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`[http-bridge] listener for ${channel} threw`, err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
set.add(callback as ChannelListener);
|
|
94
|
+
return () => {
|
|
95
|
+
set.delete(callback as ChannelListener);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// 流式聊天:fetch + ReadableStream。取消 = AbortController(server 端
|
|
101
|
+
// req close → abort agent 循环,对齐 Electron cancelStream 语义)。
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
const activeStreams = new Map<string, AbortController>();
|
|
104
|
+
|
|
105
|
+
async function startStream(
|
|
106
|
+
input: LocalBackendRequestInput,
|
|
107
|
+
onEvent: (payload: LocalBackendStreamEvent) => void,
|
|
108
|
+
): Promise<string | null> {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const streamId = crypto.randomUUID();
|
|
111
|
+
let res: Response;
|
|
112
|
+
try {
|
|
113
|
+
res = await fetch(input.path, {
|
|
114
|
+
method: input.method,
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: input.body !== undefined ? JSON.stringify(input.body) : undefined,
|
|
117
|
+
signal: controller.signal,
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
onEvent({ type: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (!res.ok || !res.body) {
|
|
124
|
+
onEvent({ type: 'error', error: `start stream failed (${res.status})` });
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
activeStreams.set(streamId, controller);
|
|
128
|
+
void (async () => {
|
|
129
|
+
const reader = res.body!.getReader();
|
|
130
|
+
const decoder = new TextDecoder();
|
|
131
|
+
try {
|
|
132
|
+
for (;;) {
|
|
133
|
+
const { done, value } = await reader.read();
|
|
134
|
+
if (done) break;
|
|
135
|
+
onEvent({ type: 'data', chunk: decoder.decode(value, { stream: true }) });
|
|
136
|
+
}
|
|
137
|
+
onEvent({ type: 'end', status: res.status });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (controller.signal.aborted) {
|
|
140
|
+
// 本地取消:server 已 abort agent 循环并落库,按正常结束上报。
|
|
141
|
+
onEvent({ type: 'end', status: 200 });
|
|
142
|
+
} else {
|
|
143
|
+
onEvent({ type: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
activeStreams.delete(streamId);
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
return streamId;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cancelStream(streamId: string): void {
|
|
153
|
+
activeStreams.get(streamId)?.abort();
|
|
154
|
+
activeStreams.delete(streamId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// 终端:ensure 返回 { session, replay }——replay 是 attach 前的输出缓冲,
|
|
159
|
+
// 经事件分发补发给已注册的 onData(TerminalView 先订阅后 ensure,顺序安全)。
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
async function terminalEnsure(options?: TerminalSpawnOptions): Promise<TerminalSession> {
|
|
162
|
+
const { session, replay } = await http<{ session: TerminalSession; replay: string }>(
|
|
163
|
+
'POST',
|
|
164
|
+
'/host/terminal/ensure',
|
|
165
|
+
options ?? {},
|
|
166
|
+
);
|
|
167
|
+
if (replay) {
|
|
168
|
+
queueMicrotask(() => {
|
|
169
|
+
const listeners = channelListeners.get('terminal:data');
|
|
170
|
+
if (!listeners) return;
|
|
171
|
+
for (const fn of Array.from(listeners)) {
|
|
172
|
+
(fn as (p: unknown) => void)({ sessionId: session.id, chunk: replay });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return session;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function createHttpBridge(): ElectronBridge {
|
|
180
|
+
const boot = bootstrap();
|
|
181
|
+
const bridge: ElectronBridge = {
|
|
182
|
+
runtime: 'local',
|
|
183
|
+
platform: boot.platform,
|
|
184
|
+
|
|
185
|
+
local: {
|
|
186
|
+
// 浏览器没有系统目录选择器(showDirectoryPicker 不给路径)。返回
|
|
187
|
+
// canceled 让 UI 走"手动输入路径"的既有分支。
|
|
188
|
+
selectDirectory: async () => ({ canceled: true, filePaths: [] }),
|
|
189
|
+
saveTextFile: async (options) => {
|
|
190
|
+
const blob = new Blob([options.content], { type: 'application/json' });
|
|
191
|
+
const url = URL.createObjectURL(blob);
|
|
192
|
+
const a = document.createElement('a');
|
|
193
|
+
a.href = url;
|
|
194
|
+
a.download = options.defaultPath || 'deeppath-insights.json';
|
|
195
|
+
a.click();
|
|
196
|
+
URL.revokeObjectURL(url);
|
|
197
|
+
return { canceled: false, filePath: a.download };
|
|
198
|
+
},
|
|
199
|
+
captureScreenshot: async () => ({
|
|
200
|
+
success: false as const,
|
|
201
|
+
error: '浏览器模式不支持窗口截图,请用系统截图工具。',
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
localBackend: {
|
|
206
|
+
request: <T>(input: LocalBackendRequestInput) =>
|
|
207
|
+
http<T>(input.method, input.path, input.body),
|
|
208
|
+
startStream,
|
|
209
|
+
cancelStream,
|
|
210
|
+
steerChat: async (chatId, content) => {
|
|
211
|
+
const result = await http<{ ok?: boolean }>('POST', '/host/steer', { chatId, content });
|
|
212
|
+
return result.ok === true;
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// 会话附件:浏览器没有文件路径,renderer 把字节读成 base64 经此端点落盘。
|
|
217
|
+
attachments: {
|
|
218
|
+
save: (input) => http('POST', '/host/attachments/save', input),
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// BS 没有原生菜单;对应快捷键由 web 内部处理,这里留 no-op。
|
|
222
|
+
onMenuNewChat: () => undefined,
|
|
223
|
+
offMenuNewChat: () => undefined,
|
|
224
|
+
onMenuOpenTerminal: () => undefined,
|
|
225
|
+
offMenuOpenTerminal: () => undefined,
|
|
226
|
+
|
|
227
|
+
onChatTitleUpdated: (callback) =>
|
|
228
|
+
subscribeChannel<{ chatId: string; title: string }>('chat-title-updated', callback),
|
|
229
|
+
|
|
230
|
+
onChatCreated: (callback) =>
|
|
231
|
+
subscribeChannel<{ chatId: string; agentId?: string | null }>('chat-created', callback),
|
|
232
|
+
|
|
233
|
+
onTaskUpdated: (callback) =>
|
|
234
|
+
subscribeChannel<{ chatId: string; taskId: string; status: string }>(
|
|
235
|
+
'task-updated',
|
|
236
|
+
callback,
|
|
237
|
+
),
|
|
238
|
+
|
|
239
|
+
// 场景包广播事件(3.1 通用化):通道名即 SSE 分发键,载荷由订阅方
|
|
240
|
+
// (包)自行收窄校验。
|
|
241
|
+
onPackEvent: (channel, callback) => subscribeChannel(channel, callback),
|
|
242
|
+
|
|
243
|
+
onTaskProcess: (callback) =>
|
|
244
|
+
subscribeChannel<{
|
|
245
|
+
chatId: string;
|
|
246
|
+
taskId: string;
|
|
247
|
+
timeline: unknown;
|
|
248
|
+
live: boolean;
|
|
249
|
+
}>('task-process', callback),
|
|
250
|
+
|
|
251
|
+
approval: {
|
|
252
|
+
onRequest: (callback) =>
|
|
253
|
+
subscribeChannel<ApprovalPromptRequest>('approval:request', callback),
|
|
254
|
+
decide: async (decision) => {
|
|
255
|
+
await http('POST', '/host/approval/decide', decision);
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
askUser: {
|
|
260
|
+
onRequest: (callback) =>
|
|
261
|
+
subscribeChannel<AskUserPromptRequest>('ask-user:request', callback),
|
|
262
|
+
answer: async (reply) => {
|
|
263
|
+
await http('POST', '/host/ask-user/answer', reply);
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
terminal: {
|
|
268
|
+
ensure: terminalEnsure,
|
|
269
|
+
write: (id, data) => http<boolean>('POST', '/host/terminal/write', { id, data }),
|
|
270
|
+
resize: (id, cols, rows) => http<boolean>('POST', '/host/terminal/resize', { id, cols, rows }),
|
|
271
|
+
onData: (callback) =>
|
|
272
|
+
subscribeChannel<{ sessionId: string; chunk: string }>('terminal:data', callback),
|
|
273
|
+
onExit: (callback) =>
|
|
274
|
+
subscribeChannel<{ sessionId: string; code: number; signal: string | null }>(
|
|
275
|
+
'terminal:exit',
|
|
276
|
+
callback,
|
|
277
|
+
),
|
|
278
|
+
onReveal: (callback) =>
|
|
279
|
+
subscribeChannel<{ sessionId: string }>('terminal:reveal', callback),
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// 浏览器通知等 Electron 专属字段不实现(web 端类型未声明,也无调用点)。
|
|
285
|
+
return bridge;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let singleton: ElectronBridge | null = null;
|
|
289
|
+
|
|
290
|
+
export function getHttpBridge(): ElectronBridge {
|
|
291
|
+
if (!singleton) singleton = createHttpBridge();
|
|
292
|
+
return singleton;
|
|
293
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getElectronBridge } from './electron-bridge';
|
|
2
|
+
|
|
3
|
+
/** Fire-and-forget local enqueue. Never throws into UI. */
|
|
4
|
+
export function trackBehavior(
|
|
5
|
+
eventName: string,
|
|
6
|
+
properties: Record<string, unknown> = {},
|
|
7
|
+
): void {
|
|
8
|
+
const bridge = getElectronBridge();
|
|
9
|
+
if (!bridge) return;
|
|
10
|
+
void bridge.localBackend
|
|
11
|
+
.request({
|
|
12
|
+
method: 'POST',
|
|
13
|
+
path: '/api/v2/insights/events',
|
|
14
|
+
body: { eventName, properties },
|
|
15
|
+
})
|
|
16
|
+
.catch(() => {});
|
|
17
|
+
}
|