@molecule/app-ide-react 1.5.0 → 1.7.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 +127 -4
- package/dist/command-metadata.d.ts +39 -0
- package/dist/command-metadata.d.ts.map +1 -1
- package/dist/command-metadata.js +46 -3
- package/dist/command-metadata.js.map +1 -1
- package/dist/components/ChatPanel.d.ts +20 -2
- package/dist/components/ChatPanel.d.ts.map +1 -1
- package/dist/components/ChatPanel.js +485 -122
- package/dist/components/ChatPanel.js.map +1 -1
- package/dist/components/EditorPanel.d.ts +1 -1
- package/dist/components/EditorPanel.d.ts.map +1 -1
- package/dist/components/EditorPanel.js +6 -1
- package/dist/components/EditorPanel.js.map +1 -1
- package/dist/components/FileExplorer.d.ts +1 -1
- package/dist/components/FileExplorer.d.ts.map +1 -1
- package/dist/components/FileExplorer.js +2 -2
- package/dist/components/FileExplorer.js.map +1 -1
- package/dist/components/FileExplorerContextMenu.d.ts +6 -1
- package/dist/components/FileExplorerContextMenu.d.ts.map +1 -1
- package/dist/components/FileExplorerContextMenu.js +19 -7
- package/dist/components/FileExplorerContextMenu.js.map +1 -1
- package/dist/components/SearchPanel.d.ts +1 -1
- package/dist/components/SearchPanel.d.ts.map +1 -1
- package/dist/components/SearchPanel.js +9 -6
- package/dist/components/SearchPanel.js.map +1 -1
- package/dist/components/ShareModal.d.ts +13 -1
- package/dist/components/ShareModal.d.ts.map +1 -1
- package/dist/components/ShareModal.js +2 -2
- package/dist/components/ShareModal.js.map +1 -1
- package/dist/components/TipCard.d.ts +7 -2
- package/dist/components/TipCard.d.ts.map +1 -1
- package/dist/components/TipCard.js +8 -6
- package/dist/components/TipCard.js.map +1 -1
- package/dist/components/ToolCallCard.d.ts.map +1 -1
- package/dist/components/ToolCallCard.js +6 -3
- package/dist/components/ToolCallCard.js.map +1 -1
- package/dist/components/UserAvatar.d.ts +10 -1
- package/dist/components/UserAvatar.d.ts.map +1 -1
- package/dist/components/UserAvatar.js +9 -6
- package/dist/components/UserAvatar.js.map +1 -1
- package/dist/types.d.ts +60 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
|
@@ -16,7 +16,7 @@ import { ActivityCard } from './ActivityCard.js';
|
|
|
16
16
|
import { AutoCommitBadge } from './AutoCommitBadge.js';
|
|
17
17
|
import { AUTO_COMMIT_DISABLED, autoCommitReducer, isAutoCommitArmed, isAutoCommitDue, isAutoCommitEnabled, parseAutoCommitCommand, resolveAutoCommitSeconds, } from './chat-autocommit-utilities.js';
|
|
18
18
|
import { CHAT_CARD_ICON_SIZE, chatCardBorder, chatCardStyle } from './chat-card-style.js';
|
|
19
|
-
import { COMMAND_CATEGORIES, COMMANDS } from './chat-commands.js';
|
|
19
|
+
import { COMMAND_CATEGORIES, COMMANDS, matchesSideChannelCommand, } from './chat-commands.js';
|
|
20
20
|
import { stripCommitCoauthorTrailer } from './chat-commit-utilities.js';
|
|
21
21
|
import { cachedPromptTokens, formatTokenTotal } from './chat-cost-utilities.js';
|
|
22
22
|
import { effortOptionsForModel, nativeEffortName, parseEffortCommand, resolveEffortArg, } from './chat-effort-utilities.js';
|
|
@@ -135,6 +135,7 @@ function renderCardSegment(seg, key) {
|
|
|
135
135
|
}
|
|
136
136
|
/** All stream event types that can trigger a notification sound. */
|
|
137
137
|
const SOUND_EVENTS = [
|
|
138
|
+
'message',
|
|
138
139
|
'done',
|
|
139
140
|
'error',
|
|
140
141
|
'tool_result',
|
|
@@ -147,6 +148,7 @@ const SOUND_EVENTS = [
|
|
|
147
148
|
];
|
|
148
149
|
/** User-friendly labels for each sound event (used as i18n defaultValues). */
|
|
149
150
|
const SOUND_EVENT_LABELS = {
|
|
151
|
+
message: 'Team message',
|
|
150
152
|
done: 'Response complete',
|
|
151
153
|
error: 'Error',
|
|
152
154
|
tool_result: 'Tool finished',
|
|
@@ -159,6 +161,7 @@ const SOUND_EVENT_LABELS = {
|
|
|
159
161
|
};
|
|
160
162
|
/** Brief descriptions for each sound event. */
|
|
161
163
|
const SOUND_EVENT_DESCRIPTIONS = {
|
|
164
|
+
message: 'A teammate posted a team-only note',
|
|
162
165
|
done: '{{agentName}} finished responding',
|
|
163
166
|
error: 'Something went wrong during a response',
|
|
164
167
|
tool_result: 'A tool call (file read, command, etc.) completed',
|
|
@@ -176,7 +179,10 @@ const SOUND_MODE_LABELS = {
|
|
|
176
179
|
whenNotFocused: 'when not focused',
|
|
177
180
|
always: 'always',
|
|
178
181
|
};
|
|
182
|
+
/** Per-device sounds preference (see the soundsConfig state comment). */
|
|
183
|
+
const SOUNDS_STORAGE_KEY = 'molecule.ide.sounds';
|
|
179
184
|
const DEFAULT_SOUNDS_CONFIG = {
|
|
185
|
+
message: 'always',
|
|
180
186
|
done: 'whenNotFocused',
|
|
181
187
|
error: 'whenNotFocused',
|
|
182
188
|
tool_result: 'off',
|
|
@@ -191,8 +197,13 @@ let audioCtx = null;
|
|
|
191
197
|
/**
|
|
192
198
|
* Play a short notification tone using the Web Audio API.
|
|
193
199
|
* Creates the AudioContext lazily on first call (after user interaction).
|
|
200
|
+
*
|
|
201
|
+
* The default variant is a single 660 Hz blip (status events). The `team`
|
|
202
|
+
* variant is the SAME blip an octave up (1320 Hz) — recognizably "a message,
|
|
203
|
+
* not a status event" while staying consistent with the sound family.
|
|
204
|
+
* @param variant - Which tone to play.
|
|
194
205
|
*/
|
|
195
|
-
function playTone() {
|
|
206
|
+
function playTone(variant = 'default') {
|
|
196
207
|
try {
|
|
197
208
|
if (!audioCtx)
|
|
198
209
|
audioCtx = new AudioContext();
|
|
@@ -201,8 +212,12 @@ function playTone() {
|
|
|
201
212
|
osc.connect(gain);
|
|
202
213
|
gain.connect(audioCtx.destination);
|
|
203
214
|
osc.type = 'sine';
|
|
204
|
-
osc.frequency.value = 660;
|
|
205
|
-
|
|
215
|
+
osc.frequency.value = variant === 'team' ? 1320 : 660;
|
|
216
|
+
// Equal-loudness compensation: hearing is far more sensitive near 1–4 kHz
|
|
217
|
+
// than at 660 Hz, so the octave-up tone needs roughly half the amplitude
|
|
218
|
+
// to sound the same volume as the default blip.
|
|
219
|
+
const peak = variant === 'team' ? 0.08 : 0.15;
|
|
220
|
+
gain.gain.setValueAtTime(peak, audioCtx.currentTime);
|
|
206
221
|
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);
|
|
207
222
|
osc.start(audioCtx.currentTime);
|
|
208
223
|
osc.stop(audioCtx.currentTime + 0.15);
|
|
@@ -928,7 +943,7 @@ export function CommitCardItem({ card, onRevert, }) {
|
|
|
928
943
|
* @returns The rendered message item.
|
|
929
944
|
*/
|
|
930
945
|
const MessageItem = memo(function MessageItem(props) {
|
|
931
|
-
const { msg, sendMessage, handleAskUserResponse, isLoading, streamingStatus, onNavigatePreview, undoneTcIds, handleUndoToggle, onFileOpen, onFileDoubleClick, onFileDiff, handleFileRevert, setInputAndCursorEnd, setModelPicker, userAvatar, onAvatarClick, discovery, buildUpgradeCta, } = props;
|
|
946
|
+
const { msg, sendMessage, handleAskUserResponse, isLoading, streamingStatus, onNavigatePreview, undoneTcIds, handleUndoToggle, onFileOpen, onFileDoubleClick, onFileDiff, handleFileRevert, setInputAndCursorEnd, setModelPicker, chatMode, userAvatar, onAvatarClick, discovery, buildUpgradeCta, agentName, canEdit, } = props;
|
|
932
947
|
const cm = getClassMap();
|
|
933
948
|
const themeMode = useThemeMode();
|
|
934
949
|
const isLight = themeMode === 'light';
|
|
@@ -936,6 +951,11 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
936
951
|
// A message sent automatically on the user's behalf (e.g. an auto-fix prompt):
|
|
937
952
|
// it has role 'user' but must NOT look like the user typed it (C2).
|
|
938
953
|
const isAutomatic = msg.role === 'user' && !!msg.automatic;
|
|
954
|
+
// A human-only team note (side channel, e.g. /teamsay): renders like a user
|
|
955
|
+
// message — author header, time, plain content — but with the gold team-only
|
|
956
|
+
// accent + badge, and never the user message's blue stripe. `role` is 'system'
|
|
957
|
+
// (the model never sees it), so this is checked before isUser.
|
|
958
|
+
const isTeamNote = !!msg.teamOnly;
|
|
939
959
|
// A real, user-typed message (the only one styled with the blue border + the
|
|
940
960
|
// user's own avatar).
|
|
941
961
|
const isUser = msg.role === 'user' && !isAutomatic;
|
|
@@ -949,7 +969,7 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
949
969
|
const wrapperSpacing = {
|
|
950
970
|
marginBottom: `${discovery ? TIMELINE_ITEM_GAP_DISCOVERY : TIMELINE_ITEM_GAP}px`,
|
|
951
971
|
};
|
|
952
|
-
return (_jsx("div", { style: wrapperSpacing, children: isUser || isAutomatic ? (_jsxs("div", { className:
|
|
972
|
+
return (_jsx("div", { style: wrapperSpacing, children: isUser || isAutomatic || isTeamNote ? (_jsxs("div", { className:
|
|
953
973
|
// Auto-sent card matches the info cards' `xs` body; a real user message keeps
|
|
954
974
|
// its slightly larger `sm` (it's a different kind of row, not an info card).
|
|
955
975
|
isAutomatic ? cm.textSize('xs') : cm.cn(cm.surfaceSecondary, cm.textSize('sm')), style: {
|
|
@@ -962,7 +982,9 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
962
982
|
// drawn by the `::before` injected via USER_ACCENT_STYLE + gated on the
|
|
963
983
|
// data-mol-id below, and the user's full-size avatar. An auto-sent message
|
|
964
984
|
// is instead a green (success) tinted card — same chrome as the info cards
|
|
965
|
-
// — so it's unmistakably agent-sent, not user-typed (C2).
|
|
985
|
+
// — so it's unmistakably agent-sent, not user-typed (C2). A team note keeps
|
|
986
|
+
// the user-message look but swaps the blue stripe (its data-mol-id differs,
|
|
987
|
+
// so USER_ACCENT_STYLE never applies) for the gold team-only border.
|
|
966
988
|
...(isAutomatic
|
|
967
989
|
? chatCardStyle(AUTO_SENT_ACCENT)
|
|
968
990
|
: {
|
|
@@ -971,14 +993,58 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
971
993
|
paddingTop: '10px',
|
|
972
994
|
paddingBottom: '10px',
|
|
973
995
|
paddingRight: '10px',
|
|
996
|
+
...(isTeamNote ? { border: `1px solid ${NOTICE_TONE.gold.accent}` } : {}),
|
|
974
997
|
}),
|
|
975
|
-
}, "data-mol-id":
|
|
998
|
+
}, "data-mol-id": isTeamNote
|
|
999
|
+
? 'chat-team-message'
|
|
1000
|
+
: isAutomatic
|
|
1001
|
+
? 'chat-automatic-message'
|
|
1002
|
+
: 'chat-user-message', children: [isAutomatic ? (_jsx(Icon, { name: "sync", size: CHAT_CARD_ICON_SIZE, "aria-hidden": "true", style: { flexShrink: 0, marginTop: 1, color: AUTO_SENT_ACCENT } })) : (_jsx(UserAvatar
|
|
1003
|
+
// An AUTHORED message shows that author's avatar — or their initial
|
|
1004
|
+
// when they have none — NEVER the viewing user's picture (which made
|
|
1005
|
+
// a teammate's avatar-less message wear the viewer's own face). Only
|
|
1006
|
+
// an author-less message (the local optimistic echo, legacy rows) is
|
|
1007
|
+
// the signed-in user's own and uses their avatar.
|
|
1008
|
+
, {
|
|
1009
|
+
// An AUTHORED message shows that author's avatar — or their initial
|
|
1010
|
+
// when they have none — NEVER the viewing user's picture (which made
|
|
1011
|
+
// a teammate's avatar-less message wear the viewer's own face). Only
|
|
1012
|
+
// an author-less message (the local optimistic echo, legacy rows) is
|
|
1013
|
+
// the signed-in user's own and uses their avatar.
|
|
1014
|
+
userAvatar: msg.author ? msg.author.avatar : userAvatar, name: msg.author?.name ?? undefined, size: 36, onClick: onAvatarClick })), _jsxs("div", { style: { flex: 1, minWidth: 0, marginTop: 1 }, children: [isAutomatic ? null : (_jsxs("div", { style: {
|
|
976
1015
|
display: 'flex',
|
|
977
1016
|
alignItems: 'baseline',
|
|
978
1017
|
gap: 8,
|
|
979
1018
|
lineHeight: 1.3,
|
|
980
1019
|
marginBottom: 1,
|
|
981
|
-
}, children: [_jsx("span", { style: { fontWeight: 600 }, children: msg.author?.name ?? t('ide.chat.you', undefined, { defaultValue: 'You' }) }),
|
|
1020
|
+
}, children: [_jsx("span", { style: { fontWeight: 600 }, children: msg.author?.name ?? t('ide.chat.you', undefined, { defaultValue: 'You' }) }), isTeamNote && (_jsx("span", { title: t('ide.chat.teamOnly.badge', { agentName: agentName ?? 'the assistant' }, {
|
|
1021
|
+
defaultValue: 'Team only — visible to your team; {{agentName}} will ignore it',
|
|
1022
|
+
}), "aria-label": t('ide.chat.teamOnly.badge', { agentName: agentName ?? 'the assistant' }, {
|
|
1023
|
+
defaultValue: 'Team only — visible to your team; {{agentName}} will ignore it',
|
|
1024
|
+
}),
|
|
1025
|
+
// Centered in the header row with a -0.5px optical nudge. Platform
|
|
1026
|
+
// font metrics make a single integer offset impossible: against
|
|
1027
|
+
// plain center, macOS rendered the glyph ~1px low and Ubuntu spot-on
|
|
1028
|
+
// (measured in Brave on both, 2026-08-27), so -0.5 splits the
|
|
1029
|
+
// difference — each platform lands within half a pixel, and retina
|
|
1030
|
+
// displays render the half-pixel crisply. (Baseline alignment was
|
|
1031
|
+
// tried and rode too HIGH: a text-less inline-flex item's
|
|
1032
|
+
// synthesized baseline is its bottom edge.)
|
|
1033
|
+
style: {
|
|
1034
|
+
display: 'inline-flex',
|
|
1035
|
+
alignSelf: 'center',
|
|
1036
|
+
flexShrink: 0,
|
|
1037
|
+
position: 'relative',
|
|
1038
|
+
top: -0.5,
|
|
1039
|
+
}, "data-mol-id": "chat-team-only-badge", children: _jsx(Icon
|
|
1040
|
+
// 16px so the glyph reads at the username's optical size — the
|
|
1041
|
+
// people glyph has internal viewBox padding, so a nominal 14px
|
|
1042
|
+
// renders visibly smaller than the 14px text next to it.
|
|
1043
|
+
, {
|
|
1044
|
+
// 16px so the glyph reads at the username's optical size — the
|
|
1045
|
+
// people glyph has internal viewBox padding, so a nominal 14px
|
|
1046
|
+
// renders visibly smaller than the 14px text next to it.
|
|
1047
|
+
name: "people", size: 16, "aria-hidden": "true", style: { color: NOTICE_TONE.gold.accent } }) })), typeof msg.timestamp === 'number' && (_jsx("span", { className: cm.textMuted, style: { fontSize: 11 }, children: relativeTimeLong(msg.timestamp) }))] })), _jsx(CollapsibleUserMessage, { content: msg.content, isLight: isLight }), msg.attachments && msg.attachments.length > 0 && (_jsx("div", { style: { marginTop: 4, display: 'flex', gap: 4, flexWrap: 'wrap' }, children: msg.attachments.map((att, ai) => (_jsxs("span", { className: cm.cn(cm.textMuted, cm.textSize('xs')), style: { display: 'inline-flex', alignItems: 'center', gap: 2 }, children: [att.mediaType.startsWith('image/')
|
|
982
1048
|
? '\uD83D\uDDBC\uFE0F'
|
|
983
1049
|
: att.mediaType.startsWith('audio/')
|
|
984
1050
|
? '\uD83C\uDFB5'
|
|
@@ -1057,11 +1123,11 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
1057
1123
|
const tc = msg.toolCalls?.find((c) => c.id === block.id);
|
|
1058
1124
|
if (!tc)
|
|
1059
1125
|
return null;
|
|
1060
|
-
return (_jsx("div", { style: { marginTop: '4px' }, children: _jsx(ToolCallCard, { id: tc.id, name: tc.name, input: tc.input, output: tc.output, status: tc.status, fileDiff: tc.fileDiff, isUndone: undoneTcIds.has(tc.id), onUndoToggle: handleUndoToggle, onFileOpen: onFileOpen, onFileDoubleClick: onFileDoubleClick, onFileDiff: onFileDiff, onFileRevert: handleFileRevert, onAskUserResponse: handleAskUserResponse }) }, tc.id));
|
|
1126
|
+
return (_jsx("div", { style: { marginTop: '4px' }, children: _jsx(ToolCallCard, { id: tc.id, name: tc.name, input: tc.input, output: tc.output, status: tc.status, fileDiff: tc.fileDiff, isUndone: undoneTcIds.has(tc.id), onUndoToggle: handleUndoToggle, onFileOpen: onFileOpen, onFileDoubleClick: onFileDoubleClick, onFileDiff: onFileDiff, onFileRevert: canEdit === false ? undefined : handleFileRevert, onAskUserResponse: canEdit === false ? undefined : handleAskUserResponse }) }, tc.id));
|
|
1061
1127
|
})) : msg.content ? (_jsx(MarkdownContent, { text: msg.content, isStreaming: msg.isStreaming, statusLabel: msg.isStreaming ? streamingStatus : undefined, statusStartedAt: typeof msg.timestamp === 'number' ? msg.timestamp : undefined, onNavigatePreview: onNavigatePreview, hideStreamingIndicator: true })) : null, msg.toolCalls &&
|
|
1062
1128
|
msg.toolCalls.length > 0 &&
|
|
1063
1129
|
(!msg.blocks || msg.blocks.length === 0) &&
|
|
1064
|
-
msg.toolCalls.map((tc) => (_jsx(ToolCallCard, { id: tc.id, name: tc.name, input: tc.input, output: tc.output, status: tc.status, fileDiff: tc.fileDiff, isUndone: undoneTcIds.has(tc.id), onUndoToggle: handleUndoToggle, onFileOpen: onFileOpen, onFileDoubleClick: onFileDoubleClick, onFileDiff: onFileDiff, onFileRevert: handleFileRevert, onAskUserResponse: handleAskUserResponse }, tc.id))), msg.aborted && (_jsx("span", { className: cm.cn(cm.textMuted, cm.textSize('xs')), style: { display: 'block', marginTop: 4, fontStyle: 'italic' }, children: t('ide.chat.responseStopped', undefined, {
|
|
1130
|
+
msg.toolCalls.map((tc) => (_jsx(ToolCallCard, { id: tc.id, name: tc.name, input: tc.input, output: tc.output, status: tc.status, fileDiff: tc.fileDiff, isUndone: undoneTcIds.has(tc.id), onUndoToggle: handleUndoToggle, onFileOpen: onFileOpen, onFileDoubleClick: onFileDoubleClick, onFileDiff: onFileDiff, onFileRevert: canEdit === false ? undefined : handleFileRevert, onAskUserResponse: canEdit === false ? undefined : handleAskUserResponse }, tc.id))), msg.aborted && (_jsx("span", { className: cm.cn(cm.textMuted, cm.textSize('xs')), style: { display: 'block', marginTop: 4, fontStyle: 'italic' }, children: t('ide.chat.responseStopped', undefined, {
|
|
1065
1131
|
defaultValue: 'Response stopped',
|
|
1066
1132
|
}) })), msg.loopLimitReached &&
|
|
1067
1133
|
!msg.isStreaming &&
|
|
@@ -1073,7 +1139,7 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
1073
1139
|
}),
|
|
1074
1140
|
action: () => {
|
|
1075
1141
|
setInputAndCursorEnd('/model ');
|
|
1076
|
-
setModelPicker({ selectedIdx: -1 });
|
|
1142
|
+
setModelPicker({ selectedIdx: -1, mode: chatMode });
|
|
1077
1143
|
},
|
|
1078
1144
|
},
|
|
1079
1145
|
{
|
|
@@ -1152,11 +1218,15 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
1152
1218
|
* @param props - Component props (see {@link MessageItemProps}).
|
|
1153
1219
|
* @returns The rendered chat inner component.
|
|
1154
1220
|
*/
|
|
1155
|
-
function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent, isPro, isAnonymous, buildUpgradeCta, buildHelpUpgradeSection, activeFile, openTabs, onFileOpen, onFileDoubleClick, onFileDiff, onFileRevert, onFileChange, onFileDeleted, onCommit, onConversationId, onActivityClick, onRenderError, onProfileClick, onReadyToBuild, awaitingSandboxBoot, onClientAction, onTurnComplete, onLoadingChange, onNavigatePreview, onRegisterPushHandler, autoSubmitSignal, openSettingsSignal, onManageCustomModels, modelSelectionSignal, openReportSignal, openShareSignal, initialInputValue, pendingMessage, pendingMessageKey, pendingMessageSuppressUser, pendingMessageUserInitiated, userEditedFile, userEditedFileKey, gitStatusTick: externalGitStatusTick, discovery, userAvatar, agentName = DEFAULT_AGENT_NAME, productName = DEFAULT_PRODUCT_NAME, version, extraCommands,
|
|
1221
|
+
function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent, isPro, isAnonymous, canEdit, canShare, buildUpgradeCta, buildHelpUpgradeSection, activeFile, openTabs, onFileOpen, onFileDoubleClick, onFileDiff, onFileRevert, onFileChange, onFileDeleted, onCommit, onConversationId, onActivityClick, onRenderError, onProfileClick, currentUserId, onReadyToBuild, awaitingSandboxBoot, onClientAction, onTurnComplete, onLoadingChange, onNavigatePreview, onRegisterPushHandler, autoSubmitSignal, openSettingsSignal, onManageCustomModels, modelSelectionSignal, openReportSignal, openShareSignal, initialInputValue, pendingMessage, pendingMessageKey, pendingMessageSuppressUser, pendingMessageUserInitiated, userEditedFile, userEditedFileKey, gitStatusTick: externalGitStatusTick, discovery, userAvatar, agentName = DEFAULT_AGENT_NAME, productName = DEFAULT_PRODUCT_NAME, version, extraCommands,
|
|
1156
1222
|
// feedbackUrl: prop kept for back-compat (callers still pass it), but no longer
|
|
1157
1223
|
// consumed here — its only use was the command-menu footer link removed in P3-21.
|
|
1158
1224
|
}) {
|
|
1159
1225
|
const cm = getClassMap();
|
|
1226
|
+
// Share-link management may be gated ABOVE canEdit by the host (molecule.dev
|
|
1227
|
+
// mints/lists/revokes at admin+) — every /share surface below uses this, so
|
|
1228
|
+
// an editor without the capability gets no dead modal.
|
|
1229
|
+
const shareAllowed = canShare ?? canEdit !== false;
|
|
1160
1230
|
const themeMode = useThemeMode();
|
|
1161
1231
|
const isLight = themeMode === 'light';
|
|
1162
1232
|
// Phone-width / touch-first branches: popovers cap with dvh, hover-revealed
|
|
@@ -1171,10 +1241,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1171
1241
|
// visible; 420px bounds it on small tablets.
|
|
1172
1242
|
const popupMaxHeight = isNarrow ? 'min(50dvh, 420px)' : '70vh';
|
|
1173
1243
|
const http = useHttpClient();
|
|
1174
|
-
// Bind the host's profile-click callback to the
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1244
|
+
// Bind the host's profile-click callback to the SIGNED-IN user's identity once.
|
|
1245
|
+
// Hosts open the *own*-profile surface from this, so it is only ever attached to
|
|
1246
|
+
// the signed-in user's OWN messages (see the per-message gate at the MessageItem
|
|
1247
|
+
// call site — a teammate's avatar must never open the viewer's profile). Stable
|
|
1248
|
+
// so MessageItem's memo isn't broken; `undefined` when the host opts out, which
|
|
1249
|
+
// keeps every avatar non-interactive.
|
|
1178
1250
|
const onUserAvatarClick = useMemo(() => onProfileClick
|
|
1179
1251
|
? () => onProfileClick({ avatar: userAvatar })
|
|
1180
1252
|
: undefined, [onProfileClick, userAvatar]);
|
|
@@ -1235,6 +1307,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1235
1307
|
// applied through handleStreamEvent) can append a card-message to the ONE message store.
|
|
1236
1308
|
// This client's OWN `card` events are appended internally by useChat's stream handler.
|
|
1237
1309
|
const appendCardMessageRef = useRef(() => { });
|
|
1310
|
+
// Ref to useChat's appendCompleteMessage — same contract as appendCardMessageRef, for a
|
|
1311
|
+
// teammate's broadcast `message` event (a complete non-streaming message, e.g. a
|
|
1312
|
+
// human-only team note). This client's OWN `message` events append internally in useChat.
|
|
1313
|
+
const appendCompleteMessageRef = useRef(() => { });
|
|
1238
1314
|
// Kept current each render so handleStreamEvent (memoized) always calls the latest.
|
|
1239
1315
|
const onReadyToBuildRef = useRef(onReadyToBuild);
|
|
1240
1316
|
onReadyToBuildRef.current = onReadyToBuild;
|
|
@@ -1281,6 +1357,13 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1281
1357
|
if (event.type === 'card' && applyingPushedRef.current) {
|
|
1282
1358
|
appendCardMessageRef.current(event.id, event.timestamp, event.card);
|
|
1283
1359
|
}
|
|
1360
|
+
// A teammate's broadcast complete message (e.g. a human-only team note) — append it
|
|
1361
|
+
// to the local store so it shows live for the collaborator too (de-duped by id;
|
|
1362
|
+
// never re-persisted — the originating server already persisted it). This client's
|
|
1363
|
+
// OWN `message` stream events are appended internally by useChat.
|
|
1364
|
+
if (event.type === 'message' && applyingPushedRef.current && event.message) {
|
|
1365
|
+
appendCompleteMessageRef.current(event.message);
|
|
1366
|
+
}
|
|
1284
1367
|
// Captured outbound side effect (email/sms/push/webhook/channel) — push an
|
|
1285
1368
|
// inline activity card into the timeline. Non-text card, mirroring how
|
|
1286
1369
|
// system cards are appended.
|
|
@@ -1328,8 +1411,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1328
1411
|
// still drives the phase STATE (useChat's setMode); it just no longer spawns a card.
|
|
1329
1412
|
const cfg = soundsConfigRef.current;
|
|
1330
1413
|
const eventType = event.type;
|
|
1331
|
-
|
|
1332
|
-
|
|
1414
|
+
// 'message' is the team-note ping: it fires only for a TEAMMATE's note
|
|
1415
|
+
// (the pushed broadcast) — never for the sender's own SSE echo.
|
|
1416
|
+
const isOwnTeamNote = event.type === 'message' && !applyingPushedRef.current;
|
|
1417
|
+
if (!isOwnTeamNote && eventType in cfg && shouldPlaySound(cfg[eventType])) {
|
|
1418
|
+
playTone(eventType === 'message' ? 'team' : 'default');
|
|
1333
1419
|
}
|
|
1334
1420
|
}, [t]);
|
|
1335
1421
|
// Countdown timer effect — ticks down and auto-sends fix message
|
|
@@ -1415,7 +1501,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1415
1501
|
}
|
|
1416
1502
|
onFileChange?.(path, content);
|
|
1417
1503
|
}, [onFileChange, onFileOpen, autoFixCountdown]);
|
|
1418
|
-
const { messages, isLoading, isRemoteStreaming, noteRemoteStreamEvent, error, errorMeta, mode, fastMode, streamingStatus, setMode, setFastMode, sendMessage, abort, clearHistory, editQueuedMessage, deleteQueuedMessage, clearQueuedForFile, appendCardMessage, retryCountdown, cancelRetry, } = useChat({
|
|
1504
|
+
const { messages, isLoading, isRemoteStreaming, noteRemoteStreamEvent, error, errorMeta, mode, fastMode, streamingStatus, setMode, setFastMode, sendMessage, abort, clearHistory, editQueuedMessage, deleteQueuedMessage, clearQueuedForFile, appendCardMessage, appendCompleteMessage, retryCountdown, cancelRetry, } = useChat({
|
|
1419
1505
|
endpoint,
|
|
1420
1506
|
projectId,
|
|
1421
1507
|
agentName,
|
|
@@ -1436,11 +1522,21 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1436
1522
|
onConversationId,
|
|
1437
1523
|
onStreamEvent: handleStreamEvent,
|
|
1438
1524
|
});
|
|
1525
|
+
// The mode a model CHANGE should target when the user hasn't scoped one
|
|
1526
|
+
// explicitly (no --plan/--execute flag, no picker-mode choice): the LIVE
|
|
1527
|
+
// conversation mode. Discovery runs in plan mode (aiContext.mode is 'plan'
|
|
1528
|
+
// throughout discovery), so it maps to 'plan' — changing the model while
|
|
1529
|
+
// discovering/planning changes the plan model, while building the execute
|
|
1530
|
+
// model. This replaces the old default of writing the legacy `chatModel`
|
|
1531
|
+
// (which silently moved BOTH modes).
|
|
1532
|
+
const liveModelMode = mode === 'plan' ? 'plan' : 'execute';
|
|
1439
1533
|
// Keep sendMessageRef in sync so the countdown effect can call the latest sendMessage
|
|
1440
1534
|
sendMessageRef.current = sendMessage;
|
|
1441
1535
|
// Keep the card-append ref current so handleStreamEvent (memoized) can append a teammate's
|
|
1442
1536
|
// broadcast `card` event to the message store.
|
|
1443
1537
|
appendCardMessageRef.current = appendCardMessage;
|
|
1538
|
+
// Same for a teammate's broadcast complete `message` event (e.g. a team note).
|
|
1539
|
+
appendCompleteMessageRef.current = appendCompleteMessage;
|
|
1444
1540
|
// User Stop. A stop is a user decision the platform must not overrule — so
|
|
1445
1541
|
// beyond killing the stream (useChat.abort also records the stop client-side
|
|
1446
1542
|
// and, via chat-abort's userInitiated flag, server-side), drop every pending
|
|
@@ -1578,7 +1674,15 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1578
1674
|
useEffect(() => {
|
|
1579
1675
|
if (typeof window !== 'undefined' && window.matchMedia?.('(pointer: coarse)').matches)
|
|
1580
1676
|
return;
|
|
1581
|
-
textareaRef.current
|
|
1677
|
+
const ta = textareaRef.current;
|
|
1678
|
+
if (!ta)
|
|
1679
|
+
return;
|
|
1680
|
+
ta.focus();
|
|
1681
|
+
// The textarea can mount with content already in it (defaultValue carries a
|
|
1682
|
+
// persisted draft, and a viewer's box holds the '/teamsay ' prefill) — a
|
|
1683
|
+
// fresh element's selection is (0,0), so without this the caret sits BEFORE
|
|
1684
|
+
// the text and typing lands in front of '/teamsay'.
|
|
1685
|
+
ta.setSelectionRange(ta.value.length, ta.value.length);
|
|
1582
1686
|
}, []);
|
|
1583
1687
|
/** Update the ref, the DOM element, and the hasInput flag without re-rendering the parent. */
|
|
1584
1688
|
const setInputValue = useCallback((val) => {
|
|
@@ -1586,7 +1690,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1586
1690
|
const ta = textareaRef.current;
|
|
1587
1691
|
if (ta && ta.value !== val)
|
|
1588
1692
|
ta.value = val;
|
|
1589
|
-
|
|
1693
|
+
// A viewer's composer is pre-filled with '/teamsay ' — the BARE prefix is
|
|
1694
|
+
// not sendable content, so it must not light the Send button.
|
|
1695
|
+
const bareSideChannel = canEdit === false && /^\/(?:teamsay|t)$/i.test(val.trim());
|
|
1696
|
+
setHasInput(Boolean(val.trim()) && !bareSideChannel);
|
|
1590
1697
|
autoResize();
|
|
1591
1698
|
// Clear persisted draft when input is emptied (e.g. on submit)
|
|
1592
1699
|
if (!val) {
|
|
@@ -1597,7 +1704,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1597
1704
|
/* sessionStorage unavailable — safe to ignore, draft simply persists */
|
|
1598
1705
|
}
|
|
1599
1706
|
}
|
|
1600
|
-
}, [draftKey, autoResize]);
|
|
1707
|
+
}, [draftKey, autoResize, canEdit]);
|
|
1601
1708
|
// Persist draft text to sessionStorage so it survives refresh (debounced)
|
|
1602
1709
|
const draftTimerRef = useRef(null);
|
|
1603
1710
|
const persistDraft = useCallback(() => {
|
|
@@ -2211,6 +2318,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2211
2318
|
activityCardsLoadedConvRef.current = null;
|
|
2212
2319
|
if (!conversationId)
|
|
2213
2320
|
return;
|
|
2321
|
+
// Activity cards carry recipients/summaries and their routes are editor+ by
|
|
2322
|
+
// design — a viewer's fetch/persist would just 403 on every panel open
|
|
2323
|
+
// (observed live in prod logs). Skip both for read-only viewers.
|
|
2324
|
+
if (canEdit === false)
|
|
2325
|
+
return;
|
|
2214
2326
|
if (prevConv && prevConv !== conversationId)
|
|
2215
2327
|
setActivityCards([]);
|
|
2216
2328
|
let cancelled = false;
|
|
@@ -2240,10 +2352,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2240
2352
|
return () => {
|
|
2241
2353
|
cancelled = true;
|
|
2242
2354
|
};
|
|
2243
|
-
}, [conversationId, projectId, http]);
|
|
2355
|
+
}, [conversationId, projectId, http, canEdit]);
|
|
2244
2356
|
useEffect(() => {
|
|
2245
2357
|
if (!conversationId || activityCardsLoadedConvRef.current !== conversationId)
|
|
2246
2358
|
return;
|
|
2359
|
+
if (canEdit === false)
|
|
2360
|
+
return;
|
|
2247
2361
|
void http
|
|
2248
2362
|
.put(`/projects/${projectId}/conversations/${conversationId}/activity-cards`, {
|
|
2249
2363
|
activityCards: activityCards.filter((c) => !c.received),
|
|
@@ -2252,7 +2366,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2252
2366
|
// Best-effort save (bound as _error per Rule 14): the in-memory cards remain the
|
|
2253
2367
|
// source of truth this session if the PUT fails.
|
|
2254
2368
|
});
|
|
2255
|
-
}, [activityCards, conversationId, projectId, http]);
|
|
2369
|
+
}, [activityCards, conversationId, projectId, http, canEdit]);
|
|
2256
2370
|
// ── Auto-tips (dismissable onboarding hints) ──────────────────────────────
|
|
2257
2371
|
// Two surfaces (see chat-tips-utilities): an ENTRY_TIP shown once on a fresh
|
|
2258
2372
|
// conversation so a brand-new user always sees how to drive the agent, plus an
|
|
@@ -2282,6 +2396,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2282
2396
|
shownTipIdsRef.current = [];
|
|
2283
2397
|
// Re-arm the entry tip so a freshly-started chat shows the onboarding hint.
|
|
2284
2398
|
entryTipShownRef.current = false;
|
|
2399
|
+
// Re-arm the viewer composer prefill ('/teamsay ') for the new conversation.
|
|
2400
|
+
viewerPrefillDoneRef.current = false;
|
|
2285
2401
|
}
|
|
2286
2402
|
}, [conversationId]);
|
|
2287
2403
|
// Entry tip: the onboarding moment. Show ONE high-value hint as soon as a fresh
|
|
@@ -2303,6 +2419,31 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2303
2419
|
const text = t(`ide.chat.tip.${ENTRY_TIP.id}`, { agentName }, { defaultValue: ENTRY_TIP.text });
|
|
2304
2420
|
setTipCards((prev) => [...prev, { id: crypto.randomUUID(), text, timestamp: Date.now() }]);
|
|
2305
2421
|
}, [conversationId, messages.length, agentName, discovery]);
|
|
2422
|
+
// Viewer orientation tip: a read-only member's explainer — GOLD like the
|
|
2423
|
+
// team-only messages and led by the same `people` icon it explains, so the
|
|
2424
|
+
// badge on a team note is self-describing. A regular dismissable tip (never a
|
|
2425
|
+
// permanent composer note); shown once per panel mount, skipped in discovery
|
|
2426
|
+
// like the other tips (mvp B1). Gated on an EXPLICIT canEdit === false so
|
|
2427
|
+
// hosts that don't do roles never see it.
|
|
2428
|
+
const viewerTipShownRef = useRef(false);
|
|
2429
|
+
useEffect(() => {
|
|
2430
|
+
if (canEdit !== false || discovery || viewerTipShownRef.current)
|
|
2431
|
+
return;
|
|
2432
|
+
viewerTipShownRef.current = true;
|
|
2433
|
+
const text = t('ide.chat.tip.viewerTeamOnly', { agentName }, {
|
|
2434
|
+
defaultValue: 'View-only access — read along and /teamsay (or just /t) the team. This gold icon marks team-only messages ({{agentName}} ignores them). Running the assistant and changing the model or settings need editor access.',
|
|
2435
|
+
});
|
|
2436
|
+
setTipCards((prev) => [
|
|
2437
|
+
...prev,
|
|
2438
|
+
{
|
|
2439
|
+
id: crypto.randomUUID(),
|
|
2440
|
+
text,
|
|
2441
|
+
timestamp: Date.now(),
|
|
2442
|
+
accent: NOTICE_TONE.gold.accent,
|
|
2443
|
+
icon: 'people',
|
|
2444
|
+
},
|
|
2445
|
+
]);
|
|
2446
|
+
}, [canEdit, discovery, agentName]);
|
|
2306
2447
|
// Surface an occasional idle tip. This effect re-runs on every message change, so
|
|
2307
2448
|
// the idle timer is continually reset by activity; it only fires once the
|
|
2308
2449
|
// conversation has been quiet for TIP_IDLE_MS — and even then only when the
|
|
@@ -2335,7 +2476,31 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2335
2476
|
}, [messages, discovery]);
|
|
2336
2477
|
// ── Sounds picker (shown when /sounds is executed) ────────────────────────
|
|
2337
2478
|
const [soundsPicker, setSoundsPicker] = useState(null);
|
|
2338
|
-
|
|
2479
|
+
// Notification sounds are a PER-DEVICE, per-user preference — localStorage,
|
|
2480
|
+
// never project settings. They used to persist via PATCH /projects settings
|
|
2481
|
+
// (shared with every member, and unreachable for read-only viewers, whose
|
|
2482
|
+
// PATCH 403s); the old project-level value is still read once as a migration
|
|
2483
|
+
// fallback when this device has no local preference yet.
|
|
2484
|
+
const [soundsConfig, setSoundsConfig] = useState(() => {
|
|
2485
|
+
try {
|
|
2486
|
+
const raw = localStorage.getItem(SOUNDS_STORAGE_KEY);
|
|
2487
|
+
if (raw)
|
|
2488
|
+
return { ...DEFAULT_SOUNDS_CONFIG, ...JSON.parse(raw) };
|
|
2489
|
+
}
|
|
2490
|
+
catch (_error) {
|
|
2491
|
+
// localStorage unavailable / bad JSON — fall through to the defaults.
|
|
2492
|
+
}
|
|
2493
|
+
return { ...DEFAULT_SOUNDS_CONFIG };
|
|
2494
|
+
});
|
|
2495
|
+
const soundsLocallySetRef = useRef(false);
|
|
2496
|
+
useEffect(() => {
|
|
2497
|
+
try {
|
|
2498
|
+
soundsLocallySetRef.current = localStorage.getItem(SOUNDS_STORAGE_KEY) != null;
|
|
2499
|
+
}
|
|
2500
|
+
catch (_error) {
|
|
2501
|
+
// localStorage unavailable — treat as unset; the project fallback may apply.
|
|
2502
|
+
}
|
|
2503
|
+
}, []);
|
|
2339
2504
|
// ── Panel overlay (/skills, /scripts, /settings — closeable popups) ──────────
|
|
2340
2505
|
// These three commands open a closeable overlay above the composer (mirrors how
|
|
2341
2506
|
// /model and /sounds own this popup region) instead of dropping an inline card
|
|
@@ -2373,16 +2538,16 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2373
2538
|
updated = { ...soundsConfig, [eventType]: next };
|
|
2374
2539
|
}
|
|
2375
2540
|
setSoundsConfig(updated);
|
|
2541
|
+
// Per-device preference — localStorage, never a project PATCH (see the
|
|
2542
|
+
// state's comment; this also makes /sounds fully usable for viewers).
|
|
2376
2543
|
try {
|
|
2377
|
-
|
|
2544
|
+
localStorage.setItem(SOUNDS_STORAGE_KEY, JSON.stringify(updated));
|
|
2545
|
+
soundsLocallySetRef.current = true;
|
|
2378
2546
|
}
|
|
2379
|
-
catch (
|
|
2380
|
-
|
|
2381
|
-
addSystemCard(t('ide.chat.soundsError', undefined, {
|
|
2382
|
-
defaultValue: 'Failed to update sound settings.',
|
|
2383
|
-
}));
|
|
2547
|
+
catch (_error) {
|
|
2548
|
+
// localStorage unavailable — the choice still applies for this session.
|
|
2384
2549
|
}
|
|
2385
|
-
}, [soundsConfig
|
|
2550
|
+
}, [soundsConfig]);
|
|
2386
2551
|
// ── Current project settings (model + maxloops + sounds) ──────────────────
|
|
2387
2552
|
// Project-scoped so any custom (bring-your-own AI) models the project has
|
|
2388
2553
|
// configured are included alongside the platform catalog.
|
|
@@ -2498,7 +2663,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2498
2663
|
if (typeof s?.autoApproveCommands === 'boolean') {
|
|
2499
2664
|
setAutoApproveCommandsEnabled(s.autoApproveCommands);
|
|
2500
2665
|
}
|
|
2501
|
-
|
|
2666
|
+
// Legacy migration only: sounds are per-device (localStorage) now — apply
|
|
2667
|
+
// the old project-level value only when this device has no local pref.
|
|
2668
|
+
if (s?.sounds && typeof s.sounds === 'object' && !soundsLocallySetRef.current) {
|
|
2502
2669
|
setSoundsConfig((prev) => ({ ...prev, ...s.sounds }));
|
|
2503
2670
|
}
|
|
2504
2671
|
// Restore the persisted auto-commit cadence in the paused state (it
|
|
@@ -2591,7 +2758,15 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2591
2758
|
return;
|
|
2592
2759
|
removedModelNotifiedRef.current = currentModel;
|
|
2593
2760
|
const removedId = currentModel;
|
|
2594
|
-
|
|
2761
|
+
// Prefer a still-available model from the SAME custom provider before a
|
|
2762
|
+
// platform model: when a BYO provider's model is renamed (custom/<prov>/A →
|
|
2763
|
+
// custom/<prov>/B), keep the user on their own endpoint rather than bouncing
|
|
2764
|
+
// them to a platform free model.
|
|
2765
|
+
const sameProviderPrefix = removedId.match(/^(custom\/[^/]+\/)/)?.[1];
|
|
2766
|
+
const sameProviderModel = sameProviderPrefix
|
|
2767
|
+
? AVAILABLE_MODELS.find((m) => m.id.startsWith(sameProviderPrefix))?.id
|
|
2768
|
+
: undefined;
|
|
2769
|
+
const fallback = sameProviderModel || FREE_TIER_MODEL || AVAILABLE_MODELS[0]?.id;
|
|
2595
2770
|
addSystemCard(fallback
|
|
2596
2771
|
? t('ide.chat.modelRemoved', { removed: removedId, fallback }, {
|
|
2597
2772
|
defaultValue: 'Your selected model "{{removed}}" is no longer available. Switched to "{{fallback}}". Type /model to pick another.',
|
|
@@ -2602,9 +2777,14 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2602
2777
|
if (fallback) {
|
|
2603
2778
|
setCurrentModel(fallback);
|
|
2604
2779
|
setSavedChatModel(fallback);
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2780
|
+
// Persisting the swap writes project settings — editor+ server-side, so a
|
|
2781
|
+
// viewer's tab keeps the in-memory switch (all this session needs) and
|
|
2782
|
+
// skips a PATCH that could only 403.
|
|
2783
|
+
if (canEdit !== false) {
|
|
2784
|
+
http.patch(`/projects/${projectId}`, { settings: { chatModel: fallback } }).catch(() => {
|
|
2785
|
+
/* persistence is best-effort; the in-memory switch is what matters */
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2608
2788
|
}
|
|
2609
2789
|
}, [
|
|
2610
2790
|
modelsLoading,
|
|
@@ -2614,6 +2794,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2614
2794
|
addSystemCard,
|
|
2615
2795
|
http,
|
|
2616
2796
|
projectId,
|
|
2797
|
+
canEdit,
|
|
2617
2798
|
]);
|
|
2618
2799
|
// ── Queued message editing ──────────────────────────────────────────────────
|
|
2619
2800
|
const [editingQueuedId, setEditingQueuedId] = useState(null);
|
|
@@ -2780,10 +2961,13 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2780
2961
|
useEffect(() => {
|
|
2781
2962
|
if (initialMessage && !hasConversation && sentInitialRef.current !== initialMessage) {
|
|
2782
2963
|
sentInitialRef.current = initialMessage;
|
|
2783
|
-
|
|
2964
|
+
// Same rule as handleSubmit: a host side-channel command (e.g. /teamsay)
|
|
2965
|
+
// renders no optimistic echo — the server's `message` event is the message.
|
|
2966
|
+
const sideChannel = matchesSideChannelCommand(extraCommands?.length ? [...COMMANDS, ...extraCommands] : COMMANDS, initialMessage);
|
|
2967
|
+
sendMessage(initialMessage, undefined, sideChannel ? { suppressUserMessage: true } : undefined);
|
|
2784
2968
|
onInitialMessageSent?.();
|
|
2785
2969
|
}
|
|
2786
|
-
}, [initialMessage, hasConversation, sendMessage, onInitialMessageSent]);
|
|
2970
|
+
}, [initialMessage, hasConversation, sendMessage, onInitialMessageSent, extraCommands]);
|
|
2787
2971
|
// ── Auto-send pending message (e.g. "Fix with AI", preview errors) ──────
|
|
2788
2972
|
// Defers sending while the AI is streaming to avoid queueing up auto-fix
|
|
2789
2973
|
// messages during active work. Messages are sent once streaming ends.
|
|
@@ -2794,6 +2978,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2794
2978
|
pendingMessageKey !== undefined &&
|
|
2795
2979
|
pendingMessageKey !== lastPendingKeyRef.current) {
|
|
2796
2980
|
lastPendingKeyRef.current = pendingMessageKey;
|
|
2981
|
+
// A read-only VIEWER never dispatches platform-composed sends (build
|
|
2982
|
+
// kickoffs, auto-fixes, preview-failure reports): the server 403s them
|
|
2983
|
+
// and the denial used to surface as a red error in the viewer's chat.
|
|
2984
|
+
// Consume the key (above) so the same message isn't retried.
|
|
2985
|
+
if (canEdit === false)
|
|
2986
|
+
return;
|
|
2797
2987
|
// A pending message is ALWAYS system-composed (an auto-fix prompt, a preview-failure
|
|
2798
2988
|
// report, a "Fix with AI" request) — never text the user typed. So it must NEVER
|
|
2799
2989
|
// render as a normal user bubble: either it's suppressed entirely (hidden build
|
|
@@ -2827,6 +3017,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2827
3017
|
pendingMessageUserInitiated,
|
|
2828
3018
|
sendMessage,
|
|
2829
3019
|
isLoading,
|
|
3020
|
+
canEdit,
|
|
2830
3021
|
]);
|
|
2831
3022
|
// When streaming ends, send any deferred message. (A user Stop also ends
|
|
2832
3023
|
// streaming, but handleAbort drops the deferred message first — and useChat's
|
|
@@ -2840,6 +3031,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2840
3031
|
deferredPendingRef.current = null;
|
|
2841
3032
|
deferredPendingSuppressRef.current = false;
|
|
2842
3033
|
deferredPendingUserInitiatedRef.current = false;
|
|
3034
|
+
// A viewer never dispatches platform-composed sends (same as the
|
|
3035
|
+
// immediate path above) — the refs are already drained, so it just drops.
|
|
3036
|
+
if (canEdit === false)
|
|
3037
|
+
return;
|
|
2843
3038
|
// Same rule as the immediate path: a deferred pending message is system-composed —
|
|
2844
3039
|
// always `automatic` (suppressed or visible), never a plain user bubble.
|
|
2845
3040
|
sendMessage(msg, undefined, {
|
|
@@ -2848,7 +3043,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2848
3043
|
...(userInitiated ? { userInitiated: true } : {}),
|
|
2849
3044
|
});
|
|
2850
3045
|
}
|
|
2851
|
-
}, [isLoading, sendMessage]);
|
|
3046
|
+
}, [isLoading, sendMessage, canEdit]);
|
|
2852
3047
|
// ── Auto-delete queued autofix messages when user edits a relevant file ────
|
|
2853
3048
|
const lastUserEditKeyRef = useRef(userEditedFileKey);
|
|
2854
3049
|
useEffect(() => {
|
|
@@ -3343,11 +3538,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3343
3538
|
return;
|
|
3344
3539
|
}
|
|
3345
3540
|
setFilePicker(null);
|
|
3346
|
-
// Show model picker when typing "/model <filter>" — scoped to
|
|
3347
|
-
//
|
|
3541
|
+
// Show model picker when typing "/model <filter>" — scoped to the mode a
|
|
3542
|
+
// --plan / --execute flag names, else to the LIVE conversation mode
|
|
3543
|
+
// (discovery = plan), so a plain pick changes the mode the user is in.
|
|
3348
3544
|
const modelMatch = val.match(/^\/model\s+/i);
|
|
3349
3545
|
if (modelMatch) {
|
|
3350
|
-
setModelPicker({ selectedIdx: -1, mode: parseModelModeCommand(val)?.mode });
|
|
3546
|
+
setModelPicker({ selectedIdx: -1, mode: parseModelModeCommand(val)?.mode ?? liveModelMode });
|
|
3351
3547
|
setCommandMenu(null);
|
|
3352
3548
|
return;
|
|
3353
3549
|
}
|
|
@@ -3358,7 +3554,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3358
3554
|
else {
|
|
3359
3555
|
setCommandMenu(null);
|
|
3360
3556
|
}
|
|
3361
|
-
}, [openFilePicker, autoResize, persistDraft]);
|
|
3557
|
+
}, [openFilePicker, autoResize, persistDraft, liveModelMode]);
|
|
3362
3558
|
// ── Execute command ────────────────────────────────────────────────────────
|
|
3363
3559
|
/** Sets textarea value and moves cursor to the end. */
|
|
3364
3560
|
const setInputAndCursorEnd = useCallback((val) => {
|
|
@@ -3372,6 +3568,17 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3372
3568
|
}
|
|
3373
3569
|
}, 0);
|
|
3374
3570
|
}, [setInputValue, autoResize]);
|
|
3571
|
+
// Viewer composer prefill: the box IS the team-chat input, so it starts with
|
|
3572
|
+
// "/teamsay " already typed (and handleSubmit re-fills it after each sent team
|
|
3573
|
+
// message). Once per mount, and never over text the user already has.
|
|
3574
|
+
const viewerPrefillDoneRef = useRef(false);
|
|
3575
|
+
useEffect(() => {
|
|
3576
|
+
if (canEdit !== false || viewerPrefillDoneRef.current)
|
|
3577
|
+
return;
|
|
3578
|
+
viewerPrefillDoneRef.current = true;
|
|
3579
|
+
if (!inputRef.current.trim())
|
|
3580
|
+
setInputAndCursorEnd('/teamsay ');
|
|
3581
|
+
}, [canEdit, setInputAndCursorEnd, conversationId]);
|
|
3375
3582
|
/**
|
|
3376
3583
|
* Select and apply a model by ID. When `mode` is given the choice persists to
|
|
3377
3584
|
* that mode's per-mode field (`planModel` / `executeModel` /
|
|
@@ -3459,6 +3666,20 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3459
3666
|
setInputAndCursorEnd(`/${id} `);
|
|
3460
3667
|
return;
|
|
3461
3668
|
}
|
|
3669
|
+
// A read-only VIEWER may run only viewerSafe commands (read / view-only /
|
|
3670
|
+
// per-user preference). Everything else writes shared project state or
|
|
3671
|
+
// triggers a Synthase turn the server 403s, so surface a read-only note
|
|
3672
|
+
// instead of a dead control that silently fails. Default-deny: an
|
|
3673
|
+
// unflagged command is unavailable to viewers.
|
|
3674
|
+
if (!canEdit) {
|
|
3675
|
+
const def = allCommands.find((c) => c.id === id);
|
|
3676
|
+
if (def && !def.viewerSafe) {
|
|
3677
|
+
addSystemCard(t('ide.chat.viewerReadOnlyCommand', undefined, {
|
|
3678
|
+
defaultValue: 'You have view-only access, so this command is unavailable. Ask an editor to make changes.',
|
|
3679
|
+
}));
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3462
3683
|
if (id === 'clear') {
|
|
3463
3684
|
setInputValue('');
|
|
3464
3685
|
await clearHistory();
|
|
@@ -3469,7 +3690,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3469
3690
|
}
|
|
3470
3691
|
else if (id === 'model') {
|
|
3471
3692
|
setInputAndCursorEnd('/model ');
|
|
3472
|
-
setModelPicker({ selectedIdx: -1 });
|
|
3693
|
+
setModelPicker({ selectedIdx: -1, mode: liveModelMode });
|
|
3473
3694
|
}
|
|
3474
3695
|
else if (id === 'mic' || id === 'dictate') {
|
|
3475
3696
|
setInputValue('');
|
|
@@ -3561,7 +3782,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3561
3782
|
defaultValue: "Today's AI allowance is used up — refreshes {{when}}.",
|
|
3562
3783
|
})
|
|
3563
3784
|
: t('ide.chat.usageAllowanceTodayLine', { percent: d.allowancePercent }, {
|
|
3564
|
-
|
|
3785
|
+
// Neutral phrasing on purpose: the allowance belongs to
|
|
3786
|
+
// the PROJECT (its owner's plan window) — "you've used"
|
|
3787
|
+
// misattributed it to whichever teammate ran /cost.
|
|
3788
|
+
defaultValue: "~{{percent}}% of today's AI allowance used.",
|
|
3565
3789
|
}))
|
|
3566
3790
|
: '';
|
|
3567
3791
|
// `inputTokens` counts only the UNCACHED prompt: every bond normalizes
|
|
@@ -3787,7 +4011,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3787
4011
|
}
|
|
3788
4012
|
else if (id === 'share') {
|
|
3789
4013
|
setInputValue('');
|
|
3790
|
-
|
|
4014
|
+
if (shareAllowed)
|
|
4015
|
+
setShareModal({ role: DEFAULT_SHARE_ROLE });
|
|
3791
4016
|
}
|
|
3792
4017
|
}, [
|
|
3793
4018
|
clearHistory,
|
|
@@ -3804,6 +4029,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3804
4029
|
refreshGitStatus,
|
|
3805
4030
|
openPanelOverlay,
|
|
3806
4031
|
extraCommandIds,
|
|
4032
|
+
canEdit,
|
|
4033
|
+
allCommands,
|
|
4034
|
+
liveModelMode,
|
|
4035
|
+
t,
|
|
3807
4036
|
]);
|
|
3808
4037
|
// When the auto-commit countdown reaches zero, fire the existing /commit path
|
|
3809
4038
|
// (no new backend) and pause until the next file change re-arms it. /commit
|
|
@@ -3839,6 +4068,26 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3839
4068
|
const trimmed = inputRef.current.trim();
|
|
3840
4069
|
if (!trimmed && attachedFiles.length === 0)
|
|
3841
4070
|
return;
|
|
4071
|
+
// Read-only VIEWER guard. A viewer cannot run the assistant (the server 403s
|
|
4072
|
+
// the chat route) or any project-mutating command, so block a plain message
|
|
4073
|
+
// or a write-command here with a read-only note instead of a silent 403 —
|
|
4074
|
+
// but let team side-channel messages (/teamsay) and viewerSafe commands
|
|
4075
|
+
// through so viewers can still talk to the team and read. (Menu-dispatched
|
|
4076
|
+
// commands are gated in executeCommand; this covers composer typing.)
|
|
4077
|
+
if (!canEdit) {
|
|
4078
|
+
const token = trimmed.match(/^\/(\S+)/)?.[1]?.toLowerCase();
|
|
4079
|
+
const def = token
|
|
4080
|
+
? allCommands.find((c) => c.id.toLowerCase() === token || c.aliases?.includes(token))
|
|
4081
|
+
: undefined;
|
|
4082
|
+
const allowed = matchesSideChannelCommand(allCommands, trimmed) || def?.viewerSafe === true;
|
|
4083
|
+
if (!allowed) {
|
|
4084
|
+
setInputValue('');
|
|
4085
|
+
addSystemCard(t('ide.chat.viewerReadOnly', undefined, {
|
|
4086
|
+
defaultValue: "You have view-only access, so you can't run the assistant here. You can still read along and use /teamsay to message the team.",
|
|
4087
|
+
}));
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
3842
4091
|
// Handle /autofix toggle locally
|
|
3843
4092
|
if (/^\/autofix$/i.test(trimmed)) {
|
|
3844
4093
|
void executeCommand('autofix');
|
|
@@ -3901,10 +4150,18 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3901
4150
|
return;
|
|
3902
4151
|
}
|
|
3903
4152
|
// Handle /share [role] locally — opens the share-link modal at the requested
|
|
3904
|
-
// role (default viewer). An unrecognized role shows usage instead.
|
|
4153
|
+
// role (default viewer). An unrecognized role shows usage instead. Below the
|
|
4154
|
+
// host's share capability (e.g. an editor where minting is admin+), say so
|
|
4155
|
+
// plainly instead of opening a modal whose every request 403s.
|
|
3905
4156
|
const shareMatch = parseShareCommand(trimmed);
|
|
3906
4157
|
if (shareMatch) {
|
|
3907
4158
|
setInputValue('');
|
|
4159
|
+
if (!shareAllowed) {
|
|
4160
|
+
addSystemCard(t('ide.chat.share.notAllowed', undefined, {
|
|
4161
|
+
defaultValue: 'Managing share links needs an admin role on this project.',
|
|
4162
|
+
}));
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
3908
4165
|
if (shareMatch.kind === 'invalid') {
|
|
3909
4166
|
addSystemCard(t('ide.chat.share.usage', { roles: SHARE_ROLES.join(', ') }, {
|
|
3910
4167
|
defaultValue: 'Usage: /share [role] — create a public link. Roles: {{roles}} (default viewer).',
|
|
@@ -3986,20 +4243,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3986
4243
|
{ action: buildUpgradeCta?.({}) ?? undefined });
|
|
3987
4244
|
}
|
|
3988
4245
|
else {
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
setSavedChatModel(name);
|
|
3993
|
-
addSystemCard(t('ide.chat.modelSet', { name: resolved?.label ?? name }, {
|
|
3994
|
-
defaultValue: `Chat model set to ${resolved?.label ?? name}`,
|
|
3995
|
-
}));
|
|
3996
|
-
}
|
|
3997
|
-
catch (error) {
|
|
3998
|
-
logger.warn('Failed to update chat model via /model command', { error });
|
|
3999
|
-
addSystemCard(t('ide.chat.modelError', undefined, {
|
|
4000
|
-
defaultValue: 'Failed to update chat model.',
|
|
4001
|
-
}));
|
|
4002
|
-
}
|
|
4246
|
+
// Unscoped /model targets the CURRENT conversation mode (discovery =
|
|
4247
|
+
// plan), same as /effort — never the legacy both-modes chatModel.
|
|
4248
|
+
await selectModel(name, resolved?.label ?? name, liveModelMode);
|
|
4003
4249
|
}
|
|
4004
4250
|
}
|
|
4005
4251
|
setInputValue('');
|
|
@@ -4157,6 +4403,21 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4157
4403
|
sendMessage(prompt);
|
|
4158
4404
|
return;
|
|
4159
4405
|
}
|
|
4406
|
+
// A host side-channel command (CommandDef.sideChannel, matched by id or alias — e.g.
|
|
4407
|
+
// molecule.dev's /teamsay + /t): send the raw text to the host's server intercept but
|
|
4408
|
+
// suppress the optimistic user bubble. The server emits the canonical `message` stream
|
|
4409
|
+
// event (persisted + broadcast to every member), which IS the visible message — so the
|
|
4410
|
+
// transcript never shows the literal "/command" text, and never shows it twice.
|
|
4411
|
+
if (matchesSideChannelCommand(allCommands, trimmed)) {
|
|
4412
|
+
// A viewer's composer is the team-chat box — re-fill the /teamsay prefix
|
|
4413
|
+
// after each sent team message so the next one is one keystroke away.
|
|
4414
|
+
if (canEdit === false)
|
|
4415
|
+
setInputAndCursorEnd('/teamsay ');
|
|
4416
|
+
else
|
|
4417
|
+
setInputValue('');
|
|
4418
|
+
sendMessage(trimmed, undefined, { suppressUserMessage: true });
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4160
4421
|
// Rewrite /explain with attachments into a proper prompt (attachments processed below)
|
|
4161
4422
|
let message = trimmed;
|
|
4162
4423
|
if (explainMatch) {
|
|
@@ -4203,7 +4464,17 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4203
4464
|
setAttachedFiles([]);
|
|
4204
4465
|
setAttachmentError(null);
|
|
4205
4466
|
sendMessage(message, chatAttachments.length > 0 ? chatAttachments : undefined);
|
|
4206
|
-
}, [
|
|
4467
|
+
}, [
|
|
4468
|
+
attachedFiles,
|
|
4469
|
+
http,
|
|
4470
|
+
projectId,
|
|
4471
|
+
sendMessage,
|
|
4472
|
+
setInputValue,
|
|
4473
|
+
runSavedScript,
|
|
4474
|
+
allCommands,
|
|
4475
|
+
selectModel,
|
|
4476
|
+
liveModelMode,
|
|
4477
|
+
]);
|
|
4207
4478
|
// External auto-submit. When the signal changes, submit the current input —
|
|
4208
4479
|
// used by the prompt → chat morph to send the prefilled prompt once the chat
|
|
4209
4480
|
// has docked into place (handleSubmit clears the input as it sends).
|
|
@@ -4239,12 +4510,22 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4239
4510
|
useEffect(() => {
|
|
4240
4511
|
if (openShareSignal !== undefined && openShareSignal !== lastOpenShareRef.current) {
|
|
4241
4512
|
lastOpenShareRef.current = openShareSignal;
|
|
4242
|
-
|
|
4513
|
+
if (shareAllowed)
|
|
4514
|
+
setShareModal({ role: DEFAULT_SHARE_ROLE });
|
|
4243
4515
|
}
|
|
4244
|
-
}, [openShareSignal]);
|
|
4516
|
+
}, [openShareSignal, shareAllowed]);
|
|
4245
4517
|
// ── Keyboard ───────────────────────────────────────────────────────────────
|
|
4246
4518
|
const filteredCmds = commandMenu
|
|
4247
|
-
? allCommands.filter((c) =>
|
|
4519
|
+
? allCommands.filter((c) =>
|
|
4520
|
+
// A showAll menu (slash-button toggle over existing text) lists every
|
|
4521
|
+
// command; a typed menu filters by the composer's text.
|
|
4522
|
+
(commandMenu.showAll === true || c.label.startsWith(inputRef.current)) &&
|
|
4523
|
+
// Viewers see only commands they can actually run (viewer-safe reads +
|
|
4524
|
+
// the /teamsay side channel) — no dead entries in the menu.
|
|
4525
|
+
(canEdit !== false || c.viewerSafe === true || c.sideChannel === true) &&
|
|
4526
|
+
// /share is gated separately: hosts commonly mint at admin+, so an
|
|
4527
|
+
// editor without the capability gets no dead menu entry either.
|
|
4528
|
+
(c.id !== 'share' || shareAllowed))
|
|
4248
4529
|
: [];
|
|
4249
4530
|
const filteredModels = useMemo(() => {
|
|
4250
4531
|
if (!modelPicker)
|
|
@@ -4259,7 +4540,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4259
4540
|
if (!q)
|
|
4260
4541
|
return AVAILABLE_MODELS;
|
|
4261
4542
|
return AVAILABLE_MODELS.filter((m) => m.id.toLowerCase().includes(q) || m.label.toLowerCase().includes(q));
|
|
4262
|
-
|
|
4543
|
+
// AVAILABLE_MODELS MUST be a dep: when a custom provider is edited (a model
|
|
4544
|
+
// renamed/added, a URL changed) the catalog refreshes in place, and without
|
|
4545
|
+
// this the picker keeps rendering the pre-edit snapshot — the new model
|
|
4546
|
+
// never appears and can't be selected. (The query is read from inputRef and
|
|
4547
|
+
// stays fresh because each keystroke re-sets modelPicker.)
|
|
4548
|
+
}, [modelPicker, AVAILABLE_MODELS]);
|
|
4263
4549
|
// ── Older models section ────────────────────────────────────────────────────
|
|
4264
4550
|
// Deprecated entries fold into a collapsed "Older models ⌄" section under the
|
|
4265
4551
|
// current models. The section auto-expands when the user's currentModel is in
|
|
@@ -4526,9 +4812,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4526
4812
|
e.preventDefault();
|
|
4527
4813
|
const idx = modelPicker.selectedIdx >= 0 ? modelPicker.selectedIdx : 0;
|
|
4528
4814
|
if (onManageCustomModels && idx === visibleModels.length) {
|
|
4815
|
+
const manageMode = modelPicker.mode === 'plan' || modelPicker.mode === 'execute'
|
|
4816
|
+
? modelPicker.mode
|
|
4817
|
+
: liveModelMode;
|
|
4529
4818
|
setModelPicker(null);
|
|
4530
4819
|
setInputValue('');
|
|
4531
|
-
onManageCustomModels();
|
|
4820
|
+
onManageCustomModels({ mode: manageMode });
|
|
4532
4821
|
return;
|
|
4533
4822
|
}
|
|
4534
4823
|
const model = visibleModels[idx];
|
|
@@ -4569,20 +4858,28 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4569
4858
|
if (commandMenu && filteredCmds.length > 0) {
|
|
4570
4859
|
if (e.key === 'ArrowDown') {
|
|
4571
4860
|
e.preventDefault();
|
|
4572
|
-
setCommandMenu((m) => m ? { selectedIdx: wrapIdx(m.selectedIdx, 1, filteredCmds.length) } : null);
|
|
4861
|
+
setCommandMenu((m) => m ? { ...m, selectedIdx: wrapIdx(m.selectedIdx, 1, filteredCmds.length) } : null);
|
|
4573
4862
|
return;
|
|
4574
4863
|
}
|
|
4575
4864
|
if (e.key === 'ArrowUp') {
|
|
4576
4865
|
e.preventDefault();
|
|
4577
|
-
setCommandMenu((m) => m ? { selectedIdx: wrapIdx(m.selectedIdx, -1, filteredCmds.length) } : null);
|
|
4866
|
+
setCommandMenu((m) => m ? { ...m, selectedIdx: wrapIdx(m.selectedIdx, -1, filteredCmds.length) } : null);
|
|
4578
4867
|
return;
|
|
4579
4868
|
}
|
|
4580
4869
|
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4870
|
+
// A showAll menu (opened by the slash BUTTON over existing text) only
|
|
4871
|
+
// executes an explicitly highlighted command — a bare Enter closes it
|
|
4872
|
+
// and falls through to submit the composer text as typed.
|
|
4873
|
+
if (commandMenu.showAll && commandMenu.selectedIdx < 0 && e.key === 'Enter') {
|
|
4874
|
+
setCommandMenu(null);
|
|
4875
|
+
}
|
|
4876
|
+
else {
|
|
4877
|
+
e.preventDefault();
|
|
4878
|
+
const cmd = filteredCmds[commandMenu.selectedIdx >= 0 ? commandMenu.selectedIdx : 0];
|
|
4879
|
+
if (cmd)
|
|
4880
|
+
void executeCommand(cmd.id);
|
|
4881
|
+
return;
|
|
4882
|
+
}
|
|
4586
4883
|
}
|
|
4587
4884
|
}
|
|
4588
4885
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -4636,8 +4933,16 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4636
4933
|
};
|
|
4637
4934
|
}
|
|
4638
4935
|
case 'mode':
|
|
4639
|
-
//
|
|
4640
|
-
//
|
|
4936
|
+
// The plan→build handoff ("🔨 Building your app") and the plan-mode
|
|
4937
|
+
// announcement a new conversation is seeded with ("📝 Plan mode") —
|
|
4938
|
+
// the server records mode cards only for those two.
|
|
4939
|
+
if (cardEvent.mode === 'plan') {
|
|
4940
|
+
return {
|
|
4941
|
+
id,
|
|
4942
|
+
text: t('ide.chat.phasePlanning', undefined, { defaultValue: '📝 Plan mode' }),
|
|
4943
|
+
timestamp,
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4641
4946
|
if (cardEvent.mode !== 'execute')
|
|
4642
4947
|
return null;
|
|
4643
4948
|
return {
|
|
@@ -4699,6 +5004,20 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4699
5004
|
// reading the guest limit they just escaped. Same reasoning the host's
|
|
4700
5005
|
// `upgrade_prompt` card factory already applies to the recorded guest card.
|
|
4701
5006
|
const isStaleAnonymousLimit = errorMeta?.requiresSignup === true && isAnonymous === false;
|
|
5007
|
+
// A read-only viewer's access-denied chat error must never sit as the
|
|
5008
|
+
// persistent red banner — every legitimate viewer action is already gated
|
|
5009
|
+
// client-side, so this only fires when something slipped through to the
|
|
5010
|
+
// server (or an older tab raced a role change). Flash a calm, gold
|
|
5011
|
+
// view-only notice for a few seconds instead, then clear.
|
|
5012
|
+
const isViewerAccessDenied = canEdit === false && !!error && /access denied/i.test(error);
|
|
5013
|
+
const [viewerDeniedFlash, setViewerDeniedFlash] = useState(false);
|
|
5014
|
+
useEffect(() => {
|
|
5015
|
+
if (!isViewerAccessDenied)
|
|
5016
|
+
return;
|
|
5017
|
+
setViewerDeniedFlash(true);
|
|
5018
|
+
const timer = setTimeout(() => setViewerDeniedFlash(false), 6000);
|
|
5019
|
+
return () => clearTimeout(timer);
|
|
5020
|
+
}, [isViewerAccessDenied, error]);
|
|
4702
5021
|
const timeline = useMemo(() => {
|
|
4703
5022
|
const items = [];
|
|
4704
5023
|
// A card-message (role:'system' carrying a cardEvent) renders as a SYSTEM CARD, not a chat
|
|
@@ -4866,11 +5185,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4866
5185
|
defaultValue: 'Show earlier messages',
|
|
4867
5186
|
}) }) })), (timeline.length > maxVisibleItems ? timeline.slice(-maxVisibleItems) : timeline).map((item) => (_jsx(ChatItemBoundary, { onError: onRenderError, render: () => {
|
|
4868
5187
|
if (item.kind === 'commit')
|
|
4869
|
-
return (_jsx(CommitCardItem, { card: item.card, onRevert: handleRevertCommit }, item.card.id));
|
|
5188
|
+
return (_jsx(CommitCardItem, { card: item.card, onRevert: canEdit === false ? undefined : handleRevertCommit }, item.card.id));
|
|
4870
5189
|
if (item.kind === 'activity')
|
|
4871
5190
|
return (_jsx(ActivityCard, { activity: item.card.activity, onActivityClick: onActivityClick }, item.card.id));
|
|
4872
5191
|
if (item.kind === 'tip')
|
|
4873
|
-
return (_jsx(TipCard, { text: item.card.text, onDismiss: () => dismissTip(item.card.id) }, item.card.id));
|
|
5192
|
+
return (_jsx(TipCard, { text: item.card.text, accent: item.card.accent, icon: item.card.icon, onDismiss: () => dismissTip(item.card.id) }, item.card.id));
|
|
4874
5193
|
if (item.kind === 'system') {
|
|
4875
5194
|
if (item.card.variant === 'settings') {
|
|
4876
5195
|
// Legacy inline branch — kept so any 'settings' card persisted
|
|
@@ -4971,12 +5290,33 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4971
5290
|
timestamp: msg.timestamp,
|
|
4972
5291
|
status: 'done',
|
|
4973
5292
|
hash,
|
|
4974
|
-
}, onRevert: handleRevertCommit }, msg.id));
|
|
5293
|
+
}, onRevert: canEdit === false ? undefined : handleRevertCommit }, msg.id));
|
|
4975
5294
|
}
|
|
4976
|
-
return (_jsx(MessageItem, { msg: msg, sendMessage: sendMessage, handleAskUserResponse: handleAskUserResponse, isLoading: isLoading, streamingStatus: streamingStatus, onNavigatePreview: onNavigatePreview, undoneTcIds: undoneTcIds, handleUndoToggle: handleUndoToggle, onFileOpen: onFileOpen, onFileDoubleClick: onFileDoubleClick, onFileDiff: onFileDiff, handleFileRevert: handleFileRevert, setInputAndCursorEnd: setInputAndCursorEnd, setModelPicker: setModelPicker,
|
|
5295
|
+
return (_jsx(MessageItem, { msg: msg, sendMessage: sendMessage, handleAskUserResponse: handleAskUserResponse, isLoading: isLoading, streamingStatus: streamingStatus, onNavigatePreview: onNavigatePreview, undoneTcIds: undoneTcIds, handleUndoToggle: handleUndoToggle, onFileOpen: onFileOpen, onFileDoubleClick: onFileDoubleClick, onFileDiff: onFileDiff, handleFileRevert: handleFileRevert, setInputAndCursorEnd: setInputAndCursorEnd, setModelPicker: setModelPicker, chatMode: liveModelMode, userAvatar: userAvatar,
|
|
5296
|
+
// Avatar clicks open the signed-in user's OWN profile, so only
|
|
5297
|
+
// their own messages get the handler: author-less ones (the
|
|
5298
|
+
// local echo, legacy solo rows) or an author matching
|
|
5299
|
+
// currentUserId. A teammate's avatar stays non-interactive —
|
|
5300
|
+
// clicking Test's face must not open Luke's profile editor.
|
|
5301
|
+
onAvatarClick: !msg.author?.id || (currentUserId != null && msg.author.id === currentUserId)
|
|
5302
|
+
? onUserAvatarClick
|
|
5303
|
+
: undefined, discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName, canEdit: canEdit }, msg.id));
|
|
4977
5304
|
} }, item.kind === 'message' ? item.msg.id : item.card.id))), error &&
|
|
4978
5305
|
!isStaleAnonymousLimit &&
|
|
4979
|
-
(errorMeta?.limitType ? (_jsx(ResourceLimitBanner, { message: error, action: buildUpgradeCta?.({ requiresSignup: errorMeta.requiresSignup }) })) :
|
|
5306
|
+
(errorMeta?.limitType ? (_jsx(ResourceLimitBanner, { message: error, action: buildUpgradeCta?.({ requiresSignup: errorMeta.requiresSignup }) })) : isViewerAccessDenied ? (
|
|
5307
|
+
// A viewer's denial is expected state, not an alarm: a brief, calm,
|
|
5308
|
+
// gold view-only notice (matching the viewer tip / team-note
|
|
5309
|
+
// treatment) that clears itself — never the persistent red banner.
|
|
5310
|
+
viewerDeniedFlash ? (_jsxs("div", { "data-mol-id": "chat-viewer-denied-notice", className: cm.cn(cm.textSize('xs'), cm.textMuted), style: {
|
|
5311
|
+
display: 'flex',
|
|
5312
|
+
alignItems: 'flex-start',
|
|
5313
|
+
gap: 8,
|
|
5314
|
+
marginBottom: 8,
|
|
5315
|
+
...chatCardStyle(NOTICE_TONE.gold.accent),
|
|
5316
|
+
lineHeight: 1.5,
|
|
5317
|
+
}, children: [_jsx(Icon, { name: "people", size: CHAT_CARD_ICON_SIZE, "aria-hidden": "true", style: { flexShrink: 0, marginTop: 1, color: NOTICE_TONE.gold.accent } }), _jsx("span", { style: { flex: 1 }, children: t('ide.chat.viewerReadOnly', undefined, {
|
|
5318
|
+
defaultValue: "You have view-only access, so you can't run the assistant here. You can still read along and use /teamsay to message the team.",
|
|
5319
|
+
}) })] })) : null) : (_jsx("div", { className: cm.cn(cm.textSize('sm'), cm.sp('p', 2), cm.sp('mb', 2), cm.bgErrorSubtle, cm.textError), style: { borderRadius: '6px' }, children: error }))), (() => {
|
|
4980
5320
|
const showActivity = isLoading || awaitingSandboxBoot;
|
|
4981
5321
|
const streamingMsg = isLoading
|
|
4982
5322
|
? [...visibleMessages].reverse().find((m) => m.isStreaming)
|
|
@@ -5775,9 +6115,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5775
6115
|
cursor: 'pointer',
|
|
5776
6116
|
textAlign: 'left',
|
|
5777
6117
|
}, children: t('ide.chat.olderModelsExpand', { count: deprecatedModels.length }, { defaultValue: 'Older models ⌄ ({{count}})' }) }))] })), onManageCustomModels && (_jsx("button", { type: "button", "data-mol-id": "chat-model-manage-custom", onClick: () => {
|
|
6118
|
+
const manageMode = modelPicker.mode === 'plan' || modelPicker.mode === 'execute'
|
|
6119
|
+
? modelPicker.mode
|
|
6120
|
+
: liveModelMode;
|
|
5778
6121
|
setModelPicker(null);
|
|
5779
6122
|
setInputValue('');
|
|
5780
|
-
onManageCustomModels();
|
|
6123
|
+
onManageCustomModels({ mode: manageMode });
|
|
5781
6124
|
}, onMouseEnter: (e) => {
|
|
5782
6125
|
;
|
|
5783
6126
|
e.currentTarget.style.background =
|
|
@@ -6173,7 +6516,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6173
6516
|
}, onMouseLeave: (e) => {
|
|
6174
6517
|
e.currentTarget.style.opacity = '0.7';
|
|
6175
6518
|
e.currentTarget.style.background = 'none';
|
|
6176
|
-
}, children: _jsx("svg", { width: "13", height: "13", viewBox: "0 0 12 12", style: { display: 'block' }, children: _jsx("path", { d: "M 2.5,6.5 L 6,3 L 9.5,6.5 M 6,3.5 L 6,10", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }) }))] }, qm.id))) })] })),
|
|
6519
|
+
}, children: _jsx("svg", { width: "13", height: "13", viewBox: "0 0 12 12", style: { display: 'block' }, children: _jsx("path", { d: "M 2.5,6.5 L 6,3 L 9.5,6.5 M 6,3.5 L 6,10", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }) }))] }, qm.id))) })] })), canEdit !== false &&
|
|
6520
|
+
pendingFiles != null &&
|
|
6177
6521
|
pendingFiles.length > 0 &&
|
|
6178
6522
|
!commandMenu &&
|
|
6179
6523
|
!modelPicker &&
|
|
@@ -6319,7 +6663,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6319
6663
|
opacity: 0.5,
|
|
6320
6664
|
transition: 'opacity 100ms, background 100ms',
|
|
6321
6665
|
...(!(f.additions || f.deletions) ? { marginLeft: 'auto' } : {}),
|
|
6322
|
-
}, children: _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", width: "12", height: "12", fill: "currentColor", children: _jsx("path", { d: "M1.22 6.28a.749.749 0 0 1 0-1.06l3.5-3.5a.749.749 0 1 1 1.06 1.06L3.561 5h7.188l.001.007L10.749 5c.058 0 .116.007.171.019A4.501 4.501 0 0 1 10.5 14H8.796a.75.75 0 0 1 0-1.5H10.5a3 3 0 1 0 0-6H3.561L5.78 8.72a.749.749 0 1 1-1.06 1.06l-3.5-3.5Z" }) }) }))] }, f.path))) }))] })), _jsxs("div", { className: cm.surfaceSecondary, style: {
|
|
6666
|
+
}, children: _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", width: "12", height: "12", fill: "currentColor", children: _jsx("path", { d: "M1.22 6.28a.749.749 0 0 1 0-1.06l3.5-3.5a.749.749 0 1 1 1.06 1.06L3.561 5h7.188l.001.007L10.749 5c.058 0 .116.007.171.019A4.501 4.501 0 0 1 10.5 14H8.796a.75.75 0 0 1 0-1.5H10.5a3 3 0 1 0 0-6H3.561L5.78 8.72a.749.749 0 1 1-1.06 1.06l-3.5-3.5Z" }) }) }))] }, f.path))) }))] })), _jsxs("div", { className: cm.surfaceSecondary, ...(canEdit === false ? { 'data-mol-viewer': '' } : {}), style: {
|
|
6323
6667
|
// Round the TOP corners only (8px) so the composer reads as a self-contained
|
|
6324
6668
|
// input with its own border on all sides, flush at the bottom-left/right with
|
|
6325
6669
|
// the panel's bottom edge. Discovery rounds ALL FOUR corners (the centered
|
|
@@ -6328,11 +6672,18 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6328
6672
|
borderRadius: discovery ? 8 : '8px 8px 0 0',
|
|
6329
6673
|
padding: '8px 10px',
|
|
6330
6674
|
cursor: 'text',
|
|
6675
|
+
// View-only mode: the composer IS the team-chat box, so it wears the
|
|
6676
|
+
// team-message gold border (and the box is pre-filled with /teamsay).
|
|
6677
|
+
...(canEdit === false ? { border: `1px solid ${NOTICE_TONE.gold.accent}` } : {}),
|
|
6331
6678
|
}, onClick: (e) => {
|
|
6332
6679
|
if (!e.target.closest('button')) {
|
|
6333
6680
|
textareaRef.current?.focus();
|
|
6334
6681
|
}
|
|
6335
|
-
}, children: [_jsx("textarea", { ref: textareaRef, "data-mol-chat-input": "", defaultValue: inputRef.current, autoComplete: "off", onChange: handleInputChange, onPaste: handlePaste, placeholder:
|
|
6682
|
+
}, children: [_jsx("textarea", { ref: textareaRef, "data-mol-chat-input": "", defaultValue: inputRef.current, autoComplete: "off", onChange: handleInputChange, onPaste: handlePaste, placeholder: canEdit === false
|
|
6683
|
+
? t('ide.chat.placeholderViewer', undefined, {
|
|
6684
|
+
defaultValue: 'Message your team',
|
|
6685
|
+
})
|
|
6686
|
+
: t('ide.chat.placeholder'), rows: 1, className: cm.textSize('sm'), style: {
|
|
6336
6687
|
width: '100%',
|
|
6337
6688
|
display: 'block',
|
|
6338
6689
|
padding: 0,
|
|
@@ -6358,7 +6709,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6358
6709
|
alignItems: 'center',
|
|
6359
6710
|
marginTop: '6px',
|
|
6360
6711
|
gap: '4px',
|
|
6361
|
-
}, children: [_jsx("button", { type: "button", onClick: () => {
|
|
6712
|
+
}, children: [canEdit !== false && (_jsx("button", { type: "button", onClick: () => {
|
|
6362
6713
|
const newMode = mode === 'plan' ? 'execute' : 'plan';
|
|
6363
6714
|
setMode(newMode);
|
|
6364
6715
|
http
|
|
@@ -6392,9 +6743,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6392
6743
|
? `1px solid ${isLight ? 'rgba(217,119,6,0.55)' : 'rgba(234,179,8,0.5)'}`
|
|
6393
6744
|
: 'none',
|
|
6394
6745
|
borderRadius: '3px',
|
|
6395
|
-
cursor: 'pointer',
|
|
6746
|
+
cursor: canEdit ? 'pointer' : 'not-allowed',
|
|
6396
6747
|
color: mode === 'plan' ? (isLight ? '#d97706' : '#eab308') : 'inherit',
|
|
6397
|
-
opacity: mode === 'plan' ? 1 : 0.4,
|
|
6748
|
+
opacity: !canEdit ? 0.4 : mode === 'plan' ? 1 : 0.4,
|
|
6398
6749
|
padding: 0,
|
|
6399
6750
|
transition: 'opacity 100ms, color 100ms',
|
|
6400
6751
|
}, onMouseEnter: (e) => {
|
|
@@ -6403,7 +6754,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6403
6754
|
}, onMouseLeave: (e) => {
|
|
6404
6755
|
if (mode !== 'plan')
|
|
6405
6756
|
e.currentTarget.style.opacity = '0.4';
|
|
6406
|
-
}, children: _jsx("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z" }) }) }), fastModeAvailable && (_jsx("button", { type: "button", "data-mol-id": "chat-fast-mode-toggle", onClick: () => {
|
|
6757
|
+
}, children: _jsx("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z" }) }) })), fastModeAvailable && canEdit !== false && (_jsx("button", { type: "button", "data-mol-id": "chat-fast-mode-toggle", disabled: !canEdit, onClick: () => {
|
|
6407
6758
|
const next = !fastMode;
|
|
6408
6759
|
setFastMode(next);
|
|
6409
6760
|
http
|
|
@@ -6468,7 +6819,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6468
6819
|
}, onMouseLeave: (e) => {
|
|
6469
6820
|
if (!isListening)
|
|
6470
6821
|
e.currentTarget.style.opacity = '0.4';
|
|
6471
|
-
}, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", width: "16", height: "16", style: { display: 'block' }, children: [_jsx("rect", { x: "6", y: "1", width: "4", height: "8", rx: "2", fill: isListening ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: "1.25" }), _jsx("path", { d: "M4 7v1a4 4 0 008 0V7", fill: "none", stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round" }), _jsx("line", { x1: "8", y1: "12", x2: "8", y2: "15", stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "15", x2: "10", y2: "15", stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round" })] }) }), _jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), title: t('ide.chat.attachFile', undefined, { defaultValue: 'Attach file' }), style: {
|
|
6822
|
+
}, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", width: "16", height: "16", style: { display: 'block' }, children: [_jsx("rect", { x: "6", y: "1", width: "4", height: "8", rx: "2", fill: isListening ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: "1.25" }), _jsx("path", { d: "M4 7v1a4 4 0 008 0V7", fill: "none", stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round" }), _jsx("line", { x1: "8", y1: "12", x2: "8", y2: "15", stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "15", x2: "10", y2: "15", stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round" })] }) }), canEdit !== false && (_jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), title: t('ide.chat.attachFile', undefined, { defaultValue: 'Attach file' }), style: {
|
|
6472
6823
|
display: 'inline-flex',
|
|
6473
6824
|
alignItems: 'center',
|
|
6474
6825
|
justifyContent: 'center',
|
|
@@ -6486,7 +6837,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6486
6837
|
e.currentTarget.style.opacity = '0.85';
|
|
6487
6838
|
}, onMouseLeave: (e) => {
|
|
6488
6839
|
e.currentTarget.style.opacity = '0.4';
|
|
6489
|
-
}, children: _jsx("svg", { width: "15", height: "15", viewBox: "0 0 16 16", fill: "currentColor", style: { display: 'block' }, children: _jsx("path", { d: "M12.212 3.02a1.753 1.753 0 0 0-2.478.003l-5.83 5.83a3.007 3.007 0 0 0-.88 2.127c0 .795.315 1.551.88 2.116.567.567 1.333.89 2.126.89.79 0 1.548-.321 2.116-.89l5.48-5.48a.75.75 0 0 1 1.061 1.06l-5.48 5.48a4.492 4.492 0 0 1-3.177 1.33c-1.2 0-2.345-.487-3.187-1.33a4.483 4.483 0 0 1-1.32-3.177c0-1.195.475-2.341 1.32-3.186l5.83-5.83a3.25 3.25 0 0 1 5.553 2.297c0 .863-.343 1.691-.953 2.301L7.439 12.39c-.375.377-.884.59-1.416.593a1.998 1.998 0 0 1-1.412-.593 1.992 1.992 0 0 1 0-2.828l5.48-5.48a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-5.48 5.48a.492.492 0 0 0 0 .707.499.499 0 0 0 .352.154.51.51 0 0 0 .356-.154l5.833-5.827a1.755 1.755 0 0 0 0-2.481Z" }) }) }), [
|
|
6840
|
+
}, children: _jsx("svg", { width: "15", height: "15", viewBox: "0 0 16 16", fill: "currentColor", style: { display: 'block' }, children: _jsx("path", { d: "M12.212 3.02a1.753 1.753 0 0 0-2.478.003l-5.83 5.83a3.007 3.007 0 0 0-.88 2.127c0 .795.315 1.551.88 2.116.567.567 1.333.89 2.126.89.79 0 1.548-.321 2.116-.89l5.48-5.48a.75.75 0 0 1 1.061 1.06l-5.48 5.48a4.492 4.492 0 0 1-3.177 1.33c-1.2 0-2.345-.487-3.187-1.33a4.483 4.483 0 0 1-1.32-3.177c0-1.195.475-2.341 1.32-3.186l5.83-5.83a3.25 3.25 0 0 1 5.553 2.297c0 .863-.343 1.691-.953 2.301L7.439 12.39c-.375.377-.884.59-1.416.593a1.998 1.998 0 0 1-1.412-.593 1.992 1.992 0 0 1 0-2.828l5.48-5.48a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-5.48 5.48a.492.492 0 0 0 0 .707.499.499 0 0 0 .352.154.51.51 0 0 0 .356-.154l5.833-5.827a1.755 1.755 0 0 0 0-2.481Z" }) }) })), [
|
|
6490
6841
|
{
|
|
6491
6842
|
sym: '@',
|
|
6492
6843
|
nudgeY: 0,
|
|
@@ -6515,34 +6866,38 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6515
6866
|
defaultValue: 'Slash commands',
|
|
6516
6867
|
}),
|
|
6517
6868
|
onClick: () => {
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6869
|
+
// TOGGLE the menu regardless of the composer's current text —
|
|
6870
|
+
// never clobbering it. With text present the menu opens in
|
|
6871
|
+
// showAll mode (every command listed; picking one replaces the
|
|
6872
|
+
// input via executeCommand's prefill). An empty box still gets
|
|
6873
|
+
// the type-ahead '/' so filtering-by-typing works as before.
|
|
6874
|
+
if (commandMenu) {
|
|
6523
6875
|
setCommandMenu(null);
|
|
6876
|
+
if (inputRef.current === '/') {
|
|
6877
|
+
// Remove the slash this button added on open.
|
|
6878
|
+
setInputValue('');
|
|
6879
|
+
autoResize();
|
|
6880
|
+
}
|
|
6524
6881
|
setTimeout(() => {
|
|
6525
6882
|
textareaRef.current?.focus();
|
|
6526
6883
|
}, 0);
|
|
6884
|
+
return;
|
|
6527
6885
|
}
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
setSoundsPicker(null);
|
|
6886
|
+
// Popups are one-at-a-time — close siblings first.
|
|
6887
|
+
setMicPicker(null);
|
|
6888
|
+
setModelPicker(null);
|
|
6889
|
+
setSoundsPicker(null);
|
|
6890
|
+
const cur = inputRef.current;
|
|
6891
|
+
if (!cur)
|
|
6535
6892
|
setInputAndCursorEnd('/');
|
|
6536
|
-
|
|
6537
|
-
}
|
|
6538
|
-
else {
|
|
6539
|
-
setTimeout(() => {
|
|
6540
|
-
textareaRef.current?.focus();
|
|
6541
|
-
}, 0);
|
|
6542
|
-
}
|
|
6893
|
+
setCommandMenu({ selectedIdx: -1, showAll: cur !== '' && cur !== '/' });
|
|
6543
6894
|
},
|
|
6544
6895
|
},
|
|
6545
|
-
]
|
|
6896
|
+
]
|
|
6897
|
+
// Viewers keep the / shortcut (viewer-safe commands + /teamsay) but not @ —
|
|
6898
|
+
// file mentions only feed Synthase turns.
|
|
6899
|
+
.filter(({ sym }) => canEdit !== false || sym !== '@')
|
|
6900
|
+
.map(({ sym, nudgeY, size: fontSize, title, onClick }) => (_jsx("button", { type: "button", onClick: onClick, title: title, style: {
|
|
6546
6901
|
display: 'inline-flex',
|
|
6547
6902
|
alignItems: 'center',
|
|
6548
6903
|
justifyContent: 'center',
|
|
@@ -6563,7 +6918,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6563
6918
|
e.currentTarget.style.opacity = '0.85';
|
|
6564
6919
|
}, onMouseLeave: (e) => {
|
|
6565
6920
|
e.currentTarget.style.opacity = '0.4';
|
|
6566
|
-
}, children: sym === 'slash' ? (_jsx("svg", { width: "9", height: "13", viewBox: "0 0 9 13", style: { display: 'block', position: 'relative', top: `${nudgeY}px` }, children: _jsx("line", { x1: "8", y1: "1", x2: "1", y2: "12", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) })) : (_jsx("span", { style: { position: 'relative', top: `${nudgeY}px` }, children: sym })) }, sym))),
|
|
6921
|
+
}, children: sym === 'slash' ? (_jsx("svg", { width: "9", height: "13", viewBox: "0 0 9 13", style: { display: 'block', position: 'relative', top: `${nudgeY}px` }, children: _jsx("line", { x1: "8", y1: "1", x2: "1", y2: "12", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) })) : (_jsx("span", { style: { position: 'relative', top: `${nudgeY}px` }, children: sym })) }, sym))), canEdit !== false &&
|
|
6922
|
+
contextUsage &&
|
|
6567
6923
|
(() => {
|
|
6568
6924
|
// The ring represents usage toward the auto-compaction threshold,
|
|
6569
6925
|
// not the raw context window. 100% = compaction will trigger.
|
|
@@ -6632,11 +6988,14 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6632
6988
|
transition: 'opacity 150ms',
|
|
6633
6989
|
}, children: [pct, "%"] }), _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, style: { display: 'block', transform: 'rotate(-90deg)', flexShrink: 0 }, children: [_jsx("circle", { cx: size / 2, cy: size / 2, r: r, fill: "none", stroke: "currentColor", strokeWidth: stroke, opacity: 0.15 }), _jsx("circle", { cx: size / 2, cy: size / 2, r: r, fill: "none", stroke: color, strokeWidth: stroke, strokeDasharray: `${c}`, strokeDashoffset: `${dashOffset}`, strokeLinecap: "round" })] })] }));
|
|
6634
6990
|
})(), _jsxs("div", { style: {
|
|
6635
|
-
|
|
6991
|
+
// 'auto' pins the stop/send cluster right; the context ring
|
|
6992
|
+
// (hidden for viewers even when contextUsage exists) otherwise
|
|
6993
|
+
// carries the auto margin and this becomes a 4px gap.
|
|
6994
|
+
marginLeft: canEdit !== false && contextUsage ? '4px' : 'auto',
|
|
6636
6995
|
display: 'flex',
|
|
6637
6996
|
gap: '4px',
|
|
6638
6997
|
alignItems: 'center',
|
|
6639
|
-
}, children: [(isLoading || isRemoteStreaming) && (_jsx("button", { type: "button", onClick: handleAbort, title: t('ide.chat.stop', undefined, { defaultValue: 'Stop' }), onMouseEnter: (e) => {
|
|
6998
|
+
}, children: [canEdit !== false && (isLoading || isRemoteStreaming) && (_jsx("button", { type: "button", onClick: handleAbort, title: t('ide.chat.stop', undefined, { defaultValue: 'Stop' }), onMouseEnter: (e) => {
|
|
6640
6999
|
e.currentTarget.style.background = 'rgba(248,81,73,0.3)';
|
|
6641
7000
|
e.currentTarget.style.borderColor = 'rgba(248,81,73,0.65)';
|
|
6642
7001
|
}, onMouseLeave: (e) => {
|
|
@@ -6690,7 +7049,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6690
7049
|
}
|
|
6691
7050
|
: undefined,
|
|
6692
7051
|
});
|
|
6693
|
-
} })), shareModal && (_jsx(ShareModal, { projectId: projectId, initialRole: shareModal.role, onClose: () => setShareModal(null), onCreated: (result) => {
|
|
7052
|
+
} })), shareModal && (_jsx(ShareModal, { projectId: projectId, initialRole: shareModal.role, canManage: shareAllowed, onClose: () => setShareModal(null), onCreated: (result) => {
|
|
6694
7053
|
// Surface the created link in the timeline so it persists after the
|
|
6695
7054
|
// modal closes — the role label and the public URL are both shown.
|
|
6696
7055
|
addSystemCard(t('ide.chat.share.created', { role: result.role }, { defaultValue: 'Created a {{role}} share link.' }), {
|
|
@@ -6709,8 +7068,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6709
7068
|
* @param props - Component props (see {@link MessageItemProps}).
|
|
6710
7069
|
* @returns The rendered chat panel element.
|
|
6711
7070
|
*/
|
|
6712
|
-
export function ChatPanel({ projectId, endpoint, initialMessage, onInitialMessageSent, activeFile, openTabs, onFileOpen, onFileDoubleClick, onFileDiff, onFileRevert, onFileChange, onFileDeleted, onCommit, onActivityClick, onRenderError, onProfileClick, onReadyToBuild, awaitingSandboxBoot, onClientAction, onTurnComplete, onLoadingChange, onNavigatePreview, onRegisterPushHandler, autoSubmitSignal, initialInputValue, hideConversationMenu, renderConversationHeader = true, conversationId: controlledConversationId, chatKey: controlledChatKey, onConversationId: controlledOnConversationId, openShareSignal: controlledShareSignal, openReportSignal: controlledReportSignal, openSettingsSignal: controlledSettingsSignal, onManageCustomModels, gitStatusTick, pendingMessage, pendingMessageKey, pendingMessageSuppressUser, pendingMessageUserInitiated, userEditedFile, userEditedFileKey, isPro, isAnonymous, buildUpgradeCta, buildHelpUpgradeSection, userAvatar, agentName, productName, version, extraCommands, feedbackUrl, className, }) {
|
|
7071
|
+
export function ChatPanel({ projectId, endpoint, initialMessage, onInitialMessageSent, activeFile, openTabs, onFileOpen, onFileDoubleClick, onFileDiff, onFileRevert, onFileChange, onFileDeleted, onCommit, onActivityClick, onRenderError, onProfileClick, currentUserId, onReadyToBuild, awaitingSandboxBoot, onClientAction, onTurnComplete, onLoadingChange, onNavigatePreview, onRegisterPushHandler, autoSubmitSignal, initialInputValue, hideConversationMenu, renderConversationHeader = true, conversationId: controlledConversationId, chatKey: controlledChatKey, onConversationId: controlledOnConversationId, openShareSignal: controlledShareSignal, openReportSignal: controlledReportSignal, openSettingsSignal: controlledSettingsSignal, onManageCustomModels, gitStatusTick, pendingMessage, pendingMessageKey, pendingMessageSuppressUser, pendingMessageUserInitiated, userEditedFile, userEditedFileKey, isPro, isAnonymous, canEdit = true, canShare, buildUpgradeCta, buildHelpUpgradeSection, userAvatar, agentName, productName, version, extraCommands, feedbackUrl, className, }) {
|
|
6713
7072
|
const cm = getClassMap();
|
|
7073
|
+
// Share management may be gated ABOVE canEdit by the host (see
|
|
7074
|
+
// ChatPanelProps.canShare) — gates the built-in header share button here and
|
|
7075
|
+
// is threaded into ChatInner for the /share command + modal.
|
|
7076
|
+
const shareAllowed = canShare ?? canEdit !== false;
|
|
6714
7077
|
const isNarrow = useNarrowViewport();
|
|
6715
7078
|
const isCoarse = useCoarsePointer();
|
|
6716
7079
|
const http = useHttpClient();
|
|
@@ -6863,11 +7226,11 @@ export function ChatPanel({ projectId, endpoint, initialMessage, onInitialMessag
|
|
|
6863
7226
|
textOverflow: 'ellipsis',
|
|
6864
7227
|
whiteSpace: 'nowrap',
|
|
6865
7228
|
opacity: 0.7,
|
|
6866
|
-
}, children: activeConv?.preview ?? 'Chat history' })] }), _jsx("button", { type: "button", "data-mol-id": "chat-share-button", onClick: () => setOpenShareSignal((n) => n + 1), className: cm.cn(cm.button({ variant: 'ghost', size: 'xs' }), cm.touchTarget), title: t('ide.chat.share.openShare', undefined, { defaultValue: 'Share project' }), "aria-label": t('ide.chat.share.openShare', undefined, {
|
|
7229
|
+
}, children: activeConv?.preview ?? 'Chat history' })] }), shareAllowed && (_jsx("button", { type: "button", "data-mol-id": "chat-share-button", onClick: () => setOpenShareSignal((n) => n + 1), className: cm.cn(cm.button({ variant: 'ghost', size: 'xs' }), cm.touchTarget), title: t('ide.chat.share.openShare', undefined, { defaultValue: 'Share project' }), "aria-label": t('ide.chat.share.openShare', undefined, {
|
|
6867
7230
|
defaultValue: 'Share project',
|
|
6868
|
-
}), style: { flexShrink: 0, display: 'inline-flex', alignItems: 'center' }, children: _jsx(Icon, { name: "share", size: 14, "aria-hidden": "true" }) }), _jsx("button", { type: "button", "data-mol-id": "chat-report-button", onClick: () => setOpenReportSignal((n) => n + 1), className: cm.cn(cm.button({ variant: 'ghost', size: 'xs' }), cm.touchTarget), title: t('ide.chat.report.openReport', undefined, { defaultValue: 'Report a bug' }), "aria-label": t('ide.chat.report.openReport', undefined, {
|
|
7231
|
+
}), style: { flexShrink: 0, display: 'inline-flex', alignItems: 'center' }, children: _jsx(Icon, { name: "share", size: 14, "aria-hidden": "true" }) })), _jsx("button", { type: "button", "data-mol-id": "chat-report-button", onClick: () => setOpenReportSignal((n) => n + 1), className: cm.cn(cm.button({ variant: 'ghost', size: 'xs' }), cm.touchTarget), title: t('ide.chat.report.openReport', undefined, { defaultValue: 'Report a bug' }), "aria-label": t('ide.chat.report.openReport', undefined, {
|
|
6869
7232
|
defaultValue: 'Report a bug',
|
|
6870
|
-
}), style: { flexShrink: 0, display: 'inline-flex', alignItems: 'center' }, children: _jsx(Icon, { name: "bug", size: 14, "aria-hidden": "true" }) }), _jsx("button", { type: "button", "data-mol-id": "chat-settings-button", onClick: () => setOpenSettingsSignal((n) => n + 1), className: cm.cn(cm.button({ variant: 'ghost', size: 'xs' }), cm.touchTarget), title: t('ide.chat.openSettings', undefined, { defaultValue: 'Settings' }), "aria-label": t('ide.chat.openSettings', undefined, { defaultValue: 'Settings' }), style: { flexShrink: 0, display: 'inline-flex', alignItems: 'center' }, children: _jsx(Icon, { name: "gear", size: 14, "aria-hidden": "true" }) }), _jsx("button", { type: "button", onClick: handleNewChat, className: cm.cn(cm.button({ variant: 'ghost', size: 'xs' }), cm.touchTarget), title: t('ide.chat.newChat', undefined, { defaultValue: 'New chat' }), style: { flexShrink: 0 }, children: "+" }), showDropdown && (_jsxs("div", { className: cm.cn(cm.surface, cm.borderAll), style: {
|
|
7233
|
+
}), style: { flexShrink: 0, display: 'inline-flex', alignItems: 'center' }, children: _jsx(Icon, { name: "bug", size: 14, "aria-hidden": "true" }) }), _jsx("button", { type: "button", "data-mol-id": "chat-settings-button", onClick: () => setOpenSettingsSignal((n) => n + 1), className: cm.cn(cm.button({ variant: 'ghost', size: 'xs' }), cm.touchTarget), title: t('ide.chat.openSettings', undefined, { defaultValue: 'Settings' }), "aria-label": t('ide.chat.openSettings', undefined, { defaultValue: 'Settings' }), style: { flexShrink: 0, display: 'inline-flex', alignItems: 'center' }, children: _jsx(Icon, { name: "gear", size: 14, "aria-hidden": "true" }) }), canEdit !== false && (_jsx("button", { type: "button", onClick: handleNewChat, className: cm.cn(cm.button({ variant: 'ghost', size: 'xs' }), cm.touchTarget), title: t('ide.chat.newChat', undefined, { defaultValue: 'New chat' }), style: { flexShrink: 0 }, children: "+" })), showDropdown && (_jsxs("div", { className: cm.cn(cm.surface, cm.borderAll), style: {
|
|
6871
7234
|
position: 'absolute',
|
|
6872
7235
|
top: '100%',
|
|
6873
7236
|
left: 0,
|
|
@@ -6918,7 +7281,7 @@ export function ChatPanel({ projectId, endpoint, initialMessage, onInitialMessag
|
|
|
6918
7281
|
textOverflow: 'ellipsis',
|
|
6919
7282
|
whiteSpace: 'nowrap',
|
|
6920
7283
|
width: '100%',
|
|
6921
|
-
}, children: conv.preview ?? 'New conversation' }), _jsx("span", { className: cm.cn(cm.textMuted, cm.textSize('xs')), style: { opacity: 0.55 }, children: relativeTime(conv.updatedAt) })] }, conv.id)))] }))] })), _jsx(ChatInner, { projectId: projectId, endpoint: chatEndpoint, initialMessage: initialMessage, onInitialMessageSent: onInitialMessageSent, isPro: isPro, isAnonymous: isAnonymous, buildUpgradeCta: buildUpgradeCta, buildHelpUpgradeSection: buildHelpUpgradeSection, activeFile: activeFile, openTabs: openTabs, onFileOpen: onFileOpen, onFileDoubleClick: onFileDoubleClick, onFileDiff: onFileDiff, onFileRevert: onFileRevert, onFileChange: onFileChange, onFileDeleted: onFileDeleted, onCommit: onCommit, onConversationId: reportConversationId, onActivityClick: onActivityClick, onRenderError: onRenderError, onProfileClick: onProfileClick, onReadyToBuild: onReadyToBuild, awaitingSandboxBoot: awaitingSandboxBoot, onClientAction: onClientAction, onTurnComplete: onTurnComplete, onLoadingChange: onLoadingChange, onNavigatePreview: onNavigatePreview, onRegisterPushHandler: onRegisterPushHandler, autoSubmitSignal: autoSubmitSignal, openSettingsSignal: effectiveSettingsSignal, onManageCustomModels: onManageCustomModels, openReportSignal: effectiveReportSignal, openShareSignal: effectiveShareSignal, initialInputValue: initialInputValue, pendingMessage: pendingMessage, pendingMessageKey: pendingMessageKey, pendingMessageSuppressUser: pendingMessageSuppressUser, pendingMessageUserInitiated: pendingMessageUserInitiated, userEditedFile: userEditedFile, userEditedFileKey: userEditedFileKey, gitStatusTick: gitStatusTick, discovery: hideConversationMenu, userAvatar: userAvatar, agentName: agentName, productName: productName, version: version, extraCommands: extraCommands, feedbackUrl: feedbackUrl }, chatKey)] }));
|
|
7284
|
+
}, children: conv.preview ?? 'New conversation' }), _jsx("span", { className: cm.cn(cm.textMuted, cm.textSize('xs')), style: { opacity: 0.55 }, children: relativeTime(conv.updatedAt) })] }, conv.id)))] }))] })), _jsx(ChatInner, { projectId: projectId, endpoint: chatEndpoint, initialMessage: initialMessage, onInitialMessageSent: onInitialMessageSent, isPro: isPro, isAnonymous: isAnonymous, canEdit: canEdit, canShare: shareAllowed, buildUpgradeCta: buildUpgradeCta, buildHelpUpgradeSection: buildHelpUpgradeSection, activeFile: activeFile, openTabs: openTabs, onFileOpen: onFileOpen, onFileDoubleClick: onFileDoubleClick, onFileDiff: onFileDiff, onFileRevert: onFileRevert, onFileChange: onFileChange, onFileDeleted: onFileDeleted, onCommit: onCommit, onConversationId: reportConversationId, onActivityClick: onActivityClick, onRenderError: onRenderError, onProfileClick: onProfileClick, currentUserId: currentUserId, onReadyToBuild: onReadyToBuild, awaitingSandboxBoot: awaitingSandboxBoot, onClientAction: onClientAction, onTurnComplete: onTurnComplete, onLoadingChange: onLoadingChange, onNavigatePreview: onNavigatePreview, onRegisterPushHandler: onRegisterPushHandler, autoSubmitSignal: autoSubmitSignal, openSettingsSignal: effectiveSettingsSignal, onManageCustomModels: onManageCustomModels, openReportSignal: effectiveReportSignal, openShareSignal: effectiveShareSignal, initialInputValue: initialInputValue, pendingMessage: pendingMessage, pendingMessageKey: pendingMessageKey, pendingMessageSuppressUser: pendingMessageSuppressUser, pendingMessageUserInitiated: pendingMessageUserInitiated, userEditedFile: userEditedFile, userEditedFileKey: userEditedFileKey, gitStatusTick: gitStatusTick, discovery: hideConversationMenu, userAvatar: userAvatar, agentName: agentName, productName: productName, version: version, extraCommands: extraCommands, feedbackUrl: feedbackUrl }, chatKey)] }));
|
|
6922
7285
|
}
|
|
6923
7286
|
ChatPanel.displayName = 'ChatPanel';
|
|
6924
7287
|
//# sourceMappingURL=ChatPanel.js.map
|