ompchamber 0.6.0 → 0.8.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/README.md +24 -8
- package/dist/client/index.html +1 -1
- package/dist/client/static/css/index.5c0447cefe.css +2 -0
- package/dist/client/static/js/async/{3y.4c2596f64c.js → 3y.71f2ccf3b6.js} +2 -2
- package/dist/client/static/js/async/{xw.ae67ba06fd.js → 5b.0d6ae50fe8.js} +10 -10
- package/dist/client/static/js/async/5bh.8b2363424f.js +1 -0
- package/dist/client/static/js/async/{vy.bbe114c4e0.js → bi.09688ba75c.js} +1 -2
- package/dist/client/static/js/async/c.797d933882.js +1 -0
- package/dist/client/static/js/async/{dc.6dc4b0a04b.js → dc.856fda6560.js} +2 -2
- package/dist/client/static/js/async/{g.8515cbc6f7.js → g.d1c2fe09aa.js} +2 -2
- package/dist/client/static/js/async/g.d1c2fe09aa.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{m.4b29d34f93.js → m.0896dab823.js} +2 -2
- package/dist/client/static/js/async/ma.2c261b04dd.js +2 -0
- package/dist/client/static/js/async/nam.b5663e919f.js +10 -0
- package/dist/client/static/js/async/nam.b5663e919f.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{q.542c184fec.js → q.23af2a358f.js} +3 -3
- package/dist/client/static/js/async/{s.413c2a4683.js → s.222ee974f6.js} +2 -2
- package/dist/client/static/js/async/t.4dca0e2ba8.js +1 -0
- package/dist/client/static/js/async/uf.46ad6209fa.js +2 -0
- package/dist/client/static/js/async/x4.69ec69c61b.js +6 -0
- package/dist/client/static/js/async/xn.6360379f71.js +2 -0
- package/dist/client/static/js/async/xnl.feb0021e50.js +2 -0
- package/dist/client/static/js/index.dc5f5fabe8.js +33 -0
- package/dist/client/static/js/{uy.2e8929ed0c.js → jp.50817896f3.js} +6 -6
- package/package.json +3 -1
- package/src/cli/lib/args.js +1 -1
- package/src/cli/lib/commands/restart.js +11 -0
- package/src/cli/lib/commands/update.js +21 -7
- package/src/cli/lib/runtime.js +6 -3
- package/src/client/components/common/MarkdownRenderer.tsx +69 -27
- package/src/client/components/common/code-editor/history.test.ts +118 -0
- package/src/client/components/common/code-editor/history.ts +101 -0
- package/src/client/components/common/code-editor/index.tsx +56 -66
- package/src/client/components/common/code-surface/index.tsx +84 -11
- package/src/client/components/common/code-surface/measure.ts +66 -17
- package/src/client/components/common/diagram-viewer/index.tsx +258 -0
- package/src/client/components/common/diagram-viewer/read-block.ts +53 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +1 -0
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +10 -3
- package/src/client/components/settings/LazyModal.tsx +1 -1
- package/src/client/components/settings/Modal.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +8 -1
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +11 -52
- package/src/client/components/workspace/chat-timeline/chat-input/selection.ts +57 -0
- package/src/client/components/workspace/chat-timeline/index.tsx +121 -107
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/FrameControl.tsx +172 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +155 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/drafts.ts +113 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +92 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Footer.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Header.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Lazy.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/SelectBody.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/index.tsx +16 -10
- package/src/client/components/workspace/chat-timeline/tool-renderers/index.tsx +2 -2
- package/src/client/components/workspace/file-explorer/index.tsx +18 -5
- package/src/client/hooks/chat/omp/index.ts +4 -109
- package/src/client/hooks/chat/omp/prompt-send.ts +161 -0
- package/src/client/hooks/chat/omp/session-list.tsx +4 -0
- package/src/client/hooks/chat/timeline/actions.ts +0 -9
- package/src/client/hooks/chat/timeline/ask-frames.ts +102 -0
- package/src/client/hooks/chat/timeline/extension-dialog.ts +12 -9
- package/src/client/hooks/chat/timeline/index.ts +6 -8
- package/src/client/hooks/chat/timeline/prompt-text.ts +35 -0
- package/src/client/hooks/chat/timeline/send.ts +16 -29
- package/src/client/hooks/chat/timeline/session-load.ts +48 -106
- package/src/client/hooks/chat/timeline/session-pagination.ts +144 -0
- package/src/client/hooks/editor/use-code-window.ts +118 -0
- package/src/client/hooks/ui/input-required-alert.ts +43 -0
- package/src/client/hooks/ui/notification-sound.ts +65 -0
- package/src/client/hooks/ui/theme.ts +22 -6
- package/src/client/tailwind.css +38 -12
- package/src/server/index.ts +4 -3
- package/src/server/lib/lifecycle/bun.ts +18 -0
- package/src/server/lib/lifecycle/launch-mode.test.ts +32 -0
- package/src/server/lib/lifecycle/launch-mode.ts +40 -0
- package/src/server/lib/lifecycle/restart.test.ts +21 -0
- package/src/server/lib/lifecycle/restart.ts +111 -0
- package/src/server/lib/omp/rpc/manager.ts +14 -3
- package/src/server/lib/omp/rpc/session-commands.ts +15 -1
- package/src/server/lib/omp/rpc/session-registry.ts +18 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +7 -1
- package/src/server/lib/updates/apply.ts +8 -5
- package/src/server/lib/updates/install.ts +7 -4
- package/src/server/routes/health/probe.ts +2 -1
- package/src/shared/lib/chat/ask-questions.test.ts +169 -0
- package/src/shared/lib/chat/ask-questions.ts +264 -0
- package/src/shared/lib/code/lazy-window.test.ts +161 -0
- package/src/shared/lib/code/lazy-window.ts +156 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +50 -1
- package/src/shared/lib/code/syntax-highlight.ts +14 -1
- package/src/shared/lib/code/windowed-highlight.ts +63 -0
- package/src/shared/lib/markdown/mermaid.ts +8 -6
- package/src/shared/lib/omp/session/stream-state.server.ts +16 -1
- package/src/shared/types/omp/agent.ts +2 -2
- package/src/shared/types/workspace.ts +4 -0
- package/dist/client/static/css/index.51f68210a5.css +0 -2
- package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
- package/dist/client/static/js/async/c.cd45b0ac45.js +0 -1
- package/dist/client/static/js/async/fw.6a51998a8e.js +0 -6
- package/dist/client/static/js/async/kc.bbdb35b771.js +0 -1
- package/dist/client/static/js/async/mw.ca1512563c.js +0 -10
- package/dist/client/static/js/async/rf.d626afc367.js +0 -2
- package/dist/client/static/js/async/t.cda4891526.js +0 -1
- package/dist/client/static/js/async/uf.24ca3d49c3.js +0 -2
- package/dist/client/static/js/async/v0.394ed37c6b.js +0 -2
- package/dist/client/static/js/async/v0.394ed37c6b.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/vy.bbe114c4e0.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/xn.afecebbbd9.js +0 -2
- package/dist/client/static/js/index.47d3b04d5f.js +0 -33
- package/dist/client/static/js/index.47d3b04d5f.js.LICENSE.txt +0 -4
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Ask.tsx +0 -205
- /package/dist/client/static/js/async/{3y.4c2596f64c.js.LICENSE.txt → 3y.71f2ccf3b6.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.8515cbc6f7.js.LICENSE.txt → 5b.0d6ae50fe8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{dc.6dc4b0a04b.js.LICENSE.txt → dc.856fda6560.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{fw.6a51998a8e.js.LICENSE.txt → m.0896dab823.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.4b29d34f93.js.LICENSE.txt → ma.2c261b04dd.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{mw.ca1512563c.js.LICENSE.txt → q.23af2a358f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.542c184fec.js.LICENSE.txt → s.222ee974f6.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{rf.d626afc367.js.LICENSE.txt → uf.46ad6209fa.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.413c2a4683.js.LICENSE.txt → x4.69ec69c61b.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{uf.24ca3d49c3.js.LICENSE.txt → xn.6360379f71.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{xn.afecebbbd9.js.LICENSE.txt → xnl.feb0021e50.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{async/xw.ae67ba06fd.js.LICENSE.txt → index.dc5f5fabe8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{uy.2e8929ed0c.js.LICENSE.txt → jp.50817896f3.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prompt delivery for the live omp bridge: warm the session up, attach the
|
|
8
|
+
* event stream, POST the prompt, then signal the sidebars the moment it is
|
|
9
|
+
* dispatched.
|
|
10
|
+
*
|
|
11
|
+
* The signal is the point: the server writes the live `stream` row when it
|
|
12
|
+
* dispatches the prompt, so waiting for `agent_start` (spawn + ack round trip)
|
|
13
|
+
* would leave the sidebar spinner dark for seconds after the user hit send.
|
|
14
|
+
* Extracted from useOmpAgent so that hook stays under the repo's per-file size
|
|
15
|
+
* ceiling.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useCallback } from 'preact/hooks';
|
|
19
|
+
import type { Dispatch, SetStateAction } from 'preact/compat';
|
|
20
|
+
import type { OmpAgentState } from '@/shared/types';
|
|
21
|
+
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
22
|
+
|
|
23
|
+
export interface OmpPromptSenderDeps {
|
|
24
|
+
/** Live session id the prompt targets; the caller mirrors its prop into it. */
|
|
25
|
+
sessionIdRef: { current: string | null };
|
|
26
|
+
/** Attach the event stream to a spawned session. */
|
|
27
|
+
connect: (sessionId: string) => void;
|
|
28
|
+
setState: Dispatch<SetStateAction<OmpAgentState>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface OmpPromptSender {
|
|
32
|
+
sendPrompt: (message: string, images?: { data: string; mimeType: string }[], options?: { accessMode?: ApprovalMode }) => Promise<boolean>;
|
|
33
|
+
sendNewPrompt: (
|
|
34
|
+
message: string,
|
|
35
|
+
cwd: string,
|
|
36
|
+
images?: { data: string; mimeType: string }[],
|
|
37
|
+
composerOptions?: { model?: { provider: string; modelId: string } | null; thinkingLevel?: string | null; accessMode?: ApprovalMode },
|
|
38
|
+
) => Promise<{ sessionId: string; model: { provider: string; modelId: string } | null } | null>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useOmpPromptSender(deps: OmpPromptSenderDeps): OmpPromptSender {
|
|
42
|
+
const { sessionIdRef, connect, setState } = deps;
|
|
43
|
+
|
|
44
|
+
/** Send a prompt to the omp session via the RPC bridge. */
|
|
45
|
+
const sendPrompt = useCallback(async (
|
|
46
|
+
message: string,
|
|
47
|
+
images?: { data: string; mimeType: string }[],
|
|
48
|
+
options?: { accessMode?: ApprovalMode },
|
|
49
|
+
) => {
|
|
50
|
+
const sid = sessionIdRef.current;
|
|
51
|
+
if (!sid) return false;
|
|
52
|
+
setState((prev) => ({ ...prev, isGenerating: true, error: null }));
|
|
53
|
+
try {
|
|
54
|
+
// Mirror omp-web handleSend: warm the session process up with get_state
|
|
55
|
+
// (spawns it on first use), then attach the event stream before sending
|
|
56
|
+
// the prompt so no agent events are missed.
|
|
57
|
+
const warmup = await fetch(`/api/agent/${encodeURIComponent(sid)}`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
// The mode rides the warmup too: this is the request that lazily spawns
|
|
61
|
+
// an idle session, and omp only accepts --approval-mode at spawn time.
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
type: 'get_state',
|
|
64
|
+
...(options?.accessMode ? { accessMode: options.accessMode } : {}),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
if (warmup.ok) connect(sid);
|
|
68
|
+
|
|
69
|
+
const res = await fetch(`/api/agent/${encodeURIComponent(sid)}`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
type: 'prompt',
|
|
74
|
+
message,
|
|
75
|
+
...(images?.length ? { images } : {}),
|
|
76
|
+
...(options?.accessMode ? { accessMode: options.accessMode } : {}),
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
const body = (await res.json().catch(() => ({}))) as { success?: boolean; error?: string };
|
|
80
|
+
if (!res.ok || body.error) {
|
|
81
|
+
// `session_busy`: the ack timed out behind a still-running turn, so the
|
|
82
|
+
// prompt may already be accepted — never resend it automatically.
|
|
83
|
+
setState((prev) => ({ ...prev, isGenerating: false, error: body.error ?? `HTTP ${res.status}` }));
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
// Tell the sidebars to re-read the session list now — the server wrote
|
|
87
|
+
// this session's live `stream` row at dispatch. Revalidation is
|
|
88
|
+
// leading-edge throttled, so this lands immediately.
|
|
89
|
+
window.dispatchEvent(new CustomEvent('omp:session-updated', { detail: { sessionId: sid } }));
|
|
90
|
+
return true;
|
|
91
|
+
} catch (e) {
|
|
92
|
+
setState((prev) => ({ ...prev, isGenerating: false, error: e instanceof Error ? e.message : String(e) }));
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}, [connect, sessionIdRef, setState]);
|
|
96
|
+
|
|
97
|
+
/** Spawn a brand-new omp session and send the first prompt: ensure_session
|
|
98
|
+
* first (returns omp's real session id), attach the event stream, then send
|
|
99
|
+
* the prompt through the existing session route so no agent events are
|
|
100
|
+
* missed. Model/thinking picks ride the ensure_session body so omp applies
|
|
101
|
+
* them BEFORE the first prompt (and their JSONL change entries are written
|
|
102
|
+
* up front, not after the run starts). Returns the new session id on
|
|
103
|
+
* success, or null on failure — the caller adopts the id as the active
|
|
104
|
+
* session. */
|
|
105
|
+
const sendNewPrompt = useCallback(async (
|
|
106
|
+
message: string,
|
|
107
|
+
cwd: string,
|
|
108
|
+
images?: { data: string; mimeType: string }[],
|
|
109
|
+
composerOptions?: { model?: { provider: string; modelId: string } | null; thinkingLevel?: string | null; accessMode?: ApprovalMode },
|
|
110
|
+
): Promise<{ sessionId: string; model: { provider: string; modelId: string } | null } | null> => {
|
|
111
|
+
setState((prev) => ({ ...prev, isGenerating: true, error: null }));
|
|
112
|
+
try {
|
|
113
|
+
const created = await fetch('/api/agent/new', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
type: 'ensure_session',
|
|
118
|
+
cwd,
|
|
119
|
+
...(composerOptions?.model ? composerOptions.model : {}),
|
|
120
|
+
...(composerOptions?.thinkingLevel ? { thinkingLevel: composerOptions.thinkingLevel } : {}),
|
|
121
|
+
...(composerOptions?.accessMode ? { accessMode: composerOptions.accessMode } : {}),
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
const createdBody = (await created.json().catch(() => ({}))) as {
|
|
125
|
+
success?: boolean;
|
|
126
|
+
sessionId?: string;
|
|
127
|
+
model?: { provider: string; modelId: string } | null;
|
|
128
|
+
error?: string;
|
|
129
|
+
};
|
|
130
|
+
if (!created.ok || !createdBody.sessionId) {
|
|
131
|
+
setState((prev) => ({ ...prev, isGenerating: false, error: createdBody.error ?? `HTTP ${created.status}` }));
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const sid = createdBody.sessionId;
|
|
135
|
+
connect(sid);
|
|
136
|
+
const res = await fetch(`/api/agent/${encodeURIComponent(sid)}`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
type: 'prompt',
|
|
141
|
+
message,
|
|
142
|
+
...(images?.length ? { images } : {}),
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
const body = (await res.json().catch(() => ({}))) as { success?: boolean; error?: string };
|
|
146
|
+
if (!res.ok || body.error) {
|
|
147
|
+
setState((prev) => ({ ...prev, isGenerating: false, error: body.error ?? `HTTP ${res.status}` }));
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// Same dispatch-time signal as sendPrompt: the new session's live badge
|
|
151
|
+
// must not wait for agent_start + the first JSONL write.
|
|
152
|
+
window.dispatchEvent(new CustomEvent('omp:session-updated', { detail: { sessionId: sid } }));
|
|
153
|
+
return { sessionId: sid, model: createdBody.model ?? null };
|
|
154
|
+
} catch (e) {
|
|
155
|
+
setState((prev) => ({ ...prev, isGenerating: false, error: e instanceof Error ? e.message : String(e) }));
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}, [connect, setState]);
|
|
159
|
+
|
|
160
|
+
return { sendPrompt, sendNewPrompt };
|
|
161
|
+
}
|
|
@@ -21,6 +21,7 @@ import type { ReactNode } from 'preact/compat';
|
|
|
21
21
|
import { useFetcher } from '@/client/lib/router/fetcher';
|
|
22
22
|
import { usePanelRefresh } from '@/client/hooks/workspace/panel-refresh';
|
|
23
23
|
import { SIDEBAR_IDLE_REFRESH_MS } from '@/shared/lib/workspace/refresh-cadence';
|
|
24
|
+
import { useInputRequiredAlert } from '@/client/hooks/ui/input-required-alert';
|
|
24
25
|
import type { WorkspaceFolderData } from '@/shared/types';
|
|
25
26
|
import type { SessionListPayload } from '@/server/lib/omp/session/sidebar-data.server';
|
|
26
27
|
|
|
@@ -87,6 +88,9 @@ export function SidebarDataProvider({ children, initialFolders = [] }: { childre
|
|
|
87
88
|
folder.sessions?.some((session) => session.streamStatus === 'stream'),
|
|
88
89
|
),
|
|
89
90
|
);
|
|
91
|
+
// Fires for ANY session, including one this tab never opened — the cue that an
|
|
92
|
+
// agent is blocked on an answer must not depend on the open timeline.
|
|
93
|
+
useInputRequiredAlert(fetcher.data?.folders ?? []);
|
|
90
94
|
usePanelRefresh(refresh, !hasStreaming, SIDEBAR_IDLE_REFRESH_MS);
|
|
91
95
|
|
|
92
96
|
const markSeen = useCallback((sessionId: number | string) => {
|
|
@@ -49,8 +49,6 @@ export interface ChatTimelineActionsDeps {
|
|
|
49
49
|
/** Global access-control mode, snapshotted onto queued items. */
|
|
50
50
|
accessModeRef: { current: ApprovalMode };
|
|
51
51
|
setSearchParams: (fn: (prev: URLSearchParams) => URLSearchParams, opts?: { replace?: boolean }) => void;
|
|
52
|
-
/** Advance the pending-dialog queue after the head was answered. */
|
|
53
|
-
dismissExtensionDialog: () => void;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
export interface ChatTimelineActionsResult {
|
|
@@ -64,7 +62,6 @@ export interface ChatTimelineActionsResult {
|
|
|
64
62
|
stopGenerating: () => number;
|
|
65
63
|
handleThinkingLevelChange: (level: string) => void;
|
|
66
64
|
handleModelChange: (provider: string, modelId: string) => void;
|
|
67
|
-
closeExtensionDialog: () => void;
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
export function useChatTimelineActions(deps: ChatTimelineActionsDeps): ChatTimelineActionsResult {
|
|
@@ -91,7 +88,6 @@ export function useChatTimelineActions(deps: ChatTimelineActionsDeps): ChatTimel
|
|
|
91
88
|
composerModelRef,
|
|
92
89
|
accessModeRef,
|
|
93
90
|
setSearchParams,
|
|
94
|
-
dismissExtensionDialog,
|
|
95
91
|
} = deps;
|
|
96
92
|
|
|
97
93
|
const handleSend = useCallback(async (attachments: Attachment[], options?: { steering?: boolean }) => {
|
|
@@ -302,10 +298,6 @@ export function useChatTimelineActions(deps: ChatTimelineActionsDeps): ChatTimel
|
|
|
302
298
|
void ompAgent.setModel(provider, modelId);
|
|
303
299
|
}, [isOmpSession, ompAgent, pendingComposerModelRef]);
|
|
304
300
|
|
|
305
|
-
const closeExtensionDialog = useCallback(() => {
|
|
306
|
-
dismissExtensionDialog();
|
|
307
|
-
}, [dismissExtensionDialog]);
|
|
308
|
-
|
|
309
301
|
return {
|
|
310
302
|
handleSend,
|
|
311
303
|
handleEditQueueItem,
|
|
@@ -316,6 +308,5 @@ export function useChatTimelineActions(deps: ChatTimelineActionsDeps): ChatTimel
|
|
|
316
308
|
stopGenerating,
|
|
317
309
|
handleThinkingLevelChange,
|
|
318
310
|
handleModelChange,
|
|
319
|
-
closeExtensionDialog,
|
|
320
311
|
};
|
|
321
312
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Routes omp's pending answerable dialogs to their renderer.
|
|
8
|
+
*
|
|
9
|
+
* `ask` dialogs belong on the tool card that raised them — a modal would hide
|
|
10
|
+
* every question but the one in flight, and the tool's own result (all questions
|
|
11
|
+
* and their answers) is what the user reads afterwards. Everything else
|
|
12
|
+
* (approval gates under `always-ask`, extension pickers) has no card of its own
|
|
13
|
+
* and keeps the modal.
|
|
14
|
+
*
|
|
15
|
+
* The two are told apart by the running ask tool calls: a frame whose title
|
|
16
|
+
* names one of their questions is theirs, and a frame that follows such a frame
|
|
17
|
+
* (the `editor` omp opens after "Other (type your own)") belongs with it too.
|
|
18
|
+
* Any frame left over is modal material — including an ask-shaped frame whose
|
|
19
|
+
* tool call the timeline has not seen, which must stay answerable rather than
|
|
20
|
+
* silently vanish.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createContext, useContext } from 'preact/compat';
|
|
24
|
+
import type { ChatMessageData } from '@/shared/types';
|
|
25
|
+
import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
|
|
26
|
+
import { groupAskFrames, isAskToolCall, normalizeAskText, parseAskQuestions, type AskQuestion } from '@/shared/lib/chat/ask-questions';
|
|
27
|
+
import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
|
|
28
|
+
|
|
29
|
+
export interface AskFramesSplit {
|
|
30
|
+
/** The ask's dialogs, bucketed per question, keyed by ask tool-call id. */
|
|
31
|
+
framesByTool: Map<string, ExtensionUiDialogRequest[][]>;
|
|
32
|
+
/** Oldest request no ask card owns — the one the modal renders, if any. */
|
|
33
|
+
modalRequest: ExtensionUiDialogRequest | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AskFramesHandle {
|
|
37
|
+
framesByTool: AskFramesSplit['framesByTool'];
|
|
38
|
+
/** Answer one ask dialog and drop it from the pending queue. */
|
|
39
|
+
respond: (request: ExtensionUiDialogRequest, response: ExtensionDialogResponse) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const EMPTY: AskFramesHandle = { framesByTool: new Map(), respond: () => {} };
|
|
43
|
+
|
|
44
|
+
export const AskFramesContext = createContext<AskFramesHandle>(EMPTY);
|
|
45
|
+
|
|
46
|
+
/** Pending ask dialogs for the timeline that provides them. */
|
|
47
|
+
export function useAskFrames(): AskFramesHandle {
|
|
48
|
+
return useContext(AskFramesContext);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function splitAskFrames(
|
|
52
|
+
messages: ChatMessageData[],
|
|
53
|
+
pending: ExtensionUiDialogRequest[],
|
|
54
|
+
): AskFramesSplit {
|
|
55
|
+
const framesByTool = new Map<string, ExtensionUiDialogRequest[][]>();
|
|
56
|
+
if (pending.length === 0) return { framesByTool, modalRequest: null };
|
|
57
|
+
|
|
58
|
+
// Match against EVERY ask the timeline knows, not just the running ones: a
|
|
59
|
+
// reloaded session reads its tool calls back from the JSONL, where a call
|
|
60
|
+
// still parked on a question has no result yet and so cannot be told from a
|
|
61
|
+
// finished one. The pending queue is the part that is authoritative — it only
|
|
62
|
+
// ever holds frames omp is still blocked on — so a title match is enough.
|
|
63
|
+
const openAsks = new Map<string, AskQuestion[]>();
|
|
64
|
+
for (const message of messages) {
|
|
65
|
+
for (const tool of message.toolCalls ?? []) {
|
|
66
|
+
if (!isAskToolCall(tool)) continue;
|
|
67
|
+
const questions = parseAskQuestions(tool);
|
|
68
|
+
if (questions.length > 0) openAsks.set(tool.id, questions);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const owned = new Map<string, ExtensionUiDialogRequest[]>();
|
|
73
|
+
const claimed = new Set<ExtensionUiDialogRequest>();
|
|
74
|
+
let lastOwner: string | null = null;
|
|
75
|
+
|
|
76
|
+
for (const request of pending) {
|
|
77
|
+
const title = typeof request.title === 'string' ? normalizeAskText(request.title) : '';
|
|
78
|
+
let owner: string | null = null;
|
|
79
|
+
if (title) {
|
|
80
|
+
for (const [toolId, questions] of openAsks) {
|
|
81
|
+
if (questions.some((question) => normalizeAskText(question.question) === title)) {
|
|
82
|
+
owner = toolId;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// The dialog omp opens after "Other (type your own)" names the question in
|
|
88
|
+
// its title only loosely; it continues whichever ask spoke last. An
|
|
89
|
+
// approval gate is never part of an ask, so it keeps its own modal.
|
|
90
|
+
if (!owner && request.method !== 'confirm') owner = lastOwner;
|
|
91
|
+
if (!owner) continue;
|
|
92
|
+
claimed.add(request);
|
|
93
|
+
lastOwner = owner;
|
|
94
|
+
owned.set(owner, [...(owned.get(owner) ?? []), request]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const [toolId, frames] of owned) {
|
|
98
|
+
framesByTool.set(toolId, groupAskFrames(openAsks.get(toolId) ?? [], frames));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { framesByTool, modalRequest: pending.find((request) => !claimed.has(request)) ?? null };
|
|
102
|
+
}
|
|
@@ -13,19 +13,22 @@
|
|
|
13
13
|
* tool call stayed blocked on an approval nobody could answer and the turn never
|
|
14
14
|
* reached `agent_end` — the run looked hung until Stop.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* The queue keeps EVERY pending request rather than the head alone: `ask` raises
|
|
17
|
+
* one frame per question, and those are rendered inline on the ask tool card
|
|
18
|
+
* (see ask-frames.ts) instead of in a modal, so the head is not the only
|
|
19
|
+
* answerable request at any moment. Entries leave the queue by id once their
|
|
20
|
+
* response is on the wire, or when omp withdraws them (`method: 'cancel'`).
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
|
21
24
|
import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
|
|
22
25
|
|
|
23
26
|
export interface ExtensionDialogQueue {
|
|
24
|
-
/**
|
|
25
|
-
|
|
27
|
+
/** Still-blocking requests, in omp's arrival order. */
|
|
28
|
+
pending: ExtensionUiDialogRequest[];
|
|
26
29
|
enqueue: (request: ExtensionUiDialogRequest) => void;
|
|
27
|
-
/** Advance past
|
|
28
|
-
|
|
30
|
+
/** Advance past a request once its response has been sent. */
|
|
31
|
+
resolve: (id: string) => void;
|
|
29
32
|
/** Drop a request omp withdrew (`method: 'cancel'`) without an answer. */
|
|
30
33
|
withdraw: (targetId: string) => void;
|
|
31
34
|
}
|
|
@@ -45,12 +48,12 @@ export function useExtensionDialogQueue(sessionId: string | null): ExtensionDial
|
|
|
45
48
|
// entry per omp id keeps the queue answerable exactly once.
|
|
46
49
|
setQueue((current) => (current.some((pending) => pending.id === request.id) ? current : [...current, request]));
|
|
47
50
|
}, []);
|
|
48
|
-
const
|
|
49
|
-
setQueue((current) => current.
|
|
51
|
+
const resolve = useCallback((id: string) => {
|
|
52
|
+
setQueue((current) => current.some((pending) => pending.id === id) ? current.filter((pending) => pending.id !== id) : current);
|
|
50
53
|
}, []);
|
|
51
54
|
const withdraw = useCallback((targetId: string) => {
|
|
52
55
|
setQueue((current) => current.filter((pending) => pending.id !== targetId));
|
|
53
56
|
}, []);
|
|
54
57
|
|
|
55
|
-
return {
|
|
58
|
+
return { pending: queue, enqueue, resolve, withdraw };
|
|
56
59
|
}
|
|
@@ -120,7 +120,7 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
120
120
|
sessionData,
|
|
121
121
|
adoptedSessionIdRef,
|
|
122
122
|
refreshSessionMeta,
|
|
123
|
-
|
|
123
|
+
seedSession,
|
|
124
124
|
hasMore,
|
|
125
125
|
loadingOlder,
|
|
126
126
|
loadOlderError,
|
|
@@ -183,9 +183,9 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
183
183
|
removeDeliveredFromQueue,
|
|
184
184
|
} = useChatTimelineQueue(sessionId);
|
|
185
185
|
const {
|
|
186
|
-
|
|
186
|
+
pending: extensionDialogs,
|
|
187
187
|
enqueue: enqueueExtensionDialog,
|
|
188
|
-
|
|
188
|
+
resolve: resolveExtensionDialog,
|
|
189
189
|
withdraw: withdrawExtensionDialog,
|
|
190
190
|
} = useExtensionDialogQueue(sessionId);
|
|
191
191
|
|
|
@@ -226,7 +226,7 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
226
226
|
adoptedSessionIdRef,
|
|
227
227
|
optimisticUserIdRef,
|
|
228
228
|
pendingUserDisplaysRef,
|
|
229
|
-
|
|
229
|
+
seedSession,
|
|
230
230
|
pendingComposerModelRef,
|
|
231
231
|
pendingThinkingLevelRef,
|
|
232
232
|
composerModelRef,
|
|
@@ -262,7 +262,6 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
262
262
|
stopGenerating,
|
|
263
263
|
handleThinkingLevelChange,
|
|
264
264
|
handleModelChange,
|
|
265
|
-
closeExtensionDialog,
|
|
266
265
|
} = useChatTimelineActions({
|
|
267
266
|
inputValue,
|
|
268
267
|
setInputValue,
|
|
@@ -286,7 +285,6 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
286
285
|
composerModelRef,
|
|
287
286
|
accessModeRef,
|
|
288
287
|
setSearchParams,
|
|
289
|
-
dismissExtensionDialog,
|
|
290
288
|
});
|
|
291
289
|
|
|
292
290
|
return {
|
|
@@ -331,8 +329,8 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
331
329
|
accessMode,
|
|
332
330
|
handleAccessModeChange,
|
|
333
331
|
composerModelRef,
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
extensionDialogs,
|
|
333
|
+
resolveExtensionDialog,
|
|
336
334
|
respondToExtensionUi: ompAgent.respondToExtensionUi,
|
|
337
335
|
};
|
|
338
336
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prompt assembly for the chamber composer: rewrite composer-level mentions
|
|
8
|
+
* into what oh-my-pi actually receives, then inline the text attachments the
|
|
9
|
+
* user dropped in. Extracted from the send path so that hook stays under the
|
|
10
|
+
* repo's per-file size ceiling.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { composeMessageWithTextAttachments } from '@/shared/lib/chat/attachments';
|
|
14
|
+
import { loadAgentNames } from '@/shared/lib/chat/composer/client';
|
|
15
|
+
import { translateAgentMentions, translateFileMentions } from '@/shared/lib/chat/composer/translate';
|
|
16
|
+
|
|
17
|
+
type TextFileAttachment = Parameters<typeof composeMessageWithTextAttachments>[1][number];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the outgoing prompt. `@agent` mentions are rewritten into an explicit
|
|
21
|
+
* task-tool delegation directive at send time (oh-my-pi has no `@agent`
|
|
22
|
+
* syntax); translation failures fall back to the raw prompt.
|
|
23
|
+
*/
|
|
24
|
+
export async function buildPromptText(text: string, textFiles: TextFileAttachment[]): Promise<string> {
|
|
25
|
+
let translated = text;
|
|
26
|
+
try {
|
|
27
|
+
const names = await loadAgentNames();
|
|
28
|
+
if (names.length > 0) translated = translateAgentMentions(text, names).text;
|
|
29
|
+
} catch {
|
|
30
|
+
// keep the raw prompt
|
|
31
|
+
}
|
|
32
|
+
// File mentions are namespaced (`@file:`) by the picker; strip the namespace
|
|
33
|
+
// only after the agent pass so `@file:<name>` is never mistaken for `@agent`.
|
|
34
|
+
return composeMessageWithTextAttachments(translateFileMentions(translated), textFiles);
|
|
35
|
+
}
|
|
@@ -15,33 +15,13 @@ import type { Dispatch, SetStateAction } from 'preact/compat';
|
|
|
15
15
|
import type { Attachment, ChatMessageData, OmpAgentHandle, QueuedMessageModel } from '@/shared/types';
|
|
16
16
|
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
17
17
|
import { streamChatResponse } from '@/client/hooks/chat/stream';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
import type { SessionSeed } from '@/client/hooks/chat/timeline/session-load';
|
|
19
|
+
import { buildPromptText } from '@/client/hooks/chat/timeline/prompt-text';
|
|
20
|
+
import { isTextAttachmentFile } from '@/shared/lib/chat/attachments';
|
|
21
21
|
import { createMockStreamCallbacks } from '@/shared/lib/chat/timeline/stream-callbacks';
|
|
22
22
|
import { PHASE_VERBS } from '@/shared/lib/chat/timeline/tool-phrases';
|
|
23
23
|
import { formatClock } from '@/shared/lib/format/time';
|
|
24
24
|
|
|
25
|
-
type TextFileAttachment = Parameters<typeof composeMessageWithTextAttachments>[1][number];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Build the outgoing prompt. `@agent` mentions are rewritten into an explicit
|
|
29
|
-
* task-tool delegation directive at send time (oh-my-pi has no `@agent`
|
|
30
|
-
* syntax); translation failures fall back to the raw prompt.
|
|
31
|
-
*/
|
|
32
|
-
async function buildPromptText(text: string, textFiles: TextFileAttachment[]): Promise<string> {
|
|
33
|
-
let translated = text;
|
|
34
|
-
try {
|
|
35
|
-
const names = await loadAgentNames();
|
|
36
|
-
if (names.length > 0) translated = translateAgentMentions(text, names).text;
|
|
37
|
-
} catch {
|
|
38
|
-
// keep the raw prompt
|
|
39
|
-
}
|
|
40
|
-
// File mentions are namespaced (`@file:`) by the picker; strip the namespace
|
|
41
|
-
// only after the agent pass so `@file:<name>` is never mistaken for `@agent`.
|
|
42
|
-
return composeMessageWithTextAttachments(translateFileMentions(translated), textFiles);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
25
|
export interface ChatTimelineSendDeps {
|
|
46
26
|
folders: any[];
|
|
47
27
|
selectedFolderId: number | null;
|
|
@@ -61,7 +41,9 @@ export interface ChatTimelineSendDeps {
|
|
|
61
41
|
adoptedSessionIdRef: { current: string | null };
|
|
62
42
|
optimisticUserIdRef: { current: string | null };
|
|
63
43
|
pendingUserDisplaysRef: { current: { sent: string; display: string }[] };
|
|
64
|
-
|
|
44
|
+
/** Adopt the spawned session's identity (model + the thinking level the first
|
|
45
|
+
* prompt runs with) so the composer replacing the pending view shows it. */
|
|
46
|
+
seedSession: (seed: SessionSeed) => void;
|
|
65
47
|
/** Model/thinking picked in the composer before the session existed —
|
|
66
48
|
* applied to the spawn command so the first prompt runs with them. */
|
|
67
49
|
pendingComposerModelRef: { current: { provider: string; modelId: string } | null };
|
|
@@ -106,7 +88,7 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
106
88
|
adoptedSessionIdRef,
|
|
107
89
|
optimisticUserIdRef,
|
|
108
90
|
pendingUserDisplaysRef,
|
|
109
|
-
|
|
91
|
+
seedSession,
|
|
110
92
|
pendingComposerModelRef,
|
|
111
93
|
pendingThinkingLevelRef,
|
|
112
94
|
accessModeRef,
|
|
@@ -291,7 +273,11 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
291
273
|
if (spawned) {
|
|
292
274
|
adoptedSessionIdRef.current = spawned.sessionId;
|
|
293
275
|
aiPlaceholderIdRef.current = aiPlaceholderId;
|
|
294
|
-
|
|
276
|
+
// The spawn already applied this level to the session; seed it so the
|
|
277
|
+
// composer taking over from the pending view reports the run's real
|
|
278
|
+
// setting instead of a catalog default (the JSONL that carries it is
|
|
279
|
+
// not locatable yet).
|
|
280
|
+
seedSession({ model: spawned.model, thinkingLevel: composerThinking });
|
|
295
281
|
pendingComposerModelRef.current = null;
|
|
296
282
|
pendingThinkingLevelRef.current = null;
|
|
297
283
|
fetch(`/api/chat/${encodeURIComponent(spawned.sessionId)}`, {
|
|
@@ -304,8 +290,9 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
304
290
|
next.set('sessionId', spawned.sessionId);
|
|
305
291
|
return next;
|
|
306
292
|
}, { replace: true });
|
|
307
|
-
// The
|
|
308
|
-
//
|
|
293
|
+
// The sidebar already re-read at prompt dispatch (sendNewPrompt
|
|
294
|
+
// signals it), so the new session shows its live badge without
|
|
295
|
+
// waiting for the JSONL to carry the user turn.
|
|
309
296
|
return;
|
|
310
297
|
}
|
|
311
298
|
}
|
|
@@ -342,6 +329,6 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
342
329
|
scrollToBottom,
|
|
343
330
|
})
|
|
344
331
|
);
|
|
345
|
-
}, [appSettings, folders, isOmpSession, ompAgent, selectedFolderId, sessionId, scrollToBottom, jumpToBottom, persistMessages,
|
|
332
|
+
}, [appSettings, folders, isOmpSession, ompAgent, selectedFolderId, sessionId, scrollToBottom, jumpToBottom, persistMessages, seedSession, pendingComposerModelRef, pendingThinkingLevelRef, accessModeRef]);
|
|
346
333
|
return { steerOmpAgent, executeSend };
|
|
347
334
|
}
|