@openspecui/server 3.4.1 → 3.5.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/dist/index.mjs +416 -10
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, GitConfigSchema, HOSTED_SHELL_PROTOCOL_VERSION, MarkdownParser, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalRendererEngineSchema, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getToolInitStates, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, resolveTerminalShellDefaults, sniffGlobalCli, subscribeWatcherRuntimeStatus } from "@openspecui/core";
|
|
1
|
+
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, GitConfigSchema, HOSTED_SHELL_PROTOCOL_VERSION, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getToolInitStates, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, resolveTerminalShellDefaults, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
|
|
2
2
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { access, mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -10,9 +10,13 @@ import { Hono } from "hono";
|
|
|
10
10
|
import { cors } from "hono/cors";
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
12
12
|
import { WebSocketServer } from "ws";
|
|
13
|
+
import { CustomSoundHashSchema as CustomSoundHashSchema$1, CustomSoundIdSchema, CustomSoundMetadataFileSchema, customHashFromSoundId, soundIdFromCustomHash } from "@openspecui/core/sounds";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { homedir } from "node:os";
|
|
13
16
|
import { EventEmitter } from "node:events";
|
|
14
17
|
import { execFile } from "node:child_process";
|
|
15
18
|
import { promisify } from "node:util";
|
|
19
|
+
import { NotificationGroupKeySchema, NotificationPublishInputSchema as NotificationPublishInputSchema$1, getNotificationGroupKey } from "@openspecui/core/notifications";
|
|
16
20
|
import * as pty from "@lydell/node-pty";
|
|
17
21
|
import { EventEmitter as EventEmitter$1 } from "events";
|
|
18
22
|
import { SearchQuerySchema } from "@openspecui/search";
|
|
@@ -333,6 +337,135 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
333
337
|
throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
334
338
|
}
|
|
335
339
|
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/custom-sound-service.ts
|
|
342
|
+
const METADATA_FILE = "metadatas.json";
|
|
343
|
+
function getDefaultSoundsDir() {
|
|
344
|
+
return join(homedir(), ".openspecui", "sounds");
|
|
345
|
+
}
|
|
346
|
+
function isNotFound(error) {
|
|
347
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
348
|
+
}
|
|
349
|
+
function getFallbackName(filename, hash) {
|
|
350
|
+
const trimmed = filename?.trim();
|
|
351
|
+
return trimmed ? trimmed.replace(/\.[^.]*$/, "") : hash.slice(0, 12);
|
|
352
|
+
}
|
|
353
|
+
var CustomSoundService = class {
|
|
354
|
+
soundsDir;
|
|
355
|
+
metadataPath;
|
|
356
|
+
constructor(soundsDir = getDefaultSoundsDir()) {
|
|
357
|
+
this.soundsDir = soundsDir;
|
|
358
|
+
this.metadataPath = join(soundsDir, METADATA_FILE);
|
|
359
|
+
}
|
|
360
|
+
async listAvailable() {
|
|
361
|
+
const metadatas = await this.readMetadatas();
|
|
362
|
+
const available = [];
|
|
363
|
+
for (const metadata of Object.values(metadatas)) if (await this.hasSoundFile(metadata.id)) available.push(metadata);
|
|
364
|
+
return available.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
365
|
+
}
|
|
366
|
+
async upload(input) {
|
|
367
|
+
if (!input.mime.startsWith("audio/")) throw new Error("Only audio files are supported.");
|
|
368
|
+
const hash = hashSoundBytes(input.bytes);
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
const metadatas = await this.readMetadatas();
|
|
371
|
+
const previous = metadatas[hash];
|
|
372
|
+
const metadata = {
|
|
373
|
+
id: hash,
|
|
374
|
+
name: previous?.name ?? getFallbackName(input.name, hash),
|
|
375
|
+
mime: input.mime,
|
|
376
|
+
size: input.bytes.byteLength,
|
|
377
|
+
createdAt: previous?.createdAt ?? now,
|
|
378
|
+
updatedAt: now
|
|
379
|
+
};
|
|
380
|
+
await mkdir(this.soundsDir, { recursive: true });
|
|
381
|
+
await writeFile(this.getSoundPath(hash), input.bytes);
|
|
382
|
+
await this.writeMetadatas({
|
|
383
|
+
...metadatas,
|
|
384
|
+
[hash]: metadata
|
|
385
|
+
});
|
|
386
|
+
return metadata;
|
|
387
|
+
}
|
|
388
|
+
async rename(id, name) {
|
|
389
|
+
const hash = this.requireCustomHash(id);
|
|
390
|
+
const nextName = name.trim();
|
|
391
|
+
if (!nextName) throw new Error("Sound name is required.");
|
|
392
|
+
const metadatas = await this.readMetadatas();
|
|
393
|
+
const current = metadatas[hash];
|
|
394
|
+
if (!current || !await this.hasSoundFile(hash)) throw new Error("Sound not found.");
|
|
395
|
+
const next = {
|
|
396
|
+
...current,
|
|
397
|
+
name: nextName,
|
|
398
|
+
updatedAt: Date.now()
|
|
399
|
+
};
|
|
400
|
+
await this.writeMetadatas({
|
|
401
|
+
...metadatas,
|
|
402
|
+
[hash]: next
|
|
403
|
+
});
|
|
404
|
+
return next;
|
|
405
|
+
}
|
|
406
|
+
async remove(id) {
|
|
407
|
+
const hash = this.requireCustomHash(id);
|
|
408
|
+
const next = { ...await this.readMetadatas() };
|
|
409
|
+
delete next[hash];
|
|
410
|
+
await rm(this.getSoundPath(hash), { force: true });
|
|
411
|
+
await this.writeMetadatas(next);
|
|
412
|
+
}
|
|
413
|
+
async getFile(id) {
|
|
414
|
+
const hash = customHashFromSoundId(id);
|
|
415
|
+
if (!hash) return null;
|
|
416
|
+
const metadata = (await this.readMetadatas())[hash];
|
|
417
|
+
if (!metadata || !await this.hasSoundFile(hash)) return null;
|
|
418
|
+
return {
|
|
419
|
+
metadata,
|
|
420
|
+
data: await readSoundData(this.getSoundPath(hash))
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
buildSoundId(hash) {
|
|
424
|
+
return soundIdFromCustomHash(hash);
|
|
425
|
+
}
|
|
426
|
+
requireCustomHash(id) {
|
|
427
|
+
const hash = customHashFromSoundId(id);
|
|
428
|
+
if (!hash) throw new Error("Custom sound id is required.");
|
|
429
|
+
return hash;
|
|
430
|
+
}
|
|
431
|
+
getSoundPath(hash) {
|
|
432
|
+
return join(this.soundsDir, hash);
|
|
433
|
+
}
|
|
434
|
+
async hasSoundFile(hash) {
|
|
435
|
+
try {
|
|
436
|
+
return (await stat(this.getSoundPath(hash))).isFile();
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (isNotFound(error)) return false;
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async readMetadatas() {
|
|
443
|
+
try {
|
|
444
|
+
const content = await readFile(this.metadataPath, "utf-8");
|
|
445
|
+
const parsed = JSON.parse(content);
|
|
446
|
+
const result = CustomSoundMetadataFileSchema.safeParse(parsed);
|
|
447
|
+
return result.success ? result.data : {};
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (isNotFound(error)) return {};
|
|
450
|
+
if (error instanceof SyntaxError) return {};
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async writeMetadatas(metadatas) {
|
|
455
|
+
await mkdir(this.soundsDir, { recursive: true });
|
|
456
|
+
await writeFile(this.metadataPath, JSON.stringify(metadatas, null, 2), "utf-8");
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
function hashSoundBytes(bytes) {
|
|
460
|
+
return CustomSoundHashSchema$1.parse(createHash("sha256").update(bytes).digest("hex"));
|
|
461
|
+
}
|
|
462
|
+
async function readSoundData(path) {
|
|
463
|
+
const bytes = await readFile(path);
|
|
464
|
+
const data = new ArrayBuffer(bytes.byteLength);
|
|
465
|
+
new Uint8Array(data).set(bytes);
|
|
466
|
+
return data;
|
|
467
|
+
}
|
|
468
|
+
|
|
336
469
|
//#endregion
|
|
337
470
|
//#region src/dashboard-overview-service.ts
|
|
338
471
|
const REBUILD_DEBOUNCE_MS$1 = 250;
|
|
@@ -1206,6 +1339,73 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
1206
1339
|
};
|
|
1207
1340
|
}
|
|
1208
1341
|
|
|
1342
|
+
//#endregion
|
|
1343
|
+
//#region src/notification-service.ts
|
|
1344
|
+
var NotificationService = class {
|
|
1345
|
+
notifications = [];
|
|
1346
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1347
|
+
idCounter = 0;
|
|
1348
|
+
list() {
|
|
1349
|
+
return [...this.notifications];
|
|
1350
|
+
}
|
|
1351
|
+
publish(input) {
|
|
1352
|
+
const parsed = NotificationPublishInputSchema$1.parse(input);
|
|
1353
|
+
const groupKey = getNotificationGroupKey(parsed);
|
|
1354
|
+
const createdAt = parsed.createdAt ?? Date.now();
|
|
1355
|
+
const record = {
|
|
1356
|
+
...parsed,
|
|
1357
|
+
id: `notification-${Date.now().toString(36)}-${(++this.idCounter).toString(36)}`,
|
|
1358
|
+
createdAt,
|
|
1359
|
+
groupKey
|
|
1360
|
+
};
|
|
1361
|
+
this.notifications = [record, ...this.notifications];
|
|
1362
|
+
this.emit();
|
|
1363
|
+
return record;
|
|
1364
|
+
}
|
|
1365
|
+
markRead(id) {
|
|
1366
|
+
const next = this.notifications.filter((notification) => notification.id !== id);
|
|
1367
|
+
if (next.length === this.notifications.length) return;
|
|
1368
|
+
this.notifications = next;
|
|
1369
|
+
this.emit();
|
|
1370
|
+
}
|
|
1371
|
+
markManyRead(ids) {
|
|
1372
|
+
if (ids.length === 0) return;
|
|
1373
|
+
const idSet = new Set(ids);
|
|
1374
|
+
const next = this.notifications.filter((notification) => !idSet.has(notification.id));
|
|
1375
|
+
if (next.length === this.notifications.length) return;
|
|
1376
|
+
this.notifications = next;
|
|
1377
|
+
this.emit();
|
|
1378
|
+
}
|
|
1379
|
+
clearGroup(groupKey) {
|
|
1380
|
+
const next = this.notifications.filter((notification) => notification.groupKey !== groupKey);
|
|
1381
|
+
if (next.length === this.notifications.length) return;
|
|
1382
|
+
this.notifications = next;
|
|
1383
|
+
this.emit();
|
|
1384
|
+
}
|
|
1385
|
+
clearTerminalSession(sessionId) {
|
|
1386
|
+
const next = this.notifications.filter((notification) => notification.source.type !== "terminal" || notification.source.sessionId !== sessionId);
|
|
1387
|
+
if (next.length === this.notifications.length) return;
|
|
1388
|
+
this.notifications = next;
|
|
1389
|
+
this.emit();
|
|
1390
|
+
}
|
|
1391
|
+
clearAll() {
|
|
1392
|
+
if (this.notifications.length === 0) return;
|
|
1393
|
+
this.notifications = [];
|
|
1394
|
+
this.emit();
|
|
1395
|
+
}
|
|
1396
|
+
subscribe(listener) {
|
|
1397
|
+
this.listeners.add(listener);
|
|
1398
|
+
listener(this.list());
|
|
1399
|
+
return () => {
|
|
1400
|
+
this.listeners.delete(listener);
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
emit() {
|
|
1404
|
+
const snapshot = this.list();
|
|
1405
|
+
for (const listener of this.listeners) listener(snapshot);
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1209
1409
|
//#endregion
|
|
1210
1410
|
//#region src/project-recovery-service.ts
|
|
1211
1411
|
function normalizeWorktreeBranchName(defaultBranch) {
|
|
@@ -1454,6 +1654,8 @@ var PtySession = class extends EventEmitter$1 {
|
|
|
1454
1654
|
process;
|
|
1455
1655
|
titleInterval = null;
|
|
1456
1656
|
lastTitle = "";
|
|
1657
|
+
lastOscIconTitle = "";
|
|
1658
|
+
lastOscWindowTitle = "";
|
|
1457
1659
|
buffer = [];
|
|
1458
1660
|
bufferByteLength = 0;
|
|
1459
1661
|
maxBufferLines;
|
|
@@ -1513,6 +1715,18 @@ var PtySession = class extends EventEmitter$1 {
|
|
|
1513
1715
|
get title() {
|
|
1514
1716
|
return this.lastTitle;
|
|
1515
1717
|
}
|
|
1718
|
+
get targetTitle() {
|
|
1719
|
+
return this.lastOscIconTitle || this.lastOscWindowTitle || this.lastTitle || this.command;
|
|
1720
|
+
}
|
|
1721
|
+
get oscTitle() {
|
|
1722
|
+
return this.lastOscIconTitle || this.lastOscWindowTitle;
|
|
1723
|
+
}
|
|
1724
|
+
setTargetTitle(title, target) {
|
|
1725
|
+
const trimmed = title.trim();
|
|
1726
|
+
if (!trimmed) return;
|
|
1727
|
+
if (target === "icon" || target === "both") this.lastOscIconTitle = trimmed;
|
|
1728
|
+
if (target === "window" || target === "both") this.lastOscWindowTitle = trimmed;
|
|
1729
|
+
}
|
|
1516
1730
|
appendBuffer(data) {
|
|
1517
1731
|
let chunk = data;
|
|
1518
1732
|
if (chunk.length > this.maxBufferBytes) chunk = chunk.slice(-this.maxBufferBytes);
|
|
@@ -1621,9 +1835,35 @@ var PtyManager = class {
|
|
|
1621
1835
|
|
|
1622
1836
|
//#endregion
|
|
1623
1837
|
//#region src/pty-websocket.ts
|
|
1624
|
-
function
|
|
1838
|
+
function resolveTerminalTargetTitle(session, title) {
|
|
1839
|
+
return title?.trim() || session.targetTitle || session.title || session.command;
|
|
1840
|
+
}
|
|
1841
|
+
function updateTerminalTargetTitle(session, event) {
|
|
1842
|
+
session.setTargetTitle(event.title, event.target);
|
|
1843
|
+
}
|
|
1844
|
+
function normalizeTerminalNotificationBody(value) {
|
|
1845
|
+
return value.replace(/\s+/g, " ").trim();
|
|
1846
|
+
}
|
|
1847
|
+
function getTerminalNotificationFanoutKey(event) {
|
|
1848
|
+
return normalizeTerminalNotificationBody(event.body) || normalizeTerminalNotificationBody(event.title ?? "");
|
|
1849
|
+
}
|
|
1850
|
+
function coalesceTerminalNotificationFanout(events) {
|
|
1851
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1852
|
+
for (const event of events) {
|
|
1853
|
+
const key = getTerminalNotificationFanoutKey(event);
|
|
1854
|
+
const group = groups.get(key);
|
|
1855
|
+
if (group) group.push(event);
|
|
1856
|
+
else groups.set(key, [event]);
|
|
1857
|
+
}
|
|
1858
|
+
return [...groups.values()].flatMap((group) => {
|
|
1859
|
+
if (new Set(group.map((event) => event.protocol)).size <= 1) return group;
|
|
1860
|
+
return group.find((event) => event.title) ?? group[0] ?? [];
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
function createPtyWebSocketHandler(ptyManager, notificationService) {
|
|
1625
1864
|
return (ws) => {
|
|
1626
1865
|
const cleanups = /* @__PURE__ */ new Map();
|
|
1866
|
+
const parsers = /* @__PURE__ */ new Map();
|
|
1627
1867
|
const send = (msg) => {
|
|
1628
1868
|
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
|
|
1629
1869
|
};
|
|
@@ -1640,10 +1880,68 @@ function createPtyWebSocketHandler(ptyManager) {
|
|
|
1640
1880
|
cleanups.get(sessionId)?.();
|
|
1641
1881
|
if (opts?.cols && opts?.rows && !session.isExited) session.resize(opts.cols, opts.rows);
|
|
1642
1882
|
const onData = (data) => {
|
|
1643
|
-
|
|
1883
|
+
const parser = parsers.get(sessionId) ?? new TerminalControlParser();
|
|
1884
|
+
parsers.set(sessionId, parser);
|
|
1885
|
+
const parsed = parser.push(data);
|
|
1886
|
+
const notifications = coalesceTerminalNotificationFanout(parsed.events.filter((event) => event.type === "notification"));
|
|
1887
|
+
const notificationsToPublish = new Set(notifications);
|
|
1888
|
+
for (const event of parsed.events) {
|
|
1889
|
+
if (event.type === "bell") {
|
|
1890
|
+
send({
|
|
1891
|
+
type: "bell",
|
|
1892
|
+
sessionId,
|
|
1893
|
+
createdAt: Date.now()
|
|
1894
|
+
});
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (event.type === "notification") {
|
|
1898
|
+
if (!notificationsToPublish.has(event)) continue;
|
|
1899
|
+
notificationService?.publish(terminalNotificationEventToPublishInput({
|
|
1900
|
+
event,
|
|
1901
|
+
sessionId,
|
|
1902
|
+
terminalTitle: resolveTerminalTargetTitle(session)
|
|
1903
|
+
}));
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (event.type === "title") {
|
|
1907
|
+
const previousTargetTitle = session.targetTitle;
|
|
1908
|
+
updateTerminalTargetTitle(session, event);
|
|
1909
|
+
const nextTargetTitle = resolveTerminalTargetTitle(session);
|
|
1910
|
+
if (nextTargetTitle !== previousTargetTitle) send({
|
|
1911
|
+
type: "title",
|
|
1912
|
+
sessionId,
|
|
1913
|
+
title: nextTargetTitle
|
|
1914
|
+
});
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
if (event.type === "cwd") {
|
|
1918
|
+
send({
|
|
1919
|
+
type: "cwd",
|
|
1920
|
+
sessionId,
|
|
1921
|
+
cwd: event.cwd
|
|
1922
|
+
});
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
if (event.type === "progress") {
|
|
1926
|
+
send({
|
|
1927
|
+
type: "progress",
|
|
1928
|
+
sessionId,
|
|
1929
|
+
state: event.state,
|
|
1930
|
+
value: event.value
|
|
1931
|
+
});
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
if (event.type === "prompt-state") send({
|
|
1935
|
+
type: "prompt-state",
|
|
1936
|
+
sessionId,
|
|
1937
|
+
state: event.state,
|
|
1938
|
+
exitCode: event.exitCode
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
if (parsed.output) send({
|
|
1644
1942
|
type: "output",
|
|
1645
1943
|
sessionId,
|
|
1646
|
-
data
|
|
1944
|
+
data: parsed.output
|
|
1647
1945
|
});
|
|
1648
1946
|
};
|
|
1649
1947
|
const onExit = (exitCode) => {
|
|
@@ -1655,10 +1953,15 @@ function createPtyWebSocketHandler(ptyManager) {
|
|
|
1655
1953
|
};
|
|
1656
1954
|
const onTitle = (title) => {
|
|
1657
1955
|
send({
|
|
1658
|
-
type: "title",
|
|
1956
|
+
type: "process-title",
|
|
1659
1957
|
sessionId,
|
|
1660
1958
|
title
|
|
1661
1959
|
});
|
|
1960
|
+
send({
|
|
1961
|
+
type: "title",
|
|
1962
|
+
sessionId,
|
|
1963
|
+
title: resolveTerminalTargetTitle(session)
|
|
1964
|
+
});
|
|
1662
1965
|
};
|
|
1663
1966
|
session.on("data", onData);
|
|
1664
1967
|
session.on("exit", onExit);
|
|
@@ -1667,6 +1970,7 @@ function createPtyWebSocketHandler(ptyManager) {
|
|
|
1667
1970
|
session.removeListener("data", onData);
|
|
1668
1971
|
session.removeListener("exit", onExit);
|
|
1669
1972
|
session.removeListener("title", onTitle);
|
|
1973
|
+
parsers.delete(sessionId);
|
|
1670
1974
|
cleanups.delete(sessionId);
|
|
1671
1975
|
});
|
|
1672
1976
|
};
|
|
@@ -1730,10 +2034,15 @@ function createPtyWebSocketHandler(ptyManager) {
|
|
|
1730
2034
|
data: buffer
|
|
1731
2035
|
});
|
|
1732
2036
|
if (session.title) send({
|
|
1733
|
-
type: "title",
|
|
2037
|
+
type: "process-title",
|
|
1734
2038
|
sessionId: session.id,
|
|
1735
2039
|
title: session.title
|
|
1736
2040
|
});
|
|
2041
|
+
if (session.title || session.oscTitle) send({
|
|
2042
|
+
type: "title",
|
|
2043
|
+
sessionId: session.id,
|
|
2044
|
+
title: resolveTerminalTargetTitle(session)
|
|
2045
|
+
});
|
|
1737
2046
|
if (session.isExited) send({
|
|
1738
2047
|
type: "exit",
|
|
1739
2048
|
sessionId: session.id,
|
|
@@ -2596,6 +2905,59 @@ function createReactiveSubscriptionWithInput(task) {
|
|
|
2596
2905
|
const t = initTRPC.context().create();
|
|
2597
2906
|
const router = t.router;
|
|
2598
2907
|
const publicProcedure = t.procedure;
|
|
2908
|
+
const notificationsRouter = router({
|
|
2909
|
+
list: publicProcedure.query(({ ctx }) => {
|
|
2910
|
+
return ctx.notificationService.list();
|
|
2911
|
+
}),
|
|
2912
|
+
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
2913
|
+
return observable((emit) => {
|
|
2914
|
+
const unsubscribe = ctx.notificationService.subscribe((notifications) => {
|
|
2915
|
+
emit.next(notifications);
|
|
2916
|
+
});
|
|
2917
|
+
return () => {
|
|
2918
|
+
unsubscribe();
|
|
2919
|
+
};
|
|
2920
|
+
});
|
|
2921
|
+
}),
|
|
2922
|
+
publish: publicProcedure.input(NotificationPublishInputSchema$1).mutation(({ ctx, input }) => {
|
|
2923
|
+
return ctx.notificationService.publish(input);
|
|
2924
|
+
}),
|
|
2925
|
+
markRead: publicProcedure.input(z.object({ id: z.string().min(1) })).mutation(({ ctx, input }) => {
|
|
2926
|
+
ctx.notificationService.markRead(input.id);
|
|
2927
|
+
return { success: true };
|
|
2928
|
+
}),
|
|
2929
|
+
markManyRead: publicProcedure.input(z.object({ ids: z.array(z.string().min(1)).default([]) })).mutation(({ ctx, input }) => {
|
|
2930
|
+
ctx.notificationService.markManyRead(input.ids);
|
|
2931
|
+
return { success: true };
|
|
2932
|
+
}),
|
|
2933
|
+
clearGroup: publicProcedure.input(z.object({ groupKey: NotificationGroupKeySchema })).mutation(({ ctx, input }) => {
|
|
2934
|
+
ctx.notificationService.clearGroup(input.groupKey);
|
|
2935
|
+
return { success: true };
|
|
2936
|
+
}),
|
|
2937
|
+
clearTerminalSession: publicProcedure.input(z.object({ sessionId: z.string().min(1) })).mutation(({ ctx, input }) => {
|
|
2938
|
+
ctx.notificationService.clearTerminalSession(input.sessionId);
|
|
2939
|
+
return { success: true };
|
|
2940
|
+
}),
|
|
2941
|
+
clearAll: publicProcedure.mutation(({ ctx }) => {
|
|
2942
|
+
ctx.notificationService.clearAll();
|
|
2943
|
+
return { success: true };
|
|
2944
|
+
})
|
|
2945
|
+
});
|
|
2946
|
+
const soundsRouter = router({
|
|
2947
|
+
listCustom: publicProcedure.query(({ ctx }) => {
|
|
2948
|
+
return ctx.customSoundService.listAvailable();
|
|
2949
|
+
}),
|
|
2950
|
+
renameCustom: publicProcedure.input(z.object({
|
|
2951
|
+
id: CustomSoundIdSchema,
|
|
2952
|
+
name: z.string().min(1).max(160)
|
|
2953
|
+
})).mutation(({ ctx, input }) => {
|
|
2954
|
+
return ctx.customSoundService.rename(input.id, input.name);
|
|
2955
|
+
}),
|
|
2956
|
+
deleteCustom: publicProcedure.input(z.object({ id: CustomSoundIdSchema })).mutation(async ({ ctx, input }) => {
|
|
2957
|
+
await ctx.customSoundService.remove(input.id);
|
|
2958
|
+
return { success: true };
|
|
2959
|
+
})
|
|
2960
|
+
});
|
|
2599
2961
|
const OPSX_CORE_PROFILE_WORKFLOWS = [
|
|
2600
2962
|
"propose",
|
|
2601
2963
|
"explore",
|
|
@@ -3032,20 +3394,22 @@ const configRouter = router({
|
|
|
3032
3394
|
opsx: OpsxConfigSchema.partial().optional(),
|
|
3033
3395
|
terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
|
|
3034
3396
|
dashboard: DashboardConfigSchema.partial().optional(),
|
|
3035
|
-
git: GitConfigSchema.partial().optional()
|
|
3397
|
+
git: GitConfigSchema.partial().optional(),
|
|
3398
|
+
notifications: NotificationSettingsSchema.partial().optional()
|
|
3036
3399
|
})).mutation(async ({ ctx, input }) => {
|
|
3037
3400
|
const hasCliCommand = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "command");
|
|
3038
3401
|
const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
|
|
3039
3402
|
if (hasCliCommand && !hasCliArgs) {
|
|
3040
3403
|
await ctx.configManager.setCliCommand(input.cli?.command ?? "");
|
|
3041
|
-
if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.opsx !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0 || input.git !== void 0) await ctx.configManager.writeConfig({
|
|
3404
|
+
if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.opsx !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0 || input.git !== void 0 || input.notifications !== void 0) await ctx.configManager.writeConfig({
|
|
3042
3405
|
theme: input.theme,
|
|
3043
3406
|
codeEditor: input.codeEditor,
|
|
3044
3407
|
appBaseUrl: input.appBaseUrl,
|
|
3045
3408
|
opsx: input.opsx,
|
|
3046
3409
|
terminal: input.terminal,
|
|
3047
3410
|
dashboard: input.dashboard,
|
|
3048
|
-
git: input.git
|
|
3411
|
+
git: input.git,
|
|
3412
|
+
notifications: input.notifications
|
|
3049
3413
|
});
|
|
3050
3414
|
return { success: true };
|
|
3051
3415
|
}
|
|
@@ -3687,6 +4051,8 @@ const appRouter = router({
|
|
|
3687
4051
|
init: initRouter,
|
|
3688
4052
|
realtime: realtimeRouter,
|
|
3689
4053
|
config: configRouter,
|
|
4054
|
+
notifications: notificationsRouter,
|
|
4055
|
+
sounds: soundsRouter,
|
|
3690
4056
|
cli: cliRouter,
|
|
3691
4057
|
opsx: opsxRouter,
|
|
3692
4058
|
kv: kvRouter,
|
|
@@ -4083,6 +4449,8 @@ function createServer(config) {
|
|
|
4083
4449
|
hookRuntime,
|
|
4084
4450
|
executeCli: (args) => cliExecutor.execute(args)
|
|
4085
4451
|
});
|
|
4452
|
+
const notificationService = new NotificationService();
|
|
4453
|
+
const customSoundService = new CustomSoundService();
|
|
4086
4454
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
4087
4455
|
const searchService = new SearchService(adapter, watcher, void 0, documentService);
|
|
4088
4456
|
const dashboardOverviewService = new DashboardOverviewService((reason) => loadDashboardOverview({
|
|
@@ -4111,6 +4479,38 @@ function createServer(config) {
|
|
|
4111
4479
|
embeddedUiUrl: buildEmbeddedUiUrlForPort(config.port ?? 3100)
|
|
4112
4480
|
});
|
|
4113
4481
|
});
|
|
4482
|
+
app.post("/api/notifications", async (c) => {
|
|
4483
|
+
const body = await c.req.json().catch(() => null);
|
|
4484
|
+
const parsed = NotificationPublishInputSchema.safeParse(body);
|
|
4485
|
+
if (!parsed.success) return c.json({
|
|
4486
|
+
error: "Invalid notification payload",
|
|
4487
|
+
issues: parsed.error.issues
|
|
4488
|
+
}, 400);
|
|
4489
|
+
return c.json(notificationService.publish(parsed.data));
|
|
4490
|
+
});
|
|
4491
|
+
app.post("/api/sounds/custom", async (c) => {
|
|
4492
|
+
const formData = await c.req.formData().catch(() => null);
|
|
4493
|
+
const file = formData?.get("file");
|
|
4494
|
+
const nameValue = formData?.get("name");
|
|
4495
|
+
if (!(file instanceof File)) return c.json({ error: "Audio file is required." }, 400);
|
|
4496
|
+
const metadata = await customSoundService.upload({
|
|
4497
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
4498
|
+
name: typeof nameValue === "string" ? nameValue : file.name,
|
|
4499
|
+
mime: file.type || "audio/mpeg"
|
|
4500
|
+
});
|
|
4501
|
+
return c.json(metadata);
|
|
4502
|
+
});
|
|
4503
|
+
app.get("/api/sounds/custom/:id", async (c) => {
|
|
4504
|
+
const id = c.req.param("id");
|
|
4505
|
+
const parsedId = CustomSoundHashSchema.safeParse(id);
|
|
4506
|
+
if (!parsedId.success) return c.json({ error: "Sound not found." }, 404);
|
|
4507
|
+
const file = await customSoundService.getFile(`custom:${parsedId.data}`);
|
|
4508
|
+
if (!file) return c.json({ error: "Sound not found." }, 404);
|
|
4509
|
+
return new Response(new Blob([file.data], { type: file.metadata.mime }), { headers: {
|
|
4510
|
+
"Content-Type": file.metadata.mime,
|
|
4511
|
+
"Cache-Control": "private, max-age=31536000, immutable"
|
|
4512
|
+
} });
|
|
4513
|
+
});
|
|
4114
4514
|
app.use("/trpc/*", async (c) => {
|
|
4115
4515
|
return await fetchRequestHandler({
|
|
4116
4516
|
endpoint: "/trpc",
|
|
@@ -4126,6 +4526,8 @@ function createServer(config) {
|
|
|
4126
4526
|
searchService,
|
|
4127
4527
|
dashboardOverviewService,
|
|
4128
4528
|
projectRecoveryService,
|
|
4529
|
+
notificationService,
|
|
4530
|
+
customSoundService,
|
|
4129
4531
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
4130
4532
|
watcher,
|
|
4131
4533
|
projectDir: config.projectDir
|
|
@@ -4142,6 +4544,8 @@ function createServer(config) {
|
|
|
4142
4544
|
searchService,
|
|
4143
4545
|
dashboardOverviewService,
|
|
4144
4546
|
projectRecoveryService,
|
|
4547
|
+
notificationService,
|
|
4548
|
+
customSoundService,
|
|
4145
4549
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
4146
4550
|
watcher,
|
|
4147
4551
|
projectDir: config.projectDir
|
|
@@ -4157,6 +4561,8 @@ function createServer(config) {
|
|
|
4157
4561
|
searchService,
|
|
4158
4562
|
dashboardOverviewService,
|
|
4159
4563
|
projectRecoveryService,
|
|
4564
|
+
notificationService,
|
|
4565
|
+
customSoundService,
|
|
4160
4566
|
hookRuntime,
|
|
4161
4567
|
watcher,
|
|
4162
4568
|
createContext,
|
|
@@ -4181,7 +4587,7 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
4181
4587
|
});
|
|
4182
4588
|
const ptyManager = new PtyManager(config.projectDir);
|
|
4183
4589
|
const ptyWss = new WebSocketServer({ noServer: true });
|
|
4184
|
-
const ptyHandler = createPtyWebSocketHandler(ptyManager);
|
|
4590
|
+
const ptyHandler = createPtyWebSocketHandler(ptyManager, server.notificationService);
|
|
4185
4591
|
ptyWss.on("connection", ptyHandler);
|
|
4186
4592
|
httpServer.on("upgrade", (...args) => {
|
|
4187
4593
|
const [request, socket, head] = args;
|