ompchamber 0.7.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/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.86e2256a70.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 +1 -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 +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/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 +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/tailwind.css +38 -12
- package/src/server/lib/omp/rpc/manager.ts +14 -3
- 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/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/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.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/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.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
|
@@ -13,6 +13,7 @@ import { FallbackOutput } from '@/client/components/workspace/chat-timeline/tool
|
|
|
13
13
|
import { tryParseJson } from '@/shared/lib/code/syntax-highlight';
|
|
14
14
|
import { JsonCodeBlock } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock';
|
|
15
15
|
import { getTodoSummary } from '@/shared/lib/chat/todo-parser';
|
|
16
|
+
import { parseAskQuestions } from '@/shared/lib/chat/ask-questions';
|
|
16
17
|
|
|
17
18
|
interface ToolCallCardProps {
|
|
18
19
|
tool: ToolCallData;
|
|
@@ -199,6 +200,12 @@ export const ToolCallCard = memo(function ToolCallCard({ tool, isOpen, onToggle,
|
|
|
199
200
|
.join(' · ');
|
|
200
201
|
displaySubtitle = cleaned || sub;
|
|
201
202
|
}
|
|
203
|
+
} else if (toolKey === 'ask') {
|
|
204
|
+
// The question itself is the card's body; the header says how many were
|
|
205
|
+
// asked, matching the summary the same card shows once they are answered.
|
|
206
|
+
displayTitle = 'Question';
|
|
207
|
+
const asked = parseAskQuestions(tool).length;
|
|
208
|
+
if (asked > 0) displaySubtitle = `Asked ${asked} question${asked === 1 ? '' : 's'}`;
|
|
202
209
|
} else if (mcpToolNameOf(tool)) {
|
|
203
210
|
// MCP call: `write xd://mcp__<tool>` (or a direct `mcp__<tool>` name) —
|
|
204
211
|
// the MCP tool names the action, never the transport `write`.
|
|
@@ -258,7 +265,7 @@ export const ToolCallCard = memo(function ToolCallCard({ tool, isOpen, onToggle,
|
|
|
258
265
|
isOpen={isOpen}
|
|
259
266
|
onToggle={onToggle ? handleToggle : undefined}
|
|
260
267
|
defaultExpanded={defaultExpanded}
|
|
261
|
-
alwaysExpanded={toolKey === 'yield'
|
|
268
|
+
alwaysExpanded={hasPanel && (toolKey === 'yield' || toolKey === 'ask')}
|
|
262
269
|
>
|
|
263
270
|
<ToolDetailsPanel tool={tool} />
|
|
264
271
|
|
|
@@ -7,6 +7,7 @@ import { ComposerTextarea } from '@/client/components/common/ComposerTextarea';
|
|
|
7
7
|
import { AttachmentToolbar } from '@/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar';
|
|
8
8
|
import { selectableThinkingLevels } from '@/shared/lib/models/thinking-levels';
|
|
9
9
|
import { fetchModelsData, subscribeModelsUpdated } from '@/shared/lib/models/client';
|
|
10
|
+
import { resolveThinkingLevel, selectionFor } from '@/client/components/workspace/chat-timeline/chat-input/selection';
|
|
10
11
|
|
|
11
12
|
export function ChatInput({
|
|
12
13
|
value,
|
|
@@ -96,21 +97,11 @@ export function ChatInput({
|
|
|
96
97
|
const sessionModelRef = useRef<{ provider: string; modelId: string } | null>(sessionModel ?? null);
|
|
97
98
|
sessionModelRef.current = sessionModel ?? null;
|
|
98
99
|
|
|
99
|
-
// Preserve the session's last-used thinking level when applying a model
|
|
100
|
-
// picked from the catalog: the session level is authoritative and an async
|
|
101
|
-
// fetch resolving later must not reset it to the ladder default.
|
|
102
|
-
const resolveSessionLevel = (ladder: readonly string[] | undefined, fallback: string): string => {
|
|
103
|
-
const sessionLevel = sessionThinkingLevelRef.current;
|
|
104
|
-
if (!sessionLevel) return fallback;
|
|
105
|
-
const selectable = selectableThinkingLevels(ladder ?? []);
|
|
106
|
-
return selectable.length === 0 || selectable.includes(sessionLevel) ? sessionLevel : fallback;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
100
|
// Adopt the active session's last-used model as the selected model. The
|
|
110
|
-
// thinking level must fall back to the CURRENT one, never the ladder
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
101
|
+
// thinking level must fall back to the CURRENT one, never the ladder default:
|
|
102
|
+
// this effect re-runs right after a spawn (sessionModel changes while the
|
|
103
|
+
// session's own thinking_level_change entry is not readable yet), and any
|
|
104
|
+
// concrete fallback here claims a level the run never used.
|
|
114
105
|
useEffect(() => {
|
|
115
106
|
if (!sessionModel?.provider || !sessionModel.modelId) return;
|
|
116
107
|
let active = true;
|
|
@@ -121,14 +112,7 @@ export function ChatInput({
|
|
|
121
112
|
(m) => m.id === sessionModel.modelId && m.provider === sessionModel.provider
|
|
122
113
|
);
|
|
123
114
|
if (match) {
|
|
124
|
-
setSelectedModel(prev => (
|
|
125
|
-
id: match.id,
|
|
126
|
-
name: match.name,
|
|
127
|
-
provider: match.provider,
|
|
128
|
-
contextWindow: match.contextWindow,
|
|
129
|
-
thinkingLevels: match.thinkingLevels,
|
|
130
|
-
thinkingLevel: resolveSessionLevel(match.thinkingLevels, prev?.thinkingLevel ?? match.thinkingLevels?.[0] ?? 'off'),
|
|
131
|
-
}));
|
|
115
|
+
setSelectedModel(prev => selectionFor(match, sessionThinkingLevelRef.current, prev));
|
|
132
116
|
}
|
|
133
117
|
})
|
|
134
118
|
.catch(() => {});
|
|
@@ -143,14 +127,7 @@ export function ChatInput({
|
|
|
143
127
|
if (!sessionThinkingLevel) return;
|
|
144
128
|
setSelectedModel(prev => {
|
|
145
129
|
if (!prev) return prev;
|
|
146
|
-
const
|
|
147
|
-
// The session level is authoritative (omp recorded it for this model).
|
|
148
|
-
// Only reject it when the ladder is KNOWN and explicitly excludes it;
|
|
149
|
-
// an empty ladder (catalog not resolved / model exposes none) must not
|
|
150
|
-
// fall back to 'off' and erase the session's actual level.
|
|
151
|
-
const level = selectable.length === 0 || selectable.includes(sessionThinkingLevel)
|
|
152
|
-
? sessionThinkingLevel
|
|
153
|
-
: prev.thinkingLevel;
|
|
130
|
+
const level = resolveThinkingLevel(prev.thinkingLevels, sessionThinkingLevel, prev.thinkingLevel);
|
|
154
131
|
return level === prev.thinkingLevel ? prev : { ...prev, thinkingLevel: level };
|
|
155
132
|
});
|
|
156
133
|
}, [sessionThinkingLevel]);
|
|
@@ -169,13 +146,7 @@ export function ChatInput({
|
|
|
169
146
|
if (sm?.provider && sm.modelId) {
|
|
170
147
|
const match = data.modelList.find((m: ModelEntry) => m.id === sm.modelId && m.provider === sm.provider);
|
|
171
148
|
if (match) {
|
|
172
|
-
setSelectedModel(prev => (
|
|
173
|
-
id: match.id,
|
|
174
|
-
name: match.name,
|
|
175
|
-
provider: match.provider,
|
|
176
|
-
thinkingLevels: match.thinkingLevels,
|
|
177
|
-
thinkingLevel: resolveSessionLevel(match.thinkingLevels, prev?.thinkingLevel ?? match.thinkingLevels?.[0] ?? 'off'),
|
|
178
|
-
}));
|
|
149
|
+
setSelectedModel(prev => selectionFor(match, sessionThinkingLevelRef.current, prev));
|
|
179
150
|
}
|
|
180
151
|
return;
|
|
181
152
|
}
|
|
@@ -184,13 +155,7 @@ export function ChatInput({
|
|
|
184
155
|
if (persisted?.provider && persisted.id) {
|
|
185
156
|
const match = data.modelList.find((m: ModelEntry) => m.id === persisted.id && m.provider === persisted.provider);
|
|
186
157
|
if (match) {
|
|
187
|
-
setSelectedModel(prev => (
|
|
188
|
-
id: match.id,
|
|
189
|
-
name: match.name,
|
|
190
|
-
provider: match.provider,
|
|
191
|
-
thinkingLevels: match.thinkingLevels,
|
|
192
|
-
thinkingLevel: resolveSessionLevel(match.thinkingLevels, prev?.thinkingLevel ?? match.thinkingLevels?.[0] ?? 'off'),
|
|
193
|
-
}));
|
|
158
|
+
setSelectedModel(prev => selectionFor(match, sessionThinkingLevelRef.current, prev));
|
|
194
159
|
return;
|
|
195
160
|
}
|
|
196
161
|
}
|
|
@@ -199,20 +164,14 @@ export function ChatInput({
|
|
|
199
164
|
if (defaultModel) {
|
|
200
165
|
const match = data.modelList.find((m: ModelEntry) => m.id === defaultModel.modelId && m.provider === defaultModel.provider);
|
|
201
166
|
if (match) {
|
|
202
|
-
setSelectedModel(prev => (
|
|
203
|
-
id: match.id,
|
|
204
|
-
name: match.name,
|
|
205
|
-
provider: match.provider,
|
|
206
|
-
thinkingLevels: match.thinkingLevels,
|
|
207
|
-
thinkingLevel: resolveSessionLevel(match.thinkingLevels, prev?.thinkingLevel ?? match.thinkingLevels?.[0] ?? 'off'),
|
|
208
|
-
}));
|
|
167
|
+
setSelectedModel(prev => selectionFor(match, sessionThinkingLevelRef.current, prev));
|
|
209
168
|
}
|
|
210
169
|
}
|
|
211
170
|
} else if (data.selectedModel) {
|
|
212
171
|
const selected = data.selectedModel;
|
|
213
172
|
setSelectedModel(prev => ({
|
|
214
173
|
...selected,
|
|
215
|
-
thinkingLevel:
|
|
174
|
+
thinkingLevel: resolveThinkingLevel(selected.thinkingLevels, sessionThinkingLevelRef.current, prev?.thinkingLevel ?? selected.thinkingLevel),
|
|
216
175
|
}));
|
|
217
176
|
}
|
|
218
177
|
} catch {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Catalog → composer mapping for the model/thinking pickers: one definition of
|
|
8
|
+
* how a catalog entry becomes the selected model, and of which thinking level
|
|
9
|
+
* that selection is allowed to claim. Shared by every resolution site in
|
|
10
|
+
* ChatInput so the level rule cannot drift between them.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AIModelOption, ModelEntry } from '@/shared/types';
|
|
14
|
+
import { selectableThinkingLevels } from '@/shared/lib/models/thinking-levels';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Level reported while none is known. A catalog ladder's first entry is NOT a
|
|
18
|
+
* fact about the session — every model's ladder starts at `off` — and the send
|
|
19
|
+
* path pushes any non-`auto` value onto the live session (queue replay), so an
|
|
20
|
+
* unresolved level must stay indistinguishable from "leave omp alone".
|
|
21
|
+
*/
|
|
22
|
+
export const UNKNOWN_THINKING_LEVEL = 'auto';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Level the composer may select for a model. The session's last-used level is
|
|
26
|
+
* authoritative whenever the model offers it — an async fetch resolving later
|
|
27
|
+
* must not reset it to a ladder default. Only a ladder that is KNOWN and
|
|
28
|
+
* excludes the session level rejects it; an empty ladder (catalog not resolved
|
|
29
|
+
* / model exposes none) must not erase the session's actual level. With no
|
|
30
|
+
* session level to go on, the current selection is kept, and a composer that
|
|
31
|
+
* has none yet reports {@link UNKNOWN_THINKING_LEVEL}.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveThinkingLevel(
|
|
34
|
+
ladder: readonly string[] | null | undefined,
|
|
35
|
+
sessionLevel: string | null | undefined,
|
|
36
|
+
currentLevel: string | null | undefined,
|
|
37
|
+
): string {
|
|
38
|
+
const selectable = selectableThinkingLevels(ladder ?? []);
|
|
39
|
+
if (sessionLevel && (selectable.length === 0 || selectable.includes(sessionLevel))) return sessionLevel;
|
|
40
|
+
return currentLevel ?? UNKNOWN_THINKING_LEVEL;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The composer's selection for a catalog model, carrying its thinking level. */
|
|
44
|
+
export function selectionFor(
|
|
45
|
+
match: ModelEntry,
|
|
46
|
+
sessionLevel: string | null | undefined,
|
|
47
|
+
current: AIModelOption | null,
|
|
48
|
+
): AIModelOption {
|
|
49
|
+
return {
|
|
50
|
+
id: match.id,
|
|
51
|
+
name: match.name,
|
|
52
|
+
provider: match.provider,
|
|
53
|
+
contextWindow: match.contextWindow,
|
|
54
|
+
thinkingLevels: match.thinkingLevels,
|
|
55
|
+
thinkingLevel: resolveThinkingLevel(match.thinkingLevels, sessionLevel, current?.thinkingLevel),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -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
|
+
}
|