@molecule/app-ide-react 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +443 -169
- 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 +3 -3
|
@@ -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
|
|
@@ -1265,16 +1301,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1265
1301
|
clearInterval(autoFixIntervalRef.current);
|
|
1266
1302
|
}, []);
|
|
1267
1303
|
// Ref so the stream-event callback can push activity cards without depending
|
|
1268
|
-
// on the state setter
|
|
1304
|
+
// on the state setter.
|
|
1269
1305
|
const addActivityCardRef = useRef(() => { });
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
const appendCardMessageRef = useRef(() => { });
|
|
1274
|
-
// Ref to useChat's appendCompleteMessage — same contract as appendCardMessageRef, for a
|
|
1275
|
-
// teammate's broadcast `message` event (a complete non-streaming message, e.g. a
|
|
1276
|
-
// human-only team note). This client's OWN `message` events append internally in useChat.
|
|
1277
|
-
const appendCompleteMessageRef = useRef(() => { });
|
|
1306
|
+
// (A teammate's broadcast `card` / `message` events are ingested by
|
|
1307
|
+
// useChat.applyRemoteEvent — applyPushedStreamEvent routes every pushed frame
|
|
1308
|
+
// through it before this panel's handler runs, so no append refs live here.)
|
|
1278
1309
|
// Kept current each render so handleStreamEvent (memoized) always calls the latest.
|
|
1279
1310
|
const onReadyToBuildRef = useRef(onReadyToBuild);
|
|
1280
1311
|
onReadyToBuildRef.current = onReadyToBuild;
|
|
@@ -1292,9 +1323,15 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1292
1323
|
contextWindow: event.usage.contextWindow,
|
|
1293
1324
|
});
|
|
1294
1325
|
}
|
|
1295
|
-
// Capture verification errors to trigger countdown after stream ends
|
|
1326
|
+
// Capture verification errors to trigger countdown after stream ends —
|
|
1327
|
+
// SENDER-ONLY: a pushed frame from a teammate's turn must not arm this
|
|
1328
|
+
// client's auto-fix (the sender's client owns the follow-up; two clients
|
|
1329
|
+
// arming it would double-send the fix).
|
|
1296
1330
|
// Also drop any deferred preview error — verification's countdown handles it.
|
|
1297
|
-
if (
|
|
1331
|
+
if (!applyingPushedRef.current &&
|
|
1332
|
+
event.type === 'verification_result' &&
|
|
1333
|
+
event.status === 'error' &&
|
|
1334
|
+
event.output) {
|
|
1298
1335
|
pendingVerificationRef.current = {
|
|
1299
1336
|
output: event.output,
|
|
1300
1337
|
categories: event.categories ?? [],
|
|
@@ -1312,22 +1349,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1312
1349
|
if (event.type === 'verification_result' && event.status === 'ok') {
|
|
1313
1350
|
pendingVerificationRef.current = null;
|
|
1314
1351
|
}
|
|
1315
|
-
//
|
|
1316
|
-
//
|
|
1317
|
-
// the
|
|
1318
|
-
// (applied through applyPushedStreamEvent → handleStreamEvent): append it to the local
|
|
1319
|
-
// store so it shows live for the collaborator too (de-duped by id; never re-persisted —
|
|
1320
|
-
// the originating server already persisted it, and on reload it loads with the messages).
|
|
1321
|
-
if (event.type === 'card' && applyingPushedRef.current) {
|
|
1322
|
-
appendCardMessageRef.current(event.id, event.timestamp, event.card);
|
|
1323
|
-
}
|
|
1324
|
-
// A teammate's broadcast complete message (e.g. a human-only team note) — append it
|
|
1325
|
-
// to the local store so it shows live for the collaborator too (de-duped by id;
|
|
1326
|
-
// never re-persisted — the originating server already persisted it). This client's
|
|
1327
|
-
// OWN `message` stream events are appended internally by useChat.
|
|
1328
|
-
if (event.type === 'message' && applyingPushedRef.current && event.message) {
|
|
1329
|
-
appendCompleteMessageRef.current(event.message);
|
|
1330
|
-
}
|
|
1352
|
+
// NOTE: pushed `card` / `message` events are ingested into the message store by
|
|
1353
|
+
// useChat.applyRemoteEvent (applyPushedStreamEvent calls it before this handler),
|
|
1354
|
+
// through the same append helpers as an own stream — no panel-side append here.
|
|
1331
1355
|
// Captured outbound side effect (email/sms/push/webhook/channel) — push an
|
|
1332
1356
|
// inline activity card into the timeline. Non-text card, mirroring how
|
|
1333
1357
|
// system cards are appended.
|
|
@@ -1335,9 +1359,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1335
1359
|
addActivityCardRef.current(activityFromEvent(event.activity));
|
|
1336
1360
|
}
|
|
1337
1361
|
// Discovery finished and the server selected a starting point — boot the
|
|
1338
|
-
// sandbox.
|
|
1339
|
-
//
|
|
1340
|
-
|
|
1362
|
+
// sandbox. SENDER-ONLY: the sender's client drives the boot; a watching
|
|
1363
|
+
// client's host follows the project's sandbox status instead of racing a
|
|
1364
|
+
// second boot request. The template choice is internal; this event carries
|
|
1365
|
+
// no user-facing payload and is never rendered in the transcript.
|
|
1366
|
+
if (event.type === 'ready_to_build' && !applyingPushedRef.current) {
|
|
1341
1367
|
onReadyToBuildRef.current?.();
|
|
1342
1368
|
}
|
|
1343
1369
|
// The agent asked the IDE to reload/navigate the preview, open a file, or drive the
|
|
@@ -1375,8 +1401,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1375
1401
|
// still drives the phase STATE (useChat's setMode); it just no longer spawns a card.
|
|
1376
1402
|
const cfg = soundsConfigRef.current;
|
|
1377
1403
|
const eventType = event.type;
|
|
1378
|
-
|
|
1379
|
-
|
|
1404
|
+
// 'message' is the team-note ping: it fires only for a TEAMMATE's note
|
|
1405
|
+
// (the pushed broadcast) — never for the sender's own SSE echo.
|
|
1406
|
+
const isOwnTeamNote = event.type === 'message' && !applyingPushedRef.current;
|
|
1407
|
+
if (!isOwnTeamNote && eventType in cfg && shouldPlaySound(cfg[eventType])) {
|
|
1408
|
+
playTone(eventType === 'message' ? 'team' : 'default');
|
|
1380
1409
|
}
|
|
1381
1410
|
}, [t]);
|
|
1382
1411
|
// Countdown timer effect — ticks down and auto-sends fix message
|
|
@@ -1462,10 +1491,13 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1462
1491
|
}
|
|
1463
1492
|
onFileChange?.(path, content);
|
|
1464
1493
|
}, [onFileChange, onFileOpen, autoFixCountdown]);
|
|
1465
|
-
const { messages, isLoading, isRemoteStreaming, noteRemoteStreamEvent, error, errorMeta, mode, fastMode, streamingStatus, setMode, setFastMode, sendMessage, abort, clearHistory, editQueuedMessage, deleteQueuedMessage, clearQueuedForFile,
|
|
1494
|
+
const { messages, isLoading, isRemoteStreaming, noteRemoteStreamEvent, error, errorMeta, mode, fastMode, streamingStatus, setMode, setFastMode, sendMessage, abort, clearHistory, editQueuedMessage, deleteQueuedMessage, clearQueuedForFile, applyRemoteEvent, retryCountdown, cancelRetry, } = useChat({
|
|
1466
1495
|
endpoint,
|
|
1467
1496
|
projectId,
|
|
1468
1497
|
agentName,
|
|
1498
|
+
// A read-only viewer never issues chat POSTs (no resume/retry) — a live turn
|
|
1499
|
+
// is watched via pushed frames + the reconcile poll instead.
|
|
1500
|
+
readOnly: canEdit === false,
|
|
1469
1501
|
// ALWAYS load history on mount. A persisted conversation MUST restore on refresh,
|
|
1470
1502
|
// even when an initialMessage / initialInputValue is also present. The old condition
|
|
1471
1503
|
// suppressed the load whenever a fresh message was about to be auto-sent — but on a
|
|
@@ -1483,13 +1515,23 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1483
1515
|
onConversationId,
|
|
1484
1516
|
onStreamEvent: handleStreamEvent,
|
|
1485
1517
|
});
|
|
1518
|
+
// The mode a model CHANGE should target when the user hasn't scoped one
|
|
1519
|
+
// explicitly (no --plan/--execute flag, no picker-mode choice): the LIVE
|
|
1520
|
+
// conversation mode. Discovery runs in plan mode (aiContext.mode is 'plan'
|
|
1521
|
+
// throughout discovery), so it maps to 'plan' — changing the model while
|
|
1522
|
+
// discovering/planning changes the plan model, while building the execute
|
|
1523
|
+
// model. This replaces the old default of writing the legacy `chatModel`
|
|
1524
|
+
// (which silently moved BOTH modes).
|
|
1525
|
+
const liveModelMode = mode === 'plan' ? 'plan' : 'execute';
|
|
1526
|
+
// Target for a settings PATCH that changes agent behavior (model / effort /
|
|
1527
|
+
// max loops / region / auto-fix / auto-approve): name the OPEN conversation so
|
|
1528
|
+
// the server's shared setting card lands in the transcript members are
|
|
1529
|
+
// actually watching (a project can hold several conversations).
|
|
1530
|
+
const settingsPatchUrl = useCallback(() => `/projects/${projectId}${conversationIdRef.current
|
|
1531
|
+
? `?conversationId=${encodeURIComponent(conversationIdRef.current)}`
|
|
1532
|
+
: ''}`, [projectId]);
|
|
1486
1533
|
// Keep sendMessageRef in sync so the countdown effect can call the latest sendMessage
|
|
1487
1534
|
sendMessageRef.current = sendMessage;
|
|
1488
|
-
// Keep the card-append ref current so handleStreamEvent (memoized) can append a teammate's
|
|
1489
|
-
// broadcast `card` event to the message store.
|
|
1490
|
-
appendCardMessageRef.current = appendCardMessage;
|
|
1491
|
-
// Same for a teammate's broadcast complete `message` event (e.g. a team note).
|
|
1492
|
-
appendCompleteMessageRef.current = appendCompleteMessage;
|
|
1493
1535
|
// User Stop. A stop is a user decision the platform must not overrule — so
|
|
1494
1536
|
// beyond killing the stream (useChat.abort also records the stop client-side
|
|
1495
1537
|
// and, via chat-abort's userInitiated flag, server-side), drop every pending
|
|
@@ -1627,7 +1669,15 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1627
1669
|
useEffect(() => {
|
|
1628
1670
|
if (typeof window !== 'undefined' && window.matchMedia?.('(pointer: coarse)').matches)
|
|
1629
1671
|
return;
|
|
1630
|
-
textareaRef.current
|
|
1672
|
+
const ta = textareaRef.current;
|
|
1673
|
+
if (!ta)
|
|
1674
|
+
return;
|
|
1675
|
+
ta.focus();
|
|
1676
|
+
// The textarea can mount with content already in it (defaultValue carries a
|
|
1677
|
+
// persisted draft, and a viewer's box holds the '/teamsay ' prefill) — a
|
|
1678
|
+
// fresh element's selection is (0,0), so without this the caret sits BEFORE
|
|
1679
|
+
// the text and typing lands in front of '/teamsay'.
|
|
1680
|
+
ta.setSelectionRange(ta.value.length, ta.value.length);
|
|
1631
1681
|
}, []);
|
|
1632
1682
|
/** Update the ref, the DOM element, and the hasInput flag without re-rendering the parent. */
|
|
1633
1683
|
const setInputValue = useCallback((val) => {
|
|
@@ -1635,7 +1685,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1635
1685
|
const ta = textareaRef.current;
|
|
1636
1686
|
if (ta && ta.value !== val)
|
|
1637
1687
|
ta.value = val;
|
|
1638
|
-
|
|
1688
|
+
// A viewer's composer is pre-filled with '/teamsay ' — the BARE prefix is
|
|
1689
|
+
// not sendable content, so it must not light the Send button.
|
|
1690
|
+
const bareSideChannel = canEdit === false && /^\/(?:teamsay|t)$/i.test(val.trim());
|
|
1691
|
+
setHasInput(Boolean(val.trim()) && !bareSideChannel);
|
|
1639
1692
|
autoResize();
|
|
1640
1693
|
// Clear persisted draft when input is emptied (e.g. on submit)
|
|
1641
1694
|
if (!val) {
|
|
@@ -1646,7 +1699,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
1646
1699
|
/* sessionStorage unavailable — safe to ignore, draft simply persists */
|
|
1647
1700
|
}
|
|
1648
1701
|
}
|
|
1649
|
-
}, [draftKey, autoResize]);
|
|
1702
|
+
}, [draftKey, autoResize, canEdit]);
|
|
1650
1703
|
// Persist draft text to sessionStorage so it survives refresh (debounced)
|
|
1651
1704
|
const draftTimerRef = useRef(null);
|
|
1652
1705
|
const persistDraft = useCallback(() => {
|
|
@@ -2168,17 +2221,24 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2168
2221
|
}
|
|
2169
2222
|
}, []);
|
|
2170
2223
|
// Apply a chat event broadcast by another project member (the chat push channel). The
|
|
2171
|
-
// originating member's SERVER already persisted everything
|
|
2172
|
-
//
|
|
2173
|
-
//
|
|
2174
|
-
//
|
|
2224
|
+
// originating member's SERVER already persisted everything; this client renders the
|
|
2225
|
+
// broadcast live and gets the durable copy on reload. EVERY frame of a remote turn
|
|
2226
|
+
// arrives here — text/thinking deltas, tool events, verification, cards, the user
|
|
2227
|
+
// message, done — and is ingested into the message store through useChat's
|
|
2228
|
+
// applyRemoteEvent (the same content applier as an own SSE stream), so watching a
|
|
2229
|
+
// teammate's turn looks exactly like running one. handleStreamEvent then handles the
|
|
2230
|
+
// panel-level concerns (context usage, sounds, activity cards) with the
|
|
2231
|
+
// applyingPushed flag gating the sender-only side effects (auto-fix, ready_to_build).
|
|
2175
2232
|
const applyPushedStreamEvent = useCallback((frameConversationId, event) => {
|
|
2176
2233
|
// Only apply broadcasts for the conversation this panel has open.
|
|
2177
2234
|
if (frameConversationId !== conversationIdRef.current)
|
|
2178
2235
|
return;
|
|
2179
|
-
//
|
|
2180
|
-
//
|
|
2181
|
-
|
|
2236
|
+
// Store ingestion first, so the transcript is current before any panel
|
|
2237
|
+
// side effect reads it.
|
|
2238
|
+
applyRemoteEvent(event);
|
|
2239
|
+
// Flag the window so a pushed activity card is marked `received` (rendered, not
|
|
2240
|
+
// re-PUT) and sender-only side effects are skipped. handleStreamEvent is
|
|
2241
|
+
// synchronous, so the flag is set for exactly this event's handling.
|
|
2182
2242
|
applyingPushedRef.current = true;
|
|
2183
2243
|
try {
|
|
2184
2244
|
handleStreamEvent(event);
|
|
@@ -2191,7 +2251,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2191
2251
|
// continuation), tell useChat so it confirms against the server's streaming
|
|
2192
2252
|
// flag and keeps the Stop button visible + functional for the remote turn.
|
|
2193
2253
|
noteRemoteStreamEvent();
|
|
2194
|
-
}, [handleStreamEvent, noteRemoteStreamEvent]);
|
|
2254
|
+
}, [applyRemoteEvent, handleStreamEvent, noteRemoteStreamEvent]);
|
|
2195
2255
|
// Register the pushed-event handler with the parent (Workspace) so it can deliver
|
|
2196
2256
|
// broadcast chat events from other project members; deregister on unmount.
|
|
2197
2257
|
useEffect(() => {
|
|
@@ -2289,10 +2349,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2289
2349
|
return () => {
|
|
2290
2350
|
cancelled = true;
|
|
2291
2351
|
};
|
|
2292
|
-
}, [conversationId, projectId, http]);
|
|
2352
|
+
}, [conversationId, projectId, http, canEdit]);
|
|
2293
2353
|
useEffect(() => {
|
|
2294
2354
|
if (!conversationId || activityCardsLoadedConvRef.current !== conversationId)
|
|
2295
2355
|
return;
|
|
2356
|
+
if (canEdit === false)
|
|
2357
|
+
return;
|
|
2296
2358
|
void http
|
|
2297
2359
|
.put(`/projects/${projectId}/conversations/${conversationId}/activity-cards`, {
|
|
2298
2360
|
activityCards: activityCards.filter((c) => !c.received),
|
|
@@ -2301,7 +2363,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2301
2363
|
// Best-effort save (bound as _error per Rule 14): the in-memory cards remain the
|
|
2302
2364
|
// source of truth this session if the PUT fails.
|
|
2303
2365
|
});
|
|
2304
|
-
}, [activityCards, conversationId, projectId, http]);
|
|
2366
|
+
}, [activityCards, conversationId, projectId, http, canEdit]);
|
|
2305
2367
|
// ── Auto-tips (dismissable onboarding hints) ──────────────────────────────
|
|
2306
2368
|
// Two surfaces (see chat-tips-utilities): an ENTRY_TIP shown once on a fresh
|
|
2307
2369
|
// conversation so a brand-new user always sees how to drive the agent, plus an
|
|
@@ -2331,6 +2393,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2331
2393
|
shownTipIdsRef.current = [];
|
|
2332
2394
|
// Re-arm the entry tip so a freshly-started chat shows the onboarding hint.
|
|
2333
2395
|
entryTipShownRef.current = false;
|
|
2396
|
+
// Re-arm the viewer composer prefill ('/teamsay ') for the new conversation.
|
|
2397
|
+
viewerPrefillDoneRef.current = false;
|
|
2334
2398
|
}
|
|
2335
2399
|
}, [conversationId]);
|
|
2336
2400
|
// Entry tip: the onboarding moment. Show ONE high-value hint as soon as a fresh
|
|
@@ -2364,7 +2428,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2364
2428
|
return;
|
|
2365
2429
|
viewerTipShownRef.current = true;
|
|
2366
2430
|
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.',
|
|
2431
|
+
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
2432
|
});
|
|
2369
2433
|
setTipCards((prev) => [
|
|
2370
2434
|
...prev,
|
|
@@ -2409,7 +2473,31 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2409
2473
|
}, [messages, discovery]);
|
|
2410
2474
|
// ── Sounds picker (shown when /sounds is executed) ────────────────────────
|
|
2411
2475
|
const [soundsPicker, setSoundsPicker] = useState(null);
|
|
2412
|
-
|
|
2476
|
+
// Notification sounds are a PER-DEVICE, per-user preference — localStorage,
|
|
2477
|
+
// never project settings. They used to persist via PATCH /projects settings
|
|
2478
|
+
// (shared with every member, and unreachable for read-only viewers, whose
|
|
2479
|
+
// PATCH 403s); the old project-level value is still read once as a migration
|
|
2480
|
+
// fallback when this device has no local preference yet.
|
|
2481
|
+
const [soundsConfig, setSoundsConfig] = useState(() => {
|
|
2482
|
+
try {
|
|
2483
|
+
const raw = localStorage.getItem(SOUNDS_STORAGE_KEY);
|
|
2484
|
+
if (raw)
|
|
2485
|
+
return { ...DEFAULT_SOUNDS_CONFIG, ...JSON.parse(raw) };
|
|
2486
|
+
}
|
|
2487
|
+
catch (_error) {
|
|
2488
|
+
// localStorage unavailable / bad JSON — fall through to the defaults.
|
|
2489
|
+
}
|
|
2490
|
+
return { ...DEFAULT_SOUNDS_CONFIG };
|
|
2491
|
+
});
|
|
2492
|
+
const soundsLocallySetRef = useRef(false);
|
|
2493
|
+
useEffect(() => {
|
|
2494
|
+
try {
|
|
2495
|
+
soundsLocallySetRef.current = localStorage.getItem(SOUNDS_STORAGE_KEY) != null;
|
|
2496
|
+
}
|
|
2497
|
+
catch (_error) {
|
|
2498
|
+
// localStorage unavailable — treat as unset; the project fallback may apply.
|
|
2499
|
+
}
|
|
2500
|
+
}, []);
|
|
2413
2501
|
// ── Panel overlay (/skills, /scripts, /settings — closeable popups) ──────────
|
|
2414
2502
|
// These three commands open a closeable overlay above the composer (mirrors how
|
|
2415
2503
|
// /model and /sounds own this popup region) instead of dropping an inline card
|
|
@@ -2447,16 +2535,16 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2447
2535
|
updated = { ...soundsConfig, [eventType]: next };
|
|
2448
2536
|
}
|
|
2449
2537
|
setSoundsConfig(updated);
|
|
2538
|
+
// Per-device preference — localStorage, never a project PATCH (see the
|
|
2539
|
+
// state's comment; this also makes /sounds fully usable for viewers).
|
|
2450
2540
|
try {
|
|
2451
|
-
|
|
2541
|
+
localStorage.setItem(SOUNDS_STORAGE_KEY, JSON.stringify(updated));
|
|
2542
|
+
soundsLocallySetRef.current = true;
|
|
2452
2543
|
}
|
|
2453
|
-
catch (
|
|
2454
|
-
|
|
2455
|
-
addSystemCard(t('ide.chat.soundsError', undefined, {
|
|
2456
|
-
defaultValue: 'Failed to update sound settings.',
|
|
2457
|
-
}));
|
|
2544
|
+
catch (_error) {
|
|
2545
|
+
// localStorage unavailable — the choice still applies for this session.
|
|
2458
2546
|
}
|
|
2459
|
-
}, [soundsConfig
|
|
2547
|
+
}, [soundsConfig]);
|
|
2460
2548
|
// ── Current project settings (model + maxloops + sounds) ──────────────────
|
|
2461
2549
|
// Project-scoped so any custom (bring-your-own AI) models the project has
|
|
2462
2550
|
// configured are included alongside the platform catalog.
|
|
@@ -2572,7 +2660,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2572
2660
|
if (typeof s?.autoApproveCommands === 'boolean') {
|
|
2573
2661
|
setAutoApproveCommandsEnabled(s.autoApproveCommands);
|
|
2574
2662
|
}
|
|
2575
|
-
|
|
2663
|
+
// Legacy migration only: sounds are per-device (localStorage) now — apply
|
|
2664
|
+
// the old project-level value only when this device has no local pref.
|
|
2665
|
+
if (s?.sounds && typeof s.sounds === 'object' && !soundsLocallySetRef.current) {
|
|
2576
2666
|
setSoundsConfig((prev) => ({ ...prev, ...s.sounds }));
|
|
2577
2667
|
}
|
|
2578
2668
|
// Restore the persisted auto-commit cadence in the paused state (it
|
|
@@ -2684,9 +2774,14 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2684
2774
|
if (fallback) {
|
|
2685
2775
|
setCurrentModel(fallback);
|
|
2686
2776
|
setSavedChatModel(fallback);
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2777
|
+
// Persisting the swap writes project settings — editor+ server-side, so a
|
|
2778
|
+
// viewer's tab keeps the in-memory switch (all this session needs) and
|
|
2779
|
+
// skips a PATCH that could only 403.
|
|
2780
|
+
if (canEdit !== false) {
|
|
2781
|
+
http.patch(settingsPatchUrl(), { settings: { chatModel: fallback } }).catch(() => {
|
|
2782
|
+
/* persistence is best-effort; the in-memory switch is what matters */
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2690
2785
|
}
|
|
2691
2786
|
}, [
|
|
2692
2787
|
modelsLoading,
|
|
@@ -2696,6 +2791,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2696
2791
|
addSystemCard,
|
|
2697
2792
|
http,
|
|
2698
2793
|
projectId,
|
|
2794
|
+
canEdit,
|
|
2699
2795
|
]);
|
|
2700
2796
|
// ── Queued message editing ──────────────────────────────────────────────────
|
|
2701
2797
|
const [editingQueuedId, setEditingQueuedId] = useState(null);
|
|
@@ -2879,6 +2975,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2879
2975
|
pendingMessageKey !== undefined &&
|
|
2880
2976
|
pendingMessageKey !== lastPendingKeyRef.current) {
|
|
2881
2977
|
lastPendingKeyRef.current = pendingMessageKey;
|
|
2978
|
+
// A read-only VIEWER never dispatches platform-composed sends (build
|
|
2979
|
+
// kickoffs, auto-fixes, preview-failure reports): the server 403s them
|
|
2980
|
+
// and the denial used to surface as a red error in the viewer's chat.
|
|
2981
|
+
// Consume the key (above) so the same message isn't retried.
|
|
2982
|
+
if (canEdit === false)
|
|
2983
|
+
return;
|
|
2882
2984
|
// A pending message is ALWAYS system-composed (an auto-fix prompt, a preview-failure
|
|
2883
2985
|
// report, a "Fix with AI" request) — never text the user typed. So it must NEVER
|
|
2884
2986
|
// render as a normal user bubble: either it's suppressed entirely (hidden build
|
|
@@ -2912,6 +3014,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2912
3014
|
pendingMessageUserInitiated,
|
|
2913
3015
|
sendMessage,
|
|
2914
3016
|
isLoading,
|
|
3017
|
+
canEdit,
|
|
2915
3018
|
]);
|
|
2916
3019
|
// When streaming ends, send any deferred message. (A user Stop also ends
|
|
2917
3020
|
// streaming, but handleAbort drops the deferred message first — and useChat's
|
|
@@ -2925,6 +3028,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2925
3028
|
deferredPendingRef.current = null;
|
|
2926
3029
|
deferredPendingSuppressRef.current = false;
|
|
2927
3030
|
deferredPendingUserInitiatedRef.current = false;
|
|
3031
|
+
// A viewer never dispatches platform-composed sends (same as the
|
|
3032
|
+
// immediate path above) — the refs are already drained, so it just drops.
|
|
3033
|
+
if (canEdit === false)
|
|
3034
|
+
return;
|
|
2928
3035
|
// Same rule as the immediate path: a deferred pending message is system-composed —
|
|
2929
3036
|
// always `automatic` (suppressed or visible), never a plain user bubble.
|
|
2930
3037
|
sendMessage(msg, undefined, {
|
|
@@ -2933,7 +3040,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
2933
3040
|
...(userInitiated ? { userInitiated: true } : {}),
|
|
2934
3041
|
});
|
|
2935
3042
|
}
|
|
2936
|
-
}, [isLoading, sendMessage]);
|
|
3043
|
+
}, [isLoading, sendMessage, canEdit]);
|
|
2937
3044
|
// ── Auto-delete queued autofix messages when user edits a relevant file ────
|
|
2938
3045
|
const lastUserEditKeyRef = useRef(userEditedFileKey);
|
|
2939
3046
|
useEffect(() => {
|
|
@@ -3428,11 +3535,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3428
3535
|
return;
|
|
3429
3536
|
}
|
|
3430
3537
|
setFilePicker(null);
|
|
3431
|
-
// Show model picker when typing "/model <filter>" — scoped to
|
|
3432
|
-
//
|
|
3538
|
+
// Show model picker when typing "/model <filter>" — scoped to the mode a
|
|
3539
|
+
// --plan / --execute flag names, else to the LIVE conversation mode
|
|
3540
|
+
// (discovery = plan), so a plain pick changes the mode the user is in.
|
|
3433
3541
|
const modelMatch = val.match(/^\/model\s+/i);
|
|
3434
3542
|
if (modelMatch) {
|
|
3435
|
-
setModelPicker({ selectedIdx: -1, mode: parseModelModeCommand(val)?.mode });
|
|
3543
|
+
setModelPicker({ selectedIdx: -1, mode: parseModelModeCommand(val)?.mode ?? liveModelMode });
|
|
3436
3544
|
setCommandMenu(null);
|
|
3437
3545
|
return;
|
|
3438
3546
|
}
|
|
@@ -3443,7 +3551,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3443
3551
|
else {
|
|
3444
3552
|
setCommandMenu(null);
|
|
3445
3553
|
}
|
|
3446
|
-
}, [openFilePicker, autoResize, persistDraft]);
|
|
3554
|
+
}, [openFilePicker, autoResize, persistDraft, liveModelMode]);
|
|
3447
3555
|
// ── Execute command ────────────────────────────────────────────────────────
|
|
3448
3556
|
/** Sets textarea value and moves cursor to the end. */
|
|
3449
3557
|
const setInputAndCursorEnd = useCallback((val) => {
|
|
@@ -3457,6 +3565,17 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3457
3565
|
}
|
|
3458
3566
|
}, 0);
|
|
3459
3567
|
}, [setInputValue, autoResize]);
|
|
3568
|
+
// Viewer composer prefill: the box IS the team-chat input, so it starts with
|
|
3569
|
+
// "/teamsay " already typed (and handleSubmit re-fills it after each sent team
|
|
3570
|
+
// message). Once per mount, and never over text the user already has.
|
|
3571
|
+
const viewerPrefillDoneRef = useRef(false);
|
|
3572
|
+
useEffect(() => {
|
|
3573
|
+
if (canEdit !== false || viewerPrefillDoneRef.current)
|
|
3574
|
+
return;
|
|
3575
|
+
viewerPrefillDoneRef.current = true;
|
|
3576
|
+
if (!inputRef.current.trim())
|
|
3577
|
+
setInputAndCursorEnd('/teamsay ');
|
|
3578
|
+
}, [canEdit, setInputAndCursorEnd, conversationId]);
|
|
3460
3579
|
/**
|
|
3461
3580
|
* Select and apply a model by ID. When `mode` is given the choice persists to
|
|
3462
3581
|
* that mode's per-mode field (`planModel` / `executeModel` /
|
|
@@ -3469,7 +3588,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3469
3588
|
const name = displayName ?? modelId;
|
|
3470
3589
|
try {
|
|
3471
3590
|
if (mode) {
|
|
3472
|
-
await http.patch(
|
|
3591
|
+
await http.patch(settingsPatchUrl(), {
|
|
3473
3592
|
settings: { [modeSettingKey(mode)]: modelId },
|
|
3474
3593
|
});
|
|
3475
3594
|
if (mode === 'plan')
|
|
@@ -3489,7 +3608,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3489
3608
|
: t('ide.chat.compactModelSet', { name }, { defaultValue: 'Compaction model set to {{name}}' }));
|
|
3490
3609
|
}
|
|
3491
3610
|
else {
|
|
3492
|
-
await http.patch(
|
|
3611
|
+
await http.patch(settingsPatchUrl(), { settings: { chatModel: modelId } });
|
|
3493
3612
|
setCurrentModel(modelId);
|
|
3494
3613
|
setSavedChatModel(modelId);
|
|
3495
3614
|
addSystemCard(t('ide.chat.modelSet', { name }, {
|
|
@@ -3513,7 +3632,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3513
3632
|
const nextRegions = { ...modelRegions, [modelId]: region };
|
|
3514
3633
|
setModelRegions(nextRegions);
|
|
3515
3634
|
try {
|
|
3516
|
-
await http.patch(
|
|
3635
|
+
await http.patch(settingsPatchUrl(), {
|
|
3517
3636
|
settings: { modelRegions: nextRegions },
|
|
3518
3637
|
});
|
|
3519
3638
|
}
|
|
@@ -3568,7 +3687,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3568
3687
|
}
|
|
3569
3688
|
else if (id === 'model') {
|
|
3570
3689
|
setInputAndCursorEnd('/model ');
|
|
3571
|
-
setModelPicker({ selectedIdx: -1 });
|
|
3690
|
+
setModelPicker({ selectedIdx: -1, mode: liveModelMode });
|
|
3572
3691
|
}
|
|
3573
3692
|
else if (id === 'mic' || id === 'dictate') {
|
|
3574
3693
|
setInputValue('');
|
|
@@ -3660,7 +3779,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3660
3779
|
defaultValue: "Today's AI allowance is used up — refreshes {{when}}.",
|
|
3661
3780
|
})
|
|
3662
3781
|
: t('ide.chat.usageAllowanceTodayLine', { percent: d.allowancePercent }, {
|
|
3663
|
-
|
|
3782
|
+
// Neutral phrasing on purpose: the allowance belongs to
|
|
3783
|
+
// the PROJECT (its owner's plan window) — "you've used"
|
|
3784
|
+
// misattributed it to whichever teammate ran /cost.
|
|
3785
|
+
defaultValue: "~{{percent}}% of today's AI allowance used.",
|
|
3664
3786
|
}))
|
|
3665
3787
|
: '';
|
|
3666
3788
|
// `inputTokens` counts only the UNCACHED prompt: every bond normalizes
|
|
@@ -3802,7 +3924,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3802
3924
|
setInputValue('');
|
|
3803
3925
|
const newValue = !autoFixEnabled;
|
|
3804
3926
|
try {
|
|
3805
|
-
await http.patch(
|
|
3927
|
+
await http.patch(settingsPatchUrl(), { settings: { autoFix: newValue } });
|
|
3806
3928
|
setAutoFixEnabled(newValue);
|
|
3807
3929
|
addSystemCard(newValue
|
|
3808
3930
|
? t('ide.chat.autoFixEnabled', undefined, {
|
|
@@ -3823,7 +3945,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3823
3945
|
setInputValue('');
|
|
3824
3946
|
const newValue = !autoApproveCommandsEnabled;
|
|
3825
3947
|
try {
|
|
3826
|
-
await http.patch(
|
|
3948
|
+
await http.patch(settingsPatchUrl(), {
|
|
3827
3949
|
settings: { autoApproveCommands: newValue },
|
|
3828
3950
|
});
|
|
3829
3951
|
setAutoApproveCommandsEnabled(newValue);
|
|
@@ -3886,7 +4008,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3886
4008
|
}
|
|
3887
4009
|
else if (id === 'share') {
|
|
3888
4010
|
setInputValue('');
|
|
3889
|
-
|
|
4011
|
+
if (shareAllowed)
|
|
4012
|
+
setShareModal({ role: DEFAULT_SHARE_ROLE });
|
|
3890
4013
|
}
|
|
3891
4014
|
}, [
|
|
3892
4015
|
clearHistory,
|
|
@@ -3905,6 +4028,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
3905
4028
|
extraCommandIds,
|
|
3906
4029
|
canEdit,
|
|
3907
4030
|
allCommands,
|
|
4031
|
+
liveModelMode,
|
|
3908
4032
|
t,
|
|
3909
4033
|
]);
|
|
3910
4034
|
// When the auto-commit countdown reaches zero, fire the existing /commit path
|
|
@@ -4023,10 +4147,18 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4023
4147
|
return;
|
|
4024
4148
|
}
|
|
4025
4149
|
// Handle /share [role] locally — opens the share-link modal at the requested
|
|
4026
|
-
// role (default viewer). An unrecognized role shows usage instead.
|
|
4150
|
+
// role (default viewer). An unrecognized role shows usage instead. Below the
|
|
4151
|
+
// host's share capability (e.g. an editor where minting is admin+), say so
|
|
4152
|
+
// plainly instead of opening a modal whose every request 403s.
|
|
4027
4153
|
const shareMatch = parseShareCommand(trimmed);
|
|
4028
4154
|
if (shareMatch) {
|
|
4029
4155
|
setInputValue('');
|
|
4156
|
+
if (!shareAllowed) {
|
|
4157
|
+
addSystemCard(t('ide.chat.share.notAllowed', undefined, {
|
|
4158
|
+
defaultValue: 'Managing share links needs an admin role on this project.',
|
|
4159
|
+
}));
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4030
4162
|
if (shareMatch.kind === 'invalid') {
|
|
4031
4163
|
addSystemCard(t('ide.chat.share.usage', { roles: SHARE_ROLES.join(', ') }, {
|
|
4032
4164
|
defaultValue: 'Usage: /share [role] — create a public link. Roles: {{roles}} (default viewer).',
|
|
@@ -4108,20 +4240,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4108
4240
|
{ action: buildUpgradeCta?.({}) ?? undefined });
|
|
4109
4241
|
}
|
|
4110
4242
|
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
|
-
}
|
|
4243
|
+
// Unscoped /model targets the CURRENT conversation mode (discovery =
|
|
4244
|
+
// plan), same as /effort — never the legacy both-modes chatModel.
|
|
4245
|
+
await selectModel(name, resolved?.label ?? name, liveModelMode);
|
|
4125
4246
|
}
|
|
4126
4247
|
}
|
|
4127
4248
|
setInputValue('');
|
|
@@ -4196,7 +4317,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4196
4317
|
}
|
|
4197
4318
|
try {
|
|
4198
4319
|
const nextByMode = { ...effortByMode, [targetMode]: level };
|
|
4199
|
-
await http.patch(
|
|
4320
|
+
await http.patch(settingsPatchUrl(), { settings: { effortByMode: nextByMode } });
|
|
4200
4321
|
setEffortByMode(nextByMode);
|
|
4201
4322
|
addSystemCard(t('ide.chat.effort.setMode', {
|
|
4202
4323
|
mode: targetMode,
|
|
@@ -4218,7 +4339,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4218
4339
|
if (maxLoopsMatch) {
|
|
4219
4340
|
const n = Math.max(1, Number(maxLoopsMatch[1]));
|
|
4220
4341
|
try {
|
|
4221
|
-
await http.patch(
|
|
4342
|
+
await http.patch(settingsPatchUrl(), { settings: { maxToolLoops: n } });
|
|
4222
4343
|
setCurrentMaxLoops(n);
|
|
4223
4344
|
addSystemCard(t('ide.chat.maxLoopsSet', { n }, {
|
|
4224
4345
|
defaultValue: `Max tool iterations set to ${n}`,
|
|
@@ -4285,7 +4406,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4285
4406
|
// event (persisted + broadcast to every member), which IS the visible message — so the
|
|
4286
4407
|
// transcript never shows the literal "/command" text, and never shows it twice.
|
|
4287
4408
|
if (matchesSideChannelCommand(allCommands, trimmed)) {
|
|
4288
|
-
|
|
4409
|
+
// A viewer's composer is the team-chat box — re-fill the /teamsay prefix
|
|
4410
|
+
// after each sent team message so the next one is one keystroke away.
|
|
4411
|
+
if (canEdit === false)
|
|
4412
|
+
setInputAndCursorEnd('/teamsay ');
|
|
4413
|
+
else
|
|
4414
|
+
setInputValue('');
|
|
4289
4415
|
sendMessage(trimmed, undefined, { suppressUserMessage: true });
|
|
4290
4416
|
return;
|
|
4291
4417
|
}
|
|
@@ -4335,7 +4461,17 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4335
4461
|
setAttachedFiles([]);
|
|
4336
4462
|
setAttachmentError(null);
|
|
4337
4463
|
sendMessage(message, chatAttachments.length > 0 ? chatAttachments : undefined);
|
|
4338
|
-
}, [
|
|
4464
|
+
}, [
|
|
4465
|
+
attachedFiles,
|
|
4466
|
+
http,
|
|
4467
|
+
projectId,
|
|
4468
|
+
sendMessage,
|
|
4469
|
+
setInputValue,
|
|
4470
|
+
runSavedScript,
|
|
4471
|
+
allCommands,
|
|
4472
|
+
selectModel,
|
|
4473
|
+
liveModelMode,
|
|
4474
|
+
]);
|
|
4339
4475
|
// External auto-submit. When the signal changes, submit the current input —
|
|
4340
4476
|
// used by the prompt → chat morph to send the prefilled prompt once the chat
|
|
4341
4477
|
// has docked into place (handleSubmit clears the input as it sends).
|
|
@@ -4371,12 +4507,22 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4371
4507
|
useEffect(() => {
|
|
4372
4508
|
if (openShareSignal !== undefined && openShareSignal !== lastOpenShareRef.current) {
|
|
4373
4509
|
lastOpenShareRef.current = openShareSignal;
|
|
4374
|
-
|
|
4510
|
+
if (shareAllowed)
|
|
4511
|
+
setShareModal({ role: DEFAULT_SHARE_ROLE });
|
|
4375
4512
|
}
|
|
4376
|
-
}, [openShareSignal]);
|
|
4513
|
+
}, [openShareSignal, shareAllowed]);
|
|
4377
4514
|
// ── Keyboard ───────────────────────────────────────────────────────────────
|
|
4378
4515
|
const filteredCmds = commandMenu
|
|
4379
|
-
? allCommands.filter((c) =>
|
|
4516
|
+
? allCommands.filter((c) =>
|
|
4517
|
+
// A showAll menu (slash-button toggle over existing text) lists every
|
|
4518
|
+
// command; a typed menu filters by the composer's text.
|
|
4519
|
+
(commandMenu.showAll === true || c.label.startsWith(inputRef.current)) &&
|
|
4520
|
+
// Viewers see only commands they can actually run (viewer-safe reads +
|
|
4521
|
+
// the /teamsay side channel) — no dead entries in the menu.
|
|
4522
|
+
(canEdit !== false || c.viewerSafe === true || c.sideChannel === true) &&
|
|
4523
|
+
// /share is gated separately: hosts commonly mint at admin+, so an
|
|
4524
|
+
// editor without the capability gets no dead menu entry either.
|
|
4525
|
+
(c.id !== 'share' || shareAllowed))
|
|
4380
4526
|
: [];
|
|
4381
4527
|
const filteredModels = useMemo(() => {
|
|
4382
4528
|
if (!modelPicker)
|
|
@@ -4663,9 +4809,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4663
4809
|
e.preventDefault();
|
|
4664
4810
|
const idx = modelPicker.selectedIdx >= 0 ? modelPicker.selectedIdx : 0;
|
|
4665
4811
|
if (onManageCustomModels && idx === visibleModels.length) {
|
|
4812
|
+
const manageMode = modelPicker.mode === 'plan' || modelPicker.mode === 'execute'
|
|
4813
|
+
? modelPicker.mode
|
|
4814
|
+
: liveModelMode;
|
|
4666
4815
|
setModelPicker(null);
|
|
4667
4816
|
setInputValue('');
|
|
4668
|
-
onManageCustomModels();
|
|
4817
|
+
onManageCustomModels({ mode: manageMode });
|
|
4669
4818
|
return;
|
|
4670
4819
|
}
|
|
4671
4820
|
const model = visibleModels[idx];
|
|
@@ -4706,20 +4855,28 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4706
4855
|
if (commandMenu && filteredCmds.length > 0) {
|
|
4707
4856
|
if (e.key === 'ArrowDown') {
|
|
4708
4857
|
e.preventDefault();
|
|
4709
|
-
setCommandMenu((m) => m ? { selectedIdx: wrapIdx(m.selectedIdx, 1, filteredCmds.length) } : null);
|
|
4858
|
+
setCommandMenu((m) => m ? { ...m, selectedIdx: wrapIdx(m.selectedIdx, 1, filteredCmds.length) } : null);
|
|
4710
4859
|
return;
|
|
4711
4860
|
}
|
|
4712
4861
|
if (e.key === 'ArrowUp') {
|
|
4713
4862
|
e.preventDefault();
|
|
4714
|
-
setCommandMenu((m) => m ? { selectedIdx: wrapIdx(m.selectedIdx, -1, filteredCmds.length) } : null);
|
|
4863
|
+
setCommandMenu((m) => m ? { ...m, selectedIdx: wrapIdx(m.selectedIdx, -1, filteredCmds.length) } : null);
|
|
4715
4864
|
return;
|
|
4716
4865
|
}
|
|
4717
4866
|
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4867
|
+
// A showAll menu (opened by the slash BUTTON over existing text) only
|
|
4868
|
+
// executes an explicitly highlighted command — a bare Enter closes it
|
|
4869
|
+
// and falls through to submit the composer text as typed.
|
|
4870
|
+
if (commandMenu.showAll && commandMenu.selectedIdx < 0 && e.key === 'Enter') {
|
|
4871
|
+
setCommandMenu(null);
|
|
4872
|
+
}
|
|
4873
|
+
else {
|
|
4874
|
+
e.preventDefault();
|
|
4875
|
+
const cmd = filteredCmds[commandMenu.selectedIdx >= 0 ? commandMenu.selectedIdx : 0];
|
|
4876
|
+
if (cmd)
|
|
4877
|
+
void executeCommand(cmd.id);
|
|
4878
|
+
return;
|
|
4879
|
+
}
|
|
4723
4880
|
}
|
|
4724
4881
|
}
|
|
4725
4882
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -4755,6 +4912,15 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4755
4912
|
// unrenderable card (a custom event with no registered factory, or a non-execute mode) so
|
|
4756
4913
|
// the timeline simply omits it.
|
|
4757
4914
|
const cardEventToSystemCard = useCallback((cardEvent, id, timestamp) => {
|
|
4915
|
+
// Suffix a card's copy with the member who made the change, when the
|
|
4916
|
+
// server attributed one — so a teammate watching knows WHO flipped the
|
|
4917
|
+
// model/effort/mode, live and on reload.
|
|
4918
|
+
const withBy = (text) => {
|
|
4919
|
+
const by = cardEvent.by;
|
|
4920
|
+
return by
|
|
4921
|
+
? t('ide.chat.cardBy', { text, name: by }, { defaultValue: '{{text}} — {{name}}' })
|
|
4922
|
+
: text;
|
|
4923
|
+
};
|
|
4758
4924
|
switch (cardEvent.kind) {
|
|
4759
4925
|
case 'model': {
|
|
4760
4926
|
const label = cardEvent.label || cardEvent.model;
|
|
@@ -4766,22 +4932,77 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4766
4932
|
const regionCode = def && def.provider !== 'custom' ? effectiveModelRegion(def).toUpperCase() : null;
|
|
4767
4933
|
return {
|
|
4768
4934
|
id,
|
|
4769
|
-
text: regionCode
|
|
4935
|
+
text: withBy(regionCode
|
|
4770
4936
|
? t('ide.chat.modelInUseRegion', { model: label, region: regionCode }, { defaultValue: 'Now using {{model}} ({{region}})' })
|
|
4771
|
-
: t('ide.chat.modelInUse', { model: label }, { defaultValue: 'Now using {{model}}' }),
|
|
4937
|
+
: t('ide.chat.modelInUse', { model: label }, { defaultValue: 'Now using {{model}}' })),
|
|
4772
4938
|
timestamp,
|
|
4773
4939
|
};
|
|
4774
4940
|
}
|
|
4775
4941
|
case 'mode':
|
|
4776
|
-
//
|
|
4777
|
-
//
|
|
4942
|
+
// The plan→build handoff ("🔨 Building your app") and the plan-mode
|
|
4943
|
+
// announcement a new conversation is seeded with ("📝 Plan mode") —
|
|
4944
|
+
// the server records mode cards only for those two.
|
|
4945
|
+
if (cardEvent.mode === 'plan') {
|
|
4946
|
+
return {
|
|
4947
|
+
id,
|
|
4948
|
+
text: withBy(t('ide.chat.phasePlanning', undefined, { defaultValue: '📝 Plan mode' })),
|
|
4949
|
+
timestamp,
|
|
4950
|
+
};
|
|
4951
|
+
}
|
|
4778
4952
|
if (cardEvent.mode !== 'execute')
|
|
4779
4953
|
return null;
|
|
4780
4954
|
return {
|
|
4781
4955
|
id,
|
|
4782
|
-
text: t('ide.chat.phaseBuilding', undefined, { defaultValue: '🔨 Building your app' }),
|
|
4956
|
+
text: withBy(t('ide.chat.phaseBuilding', undefined, { defaultValue: '🔨 Building your app' })),
|
|
4783
4957
|
timestamp,
|
|
4784
4958
|
};
|
|
4959
|
+
case 'setting': {
|
|
4960
|
+
// A Synthase-altering setting changed (effort / fast mode / max loops /
|
|
4961
|
+
// region / auto-fix / auto-approve) — announced to every member, live
|
|
4962
|
+
// and on reload. Copy reuses the same localized strings as the local
|
|
4963
|
+
// confirmation cards, so the shared card reads identically.
|
|
4964
|
+
let text = null;
|
|
4965
|
+
if (cardEvent.setting === 'effort') {
|
|
4966
|
+
const level = String(cardEvent.value ?? 'default');
|
|
4967
|
+
const cardMode = cardEvent.mode ?? '';
|
|
4968
|
+
text = cardEvent.label
|
|
4969
|
+
? t('ide.chat.effort.setMode', { mode: cardMode, level, model: cardEvent.label }, { defaultValue: 'Reasoning effort for {{mode}} set to {{level}} ({{model}}).' })
|
|
4970
|
+
: t('ide.chat.setting.effort', { mode: cardMode, level }, { defaultValue: 'Reasoning effort for {{mode}} set to {{level}}.' });
|
|
4971
|
+
}
|
|
4972
|
+
else if (cardEvent.setting === 'fastMode') {
|
|
4973
|
+
text = cardEvent.value
|
|
4974
|
+
? t('ide.chat.fastModeOn', undefined, {
|
|
4975
|
+
defaultValue: 'Fast mode on — faster responses at a higher rate',
|
|
4976
|
+
})
|
|
4977
|
+
: t('ide.chat.fastModeOff', undefined, { defaultValue: 'Fast mode off' });
|
|
4978
|
+
}
|
|
4979
|
+
else if (cardEvent.setting === 'maxToolLoops') {
|
|
4980
|
+
text = t('ide.chat.maxLoopsSet', { n: Number(cardEvent.value ?? 0) }, { defaultValue: 'Max tool iterations set to {{n}}' });
|
|
4981
|
+
}
|
|
4982
|
+
else if (cardEvent.setting === 'region') {
|
|
4983
|
+
text = t('ide.chat.modelInUseRegion', {
|
|
4984
|
+
model: cardEvent.label ?? '',
|
|
4985
|
+
region: String(cardEvent.value ?? '').toUpperCase(),
|
|
4986
|
+
}, { defaultValue: 'Now using {{model}} ({{region}})' });
|
|
4987
|
+
}
|
|
4988
|
+
else if (cardEvent.setting === 'autoFix') {
|
|
4989
|
+
text = cardEvent.value
|
|
4990
|
+
? t('ide.chat.autoFixEnabled', undefined, { defaultValue: 'Auto-fix enabled.' })
|
|
4991
|
+
: t('ide.chat.autoFixDisabled', undefined, { defaultValue: 'Auto-fix disabled.' });
|
|
4992
|
+
}
|
|
4993
|
+
else if (cardEvent.setting === 'autoApprove') {
|
|
4994
|
+
text = cardEvent.value
|
|
4995
|
+
? t('ide.chat.autoApproveEnabled', undefined, {
|
|
4996
|
+
defaultValue: 'Auto-approve on — destructive commands run without asking. The exfiltration guard still asks. Turn off with /autoapprove.',
|
|
4997
|
+
})
|
|
4998
|
+
: t('ide.chat.autoApproveDisabled', undefined, {
|
|
4999
|
+
defaultValue: 'Auto-approve off — destructive commands ask before running.',
|
|
5000
|
+
});
|
|
5001
|
+
}
|
|
5002
|
+
if (!text)
|
|
5003
|
+
return null;
|
|
5004
|
+
return { id, text: withBy(text), timestamp };
|
|
5005
|
+
}
|
|
4785
5006
|
case 'skills':
|
|
4786
5007
|
return {
|
|
4787
5008
|
id,
|
|
@@ -4836,6 +5057,20 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
4836
5057
|
// reading the guest limit they just escaped. Same reasoning the host's
|
|
4837
5058
|
// `upgrade_prompt` card factory already applies to the recorded guest card.
|
|
4838
5059
|
const isStaleAnonymousLimit = errorMeta?.requiresSignup === true && isAnonymous === false;
|
|
5060
|
+
// A read-only viewer's access-denied chat error must never sit as the
|
|
5061
|
+
// persistent red banner — every legitimate viewer action is already gated
|
|
5062
|
+
// client-side, so this only fires when something slipped through to the
|
|
5063
|
+
// server (or an older tab raced a role change). Flash a calm, gold
|
|
5064
|
+
// view-only notice for a few seconds instead, then clear.
|
|
5065
|
+
const isViewerAccessDenied = canEdit === false && !!error && /access denied/i.test(error);
|
|
5066
|
+
const [viewerDeniedFlash, setViewerDeniedFlash] = useState(false);
|
|
5067
|
+
useEffect(() => {
|
|
5068
|
+
if (!isViewerAccessDenied)
|
|
5069
|
+
return;
|
|
5070
|
+
setViewerDeniedFlash(true);
|
|
5071
|
+
const timer = setTimeout(() => setViewerDeniedFlash(false), 6000);
|
|
5072
|
+
return () => clearTimeout(timer);
|
|
5073
|
+
}, [isViewerAccessDenied, error]);
|
|
4839
5074
|
const timeline = useMemo(() => {
|
|
4840
5075
|
const items = [];
|
|
4841
5076
|
// A card-message (role:'system' carrying a cardEvent) renders as a SYSTEM CARD, not a chat
|
|
@@ -5003,7 +5238,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5003
5238
|
defaultValue: 'Show earlier messages',
|
|
5004
5239
|
}) }) })), (timeline.length > maxVisibleItems ? timeline.slice(-maxVisibleItems) : timeline).map((item) => (_jsx(ChatItemBoundary, { onError: onRenderError, render: () => {
|
|
5005
5240
|
if (item.kind === 'commit')
|
|
5006
|
-
return (_jsx(CommitCardItem, { card: item.card, onRevert: handleRevertCommit }, item.card.id));
|
|
5241
|
+
return (_jsx(CommitCardItem, { card: item.card, onRevert: canEdit === false ? undefined : handleRevertCommit }, item.card.id));
|
|
5007
5242
|
if (item.kind === 'activity')
|
|
5008
5243
|
return (_jsx(ActivityCard, { activity: item.card.activity, onActivityClick: onActivityClick }, item.card.id));
|
|
5009
5244
|
if (item.kind === 'tip')
|
|
@@ -5108,9 +5343,9 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5108
5343
|
timestamp: msg.timestamp,
|
|
5109
5344
|
status: 'done',
|
|
5110
5345
|
hash,
|
|
5111
|
-
}, onRevert: handleRevertCommit }, msg.id));
|
|
5346
|
+
}, onRevert: canEdit === false ? undefined : handleRevertCommit }, msg.id));
|
|
5112
5347
|
}
|
|
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,
|
|
5348
|
+
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
5349
|
// Avatar clicks open the signed-in user's OWN profile, so only
|
|
5115
5350
|
// their own messages get the handler: author-less ones (the
|
|
5116
5351
|
// local echo, legacy solo rows) or an author matching
|
|
@@ -5118,11 +5353,27 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5118
5353
|
// clicking Test's face must not open Luke's profile editor.
|
|
5119
5354
|
onAvatarClick: !msg.author?.id || (currentUserId != null && msg.author.id === currentUserId)
|
|
5120
5355
|
? onUserAvatarClick
|
|
5121
|
-
: undefined, discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName }, msg.id));
|
|
5356
|
+
: undefined, discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName, canEdit: canEdit }, msg.id));
|
|
5122
5357
|
} }, item.kind === 'message' ? item.msg.id : item.card.id))), error &&
|
|
5123
5358
|
!isStaleAnonymousLimit &&
|
|
5124
|
-
(errorMeta?.limitType ? (_jsx(ResourceLimitBanner, { message: error, action: buildUpgradeCta?.({ requiresSignup: errorMeta.requiresSignup }) })) :
|
|
5125
|
-
|
|
5359
|
+
(errorMeta?.limitType ? (_jsx(ResourceLimitBanner, { message: error, action: buildUpgradeCta?.({ requiresSignup: errorMeta.requiresSignup }) })) : isViewerAccessDenied ? (
|
|
5360
|
+
// A viewer's denial is expected state, not an alarm: a brief, calm,
|
|
5361
|
+
// gold view-only notice (matching the viewer tip / team-note
|
|
5362
|
+
// treatment) that clears itself — never the persistent red banner.
|
|
5363
|
+
viewerDeniedFlash ? (_jsxs("div", { "data-mol-id": "chat-viewer-denied-notice", className: cm.cn(cm.textSize('xs'), cm.textMuted), style: {
|
|
5364
|
+
display: 'flex',
|
|
5365
|
+
alignItems: 'flex-start',
|
|
5366
|
+
gap: 8,
|
|
5367
|
+
marginBottom: 8,
|
|
5368
|
+
...chatCardStyle(NOTICE_TONE.gold.accent),
|
|
5369
|
+
lineHeight: 1.5,
|
|
5370
|
+
}, 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, {
|
|
5371
|
+
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.",
|
|
5372
|
+
}) })] })) : 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 }))), (() => {
|
|
5373
|
+
// A remote turn (a teammate's send, another tab, a server-side
|
|
5374
|
+
// continuation) shows the same live activity indicator as an own send —
|
|
5375
|
+
// watchers see Synthase working, not a frozen transcript.
|
|
5376
|
+
const showActivity = isLoading || awaitingSandboxBoot || isRemoteStreaming;
|
|
5126
5377
|
const streamingMsg = isLoading
|
|
5127
5378
|
? [...visibleMessages].reverse().find((m) => m.isStreaming)
|
|
5128
5379
|
: undefined;
|
|
@@ -5920,9 +6171,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
5920
6171
|
cursor: 'pointer',
|
|
5921
6172
|
textAlign: 'left',
|
|
5922
6173
|
}, 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: () => {
|
|
6174
|
+
const manageMode = modelPicker.mode === 'plan' || modelPicker.mode === 'execute'
|
|
6175
|
+
? modelPicker.mode
|
|
6176
|
+
: liveModelMode;
|
|
5923
6177
|
setModelPicker(null);
|
|
5924
6178
|
setInputValue('');
|
|
5925
|
-
onManageCustomModels();
|
|
6179
|
+
onManageCustomModels({ mode: manageMode });
|
|
5926
6180
|
}, onMouseEnter: (e) => {
|
|
5927
6181
|
;
|
|
5928
6182
|
e.currentTarget.style.background =
|
|
@@ -6318,7 +6572,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6318
6572
|
}, onMouseLeave: (e) => {
|
|
6319
6573
|
e.currentTarget.style.opacity = '0.7';
|
|
6320
6574
|
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))) })] })),
|
|
6575
|
+
}, 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 &&
|
|
6576
|
+
pendingFiles != null &&
|
|
6322
6577
|
pendingFiles.length > 0 &&
|
|
6323
6578
|
!commandMenu &&
|
|
6324
6579
|
!modelPicker &&
|
|
@@ -6464,7 +6719,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6464
6719
|
opacity: 0.5,
|
|
6465
6720
|
transition: 'opacity 100ms, background 100ms',
|
|
6466
6721
|
...(!(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: {
|
|
6722
|
+
}, 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
6723
|
// Round the TOP corners only (8px) so the composer reads as a self-contained
|
|
6469
6724
|
// input with its own border on all sides, flush at the bottom-left/right with
|
|
6470
6725
|
// the panel's bottom edge. Discovery rounds ALL FOUR corners (the centered
|
|
@@ -6473,11 +6728,18 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6473
6728
|
borderRadius: discovery ? 8 : '8px 8px 0 0',
|
|
6474
6729
|
padding: '8px 10px',
|
|
6475
6730
|
cursor: 'text',
|
|
6731
|
+
// View-only mode: the composer IS the team-chat box, so it wears the
|
|
6732
|
+
// team-message gold border (and the box is pre-filled with /teamsay).
|
|
6733
|
+
...(canEdit === false ? { border: `1px solid ${NOTICE_TONE.gold.accent}` } : {}),
|
|
6476
6734
|
}, onClick: (e) => {
|
|
6477
6735
|
if (!e.target.closest('button')) {
|
|
6478
6736
|
textareaRef.current?.focus();
|
|
6479
6737
|
}
|
|
6480
|
-
}, children: [_jsx("textarea", { ref: textareaRef, "data-mol-chat-input": "", defaultValue: inputRef.current, autoComplete: "off", onChange: handleInputChange, onPaste: handlePaste, placeholder:
|
|
6738
|
+
}, children: [_jsx("textarea", { ref: textareaRef, "data-mol-chat-input": "", defaultValue: inputRef.current, autoComplete: "off", onChange: handleInputChange, onPaste: handlePaste, placeholder: canEdit === false
|
|
6739
|
+
? t('ide.chat.placeholderViewer', undefined, {
|
|
6740
|
+
defaultValue: 'Message your team',
|
|
6741
|
+
})
|
|
6742
|
+
: t('ide.chat.placeholder'), rows: 1, className: cm.textSize('sm'), style: {
|
|
6481
6743
|
width: '100%',
|
|
6482
6744
|
display: 'block',
|
|
6483
6745
|
padding: 0,
|
|
@@ -6503,7 +6765,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6503
6765
|
alignItems: 'center',
|
|
6504
6766
|
marginTop: '6px',
|
|
6505
6767
|
gap: '4px',
|
|
6506
|
-
}, children: [_jsx("button", { type: "button",
|
|
6768
|
+
}, children: [canEdit !== false && (_jsx("button", { type: "button", onClick: () => {
|
|
6507
6769
|
const newMode = mode === 'plan' ? 'execute' : 'plan';
|
|
6508
6770
|
setMode(newMode);
|
|
6509
6771
|
http
|
|
@@ -6548,7 +6810,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6548
6810
|
}, onMouseLeave: (e) => {
|
|
6549
6811
|
if (mode !== 'plan')
|
|
6550
6812
|
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: () => {
|
|
6813
|
+
}, 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
6814
|
const next = !fastMode;
|
|
6553
6815
|
setFastMode(next);
|
|
6554
6816
|
http
|
|
@@ -6613,7 +6875,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6613
6875
|
}, onMouseLeave: (e) => {
|
|
6614
6876
|
if (!isListening)
|
|
6615
6877
|
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: {
|
|
6878
|
+
}, 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
6879
|
display: 'inline-flex',
|
|
6618
6880
|
alignItems: 'center',
|
|
6619
6881
|
justifyContent: 'center',
|
|
@@ -6631,7 +6893,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6631
6893
|
e.currentTarget.style.opacity = '0.85';
|
|
6632
6894
|
}, onMouseLeave: (e) => {
|
|
6633
6895
|
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" }) }) }), [
|
|
6896
|
+
}, 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
6897
|
{
|
|
6636
6898
|
sym: '@',
|
|
6637
6899
|
nudgeY: 0,
|
|
@@ -6660,34 +6922,38 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6660
6922
|
defaultValue: 'Slash commands',
|
|
6661
6923
|
}),
|
|
6662
6924
|
onClick: () => {
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6925
|
+
// TOGGLE the menu regardless of the composer's current text —
|
|
6926
|
+
// never clobbering it. With text present the menu opens in
|
|
6927
|
+
// showAll mode (every command listed; picking one replaces the
|
|
6928
|
+
// input via executeCommand's prefill). An empty box still gets
|
|
6929
|
+
// the type-ahead '/' so filtering-by-typing works as before.
|
|
6930
|
+
if (commandMenu) {
|
|
6668
6931
|
setCommandMenu(null);
|
|
6932
|
+
if (inputRef.current === '/') {
|
|
6933
|
+
// Remove the slash this button added on open.
|
|
6934
|
+
setInputValue('');
|
|
6935
|
+
autoResize();
|
|
6936
|
+
}
|
|
6669
6937
|
setTimeout(() => {
|
|
6670
6938
|
textareaRef.current?.focus();
|
|
6671
6939
|
}, 0);
|
|
6940
|
+
return;
|
|
6672
6941
|
}
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
setSoundsPicker(null);
|
|
6942
|
+
// Popups are one-at-a-time — close siblings first.
|
|
6943
|
+
setMicPicker(null);
|
|
6944
|
+
setModelPicker(null);
|
|
6945
|
+
setSoundsPicker(null);
|
|
6946
|
+
const cur = inputRef.current;
|
|
6947
|
+
if (!cur)
|
|
6680
6948
|
setInputAndCursorEnd('/');
|
|
6681
|
-
|
|
6682
|
-
}
|
|
6683
|
-
else {
|
|
6684
|
-
setTimeout(() => {
|
|
6685
|
-
textareaRef.current?.focus();
|
|
6686
|
-
}, 0);
|
|
6687
|
-
}
|
|
6949
|
+
setCommandMenu({ selectedIdx: -1, showAll: cur !== '' && cur !== '/' });
|
|
6688
6950
|
},
|
|
6689
6951
|
},
|
|
6690
|
-
]
|
|
6952
|
+
]
|
|
6953
|
+
// Viewers keep the / shortcut (viewer-safe commands + /teamsay) but not @ —
|
|
6954
|
+
// file mentions only feed Synthase turns.
|
|
6955
|
+
.filter(({ sym }) => canEdit !== false || sym !== '@')
|
|
6956
|
+
.map(({ sym, nudgeY, size: fontSize, title, onClick }) => (_jsx("button", { type: "button", onClick: onClick, title: title, style: {
|
|
6691
6957
|
display: 'inline-flex',
|
|
6692
6958
|
alignItems: 'center',
|
|
6693
6959
|
justifyContent: 'center',
|
|
@@ -6708,7 +6974,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6708
6974
|
e.currentTarget.style.opacity = '0.85';
|
|
6709
6975
|
}, onMouseLeave: (e) => {
|
|
6710
6976
|
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))),
|
|
6977
|
+
}, 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 &&
|
|
6978
|
+
contextUsage &&
|
|
6712
6979
|
(() => {
|
|
6713
6980
|
// The ring represents usage toward the auto-compaction threshold,
|
|
6714
6981
|
// not the raw context window. 100% = compaction will trigger.
|
|
@@ -6777,11 +7044,14 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6777
7044
|
transition: 'opacity 150ms',
|
|
6778
7045
|
}, 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
7046
|
})(), _jsxs("div", { style: {
|
|
6780
|
-
|
|
7047
|
+
// 'auto' pins the stop/send cluster right; the context ring
|
|
7048
|
+
// (hidden for viewers even when contextUsage exists) otherwise
|
|
7049
|
+
// carries the auto margin and this becomes a 4px gap.
|
|
7050
|
+
marginLeft: canEdit !== false && contextUsage ? '4px' : 'auto',
|
|
6781
7051
|
display: 'flex',
|
|
6782
7052
|
gap: '4px',
|
|
6783
7053
|
alignItems: 'center',
|
|
6784
|
-
}, children: [(isLoading || isRemoteStreaming) && (_jsx("button", { type: "button", onClick: handleAbort, title: t('ide.chat.stop', undefined, { defaultValue: 'Stop' }), onMouseEnter: (e) => {
|
|
7054
|
+
}, children: [canEdit !== false && (isLoading || isRemoteStreaming) && (_jsx("button", { type: "button", onClick: handleAbort, title: t('ide.chat.stop', undefined, { defaultValue: 'Stop' }), onMouseEnter: (e) => {
|
|
6785
7055
|
e.currentTarget.style.background = 'rgba(248,81,73,0.3)';
|
|
6786
7056
|
e.currentTarget.style.borderColor = 'rgba(248,81,73,0.65)';
|
|
6787
7057
|
}, onMouseLeave: (e) => {
|
|
@@ -6835,7 +7105,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6835
7105
|
}
|
|
6836
7106
|
: undefined,
|
|
6837
7107
|
});
|
|
6838
|
-
} })), shareModal && (_jsx(ShareModal, { projectId: projectId, initialRole: shareModal.role, onClose: () => setShareModal(null), onCreated: (result) => {
|
|
7108
|
+
} })), shareModal && (_jsx(ShareModal, { projectId: projectId, initialRole: shareModal.role, canManage: shareAllowed, onClose: () => setShareModal(null), onCreated: (result) => {
|
|
6839
7109
|
// Surface the created link in the timeline so it persists after the
|
|
6840
7110
|
// modal closes — the role label and the public URL are both shown.
|
|
6841
7111
|
addSystemCard(t('ide.chat.share.created', { role: result.role }, { defaultValue: 'Created a {{role}} share link.' }), {
|
|
@@ -6854,8 +7124,12 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
|
|
|
6854
7124
|
* @param props - Component props (see {@link MessageItemProps}).
|
|
6855
7125
|
* @returns The rendered chat panel element.
|
|
6856
7126
|
*/
|
|
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, }) {
|
|
7127
|
+
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
7128
|
const cm = getClassMap();
|
|
7129
|
+
// Share management may be gated ABOVE canEdit by the host (see
|
|
7130
|
+
// ChatPanelProps.canShare) — gates the built-in header share button here and
|
|
7131
|
+
// is threaded into ChatInner for the /share command + modal.
|
|
7132
|
+
const shareAllowed = canShare ?? canEdit !== false;
|
|
6859
7133
|
const isNarrow = useNarrowViewport();
|
|
6860
7134
|
const isCoarse = useCoarsePointer();
|
|
6861
7135
|
const http = useHttpClient();
|
|
@@ -7008,11 +7282,11 @@ export function ChatPanel({ projectId, endpoint, initialMessage, onInitialMessag
|
|
|
7008
7282
|
textOverflow: 'ellipsis',
|
|
7009
7283
|
whiteSpace: 'nowrap',
|
|
7010
7284
|
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, {
|
|
7285
|
+
}, 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
7286
|
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, {
|
|
7287
|
+
}), 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
7288
|
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: {
|
|
7289
|
+
}), 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
7290
|
position: 'absolute',
|
|
7017
7291
|
top: '100%',
|
|
7018
7292
|
left: 0,
|
|
@@ -7063,7 +7337,7 @@ export function ChatPanel({ projectId, endpoint, initialMessage, onInitialMessag
|
|
|
7063
7337
|
textOverflow: 'ellipsis',
|
|
7064
7338
|
whiteSpace: 'nowrap',
|
|
7065
7339
|
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)] }));
|
|
7340
|
+
}, 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
7341
|
}
|
|
7068
7342
|
ChatPanel.displayName = 'ChatPanel';
|
|
7069
7343
|
//# sourceMappingURL=ChatPanel.js.map
|