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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from 'preact/hooks';
|
|
2
2
|
import { useSearchParams } from '@/client/lib/router/search-params';
|
|
3
|
-
import {
|
|
3
|
+
import { ExtensionDialog } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
|
|
4
|
+
import { AskFramesContext, splitAskFrames, type AskFramesHandle } from '@/client/hooks/chat/timeline/ask-frames';
|
|
5
|
+
import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
|
|
6
|
+
import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
|
|
4
7
|
import { EmptyWorkspacePrompt } from '@/client/components/workspace/chat-timeline/EmptyWorkspacePrompt';
|
|
5
8
|
import { SessionSkeleton } from '@/client/components/workspace/chat-timeline/SessionSkeleton';
|
|
6
9
|
import { UndoConfirmModal } from '@/client/components/workspace/chat-timeline/UndoConfirmModal';
|
|
@@ -71,12 +74,27 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
71
74
|
accessMode,
|
|
72
75
|
handleAccessModeChange,
|
|
73
76
|
composerModelRef,
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
extensionDialogs,
|
|
78
|
+
resolveExtensionDialog,
|
|
76
79
|
respondToExtensionUi,
|
|
77
80
|
} = useChatTimeline({ folders, appSettings });
|
|
78
81
|
const { toasts, pushToast, dismissToast } = useToasts();
|
|
79
82
|
|
|
83
|
+
// Ask dialogs render on their own tool card; anything else omp is blocked on
|
|
84
|
+
// (an approval gate, an extension picker) has no card and keeps the modal.
|
|
85
|
+
const { framesByTool, modalRequest } = useMemo(
|
|
86
|
+
() => splitAskFrames(localMessages, extensionDialogs),
|
|
87
|
+
[localMessages, extensionDialogs],
|
|
88
|
+
);
|
|
89
|
+
const respondToFrame = useCallback((request: ExtensionUiDialogRequest, response: ExtensionDialogResponse) => {
|
|
90
|
+
void respondToExtensionUi(request, response);
|
|
91
|
+
resolveExtensionDialog(request.id);
|
|
92
|
+
}, [respondToExtensionUi, resolveExtensionDialog]);
|
|
93
|
+
const askFrames = useMemo<AskFramesHandle>(
|
|
94
|
+
() => ({ framesByTool, respond: respondToFrame }),
|
|
95
|
+
[framesByTool, respondToFrame],
|
|
96
|
+
);
|
|
97
|
+
|
|
80
98
|
const [newChatInitialContent, setNewChatInitialContent] = useState<string | null>(null);
|
|
81
99
|
const { pendingUndo, undoing, requestUndo: handleRequestUndo, closeUndoConfirm, confirmUndo } = useUndoConfirmation(handleUndo);
|
|
82
100
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
@@ -170,118 +188,114 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
return (
|
|
173
|
-
<
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
<AskFramesContext.Provider value={askFrames}>
|
|
192
|
+
<div className={`flex flex-col h-full min-h-0 overflow-hidden bg-canvas relative ${className}`}>
|
|
193
|
+
{showFullSkeleton ? (
|
|
194
|
+
<SessionSkeleton />
|
|
195
|
+
) : (
|
|
196
|
+
<>
|
|
197
|
+
{/* Main chat container wrapper */}
|
|
198
|
+
<div className="relative flex-1 min-h-0 flex flex-col">
|
|
199
|
+
{activeSubagent ? (
|
|
200
|
+
<SubagentView
|
|
201
|
+
sessionId={sessionId}
|
|
202
|
+
subagent={activeSubagent}
|
|
203
|
+
onBack={handleSubagentBack}
|
|
204
|
+
provider={sessionProvider}
|
|
205
|
+
providerNames={providerNames}
|
|
206
|
+
/>
|
|
207
|
+
) : (
|
|
208
|
+
<TimelineBody
|
|
209
|
+
isMobile={isMobile}
|
|
210
|
+
userMessages={userMessages}
|
|
211
|
+
onScrollTo={handleScrollTo}
|
|
212
|
+
scrollRef={scrollRef}
|
|
213
|
+
contentRef={contentRef}
|
|
214
|
+
handleScroll={handleScroll}
|
|
215
|
+
isScrolling={isScrolling}
|
|
216
|
+
loadingOlder={loadingOlder}
|
|
217
|
+
hasMore={hasMore}
|
|
218
|
+
loadOlderError={loadOlderError}
|
|
219
|
+
loadOlder={loadOlder}
|
|
220
|
+
messages={localMessages}
|
|
221
|
+
isGenerating={isGenerating}
|
|
222
|
+
provider={sessionProvider}
|
|
223
|
+
providerNames={providerNames}
|
|
224
|
+
modelName={sessionModelName}
|
|
225
|
+
modelNames={modelNames}
|
|
226
|
+
thinkingLevel={sessionData?.thinkingLevel}
|
|
227
|
+
onUndo={handleRequestUndo}
|
|
228
|
+
onRetry={handleRetry}
|
|
229
|
+
onNewChat={handleNewChat}
|
|
230
|
+
showScrollBottom={showScrollBottom}
|
|
231
|
+
jumpToBottom={jumpToBottom}
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Input Area Footer with Docked Generating Indicator (Seamless & Transparent) */}
|
|
237
|
+
{!activeSubagent && (
|
|
238
|
+
<ComposerDock
|
|
190
239
|
isMobile={isMobile}
|
|
191
|
-
userMessages={userMessages}
|
|
192
|
-
onScrollTo={handleScrollTo}
|
|
193
|
-
scrollRef={scrollRef}
|
|
194
|
-
contentRef={contentRef}
|
|
195
|
-
handleScroll={handleScroll}
|
|
196
|
-
isScrolling={isScrolling}
|
|
197
|
-
loadingOlder={loadingOlder}
|
|
198
|
-
hasMore={hasMore}
|
|
199
|
-
loadOlderError={loadOlderError}
|
|
200
|
-
loadOlder={loadOlder}
|
|
201
|
-
messages={localMessages}
|
|
202
240
|
isGenerating={isGenerating}
|
|
241
|
+
modelName={sessionModelName}
|
|
242
|
+
generatingVerb={generatingVerb}
|
|
203
243
|
provider={sessionProvider}
|
|
204
244
|
providerNames={providerNames}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
245
|
+
messageQueue={messageQueue}
|
|
246
|
+
setMessageQueue={setMessageQueue}
|
|
247
|
+
onEditQueueItem={handleEditQueueItem}
|
|
248
|
+
onSendNowQueueItem={handleSendNowQueueItem}
|
|
249
|
+
steeringQueue={steeringQueue}
|
|
250
|
+
setSteeringQueue={setSteeringQueue}
|
|
251
|
+
inputValue={inputValue}
|
|
252
|
+
setInputValue={setInputValue}
|
|
253
|
+
rootPath={composerRoot}
|
|
254
|
+
attachments={inputAttachments}
|
|
255
|
+
setAttachments={setInputAttachments}
|
|
256
|
+
onSend={handleSend}
|
|
257
|
+
onStop={handleStop}
|
|
258
|
+
appSettings={appSettings}
|
|
259
|
+
onThinkingLevelChange={handleThinkingLevelChange}
|
|
260
|
+
onModelChange={handleModelChange}
|
|
261
|
+
accessMode={accessMode}
|
|
262
|
+
onAccessModeChange={handleAccessModeChange}
|
|
263
|
+
composerModelRef={composerModelRef}
|
|
264
|
+
sessionModel={typeof sessionData?.model === 'object' ? sessionData.model : null}
|
|
265
|
+
sessionThinkingLevel={sessionData?.thinkingLevel}
|
|
266
|
+
variant={variant}
|
|
213
267
|
/>
|
|
214
268
|
)}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
{/* Input Area Footer with Docked Generating Indicator (Seamless & Transparent) */}
|
|
218
|
-
{!activeSubagent && (
|
|
219
|
-
<ComposerDock
|
|
220
|
-
isMobile={isMobile}
|
|
221
|
-
isGenerating={isGenerating}
|
|
222
|
-
modelName={sessionModelName}
|
|
223
|
-
generatingVerb={generatingVerb}
|
|
224
|
-
provider={sessionProvider}
|
|
225
|
-
providerNames={providerNames}
|
|
226
|
-
messageQueue={messageQueue}
|
|
227
|
-
setMessageQueue={setMessageQueue}
|
|
228
|
-
onEditQueueItem={handleEditQueueItem}
|
|
229
|
-
onSendNowQueueItem={handleSendNowQueueItem}
|
|
230
|
-
steeringQueue={steeringQueue}
|
|
231
|
-
setSteeringQueue={setSteeringQueue}
|
|
232
|
-
inputValue={inputValue}
|
|
233
|
-
setInputValue={setInputValue}
|
|
234
|
-
rootPath={composerRoot}
|
|
235
|
-
attachments={inputAttachments}
|
|
236
|
-
setAttachments={setInputAttachments}
|
|
237
|
-
onSend={handleSend}
|
|
238
|
-
onStop={handleStop}
|
|
239
|
-
appSettings={appSettings}
|
|
240
|
-
onThinkingLevelChange={handleThinkingLevelChange}
|
|
241
|
-
onModelChange={handleModelChange}
|
|
242
|
-
accessMode={accessMode}
|
|
243
|
-
onAccessModeChange={handleAccessModeChange}
|
|
244
|
-
composerModelRef={composerModelRef}
|
|
245
|
-
sessionModel={typeof sessionData?.model === 'object' ? sessionData.model : null}
|
|
246
|
-
sessionThinkingLevel={sessionData?.thinkingLevel}
|
|
247
|
-
variant={variant}
|
|
248
|
-
/>
|
|
249
|
-
)}
|
|
250
|
-
</>
|
|
251
|
-
)}
|
|
269
|
+
</>
|
|
270
|
+
)}
|
|
252
271
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
{pendingUndo && (
|
|
273
|
+
<UndoConfirmModal
|
|
274
|
+
isOmpSession={Boolean(sessionId) && !sessionId.startsWith('new-') && Number.isNaN(Number(sessionId))}
|
|
275
|
+
content={pendingUndo.content}
|
|
276
|
+
undoing={undoing}
|
|
277
|
+
onClose={closeUndoConfirm}
|
|
278
|
+
onConfirm={confirmUndo}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
{newChatInitialContent !== null && (
|
|
282
|
+
<NewChatModal
|
|
283
|
+
initialContent={newChatInitialContent}
|
|
284
|
+
onClose={() => setNewChatInitialContent(null)}
|
|
285
|
+
onSend={submitNewChat}
|
|
286
|
+
appSettings={appSettings}
|
|
287
|
+
accessMode={accessMode}
|
|
288
|
+
onAccessModeChange={handleAccessModeChange}
|
|
289
|
+
composerModelRef={composerModelRef}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
273
292
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
onRespond={(request, response) => {
|
|
278
|
-
void respondToExtensionUi(request, response);
|
|
279
|
-
closeExtensionDialog();
|
|
280
|
-
}}
|
|
281
|
-
/>
|
|
282
|
-
)}
|
|
293
|
+
{modalRequest && (
|
|
294
|
+
<ExtensionDialog request={modalRequest} onRespond={respondToFrame} />
|
|
295
|
+
)}
|
|
283
296
|
|
|
284
|
-
|
|
285
|
-
|
|
297
|
+
<ToastStack toasts={toasts} onDismiss={dismissToast} />
|
|
298
|
+
</div>
|
|
299
|
+
</AskFramesContext.Provider>
|
|
286
300
|
);
|
|
287
301
|
}
|
package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/FrameControl.tsx
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The control for the dialog omp is currently blocked on.
|
|
8
|
+
*
|
|
9
|
+
* The option list is rendered from the FRAME, never from the tool arguments:
|
|
10
|
+
* omp decorates it on its way out — ` (Recommended)` on the suggested choice,
|
|
11
|
+
* `Other (type your own)` appended to every question, and whichever label ends a
|
|
12
|
+
* multi-select sweep — and those exact strings are what omp compares a response
|
|
13
|
+
* against. Reproducing them locally would only risk sending a label omp does not
|
|
14
|
+
* recognise.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
18
|
+
import { Check, CornerDownLeft } from 'lucide-preact';
|
|
19
|
+
import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
|
|
20
|
+
import { isOtherOption } from '@/shared/lib/chat/ask-questions';
|
|
21
|
+
import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
|
|
22
|
+
|
|
23
|
+
interface FrameControlProps {
|
|
24
|
+
frame: ExtensionUiDialogRequest;
|
|
25
|
+
/** Question picks several options, so every row toggles instead of answering. */
|
|
26
|
+
multi: boolean;
|
|
27
|
+
/** Labels already toggled on a multi-select question — display only. */
|
|
28
|
+
selected: string[];
|
|
29
|
+
/** Commit a single-choice answer; sent as soon as omp is listening for it. */
|
|
30
|
+
onAnswer: (value: string) => void;
|
|
31
|
+
/** Send one multi-select toggle. */
|
|
32
|
+
onToggle: (label: string) => void;
|
|
33
|
+
/** Answer a dialog that is not a choice list (confirm / input / editor). */
|
|
34
|
+
onRespond: (response: ExtensionDialogResponse) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const RECOMMENDED = / \(recommended\)$/i;
|
|
38
|
+
const DONE_ROW = /\bdone\b/i;
|
|
39
|
+
|
|
40
|
+
export function FrameControl({ frame, multi, selected, onAnswer, onToggle, onRespond }: FrameControlProps) {
|
|
41
|
+
const [text, setText] = useState('');
|
|
42
|
+
// A new dialog reuses this component; text left over from the previous one
|
|
43
|
+
// must not read as an answer the user just typed.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setText(frame.method === 'editor' ? frame.prefill ?? '' : '');
|
|
46
|
+
}, [frame.id, frame.method, frame.prefill]);
|
|
47
|
+
|
|
48
|
+
if (frame.method === 'confirm') {
|
|
49
|
+
return (
|
|
50
|
+
<div className="mt-2 flex items-center gap-2">
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => onRespond({ confirmed: true })}
|
|
54
|
+
className="flex cursor-pointer items-center gap-1.5 rounded-md bg-ink px-3 py-1 text-[11.5px] font-semibold text-paper transition-opacity hover:opacity-90"
|
|
55
|
+
>
|
|
56
|
+
<Check size={11} />
|
|
57
|
+
<span>Confirm</span>
|
|
58
|
+
</button>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => onRespond({ confirmed: false })}
|
|
62
|
+
className="cursor-pointer rounded-md border border-ink/15 px-3 py-1 text-[11.5px] font-medium text-ink/70 transition-colors hover:border-ink/30 hover:text-ink"
|
|
63
|
+
>
|
|
64
|
+
Decline
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (frame.method === 'input' || frame.method === 'editor') {
|
|
71
|
+
return (
|
|
72
|
+
<div className="mt-2 space-y-1.5">
|
|
73
|
+
{frame.title && <div className="text-[10.5px] text-ink/45">{frame.title}</div>}
|
|
74
|
+
{frame.method === 'editor' ? (
|
|
75
|
+
<textarea
|
|
76
|
+
autoFocus
|
|
77
|
+
value={text}
|
|
78
|
+
onChange={(event) => setText(event.currentTarget.value)}
|
|
79
|
+
rows={4}
|
|
80
|
+
placeholder={frame.placeholder || 'Type your answer…'}
|
|
81
|
+
className="w-full rounded-md border border-ink/15 bg-canvas/40 px-2.5 py-1.5 font-mono text-[11.5px] leading-relaxed text-ink outline-none focus:border-ink"
|
|
82
|
+
/>
|
|
83
|
+
) : (
|
|
84
|
+
<input
|
|
85
|
+
autoFocus
|
|
86
|
+
value={text}
|
|
87
|
+
placeholder={frame.placeholder || 'Type your answer…'}
|
|
88
|
+
onChange={(event) => setText(event.currentTarget.value)}
|
|
89
|
+
onKeyDown={(event) => {
|
|
90
|
+
if (event.key !== 'Enter') return;
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
onRespond({ value: text });
|
|
93
|
+
}}
|
|
94
|
+
className="w-full rounded-md border border-ink/15 bg-canvas/40 px-2.5 py-1.5 text-[11.5px] text-ink outline-none focus:border-ink"
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
onClick={() => onRespond({ value: text })}
|
|
100
|
+
className="flex cursor-pointer items-center gap-1.5 rounded-md bg-ink px-3 py-1 text-[11.5px] font-semibold text-paper transition-opacity hover:opacity-90"
|
|
101
|
+
>
|
|
102
|
+
<span>Submit</span>
|
|
103
|
+
<CornerDownLeft size={11} className="opacity-70" />
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const options = frame.options ?? [];
|
|
110
|
+
const details = frame.optionDetails ?? [];
|
|
111
|
+
if (frame.method !== 'select' || options.length === 0) return null;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="mt-2 space-y-1" role={multi ? 'group' : 'radiogroup'}>
|
|
115
|
+
{options.map((label, index) => {
|
|
116
|
+
// A single-choice question resolves "Other" to the typed text, so the
|
|
117
|
+
// row itself is a prompt to type — never a literal answer.
|
|
118
|
+
if (!multi && isOtherOption(label)) return null;
|
|
119
|
+
const description = details[index]?.description;
|
|
120
|
+
const checked = multi && selected.includes(label);
|
|
121
|
+
const finish = multi && DONE_ROW.test(label);
|
|
122
|
+
return (
|
|
123
|
+
<button
|
|
124
|
+
key={label}
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={() => (multi ? (finish ? onRespond({ value: label }) : onToggle(label)) : onAnswer(label))}
|
|
127
|
+
className={`flex w-full cursor-pointer items-start gap-2 rounded-md border px-2.5 py-1.5 text-left text-[11.5px] transition-colors ${
|
|
128
|
+
checked
|
|
129
|
+
? 'border-ink/30 bg-ink/5 text-ink'
|
|
130
|
+
: finish
|
|
131
|
+
? 'border-ink/20 bg-canvas/60 font-medium text-ink hover:border-ink/40'
|
|
132
|
+
: 'border-ink/8 bg-canvas/30 text-ink/80 hover:border-ink/25 hover:bg-ink/5 hover:text-ink'
|
|
133
|
+
}`}
|
|
134
|
+
>
|
|
135
|
+
<span
|
|
136
|
+
className={`mt-1 h-3 w-3 shrink-0 ${multi ? 'rounded-[3px]' : 'rounded-full'} border ${
|
|
137
|
+
checked ? 'border-ink bg-ink' : 'border-ink/30'
|
|
138
|
+
}`}
|
|
139
|
+
/>
|
|
140
|
+
<span className="min-w-0 flex-1">
|
|
141
|
+
<span className="flex items-center gap-1.5 font-medium">
|
|
142
|
+
<span className="truncate">{label}</span>
|
|
143
|
+
{RECOMMENDED.test(label) && !multi && (
|
|
144
|
+
<span className="shrink-0 rounded-full bg-ink/5 px-1.5 py-px font-mono text-[9px] uppercase tracking-wider text-ink/45">
|
|
145
|
+
Suggested
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
</span>
|
|
149
|
+
{description && (
|
|
150
|
+
<span className="mt-0.5 block text-[10.5px] leading-relaxed text-ink/50">{description}</span>
|
|
151
|
+
)}
|
|
152
|
+
</span>
|
|
153
|
+
</button>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
|
|
157
|
+
{!multi && (
|
|
158
|
+
<input
|
|
159
|
+
value={text}
|
|
160
|
+
placeholder="Other — type your own answer…"
|
|
161
|
+
onChange={(event) => setText(event.currentTarget.value)}
|
|
162
|
+
onKeyDown={(event) => {
|
|
163
|
+
if (event.key !== 'Enter' || !text.trim()) return;
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
onAnswer(text.trim());
|
|
166
|
+
}}
|
|
167
|
+
className="w-full rounded-md border border-ink/15 bg-canvas/40 px-2.5 py-1.5 text-[11.5px] text-ink outline-none focus:border-ink"
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One question of an `ask` call, in whichever of its three states it is in:
|
|
8
|
+
* answered (omp already recorded it), asking (its dialog is live) or still
|
|
9
|
+
* queued behind an earlier question — omp asks them strictly in order, so the
|
|
10
|
+
* later ones can only be previewed until their turn comes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CheckCircle2, Clock } from 'lucide-preact';
|
|
14
|
+
import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
|
|
15
|
+
import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
|
|
16
|
+
import type { AskAnswer, AskQuestion } from '@/shared/lib/chat/ask-questions';
|
|
17
|
+
import { FrameControl } from '@/client/components/workspace/chat-timeline/tool-renderers/ask-panel/FrameControl';
|
|
18
|
+
import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
|
|
19
|
+
|
|
20
|
+
interface QuestionBlockProps {
|
|
21
|
+
index: number;
|
|
22
|
+
total: number;
|
|
23
|
+
question: AskQuestion;
|
|
24
|
+
/** Dialogs this question raised, oldest first; the last one is live. */
|
|
25
|
+
frames: ExtensionUiDialogRequest[];
|
|
26
|
+
recorded: AskAnswer | null;
|
|
27
|
+
/** Answer picked locally for a question omp has not reached yet. */
|
|
28
|
+
draft?: string;
|
|
29
|
+
/** Unanswered, but omp has already moved past it — its dialog is gone. */
|
|
30
|
+
passed: boolean;
|
|
31
|
+
/** Labels toggled locally on a multi-select question. */
|
|
32
|
+
multiSelected: string[];
|
|
33
|
+
onAnswer: (value: string) => void;
|
|
34
|
+
onToggle: (label: string) => void;
|
|
35
|
+
onRespond: (response: ExtensionDialogResponse) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function answerParts(answer: AskAnswer): string[] {
|
|
39
|
+
const parts = [...answer.selectedOptions];
|
|
40
|
+
if (answer.customInput) parts.push(answer.customInput);
|
|
41
|
+
return parts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function QuestionBlock({
|
|
45
|
+
index,
|
|
46
|
+
total,
|
|
47
|
+
question,
|
|
48
|
+
frames,
|
|
49
|
+
recorded,
|
|
50
|
+
draft,
|
|
51
|
+
passed,
|
|
52
|
+
multiSelected,
|
|
53
|
+
onAnswer,
|
|
54
|
+
onToggle,
|
|
55
|
+
onRespond,
|
|
56
|
+
}: QuestionBlockProps) {
|
|
57
|
+
const live = frames.length > 0 ? frames[frames.length - 1] : null;
|
|
58
|
+
const answered = recorded ? answerParts(recorded) : draft ? [draft] : multiSelected;
|
|
59
|
+
const asking = Boolean(live) && !recorded && (question.multi || !draft);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="rounded-lg border border-ink/10 bg-paper p-2.5 shadow-xs">
|
|
63
|
+
<div className="flex items-start gap-2">
|
|
64
|
+
<span className="mt-px flex h-4 w-4 shrink-0 items-center justify-center rounded bg-ink/5 font-mono text-[9px] font-semibold text-ink/60">
|
|
65
|
+
{index + 1}
|
|
66
|
+
</span>
|
|
67
|
+
<div className="min-w-0 flex-1">
|
|
68
|
+
<div className="flex items-center gap-1.5">
|
|
69
|
+
<span className="text-[9px] font-semibold uppercase tracking-wider text-ink/40">
|
|
70
|
+
{question.header ?? `Question ${index + 1}`}
|
|
71
|
+
</span>
|
|
72
|
+
{question.multi && (
|
|
73
|
+
<span className="rounded-full bg-ink/5 px-1.5 py-px font-mono text-[8.5px] text-ink/45">multi</span>
|
|
74
|
+
)}
|
|
75
|
+
{total > 1 && (
|
|
76
|
+
<span className="ml-auto font-mono text-[9px] text-ink/35">
|
|
77
|
+
{index + 1}/{total}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="mt-1 text-[12.5px] leading-relaxed text-ink/90 select-text">
|
|
83
|
+
<MarkdownRenderer content={question.question} />
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{asking && live && (
|
|
87
|
+
<FrameControl
|
|
88
|
+
frame={live}
|
|
89
|
+
multi={question.multi}
|
|
90
|
+
selected={multiSelected}
|
|
91
|
+
onAnswer={onAnswer}
|
|
92
|
+
onToggle={onToggle}
|
|
93
|
+
onRespond={onRespond}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{answered.length > 0 && (
|
|
98
|
+
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
|
99
|
+
<CheckCircle2 size={11} className="shrink-0 text-success" />
|
|
100
|
+
{answered.map((part) => (
|
|
101
|
+
<span
|
|
102
|
+
key={part}
|
|
103
|
+
className="rounded-md border border-ink/12 bg-canvas/40 px-2 py-0.5 text-[11.5px] font-medium text-ink select-text"
|
|
104
|
+
>
|
|
105
|
+
{part}
|
|
106
|
+
</span>
|
|
107
|
+
))}
|
|
108
|
+
{recorded?.timedOut && (
|
|
109
|
+
<span className="rounded-full bg-ink/5 px-1.5 py-px font-mono text-[9px] text-ink/45">
|
|
110
|
+
auto-selected on timeout
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{!asking && answered.length === 0 && passed && (
|
|
117
|
+
<div className="mt-2 flex items-center gap-1.5 text-[10.5px] text-ink/45">
|
|
118
|
+
<CheckCircle2 size={11} className="shrink-0 text-ink/35" />
|
|
119
|
+
<span>Answered earlier in this run.</span>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{!asking && answered.length === 0 && !passed && (
|
|
124
|
+
<div className="mt-2 space-y-1 border-t border-ink/6 pt-2">
|
|
125
|
+
{question.options.map((option, optionIndex) => (
|
|
126
|
+
<div key={option.label} className="flex items-start gap-2 px-0.5 text-[11.5px] text-ink/45">
|
|
127
|
+
<span className="mt-1 h-3 w-3 shrink-0 rounded-full border border-ink/20" />
|
|
128
|
+
<span className="min-w-0 flex-1">
|
|
129
|
+
<span className="block truncate">
|
|
130
|
+
{option.label}
|
|
131
|
+
{question.recommended === optionIndex && (
|
|
132
|
+
<span className="ml-1.5 font-mono text-[9px] uppercase tracking-wider text-ink/35">
|
|
133
|
+
suggested
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
{option.description && (
|
|
138
|
+
<span className="mt-0.5 block text-[10.5px] leading-relaxed text-ink/35">
|
|
139
|
+
{option.description}
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
<div className="flex items-center gap-1.5 pt-0.5 text-[10.5px] text-ink/40">
|
|
146
|
+
<Clock size={10} className="shrink-0" />
|
|
147
|
+
<span>Waiting for the previous answer…</span>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|