ompchamber 0.7.0 → 1.0.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.e880517fcd.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.a06ca3b3e9.js +2 -0
- package/dist/client/static/js/async/g.a06ca3b3e9.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{m.4b29d34f93.js → m.c038c71b9f.js} +2 -2
- package/dist/client/static/js/async/ma.2c261b04dd.js +2 -0
- package/dist/client/static/js/async/ms.98960af4eb.js +67 -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/ot.84c71b308f.js +2 -0
- package/dist/client/static/js/async/ot.84c71b308f.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/xn.6360379f71.js +2 -0
- package/dist/client/static/js/async/xnl.feb0021e50.js +2 -0
- package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +12 -0
- package/dist/client/static/js/index.0de15c5e17.js +33 -0
- package/dist/client/static/js/{uy.2e8929ed0c.js → jp.50817896f3.js} +6 -6
- package/package.json +2 -1
- 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 +2 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +11 -4
- package/src/client/components/layout/session-sidebar/SessionList.tsx +1 -1
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +1 -1
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +1 -1
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +2 -2
- 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/ComposerDock.tsx +11 -4
- package/src/client/components/workspace/chat-timeline/QueueList.tsx +18 -14
- 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 +124 -108
- 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/components/workspace/git-panel/BranchToolbar.tsx +2 -2
- package/src/client/components/workspace/git-panel/index.tsx +5 -1
- package/src/client/components/workspace/terminal-panel/Header.tsx +51 -19
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +86 -282
- package/src/client/components/workspace/terminal-panel/index.tsx +76 -50
- package/src/client/components/workspace/terminal-panel/mount.ts +275 -0
- package/src/client/data/theme/terminal.ts +27 -29
- package/src/client/hooks/chat/omp/session-list.tsx +23 -2
- package/src/client/hooks/chat/omp/session-sidebar-controller.ts +1 -1
- package/src/client/hooks/chat/timeline/actions.ts +23 -34
- 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 +18 -28
- package/src/client/hooks/chat/timeline/prompt-text.ts +35 -0
- package/src/client/hooks/chat/timeline/queue.ts +110 -46
- package/src/client/hooks/chat/timeline/send.ts +13 -27
- 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/hooks/workspace/terminal/index.ts +217 -0
- package/src/client/hooks/workspace/terminal/socket.ts +227 -0
- package/src/client/tailwind.css +38 -12
- package/src/server/db.server.ts +19 -0
- package/src/server/lib/fs/git-sync.ts +191 -0
- package/src/server/lib/omp/rpc/manager.ts +72 -6
- package/src/server/lib/omp/rpc/session-commands.ts +6 -0
- package/src/server/lib/omp/rpc/session-registry.ts +18 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +10 -4
- package/src/server/lib/queue/delivery.server.ts +114 -0
- package/src/server/lib/queue/schema-migration.server.ts +50 -0
- package/src/server/lib/queue/store.server.ts +185 -0
- package/src/server/lib/terminal/foreground.ts +73 -0
- package/src/server/lib/terminal/history.test.ts +118 -0
- package/src/server/lib/terminal/history.ts +205 -0
- package/src/server/lib/terminal/runtime.server.ts +338 -0
- package/src/server/lib/terminal/session.server.ts +215 -0
- package/src/server/lib/terminal/shell.test.ts +97 -0
- package/src/server/lib/terminal/shell.ts +103 -0
- package/src/server/routes/fs/git.ts +22 -12
- package/src/server/routes/index.ts +2 -1
- package/src/server/routes/sessions/index.ts +11 -3
- package/src/server/routes/sessions/queue.ts +121 -0
- package/src/server/routes/sessions/session.ts +1 -132
- package/src/server/routes/settings/commands.ts +1 -1
- package/src/server/routes/terminal/index.ts +2 -2
- package/src/server/routes/terminal/run.ts +8 -100
- package/src/server/routes/terminal/sessions.ts +24 -0
- package/src/server/routes/terminal/ws.ts +149 -0
- 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/chat/omp/agent-events.test.ts +37 -1
- package/src/shared/lib/chat/omp/agent-events.ts +30 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +9 -1
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +45 -8
- 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 +2 -3
- package/src/shared/lib/workspace/session-state/store.ts +1 -4
- package/src/shared/lib/workspace/terminal/protocol.ts +222 -0
- package/src/shared/types/omp/agent.ts +14 -2
- package/src/shared/types/terminal.ts +17 -15
- package/src/shared/types/workspace.ts +6 -2
- 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/g.86e2256a70.js +0 -2
- 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/uf.24ca3d49c3.js.LICENSE.txt +0 -6
- 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.eccad0b70a.js +0 -33
- package/dist/client/static/js/index.eccad0b70a.js.LICENSE.txt +0 -4
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Ask.tsx +0 -205
- package/src/client/components/workspace/terminal-panel/InputBar.tsx +0 -82
- package/src/client/components/workspace/terminal-panel/LogEntry.tsx +0 -70
- package/src/client/hooks/workspace/terminal-output.ts +0 -66
- package/src/client/hooks/workspace/terminal.ts +0 -283
- package/src/server/lib/fs/terminal-cwd.ts +0 -49
- package/src/server/routes/terminal/stream.ts +0 -100
- /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.86e2256a70.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.c038c71b9f.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/{xn.afecebbbd9.js.LICENSE.txt → ms.98960af4eb.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 → xn.6360379f71.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{async/xw.ae67ba06fd.js.LICENSE.txt → index.0de15c5e17.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{uy.2e8929ed0c.js.LICENSE.txt → jp.50817896f3.js.LICENSE.txt} +0 -0
|
@@ -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,
|
|
@@ -177,20 +177,20 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
177
177
|
|
|
178
178
|
const {
|
|
179
179
|
messageQueue,
|
|
180
|
-
|
|
180
|
+
enqueueMessage,
|
|
181
|
+
removeMessage,
|
|
182
|
+
reorderMessages,
|
|
181
183
|
steeringQueue,
|
|
182
184
|
setSteeringQueue,
|
|
183
|
-
removeDeliveredFromQueue,
|
|
184
185
|
} = useChatTimelineQueue(sessionId);
|
|
185
186
|
const {
|
|
186
|
-
|
|
187
|
+
pending: extensionDialogs,
|
|
187
188
|
enqueue: enqueueExtensionDialog,
|
|
188
|
-
|
|
189
|
+
resolve: resolveExtensionDialog,
|
|
189
190
|
withdraw: withdrawExtensionDialog,
|
|
190
191
|
} = useExtensionDialogQueue(sessionId);
|
|
191
192
|
|
|
192
193
|
const ompAgent = useOmpAgent(isOmpSession ? sessionId : null, createOmpAgentCallbacks({
|
|
193
|
-
removeDeliveredFromQueue,
|
|
194
194
|
setGenerating,
|
|
195
195
|
setGeneratingVerb,
|
|
196
196
|
scrollToBottom,
|
|
@@ -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,
|
|
@@ -236,21 +236,11 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
236
236
|
setSearchParams,
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// explicit send disarms it.
|
|
245
|
-
useEffect(() => {
|
|
246
|
-
if (stopHoldRef.current) return;
|
|
247
|
-
if (!isGenerating && messageQueue.length > 0) {
|
|
248
|
-
const nextMessage = messageQueue[0];
|
|
249
|
-
setMessageQueue(q => q.slice(1));
|
|
250
|
-
// The queued snapshot re-applies model/thinking/access before the prompt.
|
|
251
|
-
executeSend(nextMessage.text, nextMessage.attachments, { model: nextMessage.model });
|
|
252
|
-
}
|
|
253
|
-
}, [isGenerating, messageQueue, executeSend, setMessageQueue]);
|
|
239
|
+
// Queue auto-delivery moved server-side: the wrapper's terminal `agent_end`
|
|
240
|
+
// schedules the next queued prompt (`lib/queue/delivery.server.ts`), with a
|
|
241
|
+
// transactional head claim so no reload/tab race can double-send or wipe the
|
|
242
|
+
// table. The client panel is a view; the mount nudge lives in the queue
|
|
243
|
+
// hook. `stopHoldRef` remains only for the mock path below.
|
|
254
244
|
|
|
255
245
|
const {
|
|
256
246
|
handleSend,
|
|
@@ -262,7 +252,6 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
262
252
|
stopGenerating,
|
|
263
253
|
handleThinkingLevelChange,
|
|
264
254
|
handleModelChange,
|
|
265
|
-
closeExtensionDialog,
|
|
266
255
|
} = useChatTimelineActions({
|
|
267
256
|
inputValue,
|
|
268
257
|
setInputValue,
|
|
@@ -272,7 +261,8 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
272
261
|
sessionId,
|
|
273
262
|
appSettings,
|
|
274
263
|
messageQueue,
|
|
275
|
-
|
|
264
|
+
enqueueMessage,
|
|
265
|
+
removeMessage,
|
|
276
266
|
executeSend,
|
|
277
267
|
steerOmpAgent,
|
|
278
268
|
ompAgent,
|
|
@@ -286,7 +276,6 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
286
276
|
composerModelRef,
|
|
287
277
|
accessModeRef,
|
|
288
278
|
setSearchParams,
|
|
289
|
-
dismissExtensionDialog,
|
|
290
279
|
});
|
|
291
280
|
|
|
292
281
|
return {
|
|
@@ -305,7 +294,8 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
305
294
|
loadOlder,
|
|
306
295
|
generatingVerb,
|
|
307
296
|
messageQueue,
|
|
308
|
-
|
|
297
|
+
removeMessage,
|
|
298
|
+
reorderMessages,
|
|
309
299
|
steeringQueue,
|
|
310
300
|
setSteeringQueue,
|
|
311
301
|
inputValue,
|
|
@@ -331,8 +321,8 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
331
321
|
accessMode,
|
|
332
322
|
handleAccessModeChange,
|
|
333
323
|
composerModelRef,
|
|
334
|
-
|
|
335
|
-
|
|
324
|
+
extensionDialogs,
|
|
325
|
+
resolveExtensionDialog,
|
|
336
326
|
respondToExtensionUi: ompAgent.respondToExtensionUi,
|
|
337
327
|
};
|
|
338
328
|
}
|
|
@@ -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
|
+
}
|
|
@@ -4,84 +4,148 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Client view of the follow-up queue (SQLite `queued_messages`, owned by the
|
|
8
|
+
* server). The panel is a VIEW: mutations go through the per-item endpoints
|
|
9
|
+
* (`POST/PATCH/DELETE /api/sessions/:id/queue[/:itemId]`) and every response
|
|
10
|
+
* carries the canonical queue, which replaces local state wholesale. The old
|
|
11
|
+
* client-side replace-on-write mirror is gone — it was what let a reload
|
|
12
|
+
* hydrating over an empty state wipe the table, and a session switch PUT one
|
|
13
|
+
* session's list into another's rows.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* hydrates from the table on mount — a page reload mid-stream restores it.
|
|
16
|
-
* Steering mirrors live in the session-state blob under a separate key.
|
|
17
|
-
*
|
|
18
|
-
* Kept out of useChatTimeline so that hook stays under the size ceiling.
|
|
15
|
+
* Steering still mirrors into the per-session session-state blob (client-only
|
|
16
|
+
* by design: an interrupt-and-reply is only meaningful while a client watches
|
|
17
|
+
* the run).
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
|
-
import type { SetStateAction } from 'preact/compat';
|
|
22
20
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
23
21
|
import type { QueuedMessage } from '@/shared/types';
|
|
24
22
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
25
23
|
|
|
26
24
|
const STEERING_STATE_KEY = 'chat.steeringQueue';
|
|
27
25
|
|
|
26
|
+
function queueUrl(sessionId: string): string {
|
|
27
|
+
return `/api/sessions/${encodeURIComponent(sessionId)}/queue`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readQueueResponse(res: Response): Promise<QueuedMessage[] | null> {
|
|
31
|
+
if (!res.ok) return null;
|
|
32
|
+
const data = (await res.json().catch(() => null)) as { queue?: unknown } | null;
|
|
33
|
+
return Array.isArray(data?.queue) ? (data.queue as QueuedMessage[]) : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export interface ChatTimelineQueueResult {
|
|
29
37
|
messageQueue: QueuedMessage[];
|
|
30
|
-
|
|
38
|
+
/** Append one item; local state updates optimistically, then reconciles
|
|
39
|
+
* from the canonical response. No-op without a session. */
|
|
40
|
+
enqueueMessage: (item: Omit<QueuedMessage, 'id'>) => void;
|
|
41
|
+
/** Edit one item's text (PATCH). */
|
|
42
|
+
editMessageText: (id: string, text: string) => void;
|
|
43
|
+
/** Remove one item by id (DELETE). */
|
|
44
|
+
removeMessage: (id: string) => void;
|
|
45
|
+
/** Reorder the queue to these ids (PUT, drag-and-drop). */
|
|
46
|
+
reorderMessages: (orderedIds: string[]) => void;
|
|
47
|
+
/** Replace the local list from a fetch (mount hydration, manual refresh). */
|
|
48
|
+
refresh: () => void;
|
|
49
|
+
/** Client-only steering mirror (session-state blob). */
|
|
31
50
|
steeringQueue: QueuedMessage[];
|
|
32
|
-
setSteeringQueue: (
|
|
33
|
-
/** Drop an exactly-matching text from whichever mirror queue holds it. */
|
|
34
|
-
removeDeliveredFromQueue: (text: string) => void;
|
|
51
|
+
setSteeringQueue: (value: QueuedMessage[] | ((prev: QueuedMessage[]) => QueuedMessage[])) => void;
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
export function useChatTimelineQueue(sessionId: string | null): ChatTimelineQueueResult {
|
|
38
55
|
const [messageQueue, setMessageQueueState] = useState<QueuedMessage[]>([]);
|
|
39
56
|
const [steeringQueue, setSteeringQueue] = useSessionState<QueuedMessage[]>(STEERING_STATE_KEY, []);
|
|
40
57
|
|
|
41
|
-
// Hydrate from the table once per session
|
|
42
|
-
//
|
|
58
|
+
// Hydrate from the table once per session, and re-read when the tab
|
|
59
|
+
// regains focus (a second tab may have claimed/delivered the head). A
|
|
60
|
+
// generation-safe ref skips the stale-response race on quick switches.
|
|
43
61
|
const hydrateEpochRef = useRef(0);
|
|
44
|
-
|
|
62
|
+
const cancelledRef = useRef(false);
|
|
63
|
+
const refresh = useCallback(() => {
|
|
45
64
|
if (!sessionId) {
|
|
46
65
|
setMessageQueueState([]);
|
|
47
66
|
return;
|
|
48
67
|
}
|
|
49
68
|
const epoch = ++hydrateEpochRef.current;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.then(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
setMessageQueueState(Array.isArray(data.queue) ? data.queue : []);
|
|
69
|
+
fetch(queueUrl(sessionId))
|
|
70
|
+
.then((res) => readQueueResponse(res))
|
|
71
|
+
.then((queue) => {
|
|
72
|
+
if (cancelledRef.current || hydrateEpochRef.current !== epoch) return;
|
|
73
|
+
setMessageQueueState(queue ?? []);
|
|
56
74
|
})
|
|
57
75
|
.catch(() => {});
|
|
58
|
-
return () => { cancelled = true; };
|
|
59
76
|
}, [sessionId]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
cancelledRef.current = false;
|
|
79
|
+
refresh();
|
|
80
|
+
const onFocus = () => refresh();
|
|
81
|
+
window.addEventListener('focus', onFocus);
|
|
82
|
+
return () => {
|
|
83
|
+
cancelledRef.current = true;
|
|
84
|
+
window.removeEventListener('focus', onFocus);
|
|
85
|
+
};
|
|
86
|
+
}, [refresh]);
|
|
60
87
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
88
|
+
// Mounted session with queued items: offer the server a delivery slot. The
|
|
89
|
+
// server-side claim makes this harmless when a run is active or another tab
|
|
90
|
+
// already took the head — this used to be the client auto-delivery effect,
|
|
91
|
+
// which fired into whatever session was open at the time.
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!sessionId || messageQueue.length === 0) return;
|
|
94
|
+
fetch(`${queueUrl(sessionId)}/deliver`, { method: 'POST' }).catch(() => {});
|
|
95
|
+
}, [sessionId, messageQueue.length > 0]);
|
|
96
|
+
|
|
97
|
+
const enqueueMessage = useCallback((item: Omit<QueuedMessage, 'id'>) => {
|
|
64
98
|
if (!sessionId) return;
|
|
65
|
-
|
|
66
|
-
|
|
99
|
+
const optimistic: QueuedMessage = { ...item, id: `queue-${Date.now()}` };
|
|
100
|
+
setMessageQueueState((prev) => [...prev, optimistic]);
|
|
101
|
+
fetch(queueUrl(sessionId), {
|
|
102
|
+
method: 'POST',
|
|
67
103
|
headers: { 'Content-Type': 'application/json' },
|
|
68
|
-
body: JSON.stringify(
|
|
69
|
-
})
|
|
104
|
+
body: JSON.stringify(item),
|
|
105
|
+
})
|
|
106
|
+
.then((res) => readQueueResponse(res))
|
|
107
|
+
.then((queue) => { if (queue) setMessageQueueState(queue); })
|
|
108
|
+
.catch(() => {});
|
|
70
109
|
}, [sessionId]);
|
|
71
110
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
111
|
+
const editMessageText = useCallback((id: string, text: string) => {
|
|
112
|
+
if (!sessionId) return;
|
|
113
|
+
fetch(`${queueUrl(sessionId)}/${encodeURIComponent(id)}`, {
|
|
114
|
+
method: 'PATCH',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ text }),
|
|
117
|
+
})
|
|
118
|
+
.then((res) => readQueueResponse(res))
|
|
119
|
+
.then((queue) => { if (queue) setMessageQueueState(queue); })
|
|
120
|
+
.catch(() => {});
|
|
121
|
+
}, [sessionId]);
|
|
122
|
+
|
|
123
|
+
const removeMessage = useCallback((id: string) => {
|
|
124
|
+
if (!sessionId) return;
|
|
125
|
+
// Optimistic removal, reconciled only on failure (404 = already claimed —
|
|
126
|
+
// the item is gone either way).
|
|
127
|
+
setMessageQueueState((prev) => prev.filter((i) => i.id !== id));
|
|
128
|
+
fetch(`${queueUrl(sessionId)}/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
129
|
+
.then((res) => readQueueResponse(res))
|
|
130
|
+
.then((queue) => { if (queue) setMessageQueueState(queue); })
|
|
131
|
+
.catch(() => {});
|
|
132
|
+
}, [sessionId]);
|
|
79
133
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
134
|
+
const reorderMessages = useCallback((orderedIds: string[]) => {
|
|
135
|
+
if (!sessionId) return;
|
|
136
|
+
setMessageQueueState((prev) => {
|
|
137
|
+
const byId = new Map(prev.map((i) => [i.id, i]));
|
|
138
|
+
return orderedIds.map((id) => byId.get(id)).filter((i): i is QueuedMessage => Boolean(i));
|
|
139
|
+
});
|
|
140
|
+
fetch(queueUrl(sessionId), {
|
|
141
|
+
method: 'PUT',
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
body: JSON.stringify({ orderedIds }),
|
|
144
|
+
})
|
|
145
|
+
.then((res) => readQueueResponse(res))
|
|
146
|
+
.then((queue) => { if (queue) setMessageQueueState(queue); })
|
|
147
|
+
.catch(() => {});
|
|
148
|
+
}, [sessionId]);
|
|
85
149
|
|
|
86
|
-
return { messageQueue,
|
|
150
|
+
return { messageQueue, enqueueMessage, editMessageText, removeMessage, reorderMessages, refresh, steeringQueue, setSteeringQueue };
|
|
87
151
|
}
|
|
@@ -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)}`, {
|
|
@@ -343,6 +329,6 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
343
329
|
scrollToBottom,
|
|
344
330
|
})
|
|
345
331
|
);
|
|
346
|
-
}, [appSettings, folders, isOmpSession, ompAgent, selectedFolderId, sessionId, scrollToBottom, jumpToBottom, persistMessages,
|
|
332
|
+
}, [appSettings, folders, isOmpSession, ompAgent, selectedFolderId, sessionId, scrollToBottom, jumpToBottom, persistMessages, seedSession, pendingComposerModelRef, pendingThinkingLevelRef, accessModeRef]);
|
|
347
333
|
return { steerOmpAgent, executeSend };
|
|
348
334
|
}
|