ompchamber 1.1.0 → 1.2.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/client/index.html +1 -1
- package/dist/client/static/css/index.b938f4a693.css +2 -0
- package/dist/client/static/js/77.8022a800b5.js +139 -0
- package/dist/client/static/js/async/{1k.5974df3b87.js → 1k.a411fe5b43.js} +2 -2
- package/dist/client/static/js/async/{3y.71f2ccf3b6.js → 3y.3186ecdd63.js} +5 -5
- package/dist/client/static/js/async/5b.d88304bc13.js +1 -0
- package/dist/client/static/js/async/66.86544ad741.js +93 -0
- package/dist/client/static/js/async/7g.db82aca7ba.js +1 -0
- package/dist/client/static/js/async/7l.fadbbc0583.js +1 -0
- package/dist/client/static/js/async/{dc.856fda6560.js → ar.ad5c8822a8.js} +2 -2
- package/dist/client/static/js/async/cx.9c8268d542.js +226 -0
- package/dist/client/static/js/async/e.6443e739a2.js +79 -0
- package/dist/client/static/js/async/f7.46971e9d6d.js +2 -0
- package/dist/client/static/js/async/g.ee41bfed7f.js +2 -0
- package/dist/client/static/js/async/h1.d7091cef78.js +1 -0
- package/dist/client/static/js/async/hs.e7c59d8090.js +41 -0
- package/dist/client/static/js/async/jl.4911fa3383.js +1 -0
- package/dist/client/static/js/async/{k.7466330d1a.js → k.d16cd09ae7.js} +2 -2
- package/dist/client/static/js/async/{m.c038c71b9f.js → m.2e1ea3db9e.js} +2 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js +2 -0
- package/dist/client/static/js/async/mt.853be2b5aa.js +1 -0
- package/dist/client/static/js/async/{oc.9c060c9779.js → oc.eb22093f05.js} +1 -1
- package/dist/client/static/js/async/{og.634fba99ac.js → og.53f068804c.js} +1 -1
- package/dist/client/static/js/async/{5b.a5ccbc19dc.js → q.b453193538.js} +10 -10
- package/dist/client/static/js/async/q5.88b50ffa95.js +286 -0
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +21 -0
- package/dist/client/static/js/async/{sy.d16be843a7.js → s.aad2ecebc6.js} +1 -1
- package/dist/client/static/js/async/{tr.e878658140.js → tr.2a0d2b7600.js} +1 -1
- package/dist/client/static/js/async/vt.20406a5604.js +1 -0
- package/dist/client/static/js/async/x1.fe8af17016.js +90 -0
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +149 -0
- package/dist/client/static/js/async/yx.af2936c368.js +10 -0
- package/dist/client/static/js/index.57b39ffa7f.js +34 -0
- package/package.json +1 -1
- package/src/client/components/common/file-icon/index.tsx +4 -1
- package/src/client/components/common/image-viewer/index.tsx +224 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +57 -43
- package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +31 -24
- package/src/client/components/settings/Modal.tsx +1 -0
- package/src/client/components/settings/categories/ChatSettings.tsx +6 -2
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +99 -0
- package/src/client/components/workspace/btw-panel/AskCard.tsx +155 -0
- package/src/client/components/workspace/btw-panel/Composer.tsx +181 -0
- package/src/client/components/workspace/btw-panel/Messages.tsx +100 -0
- package/src/client/components/workspace/btw-panel/TopicMenu.tsx +106 -0
- package/src/client/components/workspace/btw-panel/attachments.ts +80 -0
- package/src/client/components/workspace/btw-panel/index.tsx +83 -0
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +37 -21
- package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
- package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +10 -6
- package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/chat-input/DropOverlay.tsx +17 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +44 -40
- package/src/client/components/workspace/chat-timeline/index.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +25 -7
- package/src/client/components/workspace/editor/Toolbar.tsx +32 -23
- package/src/client/components/workspace/editor/index.tsx +45 -36
- package/src/client/hooks/chat/btw/index.ts +180 -0
- package/src/client/hooks/chat/btw/intercept.ts +43 -0
- package/src/client/hooks/chat/btw/mode.ts +179 -0
- package/src/client/hooks/chat/composer/attachments.ts +198 -0
- package/src/client/hooks/chat/composer/drop-entries.ts +204 -0
- package/src/client/hooks/chat/composer/drop-payload.test.ts +97 -0
- package/src/client/hooks/chat/composer/drop-payload.ts +130 -0
- package/src/client/hooks/chat/composer/drop-references.ts +106 -0
- package/src/client/hooks/chat/composer/file-drop.test.ts +194 -0
- package/src/client/hooks/chat/composer/file-drop.ts +226 -0
- package/src/client/hooks/chat/composer/file-reads.test.ts +211 -0
- package/src/client/hooks/chat/composer/file-reads.ts +238 -0
- package/src/client/hooks/chat/composer/index.ts +7 -1
- package/src/client/hooks/chat/composer/pipeline.ts +181 -0
- package/src/client/hooks/chat/omp/index.ts +3 -3
- package/src/client/hooks/chat/omp/prompt-send.ts +5 -5
- package/src/client/hooks/chat/timeline/actions.ts +16 -1
- package/src/client/hooks/chat/timeline/index.ts +16 -8
- package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
- package/src/client/hooks/chat/timeline/send.ts +29 -65
- package/src/client/hooks/chat/timeline/session-load.ts +2 -1
- package/src/client/hooks/chat/timeline/session-pagination.ts +161 -14
- package/src/client/hooks/chat/timeline/user-turns.ts +149 -0
- package/src/client/hooks/editor/file-actions.ts +49 -0
- package/src/client/hooks/editor/use-file-editor.ts +38 -27
- package/src/client/hooks/editor/use-file-transfer.ts +78 -0
- package/src/client/tailwind.css +22 -0
- package/src/server/db.server.ts +21 -287
- package/src/server/lib/btw/frames.test.ts +68 -0
- package/src/server/lib/btw/frames.ts +56 -0
- package/src/server/lib/btw/prompt.ts +28 -0
- package/src/server/lib/btw/registry.server.ts +141 -0
- package/src/server/lib/btw/runtime.server.ts +324 -0
- package/src/server/lib/btw/schema.server.ts +44 -0
- package/src/server/lib/btw/service.server.ts +171 -0
- package/src/server/lib/btw/session-copy.server.ts +213 -0
- package/src/server/lib/btw/session-copy.test.ts +166 -0
- package/src/server/lib/btw/store.server.ts +240 -0
- package/src/server/lib/db/schema.ts +110 -0
- package/src/server/lib/db/seed.ts +160 -0
- package/src/server/lib/db/workspace-sync.ts +70 -0
- package/src/server/lib/fs/root.ts +37 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +207 -0
- package/src/server/lib/omp/rpc/manager.ts +27 -123
- package/src/server/lib/omp/session/auto-title.server.test.ts +137 -0
- package/src/server/lib/omp/session/auto-title.server.ts +152 -0
- package/src/server/lib/omp/session/blobs.server.test.ts +100 -0
- package/src/server/lib/omp/session/blobs.server.ts +76 -0
- package/src/server/lib/omp/session/file-scan.ts +174 -0
- package/src/server/lib/omp/session/files.test.ts +147 -0
- package/src/server/lib/omp/session/files.ts +52 -35
- package/src/server/lib/omp/session/locator.ts +2 -2
- package/src/server/lib/omp/session/merge-stored.test.ts +118 -0
- package/src/server/lib/omp/session/merge-stored.ts +171 -0
- package/src/server/lib/omp/session/messages.ts +34 -0
- package/src/server/lib/omp/session/reader.ts +2 -2
- package/src/server/lib/omp/session/subagent-presence.ts +9 -6
- package/src/server/lib/queue/delivery.server.test.ts +57 -0
- package/src/server/lib/queue/delivery.server.ts +25 -3
- package/src/server/plugins/ssr.ts +29 -2
- package/src/server/routes/btw/events.ts +70 -0
- package/src/server/routes/btw/index.ts +8 -0
- package/src/server/routes/btw/topics.ts +87 -0
- package/src/server/routes/btw/ws.ts +136 -0
- package/src/server/routes/chat/index.ts +2 -0
- package/src/server/routes/chat/session.ts +1 -115
- package/src/server/routes/chat/turns.ts +87 -0
- package/src/server/routes/fs/index.ts +4 -1
- package/src/server/routes/fs/raw.ts +85 -0
- package/src/server/routes/fs/read.ts +58 -3
- package/src/server/routes/index.ts +7 -1
- package/src/shared/lib/chat/attachments.test.ts +208 -0
- package/src/shared/lib/chat/attachments.ts +200 -15
- package/src/shared/lib/chat/btw/client.ts +94 -0
- package/src/shared/lib/chat/composer/client.test.ts +48 -0
- package/src/shared/lib/chat/composer/client.ts +6 -1
- package/src/shared/lib/chat/composer/composer.test.ts +39 -1
- package/src/shared/lib/chat/composer/trigger.ts +45 -1
- package/src/shared/lib/chat/omp/agent-events.ts +33 -80
- package/src/shared/lib/chat/omp/socket.ts +11 -6
- package/src/shared/lib/chat/omp/sse.ts +11 -6
- package/src/shared/lib/chat/omp/tool-results.ts +102 -0
- package/src/shared/lib/chat/omp/transport.ts +19 -3
- package/src/shared/lib/chat/text-file.ts +148 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -32
- package/src/shared/lib/chat/timeline/stream-coalescer.ts +66 -0
- package/src/shared/lib/chat/timeline/turns.ts +7 -0
- package/src/shared/lib/fs/file-kind.ts +50 -0
- package/src/shared/lib/fs/paths.ts +19 -1
- package/src/shared/lib/markdown/math.test.ts +20 -3
- package/src/shared/lib/omp/session/inlined-attachments.test.ts +82 -0
- package/src/shared/lib/omp/session/mapper.ts +24 -4
- package/src/shared/lib/omp/session/messages-map.ts +16 -3
- package/src/shared/lib/omp/session/messages-parse.ts +2 -2
- package/src/shared/lib/omp/session/parse-message-blocks.ts +85 -5
- package/src/shared/types/btw.ts +65 -0
- package/src/shared/types/chat.ts +39 -1
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/omp/agent.ts +14 -1
- package/src/shared/types/omp/session.ts +4 -1
- package/src/shared/types/settings/state.ts +5 -0
- package/dist/client/static/css/index.b859cc1cc5.css +0 -2
- package/dist/client/static/js/async/5bh.8b2363424f.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.7a6a377ecc.js +0 -1
- package/dist/client/static/js/async/cx.ca5feec570.js +0 -226
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/g.a06ca3b3e9.js +0 -2
- package/dist/client/static/js/async/h1.e8d5aa8e80.js +0 -1
- package/dist/client/static/js/async/hs.6d480d0ded.js +0 -41
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/nam.b5663e919f.js +0 -10
- package/dist/client/static/js/async/q.23af2a358f.js +0 -21
- package/dist/client/static/js/async/q5.e683f7c26e.js +0 -286
- package/dist/client/static/js/async/s.222ee974f6.js +0 -2
- package/dist/client/static/js/async/uf.46ad6209fa.js +0 -2
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/index.6e065017eb.js +0 -33
- package/dist/client/static/js/jp.50817896f3.js +0 -139
- /package/dist/client/static/js/{jp.50817896f3.js.LICENSE.txt → 77.8022a800b5.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.5974df3b87.js.LICENSE.txt → 1k.a411fe5b43.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.71f2ccf3b6.js.LICENSE.txt → 3y.3186ecdd63.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{dc.856fda6560.js.LICENSE.txt → ar.ad5c8822a8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.c038c71b9f.js.LICENSE.txt → f7.46971e9d6d.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{5b.a5ccbc19dc.js.LICENSE.txt → g.ee41bfed7f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.23af2a358f.js.LICENSE.txt → m.2e1ea3db9e.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.222ee974f6.js.LICENSE.txt → m3.d2b05f9604.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.a06ca3b3e9.js.LICENSE.txt → q.b453193538.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{uf.46ad6209fa.js.LICENSE.txt → q8.7f1b48e9cd.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{nam.b5663e919f.js.LICENSE.txt → yx.af2936c368.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.6e065017eb.js.LICENSE.txt → index.57b39ffa7f.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WebSocket transport for a session's BTW frame stream — the default sibling
|
|
8
|
+
* of `GET /api/btw/:sessionId/events`, mirroring the agent stream's protocol:
|
|
9
|
+
* one duplex socket per session, bare JSON frames, `connected` first, a
|
|
10
|
+
* protocol-level ping/pong keepalive, and the same latest-wins coalescer.
|
|
11
|
+
*
|
|
12
|
+
* A client that attaches replays nothing: the socket's first `btw_state` is
|
|
13
|
+
* the session's current history, so a reload lands on the same panel a live
|
|
14
|
+
* stream would have produced.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Elysia, t } from 'elysia';
|
|
18
|
+
import { createEventCoalescer, type EventCoalescer } from '@/server/lib/sse';
|
|
19
|
+
import { STREAM_HEARTBEAT_MS } from '@/shared/lib/workspace/refresh-cadence';
|
|
20
|
+
import { btwStateFor, subscribeBtw } from '@/server/lib/btw/registry.server';
|
|
21
|
+
import type { BtwFrame } from '@/shared/types';
|
|
22
|
+
|
|
23
|
+
/** Coalesce frames once this many bytes sit unsent. */
|
|
24
|
+
const HIGH_WATER_BYTES = 256 * 1024;
|
|
25
|
+
|
|
26
|
+
/** Longest a coalesced frame may wait before it is flushed anyway. */
|
|
27
|
+
const FLUSH_DELAY_MS = 50;
|
|
28
|
+
|
|
29
|
+
interface ConnectionState {
|
|
30
|
+
closed: boolean;
|
|
31
|
+
unsubscribe: (() => void) | null;
|
|
32
|
+
heartbeat: ReturnType<typeof setInterval> | null;
|
|
33
|
+
awaitingPong: boolean;
|
|
34
|
+
coalescer: EventCoalescer | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const connections = new WeakMap<object, ConnectionState>();
|
|
38
|
+
|
|
39
|
+
export const btwWsRoutes = new Elysia({ prefix: '/api/btw' }).ws('/:sessionId/ws', {
|
|
40
|
+
params: t.Object({ sessionId: t.String() }),
|
|
41
|
+
|
|
42
|
+
open(ws) {
|
|
43
|
+
const sessionId = ws.data.params.sessionId;
|
|
44
|
+
const state: ConnectionState = { closed: false, unsubscribe: null, heartbeat: null, awaitingPong: false, coalescer: null };
|
|
45
|
+
connections.set(ws, state);
|
|
46
|
+
// Unchecked: Elysia exposes the server socket untyped. Both members are
|
|
47
|
+
// optional, so an absent one degrades (no byte count / no keepalive)
|
|
48
|
+
// instead of failing.
|
|
49
|
+
const raw = ws.raw as { bufferedAmount?: number; ping?: () => void };
|
|
50
|
+
|
|
51
|
+
const teardown = () => {
|
|
52
|
+
if (state.closed) return;
|
|
53
|
+
state.closed = true;
|
|
54
|
+
state.coalescer?.dispose();
|
|
55
|
+
state.coalescer = null;
|
|
56
|
+
if (state.heartbeat !== null) {
|
|
57
|
+
clearInterval(state.heartbeat);
|
|
58
|
+
state.heartbeat = null;
|
|
59
|
+
}
|
|
60
|
+
if (state.unsubscribe) {
|
|
61
|
+
try {
|
|
62
|
+
state.unsubscribe();
|
|
63
|
+
} catch {
|
|
64
|
+
// Subscriber cleanup is best-effort.
|
|
65
|
+
}
|
|
66
|
+
state.unsubscribe = null;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
ws.close();
|
|
70
|
+
} catch {
|
|
71
|
+
// Already gone.
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
state.coalescer = createEventCoalescer({
|
|
76
|
+
send: (_event, data) => {
|
|
77
|
+
try {
|
|
78
|
+
ws.send(JSON.stringify(data));
|
|
79
|
+
} catch {
|
|
80
|
+
teardown();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
isBackpressured: () => (raw.bufferedAmount ?? 0) > HIGH_WATER_BYTES,
|
|
84
|
+
flushDelayMs: FLUSH_DELAY_MS,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
state.heartbeat = setInterval(() => {
|
|
88
|
+
if (state.closed) return;
|
|
89
|
+
if (state.awaitingPong) {
|
|
90
|
+
teardown();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
state.awaitingPong = true;
|
|
94
|
+
if (!state.coalescer?.flush()) return;
|
|
95
|
+
try {
|
|
96
|
+
raw.ping?.();
|
|
97
|
+
} catch {
|
|
98
|
+
teardown();
|
|
99
|
+
}
|
|
100
|
+
}, STREAM_HEARTBEAT_MS);
|
|
101
|
+
|
|
102
|
+
const push = (frame: BtwFrame) => state.coalescer?.push('', frame);
|
|
103
|
+
state.unsubscribe = subscribeBtw(sessionId, push);
|
|
104
|
+
push({ type: 'connected', sessionId });
|
|
105
|
+
void btwStateFor(sessionId)
|
|
106
|
+
.then((snapshot) => push({ type: 'btw_state', state: snapshot }))
|
|
107
|
+
.catch((error: unknown) => {
|
|
108
|
+
push({ type: 'btw_error', topicId: null, message: error instanceof Error ? error.message : String(error) });
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
pong(ws) {
|
|
113
|
+
const state = connections.get(ws);
|
|
114
|
+
if (state) state.awaitingPong = false;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
message() {
|
|
118
|
+
// Observer-only: commands go through POST /api/btw/:sessionId.
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
close(ws) {
|
|
122
|
+
const state = connections.get(ws);
|
|
123
|
+
if (!state || state.closed) return;
|
|
124
|
+
state.closed = true;
|
|
125
|
+
state.coalescer?.dispose();
|
|
126
|
+
state.coalescer = null;
|
|
127
|
+
if (state.heartbeat !== null) clearInterval(state.heartbeat);
|
|
128
|
+
if (state.unsubscribe) {
|
|
129
|
+
try {
|
|
130
|
+
state.unsubscribe();
|
|
131
|
+
} catch {
|
|
132
|
+
// Subscriber cleanup is best-effort.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -3,10 +3,12 @@ import * as chatRoot from '@/server/routes/chat/root';
|
|
|
3
3
|
import * as chatSession from '@/server/routes/chat/session';
|
|
4
4
|
import * as chatRewind from '@/server/routes/chat/rewind';
|
|
5
5
|
import * as chatStream from '@/server/routes/chat/stream';
|
|
6
|
+
import * as chatTurns from '@/server/routes/chat/turns';
|
|
6
7
|
|
|
7
8
|
export const chatBindings: HandlerBinding[] = [
|
|
8
9
|
...bindingsFor(chatRoot, '/api/chat'),
|
|
9
10
|
...bindingsFor(chatSession, '/api/chat/:sessionId'),
|
|
10
11
|
...bindingsFor(chatRewind, '/api/chat/:sessionId/rewind'),
|
|
11
12
|
...bindingsFor(chatStream, '/api/chat/stream'),
|
|
13
|
+
...bindingsFor(chatTurns, '/api/chat/:sessionId/turns'),
|
|
12
14
|
];
|
|
@@ -6,6 +6,7 @@ import { getSessionData } from '@/client/data/mock/chat';
|
|
|
6
6
|
import { isMockMode } from '@/server/mock.server';
|
|
7
7
|
import { findSessionFileById } from '@/server/lib/omp/session/locator';
|
|
8
8
|
import { loadSessionMessages, loadSessionModel, loadSessionThinkingLevel, loadSessionTitle } from '@/server/lib/omp/session/messages';
|
|
9
|
+
import { mergeOmpAttachments } from '@/server/lib/omp/session/merge-stored';
|
|
9
10
|
import { readRawHeaderLine } from '@/server/lib/omp/session/files';
|
|
10
11
|
import { formatNewSessionTitle } from '@/shared/lib/omp/session/default-title';
|
|
11
12
|
|
|
@@ -15,121 +16,6 @@ import { formatNewSessionTitle } from '@/shared/lib/omp/session/default-title';
|
|
|
15
16
|
* payload (and the mobile parse+mount cost) bounded. */
|
|
16
17
|
const DEFAULT_MESSAGE_WINDOW = 120;
|
|
17
18
|
|
|
18
|
-
interface StoredAttachment {
|
|
19
|
-
name?: string;
|
|
20
|
-
preview?: string;
|
|
21
|
-
type?: string;
|
|
22
|
-
size?: number;
|
|
23
|
-
content?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface StoredMessage {
|
|
27
|
-
id?: string;
|
|
28
|
-
role: string;
|
|
29
|
-
content: string;
|
|
30
|
-
date?: string;
|
|
31
|
-
timestamp?: string;
|
|
32
|
-
attachments?: StoredAttachment[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isRawComposerInput(content: string): boolean {
|
|
36
|
-
return /^\s*\//.test(content) || /(^|\s)@[A-Za-z0-9_-]+/.test(content);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Whether a JSONL-derived user turn (a) and a stored one (b) are the same
|
|
40
|
-
* request despite omp rewriting the delivered prompt. */
|
|
41
|
-
function userTurnsRelate(a: string, b: string): boolean {
|
|
42
|
-
if (a === b || a.startsWith(b) || b.startsWith(a)) return true;
|
|
43
|
-
// `@agent` is delivered to omp as a task-tool delegation prompt.
|
|
44
|
-
if (a.startsWith('Use the task tool to delegate this request') && /(^|\s)@[A-Za-z0-9_-]+/.test(b)) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
// `/skill:<name> <args>`: the JSONL only records a synthesized `/skill:<name>`.
|
|
48
|
-
const token = a.split(' ')[0];
|
|
49
|
-
return a.startsWith('/skill:') && Boolean(token) && b.startsWith(token);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Merge user turns and attachments from the chamber DB copy onto the
|
|
53
|
-
* JSONL-loaded messages. omp's JSONL rewrites raw composer input (`@agent` →
|
|
54
|
-
* task-tool prompt, `/skill:a b` → `/skill:a`) and writes no user entry at all
|
|
55
|
-
* for `/command` turns, so the raw DB copy restores both the displayed text
|
|
56
|
-
* and the missing bubbles. */
|
|
57
|
-
function mergeOmpAttachments(
|
|
58
|
-
messages: StoredMessage[],
|
|
59
|
-
storedMessagesJson: string | undefined,
|
|
60
|
-
): StoredMessage[] {
|
|
61
|
-
if (!storedMessagesJson) return messages;
|
|
62
|
-
let stored: StoredMessage[] = [];
|
|
63
|
-
try {
|
|
64
|
-
stored = JSON.parse(storedMessagesJson);
|
|
65
|
-
} catch {
|
|
66
|
-
return messages;
|
|
67
|
-
}
|
|
68
|
-
if (!Array.isArray(stored)) return messages;
|
|
69
|
-
const storedUsers = stored.filter((m) => m?.role === 'user' && typeof m.content === 'string');
|
|
70
|
-
|
|
71
|
-
const jsonlIndex = new Map<string, number>();
|
|
72
|
-
messages.forEach((m, index) => {
|
|
73
|
-
if (m.id) jsonlIndex.set(m.id, index);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const used = new Set<number>();
|
|
77
|
-
const merged = messages.map((m) => {
|
|
78
|
-
if (m.role !== 'user') return m;
|
|
79
|
-
const storedIndex = storedUsers.findIndex((s, i) => !used.has(i) && userTurnsRelate(m.content, s.content));
|
|
80
|
-
if (storedIndex === -1) return m;
|
|
81
|
-
used.add(storedIndex);
|
|
82
|
-
const storedMsg = storedUsers[storedIndex];
|
|
83
|
-
const content = storedMsg.content !== m.content && isRawComposerInput(storedMsg.content)
|
|
84
|
-
? storedMsg.content
|
|
85
|
-
: m.content;
|
|
86
|
-
const attachments = m.attachments?.length ? m.attachments : storedMsg.attachments;
|
|
87
|
-
const next = { ...m, content };
|
|
88
|
-
return attachments?.length ? { ...next, attachments } : next;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Unmatched raw turns are anchored before the next stored message the JSONL
|
|
92
|
-
// still carries; splice highest-anchor-first so earlier indices stay valid.
|
|
93
|
-
const insertions = new Map<number, StoredMessage[]>();
|
|
94
|
-
storedUsers.forEach((storedMsg, userIndex) => {
|
|
95
|
-
if (used.has(userIndex) || !isRawComposerInput(storedMsg.content)) return;
|
|
96
|
-
if (storedMsg.id && jsonlIndex.has(storedMsg.id)) return;
|
|
97
|
-
let anchor = merged.length;
|
|
98
|
-
for (let i = stored.indexOf(storedMsg) + 1; i < stored.length; i += 1) {
|
|
99
|
-
const id = stored[i]?.id;
|
|
100
|
-
const found = typeof id === 'string' ? jsonlIndex.get(id) : undefined;
|
|
101
|
-
if (found !== undefined) {
|
|
102
|
-
anchor = found;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const group = insertions.get(anchor);
|
|
107
|
-
if (group) group.push(storedMsg);
|
|
108
|
-
else insertions.set(anchor, [storedMsg]);
|
|
109
|
-
});
|
|
110
|
-
for (const anchor of [...insertions.keys()].sort((a, b) => b - a)) {
|
|
111
|
-
const group = insertions.get(anchor);
|
|
112
|
-
if (group) merged.splice(anchor, 0, ...group);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const byContent = new Map<string, StoredAttachment[]>();
|
|
116
|
-
for (const m of stored) {
|
|
117
|
-
if (m?.role === 'user' && Array.isArray(m.attachments) && m.attachments.length > 0) {
|
|
118
|
-
byContent.set(m.content, m.attachments);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return merged.map((m) => {
|
|
122
|
-
if (m.role !== 'user' || m.attachments?.length) return m;
|
|
123
|
-
// The JSONL content may carry inlined text-file blocks appended to the
|
|
124
|
-
// original prompt, so match by prefix instead of exact equality.
|
|
125
|
-
const atts = byContent.get(m.content)
|
|
126
|
-
?? [...byContent.entries()].find(([storedContent]) =>
|
|
127
|
-
m.content.startsWith(storedContent) || storedContent.startsWith(m.content)
|
|
128
|
-
)?.[1];
|
|
129
|
-
return atts ? { ...m, attachments: atts } : m;
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
19
|
/** Slice a fully loaded message list into the requested tail window. */
|
|
134
20
|
function windowMessages<T>(
|
|
135
21
|
messages: T[],
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Full-history user-turn index for the timeline jump rail.
|
|
8
|
+
*
|
|
9
|
+
* The timeline only mounts a window of a long session (the newest 120 messages
|
|
10
|
+
* plus whatever "Load earlier messages" pulled in), so a rail built from the
|
|
11
|
+
* mounted rows shows a fraction of the conversation and cannot reach a turn
|
|
12
|
+
* that was never paged in. This endpoint answers the same id/position question
|
|
13
|
+
* the timeline does — the JSONL messages with the chamber DB overlay merged —
|
|
14
|
+
* but for the WHOLE session, and strips each row down to what a tick needs.
|
|
15
|
+
*
|
|
16
|
+
* Positions are indices into that full merged list, which is exactly the list
|
|
17
|
+
* `GET /api/chat/:sessionId` pages through with `?before=`: a rail click pages
|
|
18
|
+
* windows from that index until the target row is mounted, so the cursor the
|
|
19
|
+
* rail carries is the same coordinate space the loader uses.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { json } from '@/server/lib/remix-compat';
|
|
23
|
+
import type { LoaderFunctionArgs } from '@/server/lib/remix-compat';
|
|
24
|
+
import { getDb } from '@/server/db.server';
|
|
25
|
+
import { getSessionData } from '@/client/data/mock/chat';
|
|
26
|
+
import { isMockMode } from '@/server/mock.server';
|
|
27
|
+
import { findSessionFileById } from '@/server/lib/omp/session/locator';
|
|
28
|
+
import { loadSessionMessages } from '@/server/lib/omp/session/messages';
|
|
29
|
+
import { mergeOmpAttachments } from '@/server/lib/omp/session/merge-stored';
|
|
30
|
+
import { TURN_PREVIEW_CHARS } from '@/shared/lib/chat/timeline/turns';
|
|
31
|
+
import type { UserTurnRef } from '@/shared/types/chat';
|
|
32
|
+
|
|
33
|
+
/** One rail entry per user row: its id (the DOM jump target), its position in
|
|
34
|
+
* the full list (the paging cursor), and enough text for the tooltip. */
|
|
35
|
+
function toUserTurns(messages: Array<{ id?: string; role?: string; content?: string; date?: string; timestamp?: string }>): UserTurnRef[] {
|
|
36
|
+
const turns: UserTurnRef[] = [];
|
|
37
|
+
messages.forEach((message, index) => {
|
|
38
|
+
if (message?.role !== 'user') return;
|
|
39
|
+
const content = typeof message.content === 'string' ? message.content : '';
|
|
40
|
+
turns.push({
|
|
41
|
+
id: typeof message.id === 'string' && message.id ? message.id : `turn-${index}`,
|
|
42
|
+
index,
|
|
43
|
+
preview: content.slice(0, TURN_PREVIEW_CHARS),
|
|
44
|
+
date: message.date,
|
|
45
|
+
timestamp: message.timestamp,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return turns;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function loader({ params }: LoaderFunctionArgs) {
|
|
52
|
+
const { sessionId } = params;
|
|
53
|
+
if (!sessionId) return json({ error: 'Session ID is required' }, { status: 400 });
|
|
54
|
+
|
|
55
|
+
const mock = isMockMode();
|
|
56
|
+
try {
|
|
57
|
+
if (!mock) {
|
|
58
|
+
const filePath = await findSessionFileById(sessionId);
|
|
59
|
+
if (filePath) {
|
|
60
|
+
const messages = await loadSessionMessages(filePath);
|
|
61
|
+
const db = await getDb();
|
|
62
|
+
const overlay = await db.get('SELECT messages FROM chat_sessions WHERE session_id = ?', [sessionId]);
|
|
63
|
+
const overlaid = mergeOmpAttachments(messages, overlay?.messages);
|
|
64
|
+
return json({ turns: toUserTurns(overlaid), total: overlaid.length, source: 'omp-jsonl' });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Chamber-created sessions (mock or synthetic ids) answer from the DB copy,
|
|
69
|
+
// mirroring the timeline loader's fallback.
|
|
70
|
+
const db = await getDb();
|
|
71
|
+
const existing = await db.get('SELECT messages FROM chat_sessions WHERE session_id = ?', [sessionId]);
|
|
72
|
+
let messages: unknown[] = [];
|
|
73
|
+
if (existing?.messages) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed: unknown = JSON.parse(existing.messages);
|
|
76
|
+
if (Array.isArray(parsed)) messages = parsed;
|
|
77
|
+
} catch {
|
|
78
|
+
messages = [];
|
|
79
|
+
}
|
|
80
|
+
} else if (mock) {
|
|
81
|
+
messages = getSessionData(sessionId)?.messages ?? [];
|
|
82
|
+
}
|
|
83
|
+
return json({ turns: toUserTurns(messages as Array<{ role?: string; content?: string }>), total: messages.length, source: mock ? 'mock' : 'db' });
|
|
84
|
+
} catch (error: unknown) {
|
|
85
|
+
return json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bindingsFor, type HandlerBinding } from '@/server/lib/route-adapter';
|
|
2
|
-
import { browseDirectories, listDirectory, listFiles, readFile } from '@/server/routes/fs/read';
|
|
2
|
+
import { browseDirectories, listDirectory, listFiles, readFile, readReferencedFile } from '@/server/routes/fs/read';
|
|
3
|
+
import { serveRawFile } from '@/server/routes/fs/raw';
|
|
3
4
|
import * as fsAction from '@/server/routes/fs/action';
|
|
4
5
|
import * as fsGit from '@/server/routes/fs/git';
|
|
5
6
|
import * as fsReplace from '@/server/routes/fs/replace';
|
|
@@ -16,4 +17,6 @@ export const fsBindings: HandlerBinding[] = [
|
|
|
16
17
|
{ method: 'GET', path: '/api/fs/dir', handler: listDirectory },
|
|
17
18
|
{ method: 'GET', path: '/api/fs/list', handler: listFiles },
|
|
18
19
|
{ method: 'GET', path: '/api/fs/read', handler: readFile },
|
|
20
|
+
{ method: 'GET', path: '/api/fs/read-reference', handler: readReferencedFile },
|
|
21
|
+
{ method: 'GET', path: '/api/fs/raw', handler: serveRawFile },
|
|
19
22
|
];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { json, type LoaderFunctionArgs } from '@/server/lib/remix-compat';
|
|
7
|
+
import { errorResponse } from '@/server/lib/route-adapter';
|
|
8
|
+
import { isMockMode } from '@/server/mock.server';
|
|
9
|
+
import { getDefaultFsRoot, resolveRoot, resolveWithinRoot } from '@/server/lib/fs/root';
|
|
10
|
+
import { getImageMimeType } from '@/shared/lib/fs/file-kind';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/fs/raw?path=<rel>&root=<abs>&repo=<rel> — the file's bytes, verbatim.
|
|
14
|
+
*
|
|
15
|
+
* Images reach the editor through this route instead of `/api/fs/read`, which
|
|
16
|
+
* answers with JSON and therefore cannot carry them: a PNG's bytes are not
|
|
17
|
+
* UTF-8, so `Bun.file().text()` returned replacement characters and the panel
|
|
18
|
+
* painted mojibake. The browser can only paint a picture from a real byte
|
|
19
|
+
* stream with a real `content-type`, which is exactly what this returns.
|
|
20
|
+
*
|
|
21
|
+
* Scope resolution is identical to `/api/fs/read` (same root allow-list, same
|
|
22
|
+
* `resolveWithinRoot` containment), so a path that reads as text can be served
|
|
23
|
+
* as bytes and vice versa.
|
|
24
|
+
*/
|
|
25
|
+
export async function serveRawFile({ request }: LoaderFunctionArgs) {
|
|
26
|
+
const url = new URL(request.url);
|
|
27
|
+
const filePath = url.searchParams.get('path');
|
|
28
|
+
|
|
29
|
+
if (!filePath) {
|
|
30
|
+
return json({ error: 'Missing path' }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let baseDir = await resolveRoot(url.searchParams.get('root'), await getDefaultFsRoot(isMockMode()));
|
|
34
|
+
|
|
35
|
+
const repo = url.searchParams.get('repo');
|
|
36
|
+
if (repo && repo !== '.') {
|
|
37
|
+
const repoDir = resolveWithinRoot(baseDir, repo);
|
|
38
|
+
if (repoDir) {
|
|
39
|
+
baseDir = repoDir;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cleanPath = filePath.replace(/^\/+/, '');
|
|
44
|
+
const fullPath = resolveWithinRoot(baseDir, cleanPath);
|
|
45
|
+
|
|
46
|
+
if (!fullPath) {
|
|
47
|
+
return json({ error: 'Invalid path' }, { status: 403 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const file = Bun.file(fullPath);
|
|
52
|
+
if (!(await file.exists())) {
|
|
53
|
+
return json({ error: 'File not found' }, { status: 404 });
|
|
54
|
+
}
|
|
55
|
+
const stat = await file.stat();
|
|
56
|
+
if (stat.isDirectory()) {
|
|
57
|
+
return json({ error: 'Cannot read a directory' }, { status: 400 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// `no-store` because a raw read is how a refreshed view sees a file that
|
|
61
|
+
// changed on disk under the same path; an immutable image URL would pin the
|
|
62
|
+
// old bytes for the life of the tab.
|
|
63
|
+
return new Response(file.stream(), {
|
|
64
|
+
headers: {
|
|
65
|
+
'content-type': getImageMimeType(cleanPath) ?? 'application/octet-stream',
|
|
66
|
+
'content-length': String(stat.size),
|
|
67
|
+
'content-disposition': `inline; filename="${safeFileName(cleanPath)}"`,
|
|
68
|
+
'cache-control': 'no-store',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return errorResponse(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The filename for `content-disposition`. Only ASCII-printable characters
|
|
78
|
+
* survive: the header is latin-1, and a non-ASCII byte (or a quote) in a
|
|
79
|
+
* filename would either corrupt the header or break out of the quoted string.
|
|
80
|
+
*/
|
|
81
|
+
function safeFileName(cleanPath: string): string {
|
|
82
|
+
const base = cleanPath.split('/').pop() ?? 'file';
|
|
83
|
+
const ascii = base.replace(/[^\x20-\x7e]/g, '_').replace(/["\\]/g, '_');
|
|
84
|
+
return ascii || 'file';
|
|
85
|
+
}
|
|
@@ -10,8 +10,9 @@ import { dirname, isAbsolute, join, resolve } from 'path';
|
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { isMockMode } from '@/server/mock.server';
|
|
13
|
-
import { getDefaultFsRoot, resolveRoot, resolveWithinRoot } from '@/server/lib/fs/root';
|
|
13
|
+
import { getDefaultFsRoot, resolveReferencedPath, resolveRoot, resolveWithinRoot } from '@/server/lib/fs/root';
|
|
14
14
|
import { collectIgnoredPaths } from '@/server/lib/fs/git-ignore';
|
|
15
|
+
import { getImageMimeType } from '@/shared/lib/fs/file-kind';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* GET /api/fs/browse?path=<abs> — list subdirectories of a folder for the
|
|
@@ -230,7 +231,6 @@ export async function readFile({ request }: LoaderFunctionArgs) {
|
|
|
230
231
|
if (!filePath) {
|
|
231
232
|
return json({ error: 'Missing path' }, { status: 400 });
|
|
232
233
|
}
|
|
233
|
-
|
|
234
234
|
let baseDir = await resolveRoot(url.searchParams.get('root'), await getDefaultFsRoot(isMockMode()));
|
|
235
235
|
|
|
236
236
|
// If repo is specified (e.g. nested git project), resolve inside the repo
|
|
@@ -254,13 +254,68 @@ export async function readFile({ request }: LoaderFunctionArgs) {
|
|
|
254
254
|
if (!(await file.exists())) {
|
|
255
255
|
return json({ error: 'File not found' }, { status: 404 });
|
|
256
256
|
}
|
|
257
|
-
|
|
257
|
+
const stat = await file.stat();
|
|
258
|
+
if (stat.isDirectory()) {
|
|
258
259
|
return json({ error: 'Cannot read a directory' }, { status: 400 });
|
|
259
260
|
}
|
|
260
261
|
|
|
262
|
+
// An image has no text: decoding its bytes here returned replacement
|
|
263
|
+
// characters, which every reader painted as mojibake. Every in-repo reader
|
|
264
|
+
// branches on the extension first and takes `/api/fs/raw`, so this answers
|
|
265
|
+
// the remaining callers with the real reason instead of garbage.
|
|
266
|
+
if (getImageMimeType(cleanPath)) {
|
|
267
|
+
return json({ error: 'Image files are served by /api/fs/raw' }, { status: 400 });
|
|
268
|
+
}
|
|
269
|
+
|
|
261
270
|
const content = await file.text();
|
|
262
271
|
return json({ content });
|
|
263
272
|
} catch (error) {
|
|
264
273
|
return errorResponse(error);
|
|
265
274
|
}
|
|
266
275
|
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* GET /api/fs/read-reference?path=<abs path or URL> — read a file a drop or a
|
|
279
|
+
* link pointed at.
|
|
280
|
+
*
|
|
281
|
+
* A download dragged out of a web page (WhatsApp Web, Gmail, Slack) carries no
|
|
282
|
+
* bytes, only a reference, and the page cannot fetch a `file://` path itself.
|
|
283
|
+
* The path is therefore resolved against the same allow-list that governs a
|
|
284
|
+
* client-supplied `root` — the app root, a path beneath it, or a registered
|
|
285
|
+
* workspace — so this cannot be used to read an arbitrary file.
|
|
286
|
+
*
|
|
287
|
+
* Text only: an image or binary has no string form to return, and the drop's
|
|
288
|
+
* own `File` is the path that carries those.
|
|
289
|
+
*/
|
|
290
|
+
export async function readReferencedFile({ request }: LoaderFunctionArgs) {
|
|
291
|
+
const url = new URL(request.url);
|
|
292
|
+
const reference = url.searchParams.get('path');
|
|
293
|
+
if (!reference) {
|
|
294
|
+
return json({ error: 'Missing path' }, { status: 400 });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// A remote reference is not fetched here: the chamber has no policy for
|
|
298
|
+
// outbound requests and a drag from a page is not a reason to add one.
|
|
299
|
+
if (/^https?:\/\//i.test(reference)) {
|
|
300
|
+
return json({ error: 'Remote references are not supported' }, { status: 400 });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const filePath = await resolveReferencedPath(reference.replace(/^file:\/\//, ''));
|
|
304
|
+
if (!filePath) {
|
|
305
|
+
return json({ error: 'File not found' }, { status: 404 });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const file = Bun.file(filePath);
|
|
310
|
+
const stat = await file.stat();
|
|
311
|
+
if (stat.isDirectory()) {
|
|
312
|
+
return json({ error: 'Cannot read a directory' }, { status: 400 });
|
|
313
|
+
}
|
|
314
|
+
if (getImageMimeType(filePath)) {
|
|
315
|
+
return json({ error: 'Image files are served by /api/fs/raw' }, { status: 400 });
|
|
316
|
+
}
|
|
317
|
+
return json({ content: await file.text(), name: path.basename(filePath) });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return errorResponse(error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -2,6 +2,8 @@ import { Elysia } from 'elysia';
|
|
|
2
2
|
import { mountBindings, type HandlerBinding } from '@/server/lib/route-adapter';
|
|
3
3
|
import { agentBindings } from '@/server/routes/agent';
|
|
4
4
|
import { agentWsRoutes } from '@/server/routes/agent/ws';
|
|
5
|
+
import { btwBindings } from '@/server/routes/btw';
|
|
6
|
+
import { btwWsRoutes } from '@/server/routes/btw/ws';
|
|
5
7
|
import { chatBindings } from '@/server/routes/chat';
|
|
6
8
|
import { sessionsBindings } from '@/server/routes/sessions';
|
|
7
9
|
import { settingsBindings } from '@/server/routes/settings';
|
|
@@ -20,6 +22,7 @@ import { wellKnownBindings } from '@/server/routes/well-known';
|
|
|
20
22
|
|
|
21
23
|
const allBindings: HandlerBinding[] = [
|
|
22
24
|
...agentBindings,
|
|
25
|
+
...btwBindings,
|
|
23
26
|
...chatBindings,
|
|
24
27
|
...sessionsBindings,
|
|
25
28
|
...settingsBindings,
|
|
@@ -36,4 +39,7 @@ const allBindings: HandlerBinding[] = [
|
|
|
36
39
|
...wellKnownBindings,
|
|
37
40
|
];
|
|
38
41
|
|
|
39
|
-
export const apiRoutes = mountBindings(new Elysia(), allBindings)
|
|
42
|
+
export const apiRoutes = mountBindings(new Elysia(), allBindings)
|
|
43
|
+
.use(agentWsRoutes)
|
|
44
|
+
.use(btwWsRoutes)
|
|
45
|
+
.use(terminalWsRoutes);
|