@molecule/app-ide-react 1.6.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 +70 -4
- package/dist/command-metadata.d.ts.map +1 -1
- package/dist/command-metadata.js +9 -2
- package/dist/command-metadata.js.map +1 -1
- package/dist/components/ChatPanel.d.ts +14 -2
- package/dist/components/ChatPanel.d.ts.map +1 -1
- package/dist/components/ChatPanel.js +332 -114
- 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/ToolCallCard.d.ts.map +1 -1
- package/dist/components/ToolCallCard.js +6 -3
- package/dist/components/ToolCallCard.js.map +1 -1
- package/dist/types.d.ts +36 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -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, agentName, } = 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';
|
|
@@ -1006,13 +1021,30 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
1006
1021
|
defaultValue: 'Team only — visible to your team; {{agentName}} will ignore it',
|
|
1007
1022
|
}), "aria-label": t('ide.chat.teamOnly.badge', { agentName: agentName ?? 'the assistant' }, {
|
|
1008
1023
|
defaultValue: 'Team only — visible to your team; {{agentName}} will ignore it',
|
|
1009
|
-
}),
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
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.
|
|
1012
1043
|
, {
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
|
|
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/')
|
|
1016
1048
|
? '\uD83D\uDDBC\uFE0F'
|
|
1017
1049
|
: att.mediaType.startsWith('audio/')
|
|
1018
1050
|
? '\uD83C\uDFB5'
|
|
@@ -1091,11 +1123,11 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
1091
1123
|
const tc = msg.toolCalls?.find((c) => c.id === block.id);
|
|
1092
1124
|
if (!tc)
|
|
1093
1125
|
return null;
|
|
1094
|
-
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));
|
|
1095
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 &&
|
|
1096
1128
|
msg.toolCalls.length > 0 &&
|
|
1097
1129
|
(!msg.blocks || msg.blocks.length === 0) &&
|
|
1098
|
-
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, {
|
|
1099
1131
|
defaultValue: 'Response stopped',
|
|
1100
1132
|
}) })), msg.loopLimitReached &&
|
|
1101
1133
|
!msg.isStreaming &&
|
|
@@ -1107,7 +1139,7 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
1107
1139
|
}),
|
|
1108
1140
|
action: () => {
|
|
1109
1141
|
setInputAndCursorEnd('/model ');
|
|
1110
|
-
setModelPicker({ selectedIdx: -1 });
|
|
1142
|
+
setModelPicker({ selectedIdx: -1, mode: chatMode });
|
|
1111
1143
|
},
|
|
1112
1144
|
},
|
|
1113
1145
|
{
|
|
@@ -1186,11 +1218,15 @@ const MessageItem = memo(function MessageItem(props) {
|
|
|
1186
1218
|
* @param props - Component props (see {@link MessageItemProps}).
|
|
1187
1219
|
* @returns The rendered chat inner component.
|
|
1188
1220
|
*/
|
|
1189
|
-
function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent, isPro, isAnonymous, canEdit, 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,
|
|
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,
|
|
1190
1222
|
// feedbackUrl: prop kept for back-compat (callers still pass it), but no longer
|
|
1191
1223
|
// consumed here — its only use was the command-menu footer link removed in P3-21.
|
|
1192
1224
|
}) {
|
|
1193
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;
|
|
1194
1230
|
const themeMode = useThemeMode();
|
|
1195
1231
|
const isLight = themeMode === 'light';
|
|
1196
1232
|
// Phone-width / touch-first branches: popovers cap with dvh, hover-revealed
|
|
@@ -1375,8 +1411,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1375
1411
|
// still drives the phase STATE (useChat's setMode); it just no longer spawns a card.
|
|
1376
1412
|
const cfg = soundsConfigRef.current;
|
|
1377
1413
|
const eventType = event.type;
|
|
1378
|
-
|
|
1379
|
-
|
|
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');
|
|
1380
1419
|
}
|
|
1381
1420
|
}, [t]);
|
|
1382
1421
|
// Countdown timer effect — ticks down and auto-sends fix message
|
|
@@ -1483,6 +1522,14 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1483
1522
|
onConversationId,
|
|
1484
1523
|
onStreamEvent: handleStreamEvent,
|
|
1485
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';
|
|
1486
1533
|
// Keep sendMessageRef in sync so the countdown effect can call the latest sendMessage
|
|
1487
1534
|
sendMessageRef.current = sendMessage;
|
|
1488
1535
|
// Keep the card-append ref current so handleStreamEvent (memoized) can append a teammate's
|
|
@@ -1627,7 +1674,15 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1627
1674
|
useEffect(() => {
|
|
1628
1675
|
if (typeof window !== 'undefined' && window.matchMedia?.('(pointer: coarse)').matches)
|
|
1629
1676
|
return;
|
|
1630
|
-
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);
|
|
1631
1686
|
}, []);
|
|
1632
1687
|
/** Update the ref, the DOM element, and the hasInput flag without re-rendering the parent. */
|
|
1633
1688
|
const setInputValue = useCallback((val) => {
|
|
@@ -1635,7 +1690,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1635
1690
|
const ta = textareaRef.current;
|
|
1636
1691
|
if (ta && ta.value !== val)
|
|
1637
1692
|
ta.value = val;
|
|
1638
|
-
|
|
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);
|
|
1639
1697
|
autoResize();
|
|
1640
1698
|
// Clear persisted draft when input is emptied (e.g. on submit)
|
|
1641
1699
|
if (!val) {
|
|
@@ -1646,7 +1704,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1646
1704
|
/* sessionStorage unavailable — safe to ignore, draft simply persists */
|
|
1647
1705
|
}
|
|
1648
1706
|
}
|
|
1649
|
-
}, [draftKey, autoResize]);
|
|
1707
|
+
}, [draftKey, autoResize, canEdit]);
|
|
1650
1708
|
// Persist draft text to sessionStorage so it survives refresh (debounced)
|
|
1651
1709
|
const draftTimerRef = useRef(null);
|
|
1652
1710
|
const persistDraft = useCallback(() => {
|
|
@@ -2260,6 +2318,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2260
2318
|
activityCardsLoadedConvRef.current = null;
|
|
2261
2319
|
if (!conversationId)
|
|
2262
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;
|
|
2263
2326
|
if (prevConv && prevConv !== conversationId)
|
|
2264
2327
|
setActivityCards([]);
|
|
2265
2328
|
let cancelled = false;
|
|
@@ -2289,10 +2352,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2289
2352
|
return () => {
|
|
2290
2353
|
cancelled = true;
|
|
2291
2354
|
};
|
|
2292
|
-
}, [conversationId, projectId, http]);
|
|
2355
|
+
}, [conversationId, projectId, http, canEdit]);
|
|
2293
2356
|
useEffect(() => {
|
|
2294
2357
|
if (!conversationId || activityCardsLoadedConvRef.current !== conversationId)
|
|
2295
2358
|
return;
|
|
2359
|
+
if (canEdit === false)
|
|
2360
|
+
return;
|
|
2296
2361
|
void http
|
|
2297
2362
|
.put(`/projects/${projectId}/conversations/${conversationId}/activity-cards`, {
|
|
2298
2363
|
activityCards: activityCards.filter((c) => !c.received),
|
|
@@ -2301,7 +2366,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2301
2366
|
// Best-effort save (bound as _error per Rule 14): the in-memory cards remain the
|
|
2302
2367
|
// source of truth this session if the PUT fails.
|
|
2303
2368
|
});
|
|
2304
|
-
}, [activityCards, conversationId, projectId, http]);
|
|
2369
|
+
}, [activityCards, conversationId, projectId, http, canEdit]);
|
|
2305
2370
|
// ── Auto-tips (dismissable onboarding hints) ──────────────────────────────
|
|
2306
2371
|
// Two surfaces (see chat-tips-utilities): an ENTRY_TIP shown once on a fresh
|
|
2307
2372
|
// conversation so a brand-new user always sees how to drive the agent, plus an
|
|
@@ -2331,6 +2396,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2331
2396
|
shownTipIdsRef.current = [];
|
|
2332
2397
|
// Re-arm the entry tip so a freshly-started chat shows the onboarding hint.
|
|
2333
2398
|
entryTipShownRef.current = false;
|
|
2399
|
+
// Re-arm the viewer composer prefill ('/teamsay ') for the new conversation.
|
|
2400
|
+
viewerPrefillDoneRef.current = false;
|
|
2334
2401
|
}
|
|
2335
2402
|
}, [conversationId]);
|
|
2336
2403
|
// Entry tip: the onboarding moment. Show ONE high-value hint as soon as a fresh
|
|
@@ -2364,7 +2431,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2364
2431
|
return;
|
|
2365
2432
|
viewerTipShownRef.current = true;
|
|
2366
2433
|
const text = t('ide.chat.tip.viewerTeamOnly', { agentName }, {
|
|
2367
|
-
defaultValue: 'View-only access — read along and /teamsay the team. This gold icon marks team-only messages ({{agentName}} ignores them). Running the assistant and changing the model or settings need editor access.',
|
|
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.',
|
|
2368
2435
|
});
|
|
2369
2436
|
setTipCards((prev) => [
|
|
2370
2437
|
...prev,
|
|
@@ -2409,7 +2476,31 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2409
2476
|
}, [messages, discovery]);
|
|
2410
2477
|
// ── Sounds picker (shown when /sounds is executed) ────────────────────────
|
|
2411
2478
|
const [soundsPicker, setSoundsPicker] = useState(null);
|
|
2412
|
-
|
|
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
|
+
}, []);
|
|
2413
2504
|
// ── Panel overlay (/skills, /scripts, /settings — closeable popups) ──────────
|
|
2414
2505
|
// These three commands open a closeable overlay above the composer (mirrors how
|
|
2415
2506
|
// /model and /sounds own this popup region) instead of dropping an inline card
|
|
@@ -2447,16 +2538,16 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2447
2538
|
updated = { ...soundsConfig, [eventType]: next };
|
|
2448
2539
|
}
|
|
2449
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).
|
|
2450
2543
|
try {
|
|
2451
|
-
|
|
2544
|
+
localStorage.setItem(SOUNDS_STORAGE_KEY, JSON.stringify(updated));
|
|
2545
|
+
soundsLocallySetRef.current = true;
|
|
2452
2546
|
}
|
|
2453
|
-
catch (
|
|
2454
|
-
|
|
2455
|
-
addSystemCard(t('ide.chat.soundsError', undefined, {
|
|
2456
|
-
defaultValue: 'Failed to update sound settings.',
|
|
2457
|
-
}));
|
|
2547
|
+
catch (_error) {
|
|
2548
|
+
// localStorage unavailable — the choice still applies for this session.
|
|
2458
2549
|
}
|
|
2459
|
-
}, [soundsConfig
|
|
2550
|
+
}, [soundsConfig]);
|
|
2460
2551
|
// ── Current project settings (model + maxloops + sounds) ──────────────────
|
|
2461
2552
|
// Project-scoped so any custom (bring-your-own AI) models the project has
|
|
2462
2553
|
// configured are included alongside the platform catalog.
|
|
@@ -2572,7 +2663,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2572
2663
|
if (typeof s?.autoApproveCommands === 'boolean') {
|
|
2573
2664
|
setAutoApproveCommandsEnabled(s.autoApproveCommands);
|
|
2574
2665
|
}
|
|
2575
|
-
|
|
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) {
|
|
2576
2669
|
setSoundsConfig((prev) => ({ ...prev, ...s.sounds }));
|
|
2577
2670
|
}
|
|
2578
2671
|
// Restore the persisted auto-commit cadence in the paused state (it
|
|
@@ -2684,9 +2777,14 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2684
2777
|
if (fallback) {
|
|
2685
2778
|
setCurrentModel(fallback);
|
|
2686
2779
|
setSavedChatModel(fallback);
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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
|
+
}
|
|
2690
2788
|
}
|
|
2691
2789
|
}, [
|
|
2692
2790
|
modelsLoading,
|
|
@@ -2696,6 +2794,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2696
2794
|
addSystemCard,
|
|
2697
2795
|
http,
|
|
2698
2796
|
projectId,
|
|
2797
|
+
canEdit,
|
|
2699
2798
|
]);
|
|
2700
2799
|
// ── Queued message editing ──────────────────────────────────────────────────
|
|
2701
2800
|
const [editingQueuedId, setEditingQueuedId] = useState(null);
|
|
@@ -2879,6 +2978,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2879
2978
|
pendingMessageKey !== undefined &&
|
|
2880
2979
|
pendingMessageKey !== lastPendingKeyRef.current) {
|
|
2881
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;
|
|
2882
2987
|
// A pending message is ALWAYS system-composed (an auto-fix prompt, a preview-failure
|
|
2883
2988
|
// report, a "Fix with AI" request) — never text the user typed. So it must NEVER
|
|
2884
2989
|
// render as a normal user bubble: either it's suppressed entirely (hidden build
|
|
@@ -2912,6 +3017,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2912
3017
|
pendingMessageUserInitiated,
|
|
2913
3018
|
sendMessage,
|
|
2914
3019
|
isLoading,
|
|
3020
|
+
canEdit,
|
|
2915
3021
|
]);
|
|
2916
3022
|
// When streaming ends, send any deferred message. (A user Stop also ends
|
|
2917
3023
|
// streaming, but handleAbort drops the deferred message first — and useChat's
|
|
@@ -2925,6 +3031,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2925
3031
|
deferredPendingRef.current = null;
|
|
2926
3032
|
deferredPendingSuppressRef.current = false;
|
|
2927
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;
|
|
2928
3038
|
// Same rule as the immediate path: a deferred pending message is system-composed —
|
|
2929
3039
|
// always `automatic` (suppressed or visible), never a plain user bubble.
|
|
2930
3040
|
sendMessage(msg, undefined, {
|
|
@@ -2933,7 +3043,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2933
3043
|
...(userInitiated ? { userInitiated: true } : {}),
|
|
2934
3044
|
});
|
|
2935
3045
|
}
|
|
2936
|
-
}, [isLoading, sendMessage]);
|
|
3046
|
+
}, [isLoading, sendMessage, canEdit]);
|
|
2937
3047
|
// ── Auto-delete queued autofix messages when user edits a relevant file ────
|
|
2938
3048
|
const lastUserEditKeyRef = useRef(userEditedFileKey);
|
|
2939
3049
|
useEffect(() => {
|
|
@@ -3428,11 +3538,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3428
3538
|
return;
|
|
3429
3539
|
}
|
|
3430
3540
|
setFilePicker(null);
|
|
3431
|
-
// Show model picker when typing "/model <filter>" — scoped to
|
|
3432
|
-
//
|
|
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.
|
|
3433
3544
|
const modelMatch = val.match(/^\/model\s+/i);
|
|
3434
3545
|
if (modelMatch) {
|
|
3435
|
-
setModelPicker({ selectedIdx: -1, mode: parseModelModeCommand(val)?.mode });
|
|
3546
|
+
setModelPicker({ selectedIdx: -1, mode: parseModelModeCommand(val)?.mode ?? liveModelMode });
|
|
3436
3547
|
setCommandMenu(null);
|
|
3437
3548
|
return;
|
|
3438
3549
|
}
|
|
@@ -3443,7 +3554,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3443
3554
|
else {
|
|
3444
3555
|
setCommandMenu(null);
|
|
3445
3556
|
}
|
|
3446
|
-
}, [openFilePicker, autoResize, persistDraft]);
|
|
3557
|
+
}, [openFilePicker, autoResize, persistDraft, liveModelMode]);
|
|
3447
3558
|
// ── Execute command ────────────────────────────────────────────────────────
|
|
3448
3559
|
/** Sets textarea value and moves cursor to the end. */
|
|
3449
3560
|
const setInputAndCursorEnd = useCallback((val) => {
|
|
@@ -3457,6 +3568,17 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3457
3568
|
}
|
|
3458
3569
|
}, 0);
|
|
3459
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]);
|
|
3460
3582
|
/**
|
|
3461
3583
|
* Select and apply a model by ID. When `mode` is given the choice persists to
|
|
3462
3584
|
* that mode's per-mode field (`planModel` / `executeModel` /
|
|
@@ -3568,7 +3690,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3568
3690
|
}
|
|
3569
3691
|
else if (id === 'model') {
|
|
3570
3692
|
setInputAndCursorEnd('/model ');
|
|
3571
|
-
setModelPicker({ selectedIdx: -1 });
|
|
3693
|
+
setModelPicker({ selectedIdx: -1, mode: liveModelMode });
|
|
3572
3694
|
}
|
|
3573
3695
|
else if (id === 'mic' || id === 'dictate') {
|
|
3574
3696
|
setInputValue('');
|
|
@@ -3660,7 +3782,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3660
3782
|
defaultValue: "Today's AI allowance is used up — refreshes {{when}}.",
|
|
3661
3783
|
})
|
|
3662
3784
|
: t('ide.chat.usageAllowanceTodayLine', { percent: d.allowancePercent }, {
|
|
3663
|
-
|
|
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.",
|
|
3664
3789
|
}))
|
|
3665
3790
|
: '';
|
|
3666
3791
|
// `inputTokens` counts only the UNCACHED prompt: every bond normalizes
|
|
@@ -3886,7 +4011,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3886
4011
|
}
|
|
3887
4012
|
else if (id === 'share') {
|
|
3888
4013
|
setInputValue('');
|
|
3889
|
-
|
|
4014
|
+
if (shareAllowed)
|
|
4015
|
+
setShareModal({ role: DEFAULT_SHARE_ROLE });
|
|
3890
4016
|
}
|
|
3891
4017
|
}, [
|
|
3892
4018
|
clearHistory,
|
|
@@ -3905,6 +4031,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3905
4031
|
extraCommandIds,
|
|
3906
4032
|
canEdit,
|
|
3907
4033
|
allCommands,
|
|
4034
|
+
liveModelMode,
|
|
3908
4035
|
t,
|
|
3909
4036
|
]);
|
|
3910
4037
|
// When the auto-commit countdown reaches zero, fire the existing /commit path
|
|
@@ -4023,10 +4150,18 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4023
4150
|
return;
|
|
4024
4151
|
}
|
|
4025
4152
|
// Handle /share [role] locally — opens the share-link modal at the requested
|
|
4026
|
-
// 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.
|
|
4027
4156
|
const shareMatch = parseShareCommand(trimmed);
|
|
4028
4157
|
if (shareMatch) {
|
|
4029
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
|
+
}
|
|
4030
4165
|
if (shareMatch.kind === 'invalid') {
|
|
4031
4166
|
addSystemCard(t('ide.chat.share.usage', { roles: SHARE_ROLES.join(', ') }, {
|
|
4032
4167
|
defaultValue: 'Usage: /share [role] — create a public link. Roles: {{roles}} (default viewer).',
|
|
@@ -4108,20 +4243,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4108
4243
|
{ action: buildUpgradeCta?.({}) ?? undefined });
|
|
4109
4244
|
}
|
|
4110
4245
|
else {
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
setSavedChatModel(name);
|
|
4115
|
-
addSystemCard(t('ide.chat.modelSet', { name: resolved?.label ?? name }, {
|
|
4116
|
-
defaultValue: `Chat model set to ${resolved?.label ?? name}`,
|
|
4117
|
-
}));
|
|
4118
|
-
}
|
|
4119
|
-
catch (error) {
|
|
4120
|
-
logger.warn('Failed to update chat model via /model command', { error });
|
|
4121
|
-
addSystemCard(t('ide.chat.modelError', undefined, {
|
|
4122
|
-
defaultValue: 'Failed to update chat model.',
|
|
4123
|
-
}));
|
|
4124
|
-
}
|
|
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);
|
|
4125
4249
|
}
|
|
4126
4250
|
}
|
|
4127
4251
|
setInputValue('');
|
|
@@ -4285,7 +4409,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4285
4409
|
// event (persisted + broadcast to every member), which IS the visible message — so the
|
|
4286
4410
|
// transcript never shows the literal "/command" text, and never shows it twice.
|
|
4287
4411
|
if (matchesSideChannelCommand(allCommands, trimmed)) {
|
|
4288
|
-
|
|
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('');
|
|
4289
4418
|
sendMessage(trimmed, undefined, { suppressUserMessage: true });
|
|
4290
4419
|
return;
|
|
4291
4420
|
}
|
|
@@ -4335,7 +4464,17 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4335
4464
|
setAttachedFiles([]);
|
|
4336
4465
|
setAttachmentError(null);
|
|
4337
4466
|
sendMessage(message, chatAttachments.length > 0 ? chatAttachments : undefined);
|
|
4338
|
-
}, [
|
|
4467
|
+
}, [
|
|
4468
|
+
attachedFiles,
|
|
4469
|
+
http,
|
|
4470
|
+
projectId,
|
|
4471
|
+
sendMessage,
|
|
4472
|
+
setInputValue,
|
|
4473
|
+
runSavedScript,
|
|
4474
|
+
allCommands,
|
|
4475
|
+
selectModel,
|
|
4476
|
+
liveModelMode,
|
|
4477
|
+
]);
|
|
4339
4478
|
// External auto-submit. When the signal changes, submit the current input —
|
|
4340
4479
|
// used by the prompt → chat morph to send the prefilled prompt once the chat
|
|
4341
4480
|
// has docked into place (handleSubmit clears the input as it sends).
|
|
@@ -4371,12 +4510,22 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4371
4510
|
useEffect(() => {
|
|
4372
4511
|
if (openShareSignal !== undefined && openShareSignal !== lastOpenShareRef.current) {
|
|
4373
4512
|
lastOpenShareRef.current = openShareSignal;
|
|
4374
|
-
|
|
4513
|
+
if (shareAllowed)
|
|
4514
|
+
setShareModal({ role: DEFAULT_SHARE_ROLE });
|
|
4375
4515
|
}
|
|
4376
|
-
}, [openShareSignal]);
|
|
4516
|
+
}, [openShareSignal, shareAllowed]);
|
|
4377
4517
|
// ── Keyboard ───────────────────────────────────────────────────────────────
|
|
4378
4518
|
const filteredCmds = commandMenu
|
|
4379
|
-
? 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))
|
|
4380
4529
|
: [];
|
|
4381
4530
|
const filteredModels = useMemo(() => {
|
|
4382
4531
|
if (!modelPicker)
|
|
@@ -4663,9 +4812,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4663
4812
|
e.preventDefault();
|
|
4664
4813
|
const idx = modelPicker.selectedIdx >= 0 ? modelPicker.selectedIdx : 0;
|
|
4665
4814
|
if (onManageCustomModels && idx === visibleModels.length) {
|
|
4815
|
+
const manageMode = modelPicker.mode === 'plan' || modelPicker.mode === 'execute'
|
|
4816
|
+
? modelPicker.mode
|
|
4817
|
+
: liveModelMode;
|
|
4666
4818
|
setModelPicker(null);
|
|
4667
4819
|
setInputValue('');
|
|
4668
|
-
onManageCustomModels();
|
|
4820
|
+
onManageCustomModels({ mode: manageMode });
|
|
4669
4821
|
return;
|
|
4670
4822
|
}
|
|
4671
4823
|
const model = visibleModels[idx];
|
|
@@ -4706,20 +4858,28 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4706
4858
|
if (commandMenu && filteredCmds.length > 0) {
|
|
4707
4859
|
if (e.key === 'ArrowDown') {
|
|
4708
4860
|
e.preventDefault();
|
|
4709
|
-
setCommandMenu((m) => m ? { selectedIdx: wrapIdx(m.selectedIdx, 1, filteredCmds.length) } : null);
|
|
4861
|
+
setCommandMenu((m) => m ? { ...m, selectedIdx: wrapIdx(m.selectedIdx, 1, filteredCmds.length) } : null);
|
|
4710
4862
|
return;
|
|
4711
4863
|
}
|
|
4712
4864
|
if (e.key === 'ArrowUp') {
|
|
4713
4865
|
e.preventDefault();
|
|
4714
|
-
setCommandMenu((m) => m ? { selectedIdx: wrapIdx(m.selectedIdx, -1, filteredCmds.length) } : null);
|
|
4866
|
+
setCommandMenu((m) => m ? { ...m, selectedIdx: wrapIdx(m.selectedIdx, -1, filteredCmds.length) } : null);
|
|
4715
4867
|
return;
|
|
4716
4868
|
}
|
|
4717
4869
|
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
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
|
+
}
|
|
4723
4883
|
}
|
|
4724
4884
|
}
|
|
4725
4885
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -4773,8 +4933,16 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4773
4933
|
};
|
|
4774
4934
|
}
|
|
4775
4935
|
case 'mode':
|
|
4776
|
-
//
|
|
4777
|
-
//
|
|
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
|
+
}
|
|
4778
4946
|
if (cardEvent.mode !== 'execute')
|
|
4779
4947
|
return null;
|
|
4780
4948
|
return {
|
|
@@ -4836,6 +5004,20 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4836
5004
|
// reading the guest limit they just escaped. Same reasoning the host's
|
|
4837
5005
|
// `upgrade_prompt` card factory already applies to the recorded guest card.
|
|
4838
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]);
|
|
4839
5021
|
const timeline = useMemo(() => {
|
|
4840
5022
|
const items = [];
|
|
4841
5023
|
// A card-message (role:'system' carrying a cardEvent) renders as a SYSTEM CARD, not a chat
|
|
@@ -5003,7 +5185,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5003
5185
|
defaultValue: 'Show earlier messages',
|
|
5004
5186
|
}) }) })), (timeline.length > maxVisibleItems ? timeline.slice(-maxVisibleItems) : timeline).map((item) => (_jsx(ChatItemBoundary, { onError: onRenderError, render: () => {
|
|
5005
5187
|
if (item.kind === 'commit')
|
|
5006
|
-
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));
|
|
5007
5189
|
if (item.kind === 'activity')
|
|
5008
5190
|
return (_jsx(ActivityCard, { activity: item.card.activity, onActivityClick: onActivityClick }, item.card.id));
|
|
5009
5191
|
if (item.kind === 'tip')
|
|
@@ -5108,9 +5290,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5108
5290
|
timestamp: msg.timestamp,
|
|
5109
5291
|
status: 'done',
|
|
5110
5292
|
hash,
|
|
5111
|
-
}, onRevert: handleRevertCommit }, msg.id));
|
|
5293
|
+
}, onRevert: canEdit === false ? undefined : handleRevertCommit }, msg.id));
|
|
5112
5294
|
}
|
|
5113
|
-
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, userAvatar: userAvatar,
|
|
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,
|
|
5114
5296
|
// Avatar clicks open the signed-in user's OWN profile, so only
|
|
5115
5297
|
// their own messages get the handler: author-less ones (the
|
|
5116
5298
|
// local echo, legacy solo rows) or an author matching
|
|
@@ -5118,10 +5300,23 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5118
5300
|
// clicking Test's face must not open Luke's profile editor.
|
|
5119
5301
|
onAvatarClick: !msg.author?.id || (currentUserId != null && msg.author.id === currentUserId)
|
|
5120
5302
|
? onUserAvatarClick
|
|
5121
|
-
: undefined, discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName }, msg.id));
|
|
5303
|
+
: undefined, discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName, canEdit: canEdit }, msg.id));
|
|
5122
5304
|
} }, item.kind === 'message' ? item.msg.id : item.card.id))), error &&
|
|
5123
5305
|
!isStaleAnonymousLimit &&
|
|
5124
|
-
(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 }))), (() => {
|
|
5125
5320
|
const showActivity = isLoading || awaitingSandboxBoot;
|
|
5126
5321
|
const streamingMsg = isLoading
|
|
5127
5322
|
? [...visibleMessages].reverse().find((m) => m.isStreaming)
|
|
@@ -5920,9 +6115,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5920
6115
|
cursor: 'pointer',
|
|
5921
6116
|
textAlign: 'left',
|
|
5922
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;
|
|
5923
6121
|
setModelPicker(null);
|
|
5924
6122
|
setInputValue('');
|
|
5925
|
-
onManageCustomModels();
|
|
6123
|
+
onManageCustomModels({ mode: manageMode });
|
|
5926
6124
|
}, onMouseEnter: (e) => {
|
|
5927
6125
|
;
|
|
5928
6126
|
e.currentTarget.style.background =
|
|
@@ -6318,7 +6516,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6318
6516
|
}, onMouseLeave: (e) => {
|
|
6319
6517
|
e.currentTarget.style.opacity = '0.7';
|
|
6320
6518
|
e.currentTarget.style.background = 'none';
|
|
6321
|
-
}, 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 &&
|
|
6322
6521
|
pendingFiles.length > 0 &&
|
|
6323
6522
|
!commandMenu &&
|
|
6324
6523
|
!modelPicker &&
|
|
@@ -6464,7 +6663,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6464
6663
|
opacity: 0.5,
|
|
6465
6664
|
transition: 'opacity 100ms, background 100ms',
|
|
6466
6665
|
...(!(f.additions || f.deletions) ? { marginLeft: 'auto' } : {}),
|
|
6467
|
-
}, 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: {
|
|
6468
6667
|
// Round the TOP corners only (8px) so the composer reads as a self-contained
|
|
6469
6668
|
// input with its own border on all sides, flush at the bottom-left/right with
|
|
6470
6669
|
// the panel's bottom edge. Discovery rounds ALL FOUR corners (the centered
|
|
@@ -6473,11 +6672,18 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6473
6672
|
borderRadius: discovery ? 8 : '8px 8px 0 0',
|
|
6474
6673
|
padding: '8px 10px',
|
|
6475
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}` } : {}),
|
|
6476
6678
|
}, onClick: (e) => {
|
|
6477
6679
|
if (!e.target.closest('button')) {
|
|
6478
6680
|
textareaRef.current?.focus();
|
|
6479
6681
|
}
|
|
6480
|
-
}, 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: {
|
|
6481
6687
|
width: '100%',
|
|
6482
6688
|
display: 'block',
|
|
6483
6689
|
padding: 0,
|
|
@@ -6503,7 +6709,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6503
6709
|
alignItems: 'center',
|
|
6504
6710
|
marginTop: '6px',
|
|
6505
6711
|
gap: '4px',
|
|
6506
|
-
}, children: [_jsx("button", { type: "button",
|
|
6712
|
+
}, children: [canEdit !== false && (_jsx("button", { type: "button", onClick: () => {
|
|
6507
6713
|
const newMode = mode === 'plan' ? 'execute' : 'plan';
|
|
6508
6714
|
setMode(newMode);
|
|
6509
6715
|
http
|
|
@@ -6548,7 +6754,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6548
6754
|
}, onMouseLeave: (e) => {
|
|
6549
6755
|
if (mode !== 'plan')
|
|
6550
6756
|
e.currentTarget.style.opacity = '0.4';
|
|
6551
|
-
}, 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", disabled: !canEdit, 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: () => {
|
|
6552
6758
|
const next = !fastMode;
|
|
6553
6759
|
setFastMode(next);
|
|
6554
6760
|
http
|
|
@@ -6613,7 +6819,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6613
6819
|
}, onMouseLeave: (e) => {
|
|
6614
6820
|
if (!isListening)
|
|
6615
6821
|
e.currentTarget.style.opacity = '0.4';
|
|
6616
|
-
}, 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: {
|
|
6617
6823
|
display: 'inline-flex',
|
|
6618
6824
|
alignItems: 'center',
|
|
6619
6825
|
justifyContent: 'center',
|
|
@@ -6631,7 +6837,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6631
6837
|
e.currentTarget.style.opacity = '0.85';
|
|
6632
6838
|
}, onMouseLeave: (e) => {
|
|
6633
6839
|
e.currentTarget.style.opacity = '0.4';
|
|
6634
|
-
}, 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" }) }) })), [
|
|
6635
6841
|
{
|
|
6636
6842
|
sym: '@',
|
|
6637
6843
|
nudgeY: 0,
|
|
@@ -6660,34 +6866,38 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6660
6866
|
defaultValue: 'Slash commands',
|
|
6661
6867
|
}),
|
|
6662
6868
|
onClick: () => {
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
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) {
|
|
6668
6875
|
setCommandMenu(null);
|
|
6876
|
+
if (inputRef.current === '/') {
|
|
6877
|
+
// Remove the slash this button added on open.
|
|
6878
|
+
setInputValue('');
|
|
6879
|
+
autoResize();
|
|
6880
|
+
}
|
|
6669
6881
|
setTimeout(() => {
|
|
6670
6882
|
textareaRef.current?.focus();
|
|
6671
6883
|
}, 0);
|
|
6884
|
+
return;
|
|
6672
6885
|
}
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
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)
|
|
6680
6892
|
setInputAndCursorEnd('/');
|
|
6681
|
-
|
|
6682
|
-
}
|
|
6683
|
-
else {
|
|
6684
|
-
setTimeout(() => {
|
|
6685
|
-
textareaRef.current?.focus();
|
|
6686
|
-
}, 0);
|
|
6687
|
-
}
|
|
6893
|
+
setCommandMenu({ selectedIdx: -1, showAll: cur !== '' && cur !== '/' });
|
|
6688
6894
|
},
|
|
6689
6895
|
},
|
|
6690
|
-
]
|
|
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: {
|
|
6691
6901
|
display: 'inline-flex',
|
|
6692
6902
|
alignItems: 'center',
|
|
6693
6903
|
justifyContent: 'center',
|
|
@@ -6708,7 +6918,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6708
6918
|
e.currentTarget.style.opacity = '0.85';
|
|
6709
6919
|
}, onMouseLeave: (e) => {
|
|
6710
6920
|
e.currentTarget.style.opacity = '0.4';
|
|
6711
|
-
}, 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 &&
|
|
6712
6923
|
(() => {
|
|
6713
6924
|
// The ring represents usage toward the auto-compaction threshold,
|
|
6714
6925
|
// not the raw context window. 100% = compaction will trigger.
|
|
@@ -6777,11 +6988,14 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6777
6988
|
transition: 'opacity 150ms',
|
|
6778
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" })] })] }));
|
|
6779
6990
|
})(), _jsxs("div", { style: {
|
|
6780
|
-
|
|
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',
|
|
6781
6995
|
display: 'flex',
|
|
6782
6996
|
gap: '4px',
|
|
6783
6997
|
alignItems: 'center',
|
|
6784
|
-
}, 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) => {
|
|
6785
6999
|
e.currentTarget.style.background = 'rgba(248,81,73,0.3)';
|
|
6786
7000
|
e.currentTarget.style.borderColor = 'rgba(248,81,73,0.65)';
|
|
6787
7001
|
}, onMouseLeave: (e) => {
|
|
@@ -6835,7 +7049,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6835
7049
|
}
|
|
6836
7050
|
: undefined,
|
|
6837
7051
|
});
|
|
6838
|
-
} })), 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) => {
|
|
6839
7053
|
// Surface the created link in the timeline so it persists after the
|
|
6840
7054
|
// modal closes — the role label and the public URL are both shown.
|
|
6841
7055
|
addSystemCard(t('ide.chat.share.created', { role: result.role }, { defaultValue: 'Created a {{role}} share link.' }), {
|
|
@@ -6854,8 +7068,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6854
7068
|
* @param props - Component props (see {@link MessageItemProps}).
|
|
6855
7069
|
* @returns The rendered chat panel element.
|
|
6856
7070
|
*/
|
|
6857
|
-
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, 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, }) {
|
|
6858
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;
|
|
6859
7077
|
const isNarrow = useNarrowViewport();
|
|
6860
7078
|
const isCoarse = useCoarsePointer();
|
|
6861
7079
|
const http = useHttpClient();
|
|
@@ -7008,11 +7226,11 @@ export function ChatPanel({ projectId, endpoint, initialMessage, onInitialMessag
|
|
|
7008
7226
|
textOverflow: 'ellipsis',
|
|
7009
7227
|
whiteSpace: 'nowrap',
|
|
7010
7228
|
opacity: 0.7,
|
|
7011
|
-
}, 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, {
|
|
7012
7230
|
defaultValue: 'Share project',
|
|
7013
|
-
}), 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, {
|
|
7014
7232
|
defaultValue: 'Report a bug',
|
|
7015
|
-
}), 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: {
|
|
7016
7234
|
position: 'absolute',
|
|
7017
7235
|
top: '100%',
|
|
7018
7236
|
left: 0,
|
|
@@ -7063,7 +7281,7 @@ export function ChatPanel({ projectId, endpoint, initialMessage, onInitialMessag
|
|
|
7063
7281
|
textOverflow: 'ellipsis',
|
|
7064
7282
|
whiteSpace: 'nowrap',
|
|
7065
7283
|
width: '100%',
|
|
7066
|
-
}, 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, 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)] }));
|
|
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)] }));
|
|
7067
7285
|
}
|
|
7068
7286
|
ChatPanel.displayName = 'ChatPanel';
|
|
7069
7287
|
//# sourceMappingURL=ChatPanel.js.map
|