ompchamber 1.0.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.0d6ae50fe8.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/ComposerPicker.tsx +16 -4
- package/src/client/components/common/ComposerTextarea.tsx +1 -0
- 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/layout/RightActivityBar.tsx +1 -1
- package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
- package/src/client/components/layout/desktop-layout/index.tsx +25 -96
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
- package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
- package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +26 -12
- package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
- 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 +39 -20
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
- 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 +67 -43
- package/src/client/components/workspace/chat-timeline/index.tsx +14 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
- 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/components/workspace/model-dropdown/groups.ts +35 -2
- package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
- package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
- 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 +35 -9
- 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 +40 -4
- package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
- package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
- package/src/client/hooks/chat/timeline/index.ts +29 -9
- package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
- package/src/client/hooks/chat/timeline/send.ts +45 -70
- 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/hooks/workspace/available-width.ts +33 -0
- package/src/client/hooks/workspace/panel-widths.ts +34 -37
- 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/models/preferences.server.ts +110 -0
- package/src/server/lib/models/registry.server.ts +206 -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/rpc/utility.ts +7 -1
- 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 +260 -0
- package/src/server/lib/queue/delivery.server.ts +63 -11
- 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/server/routes/models/root.ts +88 -215
- package/src/server/routes/settings/commands.ts +59 -18
- 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 +23 -9
- package/src/shared/lib/chat/composer/commands.ts +290 -0
- package/src/shared/lib/chat/composer/composer.test.ts +195 -0
- package/src/shared/lib/chat/composer/filter.ts +21 -7
- package/src/shared/lib/chat/composer/trigger.ts +154 -21
- 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/models/preferences.test.ts +131 -0
- package/src/shared/lib/models/preferences.ts +88 -0
- package/src/shared/lib/models/provider-models.ts +8 -1
- 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/lib/omp/subagent/label.test.ts +94 -0
- package/src/shared/lib/omp/subagent/label.ts +84 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
- package/src/shared/lib/workspace/panel-widths.ts +161 -30
- package/src/shared/lib/workspace/right-panels.ts +50 -11
- package/src/shared/lib/workspace/session-state/store.ts +3 -0
- package/src/shared/types/btw.ts +65 -0
- package/src/shared/types/chat.ts +77 -66
- package/src/shared/types/index.ts +2 -0
- package/src/shared/types/models.ts +101 -0
- package/src/shared/types/omp/agent.ts +14 -1
- package/src/shared/types/omp/session.ts +4 -1
- package/src/shared/types/omp/subagent.ts +4 -0
- package/src/shared/types/settings/command.ts +13 -0
- package/src/shared/types/settings/state.ts +5 -0
- package/dist/client/static/css/index.e880517fcd.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.0de15c5e17.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.0d6ae50fe8.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.0de15c5e17.js.LICENSE.txt → index.57b39ffa7f.js.LICENSE.txt} +0 -0
|
@@ -1,34 +1,152 @@
|
|
|
1
|
-
import type { ComposerPickItem, ComposerTrigger } from '@/shared/types';
|
|
1
|
+
import type { CommandItem, ComposerMatchItem, ComposerPickItem, ComposerTrigger } from '@/shared/types';
|
|
2
2
|
|
|
3
3
|
/** Chars that may legally precede an `@` agent trigger (besides string start). */
|
|
4
4
|
const AT_PRECEDING_RE = /[\s([{]/;
|
|
5
5
|
|
|
6
|
+
/** oh-my-pi's `SKILL_NAMESPACE`: skills are addressed as `/skill:<name>`. */
|
|
7
|
+
export const SKILL_NAMESPACE = 'skill:';
|
|
8
|
+
|
|
9
|
+
/** The collapsed namespace row's token (no trailing space on accept). */
|
|
10
|
+
export const SKILL_NAMESPACE_TOKEN = `/${SKILL_NAMESPACE}`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Slash commands the chamber answers itself, reserved exactly like oh-my-pi
|
|
14
|
+
* reserves its own tokens.
|
|
15
|
+
*
|
|
16
|
+
* omp's `/btw` is TUI-only — its registry entry has `handleTui` and no
|
|
17
|
+
* text-mode `handle`, and `get_available_commands` only advertises entries the
|
|
18
|
+
* RPC dispatcher can run — so the composer would offer every command except the
|
|
19
|
+
* one it intercepts on send. The entry is byte-for-byte omp's own metadata
|
|
20
|
+
* (description and inline hint), so the popup reads the same in both clients.
|
|
21
|
+
*/
|
|
22
|
+
export const CHAMBER_COMMANDS: CommandItem[] = [
|
|
23
|
+
{
|
|
24
|
+
id: 'chamber-btw',
|
|
25
|
+
name: 'btw',
|
|
26
|
+
description: "Ask a side question, or browse this session's BTW history",
|
|
27
|
+
scope: 'system',
|
|
28
|
+
template: '',
|
|
29
|
+
isBuiltIn: true,
|
|
30
|
+
inputHint: '[question]',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/** True when the chamber intercepts this command instead of forwarding it to
|
|
35
|
+
* omp as chat text. */
|
|
36
|
+
function isChamberOwnedCommand(name: string): boolean {
|
|
37
|
+
const lower = name.toLowerCase();
|
|
38
|
+
return CHAMBER_COMMANDS.some((command) => command.name.toLowerCase() === lower);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Index of a `/` that starts the draft, ignoring leading whitespace —
|
|
43
|
+
* oh-my-pi's `findLeadingSlashCommandStart`.
|
|
44
|
+
*/
|
|
45
|
+
function leadingSlashStart(text: string): number | null {
|
|
46
|
+
const trimmed = text.trimStart();
|
|
47
|
+
if (!trimmed.startsWith('/')) return null;
|
|
48
|
+
return text.length - trimmed.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Index of a `/` that opens the trailing token (`/(?:^|\s)\/([^\s/]*)$`) —
|
|
53
|
+
* oh-my-pi's `findTrailingSlashCommandStart`.
|
|
54
|
+
*/
|
|
55
|
+
function trailingSlashStart(text: string): number | null {
|
|
56
|
+
const match = /(?:^|\s)\/([^\s/]*)$/.exec(text);
|
|
57
|
+
if (!match || match.index === undefined) return null;
|
|
58
|
+
const slashOffset = match[0].indexOf('/');
|
|
59
|
+
return match.index + slashOffset;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether prose precedes the slash token: any earlier non-blank line, or
|
|
64
|
+
* non-whitespace before it on this line — oh-my-pi's
|
|
65
|
+
* `hasPromptTextBeforeSlash`. Such a token is a mid-prompt skill lookup,
|
|
66
|
+
* which only ever surfaces skills.
|
|
67
|
+
*/
|
|
68
|
+
function hasPromptTextBeforeSlash(lines: readonly string[], line: number, before: string, slashStart: number): boolean {
|
|
69
|
+
for (let i = 0; i < line; i += 1) {
|
|
70
|
+
if ((lines[i] ?? '').trim() !== '') return true;
|
|
71
|
+
}
|
|
72
|
+
return before.slice(0, slashStart).trim() !== '';
|
|
73
|
+
}
|
|
74
|
+
|
|
6
75
|
/**
|
|
7
76
|
* Detect an `@`-mention (files + agents) or `/`-command trigger at the given
|
|
8
|
-
* caret position
|
|
77
|
+
* caret position, mirroring oh-my-pi's `CombinedAutocompleteProvider`:
|
|
78
|
+
*
|
|
79
|
+
* - `/` starts a command at the draft start (leading whitespace allowed) or as
|
|
80
|
+
* a trailing token after whitespace on the caret's line. A `/` in the middle
|
|
81
|
+
* of a token (`a/b`) never matches.
|
|
82
|
+
* - Once `<command> ` is typed, the popup switches to the `args` phase, which
|
|
83
|
+
* completes that command's declarative subcommands.
|
|
84
|
+
* - `@` is a trigger at index 0 or when preceded by whitespace, `(`, `[`, `{`.
|
|
9
85
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* index 0 or when preceded by whitespace, `(`, `[`, or `{`. The query is the
|
|
13
|
-
* run of non-whitespace characters between the trigger char and the caret, so
|
|
14
|
-
* `foo@bar` and `a/b` (query containing another trigger char) never match.
|
|
86
|
+
* `lines` is the whole draft split on newlines and `line` the caret's line
|
|
87
|
+
* index, because the mid-prompt rule inspects earlier lines.
|
|
15
88
|
*/
|
|
16
89
|
export function detectComposerTrigger(text: string, caret: number): ComposerTrigger | null {
|
|
17
90
|
const end = Math.max(0, Math.min(caret, text.length));
|
|
91
|
+
const before = text.slice(0, end);
|
|
92
|
+
// Mid-prompt detection needs the earlier lines, and every rule below is
|
|
93
|
+
// scoped to the caret's own line.
|
|
94
|
+
const lines = text.split('\n');
|
|
95
|
+
const line = (before.match(/\n/g) ?? []).length;
|
|
96
|
+
const lineStart = before.lastIndexOf('\n') + 1;
|
|
97
|
+
const beforeOnLine = before.slice(lineStart);
|
|
18
98
|
|
|
19
|
-
|
|
20
|
-
|
|
99
|
+
const leadStart = leadingSlashStart(beforeOnLine);
|
|
100
|
+
const trailStart = trailingSlashStart(beforeOnLine);
|
|
101
|
+
const midPrompt = trailStart !== null && hasPromptTextBeforeSlash(lines, line, beforeOnLine, trailStart);
|
|
102
|
+
const leadIsMidPrompt = leadStart !== null && hasPromptTextBeforeSlash(lines, line, beforeOnLine, leadStart);
|
|
21
103
|
|
|
22
|
-
|
|
104
|
+
const slashStart = midPrompt ? trailStart : leadIsMidPrompt ? null : leadStart;
|
|
23
105
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
106
|
+
if (slashStart !== null) {
|
|
107
|
+
const commandText = beforeOnLine.slice(slashStart);
|
|
108
|
+
const spaceIndex = commandText.indexOf(' ');
|
|
109
|
+
|
|
110
|
+
if (spaceIndex === -1) {
|
|
111
|
+
return {
|
|
112
|
+
kind: 'command',
|
|
113
|
+
query: commandText.slice(1),
|
|
114
|
+
start: lineStart + slashStart,
|
|
115
|
+
end,
|
|
116
|
+
phase: 'name',
|
|
117
|
+
replaceFrom: lineStart + slashStart,
|
|
118
|
+
...(midPrompt ? { midPrompt: true } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!midPrompt) {
|
|
123
|
+
// Argument phase: everything after `<command> ` completes that command's
|
|
124
|
+
// subcommands. oh-my-pi anchors the replacement on the argument text
|
|
125
|
+
// only, so earlier arguments survive acceptance.
|
|
126
|
+
const argStart = lineStart + slashStart + spaceIndex + 1;
|
|
127
|
+
return {
|
|
128
|
+
kind: 'command',
|
|
129
|
+
query: before.slice(argStart),
|
|
130
|
+
start: lineStart + slashStart,
|
|
131
|
+
end,
|
|
132
|
+
phase: 'args',
|
|
133
|
+
replaceFrom: argStart,
|
|
134
|
+
command: commandText.slice(1, spaceIndex),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// `@` mentions scan back to the token start; whitespace aborts the scan so
|
|
140
|
+
// `foo@bar` never triggers.
|
|
141
|
+
for (let i = beforeOnLine.length - 1; i >= 0; i--) {
|
|
142
|
+
const ch = beforeOnLine[i];
|
|
29
143
|
|
|
30
|
-
|
|
31
|
-
|
|
144
|
+
if (/\s/.test(ch)) return null;
|
|
145
|
+
|
|
146
|
+
if (ch === '@') {
|
|
147
|
+
if (i === 0 || AT_PRECEDING_RE.test(beforeOnLine[i - 1])) {
|
|
148
|
+
const at = lineStart + i;
|
|
149
|
+
return { kind: 'mention', query: before.slice(at + 1), start: at, end, phase: 'name', replaceFrom: at };
|
|
32
150
|
}
|
|
33
151
|
return null;
|
|
34
152
|
}
|
|
@@ -37,9 +155,24 @@ export function detectComposerTrigger(text: string, caret: number): ComposerTrig
|
|
|
37
155
|
return null;
|
|
38
156
|
}
|
|
39
157
|
|
|
40
|
-
/** Insertion text for an item: its token
|
|
158
|
+
/** Insertion text for an item: its token, plus a trailing space unless suppressed. */
|
|
41
159
|
export function tokenForItem(item: ComposerPickItem): string {
|
|
42
|
-
return `${item.token} `;
|
|
160
|
+
return item.insertWithoutSpace ? item.token : `${item.token} `;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Whether a popup accept should stand aside and let Enter mean "send".
|
|
165
|
+
*
|
|
166
|
+
* Only the chamber's own commands qualify: the composer intercepts those on
|
|
167
|
+
* send, so for `/btw` the Enter *is* the action and accept-on-Enter would
|
|
168
|
+
* swallow the very keystroke the user is aiming at. Every command omp owns
|
|
169
|
+
* keeps accepting, which is what opens its argument completions — including the
|
|
170
|
+
* ones that declare no subcommands and take free-form arguments.
|
|
171
|
+
*/
|
|
172
|
+
export function sendInsteadOfAccept(trigger: ComposerTrigger, item: ComposerMatchItem | undefined): boolean {
|
|
173
|
+
if (!item || trigger.kind !== 'command' || trigger.phase !== 'name') return false;
|
|
174
|
+
if (item.name.toLowerCase() !== trigger.query.toLowerCase()) return false;
|
|
175
|
+
return isChamberOwnedCommand(item.name);
|
|
43
176
|
}
|
|
44
177
|
|
|
45
178
|
/** Splice `token` over the trigger span in `value`, returning the new value and caret. */
|
|
@@ -48,6 +181,6 @@ export function insertToken(
|
|
|
48
181
|
trigger: ComposerTrigger,
|
|
49
182
|
token: string,
|
|
50
183
|
): { value: string; caret: number } {
|
|
51
|
-
const next = value.slice(0, trigger.
|
|
52
|
-
return { value: next, caret: trigger.
|
|
184
|
+
const next = value.slice(0, trigger.replaceFrom) + token + value.slice(trigger.end);
|
|
185
|
+
return { value: next, caret: trigger.replaceFrom + token.length };
|
|
53
186
|
}
|
|
@@ -20,40 +20,20 @@ import { normalizeThinkingLevel } from '@/shared/lib/models/thinking-levels';
|
|
|
20
20
|
import { PHASE_VERBS } from '@/shared/lib/chat/timeline/tool-phrases';
|
|
21
21
|
import { describeAssistantPhase, describeToolActivity } from '@/shared/lib/chat/timeline/tool-verbs';
|
|
22
22
|
import { FILE_MUTATION_EVENT, isFileMutatingTool } from '@/shared/lib/chat/omp/file-mutations';
|
|
23
|
+
import {
|
|
24
|
+
pairToolOutputs,
|
|
25
|
+
putToolResult,
|
|
26
|
+
recordToolResult,
|
|
27
|
+
refreshToolMessage,
|
|
28
|
+
type ToolResultHost,
|
|
29
|
+
type ToolResultRecord,
|
|
30
|
+
} from '@/shared/lib/chat/omp/tool-results';
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
isError?: boolean;
|
|
28
|
-
details?: Record<string, any>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Per-entry cap on stored tool output; the tail of a build log is what matters. */
|
|
32
|
-
const MAX_TOOL_OUTPUT_CHARS = 200_000;
|
|
33
|
-
/** Max tool results retained per session; oldest entries are dropped first. */
|
|
34
|
-
const MAX_TRACKED_TOOL_RESULTS = 200;
|
|
35
|
-
|
|
36
|
-
/** Truncate tool output to the cap, keeping the tail and marking the cut. */
|
|
37
|
-
function capToolOutput(output: string): string {
|
|
38
|
-
if (output.length <= MAX_TOOL_OUTPUT_CHARS) return output;
|
|
39
|
-
return `…[truncated]\n${output.slice(-MAX_TOOL_OUTPUT_CHARS)}`;
|
|
40
|
-
}
|
|
32
|
+
// Re-exported so stream.ts and the timeline hook keep their existing import
|
|
33
|
+
// site; the implementation lives in tool-results.ts.
|
|
34
|
+
export type { ToolResultRecord } from '@/shared/lib/chat/omp/tool-results';
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
function storeToolResult(
|
|
44
|
-
map: Map<string, ToolResultRecord>,
|
|
45
|
-
callId: string,
|
|
46
|
-
record: ToolResultRecord,
|
|
47
|
-
): void {
|
|
48
|
-
map.set(callId, { ...record, output: capToolOutput(record.output) });
|
|
49
|
-
while (map.size > MAX_TRACKED_TOOL_RESULTS) {
|
|
50
|
-
const oldest = map.keys().next().value;
|
|
51
|
-
if (oldest === undefined) break;
|
|
52
|
-
map.delete(oldest);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface OmpAgentFoldDeps {
|
|
36
|
+
export interface OmpAgentFoldDeps extends ToolResultHost {
|
|
57
37
|
sessionId: string;
|
|
58
38
|
setState: Dispatch<SetStateAction<OmpAgentState>>;
|
|
59
39
|
callbacksRef: RefObject<OmpAgentCallbacks>;
|
|
@@ -78,45 +58,9 @@ function setActivity(verb: string | undefined, deps: OmpAgentFoldDeps): void {
|
|
|
78
58
|
deps.callbacksRef.current?.onActivity?.(verb);
|
|
79
59
|
}
|
|
80
60
|
|
|
81
|
-
/**
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
toolResultsRef: RefObject<Map<string, ToolResultRecord>>,
|
|
85
|
-
): ChatMessageData {
|
|
86
|
-
if (!msg.toolCalls?.length) return msg;
|
|
87
|
-
const toolCalls = msg.toolCalls.map(tc => {
|
|
88
|
-
const res = toolResultsRef.current?.get(tc.id);
|
|
89
|
-
return res
|
|
90
|
-
? {
|
|
91
|
-
...tc,
|
|
92
|
-
output: res.output,
|
|
93
|
-
details: tc.details || res.details || undefined,
|
|
94
|
-
status: (res.isError ? 'error' : 'success') as ToolCallData['status'],
|
|
95
|
-
}
|
|
96
|
-
: tc;
|
|
97
|
-
});
|
|
98
|
-
return { ...msg, toolCalls };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Record a toolResult frame's output against its tool call id. */
|
|
102
|
-
function recordToolResult(
|
|
103
|
-
raw: Record<string, unknown>,
|
|
104
|
-
deps: OmpAgentFoldDeps,
|
|
105
|
-
): void {
|
|
106
|
-
const callId = typeof raw.toolCallId === 'string' ? raw.toolCallId : undefined;
|
|
107
|
-
if (!callId) return;
|
|
108
|
-
deps.toolResultsRef.current?.set(callId, {
|
|
109
|
-
output: extractTextFromContent(raw.content),
|
|
110
|
-
details: (raw.details && typeof raw.details === 'object' ? raw.details : undefined) as ToolResultRecord['details'],
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Re-emit the last tool-carrying message when `callId` belongs to it. */
|
|
115
|
-
function refreshToolMessage(callId: string | undefined, deps: OmpAgentFoldDeps): void {
|
|
116
|
-
if (!callId) return;
|
|
117
|
-
const last = deps.lastToolMessageRef.current;
|
|
118
|
-
if (!last?.toolCalls?.some(tc => tc.id === callId)) return;
|
|
119
|
-
deps.callbacksRef.current?.onMessageUpdate?.(pairToolOutputs(last, deps.toolResultsRef));
|
|
61
|
+
/** Narrow a deps record to the tool-output host, adding the re-emit sink. */
|
|
62
|
+
function toolHost(deps: OmpAgentFoldDeps): ToolResultHost {
|
|
63
|
+
return { ...deps, onMessageUpdate: deps.callbacksRef.current?.onMessageUpdate };
|
|
120
64
|
}
|
|
121
65
|
|
|
122
66
|
/** Flush assistant turns that stopped abnormally and were never streamed as
|
|
@@ -141,7 +85,7 @@ function materializeTerminalMessages(
|
|
|
141
85
|
if (raw.stopReason !== 'aborted' && raw.stopReason !== 'error') continue;
|
|
142
86
|
const converted = toChatMessage(raw, false);
|
|
143
87
|
if (!converted) continue;
|
|
144
|
-
const paired = pairToolOutputs(converted, deps
|
|
88
|
+
const paired = pairToolOutputs(converted, deps);
|
|
145
89
|
if (paired.toolCalls?.length) deps.lastToolMessageRef.current = paired;
|
|
146
90
|
callbacks?.onMessageEnd?.(paired);
|
|
147
91
|
if (typeof raw.errorMessage === 'string') errorMessage = raw.errorMessage;
|
|
@@ -205,7 +149,7 @@ export function foldAgentEvent(data: OmpAgentEvent, deps: OmpAgentFoldDeps): voi
|
|
|
205
149
|
const completed = data.message as Record<string, unknown> | undefined;
|
|
206
150
|
if (!completed) break;
|
|
207
151
|
if (completed.role === 'toolResult') {
|
|
208
|
-
recordToolResult(completed, deps);
|
|
152
|
+
recordToolResult(completed, toolHost(deps));
|
|
209
153
|
break;
|
|
210
154
|
}
|
|
211
155
|
if (completed.role === 'custom') break;
|
|
@@ -219,7 +163,7 @@ export function foldAgentEvent(data: OmpAgentEvent, deps: OmpAgentFoldDeps): voi
|
|
|
219
163
|
if (converted.role !== 'user' && deps.currentThinkingLevelRef.current) {
|
|
220
164
|
converted.thinkingLevel = deps.currentThinkingLevelRef.current;
|
|
221
165
|
}
|
|
222
|
-
const paired = pairToolOutputs(converted, deps
|
|
166
|
+
const paired = pairToolOutputs(converted, deps);
|
|
223
167
|
if (paired.toolCalls?.length) deps.lastToolMessageRef.current = paired;
|
|
224
168
|
callbacks?.onMessageEnd?.(paired);
|
|
225
169
|
break;
|
|
@@ -254,9 +198,8 @@ export function foldAgentEvent(data: OmpAgentEvent, deps: OmpAgentFoldDeps): voi
|
|
|
254
198
|
const partial = toolResultText(data.partialResult);
|
|
255
199
|
if (!callId || !partial) break;
|
|
256
200
|
const prev = deps.toolResultsRef.current?.get(callId)?.output ?? '';
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
refreshToolMessage(callId, deps);
|
|
201
|
+
putToolResult(deps, callId, { output: prev + partial });
|
|
202
|
+
refreshToolMessage(callId, toolHost(deps));
|
|
260
203
|
break;
|
|
261
204
|
}
|
|
262
205
|
|
|
@@ -264,13 +207,12 @@ export function foldAgentEvent(data: OmpAgentEvent, deps: OmpAgentFoldDeps): voi
|
|
|
264
207
|
const callId = typeof data.toolCallId === 'string' ? data.toolCallId : undefined;
|
|
265
208
|
if (!callId) break;
|
|
266
209
|
setActivity(PHASE_VERBS.thinking, deps);
|
|
267
|
-
|
|
268
|
-
if (map) storeToolResult(map, callId, {
|
|
210
|
+
putToolResult(deps, callId, {
|
|
269
211
|
output: toolResultText(data.result),
|
|
270
212
|
isError: data.isError === true,
|
|
271
213
|
details: (data.details && typeof data.details === 'object' ? data.details : undefined) as ToolResultRecord['details'],
|
|
272
214
|
});
|
|
273
|
-
refreshToolMessage(callId, deps);
|
|
215
|
+
refreshToolMessage(callId, toolHost(deps));
|
|
274
216
|
// A file-mutating tool just finished — tell the data-bearing right panels
|
|
275
217
|
// (files / git / context) to re-read now, not on their next poll tick.
|
|
276
218
|
// Guarded: the fold also runs headless under `bun test` (no window).
|
|
@@ -328,6 +270,17 @@ export function foldAgentEvent(data: OmpAgentEvent, deps: OmpAgentFoldDeps): voi
|
|
|
328
270
|
break;
|
|
329
271
|
}
|
|
330
272
|
|
|
273
|
+
// omp renamed the session (auto-title generation, /rename, set_session_name).
|
|
274
|
+
// Nothing in the timeline changes, but the sidebar reads titles from the
|
|
275
|
+
// session file — and the rename is a 256-byte in-place slot write that the
|
|
276
|
+
// scan cache's mtime key cannot see — so this frame is the only push
|
|
277
|
+
// signal that the list is stale.
|
|
278
|
+
case 'session_info_update': {
|
|
279
|
+
const title = typeof data.title === 'string' ? data.title.trim() : '';
|
|
280
|
+
if (title) callbacks?.onSessionTitleChanged?.(title);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
|
|
331
284
|
case 'notice': {
|
|
332
285
|
callbacks?.onNotice?.(
|
|
333
286
|
typeof data.level === 'string' ? data.level : 'info',
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
* route's observer-only contract.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type {
|
|
22
|
-
import type { AgentStreamConnection, AgentStreamHandlers } from '@/shared/lib/chat/omp/transport';
|
|
21
|
+
import type { AgentStreamConnection, AgentStreamHandlers, StreamConnection, StreamHandlers } from '@/shared/lib/chat/omp/transport';
|
|
23
22
|
import { agentSocketUrl } from '@/shared/lib/chat/omp/transport';
|
|
24
23
|
|
|
25
24
|
const RECONNECT_BASE_MS = 500;
|
|
@@ -27,7 +26,8 @@ const RECONNECT_MAX_MS = 8_000;
|
|
|
27
26
|
/** Consecutive re-dials that never opened before the stream is given up on. */
|
|
28
27
|
const MAX_FAILED_DIALS = 4;
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
/** One duplex JSON socket: reconnect with capped backoff, terminal first dial. */
|
|
30
|
+
export function connectSocket<TFrame>(url: string, handlers: StreamHandlers<TFrame>): StreamConnection {
|
|
31
31
|
let socket: WebSocket | null = null;
|
|
32
32
|
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
33
33
|
let failures = 0;
|
|
@@ -35,7 +35,7 @@ export function connectAgentSocket(sessionId: string, handlers: AgentStreamHandl
|
|
|
35
35
|
let released = false;
|
|
36
36
|
|
|
37
37
|
const dial = () => {
|
|
38
|
-
const ws = new WebSocket(
|
|
38
|
+
const ws = new WebSocket(url);
|
|
39
39
|
socket = ws;
|
|
40
40
|
|
|
41
41
|
ws.onopen = () => {
|
|
@@ -47,9 +47,9 @@ export function connectAgentSocket(sessionId: string, handlers: AgentStreamHandl
|
|
|
47
47
|
|
|
48
48
|
ws.onmessage = (event) => {
|
|
49
49
|
if (released) return;
|
|
50
|
-
let data:
|
|
50
|
+
let data: TFrame;
|
|
51
51
|
try {
|
|
52
|
-
data = JSON.parse(String(event.data)) as
|
|
52
|
+
data = JSON.parse(String(event.data)) as TFrame;
|
|
53
53
|
} catch {
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
@@ -93,3 +93,8 @@ export function connectAgentSocket(sessionId: string, handlers: AgentStreamHandl
|
|
|
93
93
|
},
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
/** WebSocket transport for a session's agent event stream. */
|
|
98
|
+
export function connectAgentSocket(sessionId: string, handlers: AgentStreamHandlers): AgentStreamConnection {
|
|
99
|
+
return connectSocket(agentSocketUrl(sessionId), handlers);
|
|
100
|
+
}
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
* (the server closed the stream) is reported upward.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type {
|
|
16
|
-
import type { AgentStreamConnection, AgentStreamHandlers } from '@/shared/lib/chat/omp/transport';
|
|
15
|
+
import type { AgentStreamConnection, AgentStreamHandlers, StreamConnection, StreamHandlers } from '@/shared/lib/chat/omp/transport';
|
|
17
16
|
import { agentEventsUrl } from '@/shared/lib/chat/omp/transport';
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/** One EventSource stream: the browser owns reconnection on transient drops. */
|
|
19
|
+
export function connectEvents<TFrame>(url: string, handlers: StreamHandlers<TFrame>): StreamConnection {
|
|
20
|
+
const source = new EventSource(url);
|
|
21
21
|
let released = false;
|
|
22
22
|
|
|
23
23
|
source.onopen = () => {
|
|
@@ -27,9 +27,9 @@ export function connectAgentEvents(sessionId: string, handlers: AgentStreamHandl
|
|
|
27
27
|
|
|
28
28
|
source.onmessage = (event) => {
|
|
29
29
|
if (released) return;
|
|
30
|
-
let data:
|
|
30
|
+
let data: TFrame;
|
|
31
31
|
try {
|
|
32
|
-
data = JSON.parse(event.data) as
|
|
32
|
+
data = JSON.parse(event.data) as TFrame;
|
|
33
33
|
} catch {
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
@@ -51,3 +51,8 @@ export function connectAgentEvents(sessionId: string, handlers: AgentStreamHandl
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/** SSE transport for a session's agent event stream. */
|
|
56
|
+
export function connectAgentEvents(sessionId: string, handlers: AgentStreamHandlers): AgentStreamConnection {
|
|
57
|
+
return connectEvents(agentEventsUrl(sessionId), handlers);
|
|
58
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tool-output bookkeeping for the live agent stream.
|
|
8
|
+
*
|
|
9
|
+
* omp reports a tool call in an assistant message and its output in separate
|
|
10
|
+
* frames (`tool_execution_update` partials, then `tool_execution_end`). The
|
|
11
|
+
* timeline needs the two halves re-joined on the message that carries the call,
|
|
12
|
+
* so this module owns the call-id → output map and the re-emit that pairs them.
|
|
13
|
+
*
|
|
14
|
+
* Split out of agent-events.ts so that file stays under the repo's per-file
|
|
15
|
+
* size ceiling. The host is a narrow structural view of the fold's deps — not
|
|
16
|
+
* the deps type itself — so neither module imports the other.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { RefObject } from 'preact/compat';
|
|
20
|
+
import type { ChatMessageData, ToolCallData } from '@/shared/types';
|
|
21
|
+
import { extractTextFromContent } from '@/shared/lib/omp/session/mapper';
|
|
22
|
+
|
|
23
|
+
/** Tool output accumulated between a `toolCall` block and its result frame. */
|
|
24
|
+
export interface ToolResultRecord {
|
|
25
|
+
output: string;
|
|
26
|
+
isError?: boolean;
|
|
27
|
+
details?: Record<string, any>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Per-entry cap on stored tool output; the tail of a build log is what matters. */
|
|
31
|
+
const MAX_TOOL_OUTPUT_CHARS = 200_000;
|
|
32
|
+
/** Max tool results retained per session; oldest entries are dropped first. */
|
|
33
|
+
const MAX_TRACKED_TOOL_RESULTS = 200;
|
|
34
|
+
|
|
35
|
+
/** The slice of the fold's deps this module needs. */
|
|
36
|
+
export interface ToolResultHost {
|
|
37
|
+
toolResultsRef: RefObject<Map<string, ToolResultRecord>>;
|
|
38
|
+
lastToolMessageRef: RefObject<ChatMessageData>;
|
|
39
|
+
/** Re-emit an updated assistant message into the timeline. */
|
|
40
|
+
onMessageUpdate?: (msg: ChatMessageData) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Truncate tool output to the cap, keeping the tail and marking the cut. */
|
|
44
|
+
function capToolOutput(output: string): string {
|
|
45
|
+
if (output.length <= MAX_TOOL_OUTPUT_CHARS) return output;
|
|
46
|
+
return `…[truncated]\n${output.slice(-MAX_TOOL_OUTPUT_CHARS)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Write a tool result into the map, enforcing both caps. */
|
|
50
|
+
function storeToolResult(
|
|
51
|
+
map: Map<string, ToolResultRecord>,
|
|
52
|
+
callId: string,
|
|
53
|
+
record: ToolResultRecord,
|
|
54
|
+
): void {
|
|
55
|
+
map.set(callId, { ...record, output: capToolOutput(record.output) });
|
|
56
|
+
while (map.size > MAX_TRACKED_TOOL_RESULTS) {
|
|
57
|
+
const oldest = map.keys().next().value;
|
|
58
|
+
if (oldest === undefined) break;
|
|
59
|
+
map.delete(oldest);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Write a result into the map, enforcing both caps. */
|
|
64
|
+
export function putToolResult(host: ToolResultHost, callId: string, record: ToolResultRecord): void {
|
|
65
|
+
const map = host.toolResultsRef.current;
|
|
66
|
+
if (map) storeToolResult(map, callId, record);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Record a toolResult frame's output against its tool call id. */
|
|
70
|
+
export function recordToolResult(raw: Record<string, unknown>, host: ToolResultHost): void {
|
|
71
|
+
const callId = typeof raw.toolCallId === 'string' ? raw.toolCallId : undefined;
|
|
72
|
+
if (!callId) return;
|
|
73
|
+
host.toolResultsRef.current?.set(callId, {
|
|
74
|
+
output: extractTextFromContent(raw.content),
|
|
75
|
+
details: (raw.details && typeof raw.details === 'object' ? raw.details : undefined) as ToolResultRecord['details'],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Re-emit the last tool-carrying assistant message with its results paired. */
|
|
80
|
+
export function pairToolOutputs(msg: ChatMessageData, host: ToolResultHost): ChatMessageData {
|
|
81
|
+
if (!msg.toolCalls?.length) return msg;
|
|
82
|
+
const toolCalls = msg.toolCalls.map(tc => {
|
|
83
|
+
const res = host.toolResultsRef.current?.get(tc.id);
|
|
84
|
+
return res
|
|
85
|
+
? {
|
|
86
|
+
...tc,
|
|
87
|
+
output: res.output,
|
|
88
|
+
details: tc.details || res.details || undefined,
|
|
89
|
+
status: (res.isError ? 'error' : 'success') as ToolCallData['status'],
|
|
90
|
+
}
|
|
91
|
+
: tc;
|
|
92
|
+
});
|
|
93
|
+
return { ...msg, toolCalls };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Re-emit the last tool-carrying message when `callId` belongs to it. */
|
|
97
|
+
export function refreshToolMessage(callId: string | undefined, host: ToolResultHost): void {
|
|
98
|
+
if (!callId) return;
|
|
99
|
+
const last = host.lastToolMessageRef.current;
|
|
100
|
+
if (!last?.toolCalls?.some(tc => tc.id === callId)) return;
|
|
101
|
+
host.onMessageUpdate?.(pairToolOutputs(last, host));
|
|
102
|
+
}
|
|
@@ -14,20 +14,25 @@
|
|
|
14
14
|
import type { OmpAgentEvent, StreamTransport } from '@/shared/types';
|
|
15
15
|
import { readChamberSetting } from '@/shared/lib/settings/client';
|
|
16
16
|
|
|
17
|
-
export interface
|
|
17
|
+
export interface StreamHandlers<TFrame> {
|
|
18
18
|
/** Transport is attached; frames may arrive. */
|
|
19
19
|
onOpen: () => void;
|
|
20
20
|
/** One decoded event frame. */
|
|
21
|
-
onFrame: (data:
|
|
21
|
+
onFrame: (data: TFrame) => void;
|
|
22
22
|
/** Transport is gone for good (no further frames without a re-dial). */
|
|
23
23
|
onClose: () => void;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export interface
|
|
26
|
+
export interface StreamConnection {
|
|
27
27
|
/** Permanently release the transport; no callbacks fire afterwards. */
|
|
28
28
|
close: () => void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Agent-stream handlers — the socket's frames feed the timeline's fold. */
|
|
32
|
+
export type AgentStreamHandlers = StreamHandlers<OmpAgentEvent>;
|
|
33
|
+
|
|
34
|
+
export type AgentStreamConnection = StreamConnection;
|
|
35
|
+
|
|
31
36
|
export const DEFAULT_STREAM_TRANSPORT: StreamTransport = 'websocket';
|
|
32
37
|
|
|
33
38
|
/** WebSocket endpoint for a session's agent event stream. */
|
|
@@ -41,6 +46,17 @@ export function agentEventsUrl(sessionId: string): string {
|
|
|
41
46
|
return `/api/agent/${encodeURIComponent(sessionId)}/events`;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/** WebSocket endpoint for a session's BTW frame stream. */
|
|
50
|
+
export function btwSocketUrl(sessionId: string): string {
|
|
51
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
52
|
+
return `${protocol}//${window.location.host}/api/btw/${encodeURIComponent(sessionId)}/ws`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** SSE endpoint for a session's BTW frame stream. */
|
|
56
|
+
export function btwEventsUrl(sessionId: string): string {
|
|
57
|
+
return `/api/btw/${encodeURIComponent(sessionId)}/events`;
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
/**
|
|
45
61
|
* Resolve the configured stream transport from the chamber settings snapshot.
|
|
46
62
|
*/
|