ompchamber 3.3.3 → 3.5.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/index.js +315 -308
- package/dist/client/static/js/index-vx0ernrv.css +1 -0
- package/dist/client/static/js/{index-8ejde0b7.js → index-w0cem7n5.js} +641 -641
- package/package.json +1 -1
- package/src/client/components/common/session-actions-menu/index.tsx +201 -0
- package/src/client/components/common/session-delete-modal/index.tsx +128 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +18 -0
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +109 -99
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +18 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +82 -64
- package/src/client/components/settings/categories/AppearanceSettings.tsx +1 -1
- package/src/client/components/settings/categories/ChatSettings.tsx +1 -1
- package/src/client/components/workspace/btw-panel/AskCard.tsx +6 -1
- package/src/client/components/workspace/btw-panel/Messages.tsx +1 -1
- package/src/client/components/workspace/btw-panel/TopicMenu.tsx +1 -1
- package/src/client/components/workspace/btw-panel/index.tsx +20 -27
- package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +29 -18
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +9 -39
- package/src/client/components/workspace/chat-timeline/chat-input/props.ts +62 -0
- package/src/client/hooks/chat/btw/index.ts +7 -73
- package/src/client/hooks/chat/btw/intercept.test.ts +80 -0
- package/src/client/hooks/chat/btw/intercept.ts +5 -2
- package/src/client/hooks/chat/btw/mode.ts +16 -76
- package/src/client/hooks/chat/timeline/actions.ts +22 -30
- package/src/client/hooks/chat/timeline/queue-actions.ts +88 -0
- package/src/client/hooks/chat/timeline/send.ts +8 -1
- package/src/client/hooks/chat/timeline/tui-only-guard.ts +58 -0
- package/src/client/hooks/workspace/session-delete.ts +94 -0
- package/src/server/lib/btw/child.server.test.ts +68 -0
- package/src/server/lib/btw/child.server.ts +17 -16
- package/src/server/lib/btw/frames.test.ts +3 -3
- package/src/server/lib/btw/frames.ts +1 -1
- package/src/server/lib/btw/lifecycle.server.test.ts +139 -0
- package/src/server/lib/btw/lifecycle.server.ts +41 -31
- package/src/server/lib/btw/prompt.ts +7 -11
- package/src/server/lib/btw/purge.server.ts +56 -0
- package/src/server/lib/btw/registry.server.ts +1 -9
- package/src/server/lib/btw/reply.ts +54 -0
- package/src/server/lib/btw/runtime.server.test.ts +128 -0
- package/src/server/lib/btw/runtime.server.ts +77 -86
- package/src/server/lib/btw/schema.server.test.ts +84 -0
- package/src/server/lib/btw/schema.server.ts +8 -0
- package/src/server/lib/btw/service.server.ts +47 -78
- package/src/server/lib/btw/store.server.ts +9 -20
- package/src/server/lib/fs/path-segment.test.ts +100 -0
- package/src/server/lib/fs/path-segment.ts +45 -0
- package/src/server/lib/omp/rpc/constants.ts +10 -0
- package/src/server/lib/omp/rpc/session-commands.ts +45 -0
- package/src/server/lib/omp/session/delete.server.ts +136 -0
- package/src/server/lib/omp/session/merge-stored-notices.test.ts +81 -0
- package/src/server/lib/omp/session/merge-stored.ts +70 -1
- package/src/server/lib/omp/session/messages.ts +27 -2
- package/src/server/lib/omp/session/reader.ts +13 -0
- package/src/server/routes/agent/command.ts +9 -1
- package/src/server/routes/btw/topics.ts +2 -59
- package/src/server/routes/sessions/delete.test.ts +84 -0
- package/src/server/routes/sessions/delete.ts +83 -0
- package/src/server/routes/sessions/index.ts +5 -0
- package/src/shared/lib/chat/btw/client.ts +0 -30
- package/src/shared/lib/chat/composer/tui-only.test.ts +90 -0
- package/src/shared/lib/chat/composer/tui-only.ts +138 -0
- package/src/shared/lib/chat/notice-text.ts +33 -0
- package/src/shared/lib/chat/omp/agent-events.ts +18 -37
- package/src/shared/lib/chat/omp/available-commands-cache.test.ts +66 -0
- package/src/shared/lib/chat/omp/command-output-ansi.test.ts +84 -0
- package/src/shared/lib/chat/omp/fold-deps.ts +48 -0
- package/src/shared/lib/chat/timeline/command-output.ts +65 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +4 -17
- package/src/shared/lib/omp/session/messages-map.ts +6 -1
- package/src/shared/lib/workspace/session-state/store.ts +20 -0
- package/src/shared/types/btw.ts +13 -34
- package/dist/client/static/js/index-bsxxt89j.css +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Check, ChevronDown, ChevronRight, CircleQuestionMark, Loader2, MoreHorizontal } from 'lucide-preact';
|
|
2
2
|
import type { SessionItemData } from '@/shared/types';
|
|
3
3
|
import { useInlineRename } from '@/client/hooks/ui/inline-rename';
|
|
4
|
+
import { SessionActionsMenu, useRowMenu } from '@/client/components/common/session-actions-menu';
|
|
4
5
|
|
|
5
6
|
export interface MobileSessionRowProps {
|
|
6
7
|
session: SessionItemData;
|
|
@@ -16,6 +17,9 @@ export interface MobileSessionRowProps {
|
|
|
16
17
|
onToggleExpand?: () => void;
|
|
17
18
|
onSelect: () => void;
|
|
18
19
|
onArchive: () => void;
|
|
20
|
+
/** Omitted for a session that cannot be deleted yet (a pending `new-…` chat,
|
|
21
|
+
* which has no transcript anywhere). */
|
|
22
|
+
onDelete?: () => void;
|
|
19
23
|
onRename?: (name: string) => void;
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -30,6 +34,7 @@ export function MobileSessionRow({
|
|
|
30
34
|
onToggleExpand,
|
|
31
35
|
onSelect,
|
|
32
36
|
onArchive,
|
|
37
|
+
onDelete,
|
|
33
38
|
onRename,
|
|
34
39
|
}: MobileSessionRowProps) {
|
|
35
40
|
const {
|
|
@@ -41,6 +46,9 @@ export function MobileSessionRow({
|
|
|
41
46
|
handleKeyDown,
|
|
42
47
|
handleBlur,
|
|
43
48
|
} = useInlineRename(session.title, onRename);
|
|
49
|
+
// One trigger for every row action; also reachable by long-press contextmenu.
|
|
50
|
+
const menu = useRowMenu();
|
|
51
|
+
const hasActions = Boolean(onRename || onArchive || onDelete);
|
|
44
52
|
|
|
45
53
|
// The roster toggle is the row's LAST element, so its glyph lands in the same
|
|
46
54
|
// column as the folder header's expand chevron — one vertical line of
|
|
@@ -71,76 +79,86 @@ export function MobileSessionRow({
|
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
return (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
type="button"
|
|
81
|
-
onClick={onSelect}
|
|
82
|
-
className="flex-1 text-left pl-2 pr-3 py-2 flex items-center justify-between min-w-0"
|
|
82
|
+
<>
|
|
83
|
+
<div
|
|
84
|
+
onContextMenu={hasActions ? menu.openAtCursor : undefined}
|
|
85
|
+
className={`w-full rounded-lg flex items-center transition-colors ${
|
|
86
|
+
isActive ? 'bg-ink/10 font-medium text-ink' : 'hover:bg-ink/5 text-ink/85'
|
|
87
|
+
}`}
|
|
83
88
|
>
|
|
84
|
-
<div className="flex items-center min-w-0 pr-2">
|
|
85
|
-
{/* Status slot, 16px at the same x as the folder header's icon slot,
|
|
86
|
-
so the left column reads as one line. Waiting on an answer
|
|
87
|
-
outranks the run spinner — the spinner says work is happening,
|
|
88
|
-
the question mark says it is YOUR turn (desktop parity). */}
|
|
89
|
-
<span className="w-4 flex-shrink-0 flex items-center justify-center">
|
|
90
|
-
{awaitingInput ? (
|
|
91
|
-
<CircleQuestionMark size={13} className="text-ink/70 animate-pulse" />
|
|
92
|
-
) : status === 'stream' ? (
|
|
93
|
-
<Loader2 size={13} className="text-ink/50 animate-spin" />
|
|
94
|
-
) : status ? (
|
|
95
|
-
<Check size={13} className="text-ink/50" />
|
|
96
|
-
) : null}
|
|
97
|
-
</span>
|
|
98
|
-
<span className="text-xs truncate leading-snug ml-2">{title}</span>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
<span className="text-[11px] text-ink/45 font-mono flex-shrink-0 ml-2">{timeAgo ?? ''}</span>
|
|
102
|
-
</button>
|
|
103
|
-
|
|
104
|
-
{onRename && (
|
|
105
89
|
<button
|
|
106
90
|
type="button"
|
|
107
|
-
onClick={
|
|
108
|
-
|
|
109
|
-
className="flex-shrink-0 p-2 text-ink/35 hover:text-ink rounded-lg cursor-pointer"
|
|
91
|
+
onClick={onSelect}
|
|
92
|
+
className="flex-1 text-left pl-2 pr-3 py-2 flex items-center justify-between min-w-0"
|
|
110
93
|
>
|
|
111
|
-
<
|
|
94
|
+
<div className="flex items-center min-w-0 pr-2">
|
|
95
|
+
{/* Status slot, 16px at the same x as the folder header's icon slot,
|
|
96
|
+
so the left column reads as one line. Waiting on an answer
|
|
97
|
+
outranks the run spinner — the spinner says work is happening,
|
|
98
|
+
the question mark says it is YOUR turn (desktop parity). */}
|
|
99
|
+
<span className="w-4 flex-shrink-0 flex items-center justify-center">
|
|
100
|
+
{awaitingInput ? (
|
|
101
|
+
<CircleQuestionMark size={13} className="text-ink/70 animate-pulse" />
|
|
102
|
+
) : status === 'stream' ? (
|
|
103
|
+
<Loader2 size={13} className="text-ink/50 animate-spin" />
|
|
104
|
+
) : status ? (
|
|
105
|
+
<Check size={13} className="text-ink/50" />
|
|
106
|
+
) : null}
|
|
107
|
+
</span>
|
|
108
|
+
<span className="text-xs truncate leading-snug ml-2">{title}</span>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<span className="text-[11px] text-ink/45 font-mono flex-shrink-0 ml-2">{timeAgo ?? ''}</span>
|
|
112
112
|
</button>
|
|
113
|
-
)}
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
114
|
+
{/* One trigger for rename / archive / delete, always visible: a touch
|
|
115
|
+
surface has no hover, and the always-on action buttons this replaced
|
|
116
|
+
consumed a third of a phone's row width.
|
|
117
|
+
`mr-1` only when the row has NO roster toggle — it reserves that
|
|
118
|
+
column so the trailing glyph's centre stays 19px from the row's right
|
|
119
|
+
edge whether the last element is this trigger or the chevron. Without
|
|
120
|
+
it the glyphs step in and out from row to row. */}
|
|
121
|
+
{hasActions && (
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={menu.openBelow}
|
|
125
|
+
title="Session actions"
|
|
126
|
+
aria-label="Session actions"
|
|
127
|
+
aria-haspopup="menu"
|
|
128
|
+
aria-expanded={menu.anchor !== null}
|
|
129
|
+
className={`flex-shrink-0 p-2 text-ink/35 hover:text-ink rounded-lg cursor-pointer ${showChevron ? '' : 'mr-1'}`}
|
|
130
|
+
>
|
|
131
|
+
<MoreHorizontal size={16} />
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
135
|
+
{/* Last element, so its glyph sits in the folder header's chevron column.
|
|
136
|
+
`p-1.5 mr-1.5` centres a 14px glyph on that column while keeping a
|
|
137
|
+
touch-sized tap target. */}
|
|
138
|
+
{showChevron && (
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={onToggleExpand}
|
|
142
|
+
title={isExpanded ? 'Collapse subagents' : 'Expand subagents'}
|
|
143
|
+
aria-expanded={isExpanded}
|
|
144
|
+
aria-label={isExpanded ? 'Collapse subagents' : 'Expand subagents'}
|
|
145
|
+
className="flex-shrink-0 p-1.5 mr-1.5 flex items-center justify-center text-ink/50 hover:text-ink rounded-lg cursor-pointer"
|
|
146
|
+
>
|
|
147
|
+
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{menu.anchor && (
|
|
153
|
+
<SessionActionsMenu
|
|
154
|
+
anchor={menu.anchor}
|
|
155
|
+
isArchived={session.is_archived === 1}
|
|
156
|
+
onRename={onRename ? startRename : undefined}
|
|
157
|
+
onArchive={onArchive}
|
|
158
|
+
onDelete={onDelete}
|
|
159
|
+
onClose={menu.close}
|
|
160
|
+
/>
|
|
143
161
|
)}
|
|
144
|
-
|
|
162
|
+
</>
|
|
145
163
|
);
|
|
146
164
|
}
|
|
@@ -11,7 +11,7 @@ interface AppearanceSettingsProps {
|
|
|
11
11
|
|
|
12
12
|
export function AppearanceSettings({ settings, onUpdate }: AppearanceSettingsProps) {
|
|
13
13
|
return (
|
|
14
|
-
<div className="w-full space-y-8 text-xs text-ink">
|
|
14
|
+
<div className="w-full space-y-8 text-xs text-ink pb-3">
|
|
15
15
|
<section className="space-y-4">
|
|
16
16
|
<div className="flex items-center space-x-2 border-b border-ink/10 pb-2">
|
|
17
17
|
<Palette size={16} className="text-ink/60" />
|
|
@@ -11,7 +11,7 @@ interface ChatSettingsProps {
|
|
|
11
11
|
|
|
12
12
|
export function ChatSettings({ settings, onUpdate }: ChatSettingsProps) {
|
|
13
13
|
return (
|
|
14
|
-
<div className="w-full space-y-7 text-xs text-ink">
|
|
14
|
+
<div className="w-full space-y-7 text-xs text-ink pb-3">
|
|
15
15
|
{/* 1. Streaming Transport (WebSocket vs SSE) */}
|
|
16
16
|
<ChatTransportSection settings={settings} onUpdate={onUpdate} />
|
|
17
17
|
|
|
@@ -31,6 +31,8 @@ export interface BtwAskCardProps {
|
|
|
31
31
|
onNewQuestion: () => void;
|
|
32
32
|
/** `✕` — leave the side-question form. */
|
|
33
33
|
onClose: () => void;
|
|
34
|
+
/** Escape — cancel the running answer first. Returns whether one was running. */
|
|
35
|
+
onCancel: () => boolean;
|
|
34
36
|
/** A side answer is streaming: starting over waits for it. */
|
|
35
37
|
running: boolean;
|
|
36
38
|
topics: BtwTopic[];
|
|
@@ -50,6 +52,7 @@ export function BtwAskCard({
|
|
|
50
52
|
onSubmit,
|
|
51
53
|
onNewQuestion,
|
|
52
54
|
onClose,
|
|
55
|
+
onCancel,
|
|
53
56
|
running,
|
|
54
57
|
topics,
|
|
55
58
|
activeTopicId,
|
|
@@ -72,7 +75,9 @@ export function BtwAskCard({
|
|
|
72
75
|
const handleKeyDown = (event: TargetedKeyboardEvent<HTMLTextAreaElement>) => {
|
|
73
76
|
if (event.key === 'Escape') {
|
|
74
77
|
event.preventDefault();
|
|
75
|
-
|
|
78
|
+
// Cancel first, close only when nothing was running — omp's Escape handler
|
|
79
|
+
// does the same, so one keypress cannot abandon a question mid-flight.
|
|
80
|
+
if (!onCancel()) onClose();
|
|
76
81
|
return;
|
|
77
82
|
}
|
|
78
83
|
if (event.key !== 'Enter' || event.shiftKey) return;
|
|
@@ -48,7 +48,7 @@ function AnswerStatus({ status }: { status: BtwTurnStatus }) {
|
|
|
48
48
|
return <div className="text-[11px] text-ink/50">Cancelled</div>;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
return <div className="text-[11px] text-error">{status === '
|
|
51
|
+
return <div className="text-[11px] text-error">{status === 'error' ? 'Error' : 'Interrupted'}</div>;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** The question row for a turn, in the chat's own user-bubble shape. */
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
* question — and promotion is the one explicit action that moves an answer into
|
|
24
24
|
* the chat, as a session branched from it.
|
|
25
25
|
*
|
|
26
|
-
* The composer card is the CHAT's own `ChatInput`, not a look-alike:
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* The composer card is the CHAT's own `ChatInput`, not a look-alike: the same
|
|
27
|
+
* attachments, drop targets and keybindings. Only the autocomplete is off
|
|
28
|
+
* (`enablePicker`) — a side question has no file tree, commands or skills to
|
|
29
|
+
* complete against — and so are the model / thinking / access controls: the side
|
|
30
|
+
* child runs on the chat's model and thinking selector, without tools, so those
|
|
31
|
+
* dropdowns would change nothing. That is exactly what omp's own `/btw` offers.
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
import { useState } from 'preact/hooks';
|
|
@@ -37,7 +38,6 @@ import { BtwAskCard } from '@/client/components/workspace/btw-panel/AskCard';
|
|
|
37
38
|
import { BtwMessages } from '@/client/components/workspace/btw-panel/Messages';
|
|
38
39
|
import { ChatInput } from '@/client/components/workspace/chat-timeline/chat-input/index';
|
|
39
40
|
import { GeneratingIndicator } from '@/client/components/workspace/chat-timeline/GeneratingIndicator';
|
|
40
|
-
import { ExtensionDialog } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
|
|
41
41
|
|
|
42
42
|
/** Tallest the answer card grows before its own scrollbar takes over. */
|
|
43
43
|
const ANSWERS_MAX_HEIGHT_PX = 520;
|
|
@@ -70,9 +70,15 @@ export function BtwForm({
|
|
|
70
70
|
// the same click burst would be a second request for work already running.
|
|
71
71
|
// An already-promoted topic needs no second button either — its branch
|
|
72
72
|
// session exists, and the server hands that id back on a repeat promote.
|
|
73
|
+
// A topic with follow-ups cannot be promoted (a branch carries one pair), so
|
|
74
|
+
// the action is offered only where the server would accept it.
|
|
73
75
|
const canPromote =
|
|
74
|
-
Boolean(mode.activeTopic) &&
|
|
75
|
-
|
|
76
|
+
Boolean(mode.activeTopic) &&
|
|
77
|
+
mode.activeTopic!.turns.length === 1 &&
|
|
78
|
+
!mode.runningTopicId &&
|
|
79
|
+
!mode.busy &&
|
|
80
|
+
!mode.activeTopic?.promotedSessionId &&
|
|
81
|
+
lastTurn?.status === 'complete';
|
|
76
82
|
const running = Boolean(mode.runningTopicId);
|
|
77
83
|
|
|
78
84
|
return (
|
|
@@ -91,6 +97,7 @@ export function BtwForm({
|
|
|
91
97
|
onSubmit={mode.submitAsk}
|
|
92
98
|
onNewQuestion={mode.resetQuestion}
|
|
93
99
|
onClose={mode.exit}
|
|
100
|
+
onCancel={mode.abort}
|
|
94
101
|
running={running}
|
|
95
102
|
topics={mode.topics}
|
|
96
103
|
activeTopicId={mode.activeTopic?.id ?? null}
|
|
@@ -107,7 +114,7 @@ export function BtwForm({
|
|
|
107
114
|
provider={mode.activeTopic?.model?.provider ?? provider}
|
|
108
115
|
providerNames={providerNames}
|
|
109
116
|
modelName={mode.activeTopic?.model?.name ?? modelName}
|
|
110
|
-
thinkingLevel={mode.thinkingLevel}
|
|
117
|
+
thinkingLevel={mode.activeTopic?.thinkingLevel}
|
|
111
118
|
isMobile={variant === 'mobile'}
|
|
112
119
|
/>
|
|
113
120
|
|
|
@@ -135,33 +142,19 @@ export function BtwForm({
|
|
|
135
142
|
setAttachments([]);
|
|
136
143
|
}}
|
|
137
144
|
isGenerating={running}
|
|
138
|
-
onStop={mode.abort}
|
|
145
|
+
onStop={() => void mode.abort()}
|
|
139
146
|
appSettings={appSettings}
|
|
140
147
|
attachments={attachments}
|
|
141
148
|
onAttachmentsChange={setAttachments}
|
|
142
149
|
rootPath={rootPath}
|
|
143
150
|
variant={variant}
|
|
144
151
|
enablePicker={false}
|
|
152
|
+
showModel={false}
|
|
153
|
+
showThinking={false}
|
|
154
|
+
showAccess={false}
|
|
145
155
|
placeholder="Ask in this btw session..."
|
|
146
|
-
accessMode={mode.accessMode}
|
|
147
|
-
onAccessModeChange={mode.setAccessMode}
|
|
148
|
-
onThinkingLevelChange={mode.setThinkingLevel}
|
|
149
|
-
onModelChange={mode.setModel}
|
|
150
|
-
sessionModel={mode.activeTopic?.model ? { provider: mode.activeTopic.model.provider, modelId: mode.activeTopic.model.id } : null}
|
|
151
|
-
sessionThinkingLevel={mode.thinkingLevel}
|
|
152
156
|
composerModelRef={composerModelRef}
|
|
153
157
|
/>
|
|
154
|
-
|
|
155
|
-
{/* A side child runs with tools, so a gated call parks its turn on an
|
|
156
|
-
approval dialog. There is no tool card here to host one inline (unlike
|
|
157
|
-
the chat's `ask`), so every answerable request is a modal — and the
|
|
158
|
-
dialog brings its own backdrop. */}
|
|
159
|
-
{dialog && (
|
|
160
|
-
<ExtensionDialog
|
|
161
|
-
request={dialog.request}
|
|
162
|
-
onRespond={(_, response) => mode.respondToDialog(dialog.request.id, response)}
|
|
163
|
-
/>
|
|
164
|
-
)}
|
|
165
158
|
</div>
|
|
166
159
|
);
|
|
167
160
|
}
|
|
@@ -3,7 +3,7 @@ import type { AIModelOption } from '@/shared/types';
|
|
|
3
3
|
import { ModelDropdown } from '@/client/components/workspace/model-dropdown/index';
|
|
4
4
|
import { ThinkingLevelDropdown } from '@/client/components/workspace/chat-timeline/chat-input/ThinkingLevelDropdown';
|
|
5
5
|
import { AccessDropdown } from '@/client/components/workspace/chat-timeline/chat-input/AccessDropdown';
|
|
6
|
-
import type
|
|
6
|
+
import { DEFAULT_APPROVAL_MODE, type ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
7
7
|
|
|
8
8
|
export interface ComposerToolbarProps {
|
|
9
9
|
isMobile: boolean;
|
|
@@ -21,8 +21,12 @@ export interface ComposerToolbarProps {
|
|
|
21
21
|
onSend: () => void;
|
|
22
22
|
/** Send is blocked while no workspace context is selected or nothing is typed. */
|
|
23
23
|
sendDisabled: boolean;
|
|
24
|
-
accessMode
|
|
25
|
-
onSelectAccess
|
|
24
|
+
accessMode?: ApprovalMode;
|
|
25
|
+
onSelectAccess?: (mode: ApprovalMode) => void;
|
|
26
|
+
/** Which selectors render; all true for the chat, all false for a side question. */
|
|
27
|
+
showModel?: boolean;
|
|
28
|
+
showThinking?: boolean;
|
|
29
|
+
showAccess?: boolean;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -45,32 +49,39 @@ export function ComposerToolbar({
|
|
|
45
49
|
onStop,
|
|
46
50
|
onSend,
|
|
47
51
|
sendDisabled,
|
|
48
|
-
accessMode,
|
|
52
|
+
accessMode = DEFAULT_APPROVAL_MODE,
|
|
49
53
|
onSelectAccess,
|
|
54
|
+
showModel = true,
|
|
55
|
+
showThinking = true,
|
|
56
|
+
showAccess = true,
|
|
50
57
|
}: ComposerToolbarProps) {
|
|
51
58
|
const showStop = isGenerating && (isMobile ? Boolean(onStop) : true);
|
|
52
59
|
|
|
53
60
|
return (
|
|
54
61
|
<div className={`flex items-center justify-between border-t border-ink/5 bg-canvas/50 rounded-b-md ${isMobile ? 'px-2 py-2' : 'px-3 py-2'}`}>
|
|
55
62
|
<div className={`flex items-center min-w-0 ${isMobile ? 'space-x-1' : 'space-x-2'}`}>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
{showModel && (
|
|
64
|
+
<ModelDropdown
|
|
65
|
+
selectedModel={selectedModel ?? undefined}
|
|
66
|
+
onSelectModel={onSelectModel}
|
|
67
|
+
onThinkingLevelChange={onThinkingLevelChange}
|
|
68
|
+
className={isMobile ? 'min-w-0 max-w-[58%]' : undefined}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
62
71
|
|
|
63
|
-
<div className="w-[1px] h-3 bg-ink/10" />
|
|
72
|
+
{showModel && showThinking && <div className="w-[1px] h-3 bg-ink/10" />}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
{showThinking && (
|
|
75
|
+
<ThinkingLevelDropdown
|
|
76
|
+
thinkingLevels={thinkingLevels}
|
|
77
|
+
currentThinking={currentThinking}
|
|
78
|
+
onSelect={onSelectThinking}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
70
81
|
|
|
71
|
-
<div className="w-[1px] h-3 bg-ink/10" />
|
|
82
|
+
{(showModel || showThinking) && showAccess && <div className="w-[1px] h-3 bg-ink/10" />}
|
|
72
83
|
|
|
73
|
-
<AccessDropdown value={accessMode} onSelect={onSelectAccess} />
|
|
84
|
+
{showAccess && onSelectAccess && <AccessDropdown value={accessMode} onSelect={onSelectAccess} />}
|
|
74
85
|
</div>
|
|
75
86
|
|
|
76
87
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
|
2
2
|
import type { ClipboardEvent, SetStateAction } from 'preact/compat';
|
|
3
3
|
import { AlertTriangle, X } from 'lucide-preact';
|
|
4
4
|
import type { AIModelOption, Attachment, ModelEntry } from '@/shared/types';
|
|
5
|
-
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
6
5
|
import { ComposerToolbar } from '@/client/components/workspace/chat-timeline/chat-input/Toolbar';
|
|
7
6
|
import { ComposerTextarea } from '@/client/components/common/ComposerTextarea';
|
|
8
7
|
import { AttachmentToolbar } from '@/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar';
|
|
@@ -13,6 +12,7 @@ import { NO_PENDING_PICK } from '@/client/hooks/chat/timeline/deferred-model';
|
|
|
13
12
|
import { primeFileReads } from '@/client/hooks/chat/composer/file-reads';
|
|
14
13
|
import { useComposerPipeline } from '@/client/hooks/chat/composer/pipeline';
|
|
15
14
|
import { DropOverlay } from '@/client/components/workspace/chat-timeline/chat-input/DropOverlay';
|
|
15
|
+
import type { ChatInputProps } from '@/client/components/workspace/chat-timeline/chat-input/props';
|
|
16
16
|
|
|
17
17
|
export function ChatInput({
|
|
18
18
|
value,
|
|
@@ -33,6 +33,9 @@ export function ChatInput({
|
|
|
33
33
|
variant = 'desktop',
|
|
34
34
|
accessMode,
|
|
35
35
|
onAccessModeChange,
|
|
36
|
+
showModel = true,
|
|
37
|
+
showThinking = true,
|
|
38
|
+
showAccess = true,
|
|
36
39
|
enablePicker = true,
|
|
37
40
|
placeholder,
|
|
38
41
|
/** Written on every model/thinking pick so the send path can snapshot the
|
|
@@ -44,43 +47,7 @@ export function ChatInput({
|
|
|
44
47
|
* by composers that never stream (the New Chat modal), where there is
|
|
45
48
|
* nothing to defer and adoption must behave as before. */
|
|
46
49
|
deferredComposerPickRef = NO_PENDING_PICK,
|
|
47
|
-
}: {
|
|
48
|
-
value: string;
|
|
49
|
-
onChange: (v: string) => void;
|
|
50
|
-
onSend: (attachments: Attachment[], options?: { steering?: boolean }) => void;
|
|
51
|
-
isGenerating: boolean;
|
|
52
|
-
onStop?: () => void;
|
|
53
|
-
className?: string;
|
|
54
|
-
disabled?: boolean;
|
|
55
|
-
appSettings?: Record<string, any>;
|
|
56
|
-
attachments?: Attachment[];
|
|
57
|
-
onAttachmentsChange?: (attachments: SetStateAction<Attachment[]>) => void;
|
|
58
|
-
onThinkingLevelChange?: (level: string) => void;
|
|
59
|
-
onModelChange?: (provider: string, modelId: string) => void;
|
|
60
|
-
/** Model last used by the active session (omp `model_change` entry). */
|
|
61
|
-
sessionModel?: { provider: string; modelId: string } | null;
|
|
62
|
-
/** Thinking level last used by the active session (omp `thinking_level_change` entry). */
|
|
63
|
-
sessionThinkingLevel?: string | null;
|
|
64
|
-
rootPath?: string | null;
|
|
65
|
-
accessMode: ApprovalMode;
|
|
66
|
-
onAccessModeChange: (mode: ApprovalMode) => void;
|
|
67
|
-
/**
|
|
68
|
-
* Whether the `@` / `/` / `!` / `#` autocomplete runs. Off for the
|
|
69
|
-
* side-question form, which has no file tree, commands or skills to offer —
|
|
70
|
-
* and whose text is never run through the mention translator.
|
|
71
|
-
*/
|
|
72
|
-
enablePicker?: boolean;
|
|
73
|
-
/** Overrides the default hint text (the side-question form names its own). */
|
|
74
|
-
placeholder?: string;
|
|
75
|
-
/**
|
|
76
|
-
* `mobile` sizes the composer for a phone: 16px text (iOS Safari zooms the
|
|
77
|
-
* viewport when focusing an input below that), thumb-sized send/stop
|
|
78
|
-
* targets, and Enter-to-newline instead of Enter-to-send.
|
|
79
|
-
*/
|
|
80
|
-
variant?: 'desktop' | 'mobile';
|
|
81
|
-
composerModelRef: { current: { provider: string; modelId: string; thinkingLevel: string } | null };
|
|
82
|
-
deferredComposerPickRef?: { current: { provider?: string; modelId?: string; thinkingLevel?: string } | null };
|
|
83
|
-
}) {
|
|
50
|
+
}: ChatInputProps) {
|
|
84
51
|
const [internalAttachments, setInternalAttachments] = useState<Attachment[]>([]);
|
|
85
52
|
|
|
86
53
|
const attachments = externalAttachments !== undefined ? externalAttachments : internalAttachments;
|
|
@@ -303,7 +270,7 @@ export function ChatInput({
|
|
|
303
270
|
appSettings={appSettings}
|
|
304
271
|
rootPath={rootPath}
|
|
305
272
|
enablePicker={enablePicker}
|
|
306
|
-
placeholder={placeholder ?? (disabled ? "Please select a workspace above to start prompting..." : "@ for files/agents; / for commands and skills
|
|
273
|
+
placeholder={placeholder ?? (disabled ? "Please select a workspace above to start prompting..." : "@ for files/agents; / for commands and skills (Paste images/files here)")}
|
|
307
274
|
className={`w-full bg-transparent border-none focus:outline-none resize-none text-ink placeholder-ink/40 disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
308
275
|
isMobile ? 'px-3 py-3 text-base min-h-[68px] max-h-40' : 'px-3 py-3 text-sm min-h-[80px]'
|
|
309
276
|
}`}
|
|
@@ -329,6 +296,9 @@ export function ChatInput({
|
|
|
329
296
|
sendDisabled={disabled || (!value.trim() && attachments.length === 0)}
|
|
330
297
|
accessMode={accessMode}
|
|
331
298
|
onSelectAccess={onAccessModeChange}
|
|
299
|
+
showModel={showModel}
|
|
300
|
+
showThinking={showThinking}
|
|
301
|
+
showAccess={showAccess}
|
|
332
302
|
/>
|
|
333
303
|
</div>
|
|
334
304
|
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The composer's props, split from its implementation so the component file
|
|
8
|
+
* stays under the repo's per-file ceiling. Every field here is documented where
|
|
9
|
+
* it changes behaviour; the component reads them all.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SetStateAction } from 'preact/compat';
|
|
13
|
+
import type { Attachment } from '@/shared/types';
|
|
14
|
+
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
15
|
+
|
|
16
|
+
export interface ChatInputProps {
|
|
17
|
+
value: string;
|
|
18
|
+
onChange: (v: string) => void;
|
|
19
|
+
onSend: (attachments: Attachment[], options?: { steering?: boolean }) => void;
|
|
20
|
+
isGenerating: boolean;
|
|
21
|
+
onStop?: () => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
appSettings?: Record<string, any>;
|
|
25
|
+
attachments?: Attachment[];
|
|
26
|
+
onAttachmentsChange?: (attachments: SetStateAction<Attachment[]>) => void;
|
|
27
|
+
onThinkingLevelChange?: (level: string) => void;
|
|
28
|
+
onModelChange?: (provider: string, modelId: string) => void;
|
|
29
|
+
/** Model last used by the active session (omp `model_change` entry). */
|
|
30
|
+
sessionModel?: { provider: string; modelId: string } | null;
|
|
31
|
+
/** Thinking level last used by the active session (omp `thinking_level_change` entry). */
|
|
32
|
+
sessionThinkingLevel?: string | null;
|
|
33
|
+
rootPath?: string | null;
|
|
34
|
+
/** The chat session's access mode; omit it on a composer whose access
|
|
35
|
+
* selector is off (the side-question form). */
|
|
36
|
+
accessMode?: ApprovalMode;
|
|
37
|
+
onAccessModeChange?: (mode: ApprovalMode) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Which of the toolbar's selectors render. The side-question composer turns
|
|
40
|
+
* all three off: its child runs on the chat's model and thinking selector and
|
|
41
|
+
* without tools, so a control there would change nothing.
|
|
42
|
+
*/
|
|
43
|
+
showModel?: boolean;
|
|
44
|
+
showThinking?: boolean;
|
|
45
|
+
showAccess?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the `@` / `/` / `!` / `#` autocomplete runs. Off for the
|
|
48
|
+
* side-question form, which has no file tree, commands or skills to offer —
|
|
49
|
+
* and whose text is never run through the mention translator.
|
|
50
|
+
*/
|
|
51
|
+
enablePicker?: boolean;
|
|
52
|
+
/** Overrides the default hint text (the side-question form names its own). */
|
|
53
|
+
placeholder?: string;
|
|
54
|
+
/**
|
|
55
|
+
* `mobile` sizes the composer for a phone: 16px text (iOS Safari zooms the
|
|
56
|
+
* viewport when focusing an input below that), thumb-sized send/stop
|
|
57
|
+
* targets, and Enter-to-newline instead of Enter-to-send.
|
|
58
|
+
*/
|
|
59
|
+
variant?: 'desktop' | 'mobile';
|
|
60
|
+
composerModelRef: { current: { provider: string; modelId: string; thinkingLevel: string } | null };
|
|
61
|
+
deferredComposerPickRef?: { current: { provider?: string; modelId?: string; thinkingLevel?: string } | null };
|
|
62
|
+
}
|