@molecule/app-ide-react 1.9.1 → 1.10.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 CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-08-29T01:05:39.979Z
6
+ Generated: 2026-08-31T12:50:49.416Z
7
7
  -->
8
8
 
9
9
  # @molecule/app-ide-react
@@ -266,22 +266,22 @@ interface ChatPanelProps {
266
266
  */
267
267
  onRenderError?: (error: Error, info: ErrorInfo) => void
268
268
  /**
269
- * Called when a user avatar in the chat timeline is clicked — the host opens
270
- * that user's profile (e.g. molecule.dev's profile modal). Receives the clicked
271
- * user's {@link ChatUserIdentity}. Omit it (the default) to render the avatars
272
- * non-interactive (static image/icon, exactly as before). Only real user
273
- * avatars are clickable — the molecule glyph on auto-sent messages is not, and
274
- * in a multi-user conversation only the SIGNED-IN user's own avatars are
275
- * (hosts open the *own*-profile editor from this, so a teammate's avatar must
276
- * not fire itsee {@link currentUserId}).
269
+ * Called when a user avatar or author name in the chat timeline is clicked —
270
+ * the host opens THAT user's profile (e.g. molecule.dev's profile modal).
271
+ * Receives the clicked author's {@link ChatUserIdentity}: a teammate's click
272
+ * carries their persisted author fields, the signed-in user's own click
273
+ * carries their own — the host compares `id` against the signed-in user to
274
+ * decide editable-own vs view-only-teammate. Omit it (the default) to render
275
+ * the avatars/names non-interactive (static, exactly as before). Only human
276
+ * authors are clickablethe molecule glyph on auto-sent messages is not.
277
277
  */
278
278
  onProfileClick?: (user: ChatUserIdentity) => void
279
279
  /**
280
- * The signed-in user's id, used to tell their OWN messages from a teammate's
281
- * (a message's persisted `author.id`). Gates avatar clickability: an authored
282
- * message fires {@link onProfileClick} only when its author IS the signed-in
283
- * user. Omit in a solo host — author-less messages (the local echo, legacy
284
- * rows) are always treated as the signed-in user's own.
280
+ * The signed-in user's id (a message's persisted `author.id`). Used to stamp
281
+ * the identity handed to {@link onProfileClick} for AUTHOR-LESS messages (the
282
+ * local optimistic echo, legacy solo rows), which are always the signed-in
283
+ * user's own. Omit in a solo host — those clicks then carry no `id`, which
284
+ * hosts treat as "the signed-in user".
285
285
  */
286
286
  currentUserId?: string
287
287
  /** Called when the server signals (via the `ready_to_build` stream event) that discovery is complete and the sandbox should boot. */
@@ -543,16 +543,24 @@ interface ChatPanelProps {
543
543
 
544
544
  #### `ChatUserIdentity`
545
545
 
546
- Identity of the user whose avatar was clicked in the chat timeline, passed to
547
- {@link ChatPanelProps.onProfileClick} so the host can open that user's profile.
546
+ Identity of the user whose avatar or name was clicked in the chat timeline,
547
+ passed to {@link ChatPanelProps.onProfileClick} so the host can open that
548
+ user's profile.
548
549
 
549
- Today the chat is solo the only avatar shown is the signed-in user's own —
550
- so the only known field is the avatar value. The interface is intentionally
551
- forward-compatible: collaborator fields (id, name) can be added here when
552
- multi-user chat lands, without changing the callback signature.
550
+ The identity is always the clicked MESSAGE AUTHOR's a teammate's click
551
+ carries their persisted `author` fields, never the viewer's. An author-less
552
+ message (the local optimistic echo, legacy solo rows) is the signed-in user's
553
+ own, so it carries {@link ChatPanelProps.currentUserId} + the viewer's avatar.
554
+ The host compares `id` against the signed-in user to pick the surface: own →
555
+ editable profile, someone else → view-only. A missing `id` means "the
556
+ signed-in user" (author-less rows in a host that passes no `currentUserId`).
553
557
 
554
558
  ```typescript
555
559
  interface ChatUserIdentity {
560
+ /** The clicked user's stable id (a persisted `author.id`), when known. */
561
+ id?: string
562
+ /** The clicked user's display name (a persisted `author.name`), when known. */
563
+ name?: string
556
564
  /** The clicked user's avatar value (data-URI / URL), if any. */
557
565
  avatar?: string | null
558
566
  }
@@ -959,6 +967,19 @@ interface PreviewPanelProps {
959
967
  * fix request. The argument is optional for backward compatibility with no-arg callers.
960
968
  */
961
969
  onPreviewStuck?: (report?: PreviewStuckReport) => void
970
+ /**
971
+ * Escalation hook: restart the BACKING dev server. The panel calls this at most
972
+ * once per broken episode (long cooldown, never during a build) when client-side
973
+ * recovery is provably useless: the server answers probes, at least one automatic
974
+ * reload already ran, and the app has still never confirmed a render for minutes.
975
+ * Document reloads cannot heal that class — e.g. a poisoned immutable module cache
976
+ * keyed to the dev server's version hash (observed live 2026-08-31); only a server
977
+ * restart mints new module URLs. Return (or resolve) `false` to DECLINE (wrong
978
+ * role, hidden tab, sandbox not running, a turn active) — the panel then burns no
979
+ * budget and may ask again later; any other result counts as "restart initiated".
980
+ * Omit the prop to disable escalation entirely (the default for generic hosts).
981
+ */
982
+ onRestartBackend?: () => Promise<boolean | void> | boolean | void
962
983
  /**
963
984
  * Called when the preview's render verdict changes ({@link PreviewRenderState}) — and
964
985
  * with the current location so the host can report WHERE. The host forwards this to the
@@ -2291,6 +2312,7 @@ function PreviewPanel({
2291
2312
  className,
2292
2313
  onPreviewError,
2293
2314
  onPreviewStuck,
2315
+ onRestartBackend,
2294
2316
  onRenderState,
2295
2317
  uiCommand,
2296
2318
  onUiResult,
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import type { JSX } from 'react';
19
19
  import type { ChatMessage } from '@molecule/app-ai-chat';
20
- import type { ChatPanelProps, IdeClientAction } from '../types.js';
20
+ import type { ChatPanelProps, ChatUserIdentity, IdeClientAction } from '../types.js';
21
21
  import type { Activity } from './activity-utilities.js';
22
22
  import { type CommandDef } from './chat-commands.js';
23
23
  import type { ModelMode } from './chat-model-mode-utilities.js';
@@ -82,8 +82,14 @@ export interface MessageItemProps {
82
82
  chatMode: 'plan' | 'execute';
83
83
  /** Signed-in user's avatar shown beside their own messages (SOC1); icon fallback when absent/unsafe. */
84
84
  userAvatar?: string | null;
85
- /** When set, the user avatar becomes clickable and fires this to open the user's profile (C5). */
86
- onAvatarClick?: () => void;
85
+ /**
86
+ * When set, the author's avatar AND name become clickable and fire this with
87
+ * the clicked AUTHOR's identity — a teammate's message opens THEIR profile,
88
+ * the signed-in user's own opens theirs (C5). See {@link ChatPanelProps.onProfileClick}.
89
+ */
90
+ onProfileClick?: (user: ChatUserIdentity) => void;
91
+ /** Signed-in user's id — stamps the identity for author-less rows (the local echo, legacy solo rows). */
92
+ currentUserId?: string;
87
93
  /** Discovery phase — gives consecutive question/answer cards roomier, uncollapsed spacing (B3). */
88
94
  discovery?: boolean;
89
95
  /**
@@ -133,9 +139,9 @@ export interface ChatInnerProps {
133
139
  onActivityClick?: (activity: Activity) => void;
134
140
  /** See {@link ChatPanelProps.onRenderError}. */
135
141
  onRenderError?: ChatPanelProps['onRenderError'];
136
- /** Called when a user avatar in the chat timeline is clicked — see {@link ChatPanelProps.onProfileClick}. */
142
+ /** Called when a user avatar or author name in the chat timeline is clicked — see {@link ChatPanelProps.onProfileClick}. */
137
143
  onProfileClick?: ChatPanelProps['onProfileClick'];
138
- /** The signed-in user's id — gates avatar clicks to their OWN messages. See {@link ChatPanelProps.currentUserId}. */
144
+ /** The signed-in user's id — stamps author-less rows' identity. See {@link ChatPanelProps.currentUserId}. */
139
145
  currentUserId?: string;
140
146
  /** Called on the `ready_to_build` stream event — discovery is done; boot the sandbox. */
141
147
  onReadyToBuild?: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../../src/components/ChatPanel.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../../src/components/ChatPanel.tsx"],"names":[],"mappings":"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"}
@@ -935,6 +935,36 @@ export function CommitCardItem({ card, onRevert, }) {
935
935
  return _jsx("div", { children: path }, path);
936
936
  }) }) }))] }) }));
937
937
  }
938
+ /**
939
+ * The clickable author-name in a message header (C5) — a reset button that
940
+ * renders exactly like the bold name span it replaces, with an underline on
941
+ * hover/focus as the affordance, and opens the author's profile like their
942
+ * avatar does. Its own component so the hover state never re-renders the
943
+ * (memoized, heavy) message row.
944
+ *
945
+ * @param props - Click handler + the name text.
946
+ * @param props.onClick - Opens the author's profile.
947
+ * @param props.children - The author's display name.
948
+ * @returns The name button.
949
+ */
950
+ function AuthorNameButton({ onClick, children, }) {
951
+ const [hover, setHover] = useState(false);
952
+ return (_jsx("button", { type: "button", onClick: onClick, onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), onFocus: () => setHover(true), onBlur: () => setHover(false), "data-mol-id": "chat-user-name-button",
953
+ // Full button reset so it is byte-identical to the former bold span at
954
+ // rest — the underline affordance is the only visual change on hover.
955
+ style: {
956
+ padding: 0,
957
+ margin: 0,
958
+ border: 'none',
959
+ background: 'transparent',
960
+ font: 'inherit',
961
+ color: 'inherit',
962
+ fontWeight: 600,
963
+ cursor: 'pointer',
964
+ textDecoration: hover ? 'underline' : 'none',
965
+ }, children: children }));
966
+ }
967
+ AuthorNameButton.displayName = 'AuthorNameButton';
938
968
  /**
939
969
  * Renders a single message (user or assistant) in the chat timeline.
940
970
  * Wrapped in React.memo so unchanged messages skip re-rendering when
@@ -943,7 +973,7 @@ export function CommitCardItem({ card, onRevert, }) {
943
973
  * @returns The rendered message item.
944
974
  */
945
975
  const MessageItem = memo(function MessageItem(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;
976
+ const { msg, sendMessage, handleAskUserResponse, isLoading, streamingStatus, onNavigatePreview, undoneTcIds, handleUndoToggle, onFileOpen, onFileDoubleClick, onFileDiff, handleFileRevert, setInputAndCursorEnd, setModelPicker, chatMode, userAvatar, onProfileClick, currentUserId, discovery, buildUpgradeCta, agentName, canEdit, } = props;
947
977
  const cm = getClassMap();
948
978
  const themeMode = useThemeMode();
949
979
  const isLight = themeMode === 'light';
@@ -959,6 +989,15 @@ const MessageItem = memo(function MessageItem(props) {
959
989
  // A real, user-typed message (the only one styled with the blue border + the
960
990
  // user's own avatar).
961
991
  const isUser = msg.role === 'user' && !isAutomatic;
992
+ // Who this message belongs to — the identity a profile click hands the host.
993
+ // An AUTHORED message is that author's (a teammate's click opens THEIR
994
+ // profile, never the viewer's); an author-less one (the local optimistic
995
+ // echo, legacy solo rows) is the signed-in user's own, stamped with
996
+ // currentUserId + the viewer's avatar so the host still knows who it is.
997
+ const authorIdentity = msg.author
998
+ ? { id: msg.author.id, name: msg.author.name, avatar: msg.author.avatar ?? null }
999
+ : { id: currentUserId, avatar: userAvatar ?? null };
1000
+ const openAuthorProfile = onProfileClick ? () => onProfileClick(authorIdentity) : undefined;
962
1001
  // Spacing follows the one timeline convention (see TIMELINE_ITEM_GAP above): a
963
1002
  // single bottom margin, no top margin, no negatives — so a message can never
964
1003
  // pull itself up over the previous item's spacing. Discovery is roomier but
@@ -980,11 +1019,16 @@ const MessageItem = memo(function MessageItem(props) {
980
1019
  // A real user message keeps its classic look: a gray surface with the
981
1020
  // molecule brand's ANIMATED blue gradient stripe (3px) on the left edge,
982
1021
  // drawn by the `::before` injected via USER_ACCENT_STYLE + gated on the
983
- // data-mol-id below, and the user's full-size avatar. An auto-sent message
984
- // is instead a green (success) tinted card — same chrome as the info cards
985
- // so it's unmistakably agent-sent, not user-typed (C2). A team note keeps
986
- // the user-message look but swaps the blue stripe (its data-mol-id differs,
987
- // so USER_ACCENT_STYLE never applies) for the gold team-only border.
1022
+ // data-mol-id below, PLUS a solid blue outline ring (host-tunable via
1023
+ // `--mol-chat-accent-border`) so a sent-to-Synthase message reads bordered
1024
+ // the same way a team note does, and the user's full-size avatar. An
1025
+ // auto-sent message is instead a green (success) tinted card same chrome
1026
+ // as the info cards so it's unmistakably agent-sent, not user-typed (C2).
1027
+ // A team note keeps the user-message look but swaps the blue chrome (its
1028
+ // data-mol-id differs, so USER_ACCENT_STYLE never applies) for the gold
1029
+ // team-only border — with its own solid 3px left band (an inset box-shadow,
1030
+ // which starts at the border's inner edge exactly like the stripe's
1031
+ // `inset: 0` ::before, so the two rows' left accents match in geometry).
988
1032
  ...(isAutomatic
989
1033
  ? chatCardStyle(AUTO_SENT_ACCENT)
990
1034
  : {
@@ -993,7 +1037,12 @@ const MessageItem = memo(function MessageItem(props) {
993
1037
  paddingTop: '10px',
994
1038
  paddingBottom: '10px',
995
1039
  paddingRight: '10px',
996
- ...(isTeamNote ? { border: `1px solid ${NOTICE_TONE.gold.accent}` } : {}),
1040
+ border: isTeamNote
1041
+ ? `1px solid ${NOTICE_TONE.gold.accent}`
1042
+ : '1px solid var(--mol-chat-accent-border, var(--mol-color-primary, #3060c0))',
1043
+ ...(isTeamNote
1044
+ ? { boxShadow: `inset 3px 0 0 0 ${NOTICE_TONE.gold.accent}` }
1045
+ : {}),
997
1046
  }),
998
1047
  }, "data-mol-id": isTeamNote
999
1048
  ? 'chat-team-message'
@@ -1004,20 +1053,24 @@ const MessageItem = memo(function MessageItem(props) {
1004
1053
  // when they have none — NEVER the viewing user's picture (which made
1005
1054
  // a teammate's avatar-less message wear the viewer's own face). Only
1006
1055
  // an author-less message (the local optimistic echo, legacy rows) is
1007
- // the signed-in user's own and uses their avatar.
1056
+ // the signed-in user's own and uses their avatar. Clicking opens the
1057
+ // AUTHOR's profile (own and teammate alike — the host decides which
1058
+ // surface from the identity's id).
1008
1059
  , {
1009
1060
  // An AUTHORED message shows that author's avatar — or their initial
1010
1061
  // when they have none — NEVER the viewing user's picture (which made
1011
1062
  // a teammate's avatar-less message wear the viewer's own face). Only
1012
1063
  // an author-less message (the local optimistic echo, legacy rows) is
1013
- // the signed-in user's own and uses their avatar.
1014
- userAvatar: msg.author ? msg.author.avatar : userAvatar, name: msg.author?.name ?? undefined, size: 36, onClick: onAvatarClick })), _jsxs("div", { style: { flex: 1, minWidth: 0, marginTop: 1 }, children: [isAutomatic ? null : (_jsxs("div", { style: {
1064
+ // the signed-in user's own and uses their avatar. Clicking opens the
1065
+ // AUTHOR's profile (own and teammate alike the host decides which
1066
+ // surface from the identity's id).
1067
+ userAvatar: authorIdentity.avatar, name: msg.author?.name ?? undefined, size: 36, onClick: openAuthorProfile })), _jsxs("div", { style: { flex: 1, minWidth: 0, marginTop: 1 }, children: [isAutomatic ? null : (_jsxs("div", { style: {
1015
1068
  display: 'flex',
1016
1069
  alignItems: 'baseline',
1017
1070
  gap: 8,
1018
1071
  lineHeight: 1.3,
1019
1072
  marginBottom: 1,
1020
- }, children: [_jsx("span", { style: { fontWeight: 600 }, children: msg.author?.name ?? t('ide.chat.you', undefined, { defaultValue: 'You' }) }), isTeamNote && (_jsx("span", { title: t('ide.chat.teamOnly.badge', { agentName: agentName ?? 'the assistant' }, {
1073
+ }, children: [openAuthorProfile ? (_jsx(AuthorNameButton, { onClick: openAuthorProfile, children: msg.author?.name ?? t('ide.chat.you', undefined, { defaultValue: 'You' }) })) : (_jsx("span", { style: { fontWeight: 600 }, children: msg.author?.name ?? t('ide.chat.you', undefined, { defaultValue: 'You' }) })), isTeamNote && (_jsx("span", { title: t('ide.chat.teamOnly.badge', { agentName: agentName ?? 'the assistant' }, {
1021
1074
  defaultValue: 'Team only — visible to your team; {{agentName}} will ignore it',
1022
1075
  }), "aria-label": t('ide.chat.teamOnly.badge', { agentName: agentName ?? 'the assistant' }, {
1023
1076
  defaultValue: 'Team only — visible to your team; {{agentName}} will ignore it',
@@ -1241,15 +1294,6 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
1241
1294
  // visible; 420px bounds it on small tablets.
1242
1295
  const popupMaxHeight = isNarrow ? 'min(50dvh, 420px)' : '70vh';
1243
1296
  const http = useHttpClient();
1244
- // Bind the host's profile-click callback to the SIGNED-IN user's identity once.
1245
- // Hosts open the *own*-profile surface from this, so it is only ever attached to
1246
- // the signed-in user's OWN messages (see the per-message gate at the MessageItem
1247
- // call site — a teammate's avatar must never open the viewer's profile). Stable
1248
- // so MessageItem's memo isn't broken; `undefined` when the host opts out, which
1249
- // keeps every avatar non-interactive.
1250
- const onUserAvatarClick = useMemo(() => onProfileClick
1251
- ? () => onProfileClick({ avatar: userAvatar })
1252
- : undefined, [onProfileClick, userAvatar]);
1253
1297
  // If there's already a conversation (conversationId in the URL), always load
1254
1298
  // history — even when initialMessage is set. This prevents a refresh from
1255
1299
  // re-sending the initial prompt instead of restoring the existing conversation.
@@ -1458,6 +1502,13 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
1458
1502
  const sendMessageRef = useRef(() => { });
1459
1503
  useEffect(() => {
1460
1504
  if (autoFixCountdown && autoFixCountdown.secondsLeft === 0 && !autoFixCountdown.paused) {
1505
+ // Defense-in-depth: arming is sender-only (a viewer never owns a stream),
1506
+ // but an autonomous agent-turn dispatch must never fire from a read-only
1507
+ // client regardless of how the countdown came to exist.
1508
+ if (canEdit === false) {
1509
+ setAutoFixCountdown(null);
1510
+ return;
1511
+ }
1461
1512
  const msg = `Fix these issues:\n\n${autoFixCountdown.output}`;
1462
1513
  setAutoFixCountdown(null);
1463
1514
  // Auto-sent on the user's behalf — flag it so the chat renders it in the
@@ -1465,7 +1516,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
1465
1516
  // user typed it (C2).
1466
1517
  sendMessageRef.current(msg, undefined, { automatic: true });
1467
1518
  }
1468
- }, [autoFixCountdown]);
1519
+ }, [autoFixCountdown, canEdit]);
1469
1520
  // Auto-pause countdown when user starts typing
1470
1521
  const handleAutoFixPauseOnInput = useCallback(() => {
1471
1522
  setAutoFixCountdown((prev) => (prev && !prev.paused ? { ...prev, paused: true } : prev));
@@ -1483,6 +1534,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
1483
1534
  // overwrite it), and `…PersistedRef` tracks the value we believe is on the
1484
1535
  // server so we only PATCH genuine changes (never the value we just hydrated).
1485
1536
  const [autoCommitLoaded, setAutoCommitLoaded] = useState(false);
1537
+ // Live canEdit for async callbacks (the settings hydrate) that must not
1538
+ // capture a stale role from mount time.
1539
+ const canEditRef = useRef(canEdit);
1540
+ canEditRef.current = canEdit;
1486
1541
  const autoCommitPersistedRef = useRef(0);
1487
1542
  const autoCommitPatchTimerRef = useRef(null);
1488
1543
  // Set after debouncedFetchPendingFiles is defined below (it depends on state
@@ -2701,11 +2756,15 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
2701
2756
  // Restore the persisted auto-commit cadence in the paused state (it
2702
2757
  // re-arms on the next file change). Auto-commit is ON by default: a
2703
2758
  // project that never set `autoCommitSeconds` resolves to the default
2704
- // cadence; an explicit 0 (the user turned it off) stays off.
2759
+ // cadence; an explicit 0 (the user turned it off) stays off. Never for
2760
+ // a read-only VIEWER: /commit is an editor action, so arming the
2761
+ // countdown on a viewer's client only produces a doomed dispatch (and
2762
+ // the denial card it minted) when it lapses.
2705
2763
  const savedAutoCommit = resolveAutoCommitSeconds(s?.autoCommitSeconds);
2706
2764
  autoCommitPersistedRef.current = savedAutoCommit;
2707
- if (savedAutoCommit > 0)
2765
+ if (savedAutoCommit > 0 && canEditRef.current !== false) {
2708
2766
  dispatchAutoCommit({ type: 'hydrate', seconds: savedAutoCommit });
2767
+ }
2709
2768
  setAutoCommitLoaded(true);
2710
2769
  })
2711
2770
  .catch(() => {
@@ -2788,6 +2847,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
2788
2847
  useEffect(() => {
2789
2848
  if (!autoCommitLoaded)
2790
2849
  return;
2850
+ // A viewer never mirrors auto-commit state back to the project — the PATCH
2851
+ // is editor-gated server-side and their reducer stays disarmed anyway.
2852
+ if (canEdit === false)
2853
+ return;
2791
2854
  const seconds = autoCommit.intervalSeconds;
2792
2855
  if (autoCommitPersistedRef.current === seconds)
2793
2856
  return;
@@ -2806,7 +2869,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
2806
2869
  if (autoCommitPatchTimerRef.current)
2807
2870
  clearTimeout(autoCommitPatchTimerRef.current);
2808
2871
  };
2809
- }, [autoCommit.intervalSeconds, autoCommitLoaded, http, projectId]);
2872
+ }, [autoCommit.intervalSeconds, autoCommitLoaded, http, projectId, canEdit]);
2810
2873
  // ── Removed-model recovery ──────────────────────────────────────────────────
2811
2874
  // If the saved chatModel is no longer in the catalog (a provider retired it,
2812
2875
  // or we pruned the entry), notify once and fall back to the free-tier model.
@@ -3028,13 +3091,18 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
3028
3091
  useEffect(() => {
3029
3092
  if (initialMessage && !hasConversation && sentInitialRef.current !== initialMessage) {
3030
3093
  sentInitialRef.current = initialMessage;
3031
- // Same rule as handleSubmit: a host side-channel command (e.g. /teamsay)
3032
- // renders no optimistic echo the server's `message` event is the message.
3094
+ // A read-only VIEWER never auto-sends the initial prompt: the host sources
3095
+ // it from PROJECT-keyed localStorage, which survives a role demotion and a
3096
+ // shared browser — a stale entry would auto-POST an agent turn the server
3097
+ // 403s, surfacing a spurious red error on mount. Consume the ref (above)
3098
+ // so it is never retried either.
3099
+ if (canEdit === false)
3100
+ return;
3033
3101
  const sideChannel = matchesSideChannelCommand(extraCommands?.length ? [...COMMANDS, ...extraCommands] : COMMANDS, initialMessage);
3034
3102
  sendMessage(initialMessage, undefined, sideChannel ? { suppressUserMessage: true } : undefined);
3035
3103
  onInitialMessageSent?.();
3036
3104
  }
3037
- }, [initialMessage, hasConversation, sendMessage, onInitialMessageSent, extraCommands]);
3105
+ }, [initialMessage, hasConversation, sendMessage, onInitialMessageSent, extraCommands, canEdit]);
3038
3106
  // ── Auto-send pending message (e.g. "Fix with AI", preview errors) ──────
3039
3107
  // Defers sending while the AI is streaming to avoid queueing up auto-fix
3040
3108
  // messages during active work. Messages are sent once streaming ends.
@@ -4151,11 +4219,17 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
4151
4219
  // the exact render the countdown hits zero): never commit mid-turn — the
4152
4220
  // effect re-runs when the hold clears and fires then.
4153
4221
  useEffect(() => {
4154
- if (!isAutoCommitDue(autoCommit) || autoCommitHeld)
4222
+ // A read-only VIEWER never fires auto-commit: /commit is an editor action,
4223
+ // and the client-side dispatch here surfaced the "view-only access, so this
4224
+ // command is unavailable" denial card out of nowhere on viewers whenever a
4225
+ // synced countdown lapsed (observed 2026-08-31, right after a watched turn
4226
+ // settled and released the hold). The hydrate below is also viewer-gated,
4227
+ // so this is the belt for a mid-session demotion.
4228
+ if (canEdit === false || !isAutoCommitDue(autoCommit) || autoCommitHeld)
4155
4229
  return;
4156
4230
  void executeCommand('commit');
4157
4231
  dispatchAutoCommit({ type: 'fired' });
4158
- }, [autoCommit, autoCommitHeld, executeCommand]);
4232
+ }, [autoCommit, autoCommitHeld, executeCommand, canEdit]);
4159
4233
  // ── Submit ─────────────────────────────────────────────────────────────────
4160
4234
  const handleSubmit = useCallback(async () => {
4161
4235
  // Stop voice recognition on submit
@@ -4537,7 +4611,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
4537
4611
  setInputAndCursorEnd('/teamsay ');
4538
4612
  else
4539
4613
  setInputValue('');
4540
- sendMessage(trimmed, undefined, { suppressUserMessage: true });
4614
+ // sideChannel: a team note is a human-to-human message, not a turn — it
4615
+ // goes out immediately even while a turn streams (it used to queue
4616
+ // silently behind the sender's own active turn, and a viewer's note
4617
+ // used to tear down their remote-turn tracking).
4618
+ sendMessage(trimmed, undefined, { suppressUserMessage: true, sideChannel: true });
4541
4619
  return;
4542
4620
  }
4543
4621
  // Rewrite /explain with attachments into a proper prompt (attachments processed below)
@@ -5550,14 +5628,11 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
5550
5628
  }, onRevert: canEdit === false ? undefined : handleRevertCommit }, msg.id));
5551
5629
  }
5552
5630
  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,
5553
- // Avatar clicks open the signed-in user's OWN profile, so only
5554
- // their own messages get the handler: author-less ones (the
5555
- // local echo, legacy solo rows) or an author matching
5556
- // currentUserId. A teammate's avatar stays non-interactive —
5557
- // clicking Test's face must not open Luke's profile editor.
5558
- onAvatarClick: !msg.author?.id || (currentUserId != null && msg.author.id === currentUserId)
5559
- ? onUserAvatarClick
5560
- : undefined, discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName, canEdit: canEdit }, msg.id));
5631
+ // Avatar/name clicks open the clicked AUTHOR's profile
5632
+ // MessageItem builds the identity from the message's own
5633
+ // author (teammates included), so the host shows their
5634
+ // view-only profile and the viewer's editable own.
5635
+ onProfileClick: onProfileClick, currentUserId: currentUserId, discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName, canEdit: canEdit }, msg.id));
5561
5636
  } }, item.kind === 'message' ? item.msg.id : item.card.id))), error &&
5562
5637
  !isStaleAnonymousLimit &&
5563
5638
  (errorMeta?.limitType ? (_jsx(ResourceLimitBanner, { message: error, action: buildUpgradeCta?.({ requiresSignup: errorMeta.requiresSignup }) })) : isViewerAccessDenied ? (
@@ -5578,7 +5653,14 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
5578
5653
  // continuation) shows the same live activity indicator as an own send —
5579
5654
  // watchers see Synthase working, not a frozen transcript.
5580
5655
  const showActivity = isLoading || awaitingSandboxBoot || isRemoteStreaming;
5581
- const streamingMsg = isLoading
5656
+ // A remote turn streams real messages into the same store, so it gets the
5657
+ // SAME activity treatment as an own send. Only the no-turn-at-all case
5658
+ // (awaitingSandboxBoot alone) is a sandbox-boot wait — labeling every
5659
+ // !isLoading render with the boot copy showed viewers "Waiting for the
5660
+ // development environment to finish starting…" over a running sandbox
5661
+ // for the whole remote turn (observed 2026-08-31).
5662
+ const streamingLike = isLoading || isRemoteStreaming;
5663
+ const streamingMsg = streamingLike
5582
5664
  ? [...visibleMessages].reverse().find((m) => m.isStreaming)
5583
5665
  : undefined;
5584
5666
  // Turn start = the last genuine user message, so the elapsed timer counts up across
@@ -5595,7 +5677,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
5595
5677
  // matches the elapsed timer's span: it climbs monotonically and only plateaus during
5596
5678
  // tool-execution gaps, rather than vanishing/restarting at each new assistant message.
5597
5679
  const turnTokens = estimateTurnTokens(messages);
5598
- const label = isLoading
5680
+ const label = streamingLike
5599
5681
  ? (streamingStatus ?? (streamingMsg ? streamingActivityLabel(streamingMsg) : undefined))
5600
5682
  : t('ide.chat.awaitingSandbox', undefined, {
5601
5683
  defaultValue: 'Waiting for the development environment to finish starting…',
@@ -5605,7 +5687,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
5605
5687
  opacity: showActivity ? 1 : 0,
5606
5688
  pointerEvents: showActivity ? 'auto' : 'none',
5607
5689
  transition: 'opacity 0.18s ease-out',
5608
- }, children: showActivity && (_jsx(StreamingIndicator, { label: label, tokens: isLoading ? turnTokens : undefined, startedAt: isLoading ? turnStartedAt : undefined })) }));
5690
+ }, children: showActivity && (_jsx(StreamingIndicator, { label: label, tokens: streamingLike ? turnTokens : undefined, startedAt: streamingLike ? turnStartedAt : undefined })) }));
5609
5691
  })(), _jsx("div", { ref: messagesEndRef })] }), autoFixCountdown && (_jsxs("div", { className: cm.cn(cm.shrink0, cm.borderT), style: {
5610
5692
  display: 'flex',
5611
5693
  alignItems: 'center',