@molecule/app-ide-react 1.11.3 → 1.12.1

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.
Files changed (43) hide show
  1. package/README.md +237 -1
  2. package/dist/command-metadata.d.ts.map +1 -1
  3. package/dist/command-metadata.js +9 -0
  4. package/dist/command-metadata.js.map +1 -1
  5. package/dist/components/ChatPanel.d.ts +5 -0
  6. package/dist/components/ChatPanel.d.ts.map +1 -1
  7. package/dist/components/ChatPanel.js +128 -24
  8. package/dist/components/ChatPanel.js.map +1 -1
  9. package/dist/components/ChatTimestamp.d.ts +33 -0
  10. package/dist/components/ChatTimestamp.d.ts.map +1 -0
  11. package/dist/components/ChatTimestamp.js +30 -0
  12. package/dist/components/ChatTimestamp.js.map +1 -0
  13. package/dist/components/PreviewPanel.d.ts.map +1 -1
  14. package/dist/components/PreviewPanel.js +21 -2
  15. package/dist/components/PreviewPanel.js.map +1 -1
  16. package/dist/components/chat-timestamps-utilities.d.ts +119 -0
  17. package/dist/components/chat-timestamps-utilities.d.ts.map +1 -0
  18. package/dist/components/chat-timestamps-utilities.js +173 -0
  19. package/dist/components/chat-timestamps-utilities.js.map +1 -0
  20. package/dist/components/index.d.ts +2 -0
  21. package/dist/components/index.d.ts.map +1 -1
  22. package/dist/components/index.js +2 -0
  23. package/dist/components/index.js.map +1 -1
  24. package/dist/customEventCards.d.ts +7 -0
  25. package/dist/customEventCards.d.ts.map +1 -1
  26. package/dist/customEventCards.js.map +1 -1
  27. package/dist/hooks/index.d.ts +1 -0
  28. package/dist/hooks/index.d.ts.map +1 -1
  29. package/dist/hooks/index.js +1 -0
  30. package/dist/hooks/index.js.map +1 -1
  31. package/dist/hooks/useChatTimestampsVisible.d.ts +20 -0
  32. package/dist/hooks/useChatTimestampsVisible.d.ts.map +1 -0
  33. package/dist/hooks/useChatTimestampsVisible.js +74 -0
  34. package/dist/hooks/useChatTimestampsVisible.js.map +1 -0
  35. package/dist/index.d.ts +12 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +12 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/settings-metadata.d.ts +1 -1
  40. package/dist/settings-metadata.d.ts.map +1 -1
  41. package/dist/settings-metadata.js +6 -0
  42. package/dist/settings-metadata.js.map +1 -1
  43. package/package.json +3 -3
@@ -3,13 +3,14 @@ import { Fragment, memo, useCallback, useEffect, useMemo, useReducer, useRef, us
3
3
  import { formatTokenCount, isDeprecated, partitionByDeprecation, PROVIDER_BRAND_COLORS, } from '@molecule/app-ai-models';
4
4
  import { getProvider as getVoiceProvider, listVoiceEngines, selectVoiceEngine, voiceEngineCoversLanguage, } from '@molecule/app-ai-voice';
5
5
  import { getCountryFlag } from '@molecule/app-country-flags';
6
- import { t } from '@molecule/app-i18n';
6
+ import { getLocale, t } from '@molecule/app-i18n';
7
7
  import { getLogger } from '@molecule/app-logger';
8
8
  import { DEFAULT_AGENT_NAME, DEFAULT_PRODUCT_NAME, useAIModels, useChat, useHttpClient, useThemeMode, } from '@molecule/app-react';
9
9
  import { getClassMap } from '@molecule/app-ui';
10
10
  import { Tooltip } from '@molecule/app-ui-react/components/Tooltip.js';
11
11
  import { timelineSortKey } from '../chatTimelineOrdering.js';
12
12
  import { getCustomEventCardFactory } from '../customEventCards.js';
13
+ import { useChatTimestampsVisible, useMinuteNow } from '../hooks/useChatTimestampsVisible.js';
13
14
  import { useCoarsePointer, useNarrowViewport } from '../hooks/useViewport.js';
14
15
  import { activityFromEvent } from './activity-utilities.js';
15
16
  import { ActivityCard } from './ActivityCard.js';
@@ -29,8 +30,10 @@ import { buildSettingsList, summarizeSounds } from './chat-settings-utilities.js
29
30
  import { buildShareUrl, DEFAULT_SHARE_ROLE, parseShareCommand, SHARE_ROLES, } from './chat-share-utilities.js';
30
31
  import { buildNewSkillTemplate, loadProjectSkills, newSkillPath, parseSkillMeta, pickRelevantSkill, recentUserText, } from './chat-skills-utilities.js';
31
32
  import { estimateTurnTokens } from './chat-stream-utilities.js';
33
+ import { getChatTimestampsVisible, parseTimestampsCommand, planChatTimestamps, resolveChatLocale, setChatTimestampsVisible, } from './chat-timestamps-utilities.js';
32
34
  import { ENTRY_TIP, pickIdleTip, shouldShowIdleTip, TIP_IDLE_MS, TIP_MIN_MESSAGES, } from './chat-tips-utilities.js';
33
35
  import { ChatItemBoundary } from './ChatItemBoundary.js';
36
+ import { ChatTimestamp } from './ChatTimestamp.js';
34
37
  import { HelpCard } from './HelpCard.js';
35
38
  import { Icon } from './Icon.js';
36
39
  import { MarkdownContent } from './MarkdownContent.js';
@@ -259,24 +262,6 @@ function relativeTime(iso) {
259
262
  const d = Math.floor(h / 24);
260
263
  return `${d}d ago`;
261
264
  }
262
- /**
263
- * Long-form relative time (e.g. "just now", "43 minutes ago", "2 hours ago",
264
- * "3 days ago") for the Slack-style message header.
265
- * @param ms - Epoch milliseconds of the message.
266
- * @returns A human-readable long-form relative time string.
267
- */
268
- function relativeTimeLong(ms) {
269
- const min = Math.floor((Date.now() - ms) / 60_000);
270
- if (min < 1)
271
- return 'just now';
272
- if (min < 60)
273
- return `${min} minute${min === 1 ? '' : 's'} ago`;
274
- const h = Math.floor(min / 60);
275
- if (h < 24)
276
- return `${h} hour${h === 1 ? '' : 's'} ago`;
277
- const d = Math.floor(h / 24);
278
- return `${d} day${d === 1 ? '' : 's'} ago`;
279
- }
280
265
  /**
281
266
  * Format an AI-allowance window reset (epoch ms) as a short countdown phrase for
282
267
  * the /cost used-up line ("refreshes in about 3 hours"). Under an hour reads "in
@@ -986,7 +971,7 @@ AuthorNameButton.displayName = 'AuthorNameButton';
986
971
  * @returns The rendered message item.
987
972
  */
988
973
  const MessageItem = memo(function MessageItem(props) {
989
- const { msg, sendMessage, handleAskUserResponse, isLoading, streamingStatus, onNavigatePreview, undoneTcIds, handleUndoToggle, onFileOpen, onFileDoubleClick, onFileDiff, handleFileRevert, setInputAndCursorEnd, setModelPicker, chatMode, userAvatar, onProfileClick, currentUserId, discovery, buildUpgradeCta, agentName, canEdit, } = props;
974
+ const { msg, sendMessage, handleAskUserResponse, isLoading, streamingStatus, onNavigatePreview, undoneTcIds, handleUndoToggle, onFileOpen, onFileDoubleClick, onFileDiff, handleFileRevert, setInputAndCursorEnd, setModelPicker, chatMode, userAvatar, onProfileClick, currentUserId, discovery, buildUpgradeCta, agentName, canEdit, showTimestamp, } = props;
990
975
  const cm = getClassMap();
991
976
  const themeMode = useThemeMode();
992
977
  const isLight = themeMode === 'light';
@@ -1110,7 +1095,7 @@ const MessageItem = memo(function MessageItem(props) {
1110
1095
  // 16px so the glyph reads at the username's optical size — the
1111
1096
  // people glyph has internal viewBox padding, so a nominal 14px
1112
1097
  // renders visibly smaller than the 14px text next to it.
1113
- 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/')
1098
+ name: "people", size: 16, "aria-hidden": "true", style: { color: NOTICE_TONE.gold.accent } }) })), showTimestamp && typeof msg.timestamp === 'number' && (_jsx(ChatTimestamp, { timestamp: 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/')
1114
1099
  ? '\uD83D\uDDBC\uFE0F'
1115
1100
  : att.mediaType.startsWith('audio/')
1116
1101
  ? '\uD83C\uDFB5'
@@ -2608,6 +2593,18 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
2608
2593
  // localStorage unavailable — treat as unset; the project fallback may apply.
2609
2594
  }
2610
2595
  }, []);
2596
+ // ── Timestamps (/timestamps) ────────────────────────────────────────────────
2597
+ // A per-device display preference (localStorage, like sounds), off by default.
2598
+ // The minute clock re-plans which items show a time (see planChatTimestamps),
2599
+ // so labels advance and runs of identical labels regroup as time passes.
2600
+ const timestampsVisible = useChatTimestampsVisible();
2601
+ const minuteNow = useMinuteNow();
2602
+ const applyTimestampsVisible = useCallback((visible) => {
2603
+ setChatTimestampsVisible(visible);
2604
+ addSystemCard(visible
2605
+ ? t('ide.chat.timestampsShown', undefined, { defaultValue: 'Timestamps shown.' })
2606
+ : t('ide.chat.timestampsHidden', undefined, { defaultValue: 'Timestamps hidden.' }), { clientOnly: true });
2607
+ }, [addSystemCard]);
2611
2608
  // ── Panel overlay (/skills, /scripts, /settings — closeable popups) ──────────
2612
2609
  // These three commands open a closeable overlay above the composer (mirrors how
2613
2610
  // /model and /sounds own this popup region) instead of dropping an inline card
@@ -4197,6 +4194,10 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
4197
4194
  setInputValue('');
4198
4195
  setSoundsPicker({ selectedIdx: -1 });
4199
4196
  }
4197
+ else if (id === 'timestamps') {
4198
+ setInputValue('');
4199
+ applyTimestampsVisible(!getChatTimestampsVisible());
4200
+ }
4200
4201
  else if (id === 'settings') {
4201
4202
  setInputValue('');
4202
4203
  // Opens the settings + command-reference card in a closeable overlay.
@@ -4241,6 +4242,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
4241
4242
  setShareModal({ role: DEFAULT_SHARE_ROLE });
4242
4243
  }
4243
4244
  }, [
4245
+ applyTimestampsVisible,
4244
4246
  clearHistory,
4245
4247
  setInputAndCursorEnd,
4246
4248
  http,
@@ -4338,6 +4340,22 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
4338
4340
  setSoundsPicker({ selectedIdx: -1 });
4339
4341
  return;
4340
4342
  }
4343
+ // Handle /timestamps [on | off] locally — a per-device display preference.
4344
+ const timestampsCommand = parseTimestampsCommand(trimmed);
4345
+ if (timestampsCommand) {
4346
+ setInputValue('');
4347
+ if (timestampsCommand.action === 'invalid') {
4348
+ addSystemCard(t('ide.chat.timestampsUsage', undefined, {
4349
+ defaultValue: 'Usage: /timestamps [on | off]',
4350
+ }), { clientOnly: true });
4351
+ }
4352
+ else {
4353
+ applyTimestampsVisible(timestampsCommand.action === 'toggle'
4354
+ ? !getChatTimestampsVisible()
4355
+ : timestampsCommand.action === 'on');
4356
+ }
4357
+ return;
4358
+ }
4341
4359
  // Handle /settings command locally
4342
4360
  if (/^\/settings$/i.test(trimmed)) {
4343
4361
  void executeCommand('settings');
@@ -4713,6 +4731,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
4713
4731
  setAttachmentError(null);
4714
4732
  sendMessage(message, chatAttachments.length > 0 ? chatAttachments : undefined);
4715
4733
  }, [
4734
+ applyTimestampsVisible,
4716
4735
  attachedFiles,
4717
4736
  http,
4718
4737
  projectId,
@@ -5371,6 +5390,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
5371
5390
  ...(card.icon ? { icon: card.icon } : {}),
5372
5391
  ...(card.content ? { content: card.content } : {}),
5373
5392
  ...(card.coversLimitType ? { coversLimitType: card.coversLimitType } : {}),
5393
+ ...(card.critical ? { critical: true } : {}),
5374
5394
  timestamp,
5375
5395
  };
5376
5396
  }
@@ -5509,8 +5529,92 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
5509
5529
  ? t('ide.chat.settings.on', undefined, { defaultValue: 'On' })
5510
5530
  : t('ide.chat.settings.off', undefined, { defaultValue: 'Off' }),
5511
5531
  sounds: t('ide.chat.settings.soundsSummary', { enabled: soundsSummary.enabled, total: soundsSummary.total }, { defaultValue: '{{enabled}} of {{total}} events enabled' }),
5532
+ timestamps: timestampsVisible
5533
+ ? t('ide.chat.settings.on', undefined, { defaultValue: 'On' })
5534
+ : t('ide.chat.settings.off', undefined, { defaultValue: 'Off' }),
5512
5535
  });
5513
5536
  };
5537
+ const renderedTimeline = timeline.length > maxVisibleItems ? timeline.slice(-maxVisibleItems) : timeline;
5538
+ /**
5539
+ * Where (if anywhere) a timeline item carries its time. Typed user messages and
5540
+ * team notes carry it in their header row and always qualify; Synthase replies
5541
+ * and event cards get a line above them, and qualify with timestamps off only
5542
+ * when critical. The command browsers (/settings, /skills, /scripts, /help) are
5543
+ * this viewer's UI, not something that happened, so they never carry one.
5544
+ *
5545
+ * @param item - The timeline item.
5546
+ * @returns Its slot, or `null` when it carries no timestamp.
5547
+ */
5548
+ const timestampSlotFor = (item) => {
5549
+ if (item.kind === 'message') {
5550
+ const { msg } = item;
5551
+ if (typeof msg.timestamp !== 'number')
5552
+ return null;
5553
+ const base = { id: msg.id, timestamp: msg.timestamp, align: 'left' };
5554
+ if ((msg.role === 'user' && !msg.automatic) || msg.teamOnly)
5555
+ return { ...base, alwaysShown: true, placement: 'header' };
5556
+ // An assistant placeholder with nothing rendered yet (still thinking).
5557
+ if (!msg.commitRecord && !msg.content && !msg.blocks?.length && !msg.toolCalls?.length)
5558
+ return null;
5559
+ // A reply that failed verification or hit a resource limit is a critical event.
5560
+ const critical = !!msg.blocks?.some((block) => {
5561
+ const b = block;
5562
+ return (b.type === 'verification' && b.status === 'error') || b.type === 'resource_limit';
5563
+ });
5564
+ return { ...base, alwaysShown: critical, placement: 'line' };
5565
+ }
5566
+ const { card } = item;
5567
+ if (typeof card.timestamp !== 'number')
5568
+ return null;
5569
+ const base = { id: card.id, timestamp: card.timestamp, placement: 'line' };
5570
+ if (item.kind === 'commit')
5571
+ return { ...base, alwaysShown: item.card.status === 'error', align: 'left' };
5572
+ if (item.kind === 'activity')
5573
+ return { ...base, alwaysShown: item.card.activity.status === 'failed', align: 'left' };
5574
+ if (item.kind === 'tip')
5575
+ return { ...base, alwaysShown: false, align: 'left' };
5576
+ const systemCard = item.card;
5577
+ if (systemCard.variant === 'settings' ||
5578
+ systemCard.variant === 'skills' ||
5579
+ systemCard.variant === 'skillsCreate' ||
5580
+ systemCard.variant === 'scripts' ||
5581
+ systemCard.variant === 'help')
5582
+ return null;
5583
+ // Centered one-line notices center their time to match.
5584
+ const centered = systemCard.variant === 'skillsLoaded' ||
5585
+ (systemCard.variant === undefined &&
5586
+ !(systemCard.tone || systemCard.emphasized || systemCard.action) &&
5587
+ !systemCard.text.includes('\n'));
5588
+ return {
5589
+ ...base,
5590
+ alwaysShown: systemCard.variant === undefined && !!systemCard.critical,
5591
+ align: centered ? 'center' : 'left',
5592
+ };
5593
+ };
5594
+ const timestampSlots = renderedTimeline.map(timestampSlotFor);
5595
+ const timestampSlotById = new Map(timestampSlots.flatMap((slot) => (slot ? [[slot.id, slot]] : [])));
5596
+ const shownTimestampIds = planChatTimestamps(timestampSlots, {
5597
+ visible: timestampsVisible,
5598
+ now: minuteNow,
5599
+ locale: resolveChatLocale(getLocale),
5600
+ });
5601
+ /**
5602
+ * Puts the item's time on its own line above a Synthase reply or an event card,
5603
+ * when the timestamp plan shows it. Header-placed times render inside MessageItem.
5604
+ *
5605
+ * @param item - The timeline item being rendered.
5606
+ * @param node - The item's rendered element tree.
5607
+ * @returns The tree, preceded by its timestamp line when one applies.
5608
+ */
5609
+ const withTimelineTimestamp = (item, node) => {
5610
+ if (node === null)
5611
+ return node;
5612
+ const id = item.kind === 'message' ? item.msg.id : item.card.id;
5613
+ const slot = timestampSlotById.get(id);
5614
+ if (!slot || slot.placement !== 'line' || !shownTimestampIds.has(id))
5615
+ return node;
5616
+ return (_jsxs(_Fragment, { children: [_jsx(ChatTimestamp, { timestamp: slot.timestamp, variant: "line", align: slot.align }), node] }));
5617
+ };
5514
5618
  // ── Render ─────────────────────────────────────────────────────────────────
5515
5619
  return (_jsxs("div", { style: {
5516
5620
  display: 'flex',
@@ -5566,7 +5670,7 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
5566
5670
  opacity: 0.7,
5567
5671
  }, children: t('ide.chat.showEarlier', undefined, {
5568
5672
  defaultValue: 'Show earlier messages',
5569
- }) }) })), (timeline.length > maxVisibleItems ? timeline.slice(-maxVisibleItems) : timeline).map((item) => (_jsx(ChatItemBoundary, { onError: onRenderError, render: () => {
5673
+ }) }) })), renderedTimeline.map((item) => (_jsx(ChatItemBoundary, { onError: onRenderError, render: () => withTimelineTimestamp(item, (() => {
5570
5674
  if (item.kind === 'commit')
5571
5675
  return (_jsx(CommitCardItem, { card: item.card, onRevert: canEdit === false ? undefined : handleRevertCommit }, item.card.id));
5572
5676
  if (item.kind === 'activity')
@@ -5680,8 +5784,8 @@ function ChatInner({ projectId, endpoint, initialMessage, onInitialMessageSent,
5680
5784
  // MessageItem builds the identity from the message's own
5681
5785
  // author (teammates included), so the host shows their
5682
5786
  // view-only profile and the viewer's editable own.
5683
- onProfileClick: onProfileClick, currentUserId: currentUserId, discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName, canEdit: canEdit }, msg.id));
5684
- } }, item.kind === 'message' ? item.msg.id : item.card.id))), error &&
5787
+ onProfileClick: onProfileClick, currentUserId: currentUserId, showTimestamp: shownTimestampIds.has(msg.id), discovery: discovery, buildUpgradeCta: buildUpgradeCta, agentName: agentName, canEdit: canEdit }, msg.id));
5788
+ })()) }, item.kind === 'message' ? item.msg.id : item.card.id))), error &&
5685
5789
  !isStaleAnonymousLimit &&
5686
5790
  (errorMeta?.limitType ? (_jsx(ResourceLimitBanner, { message: error, action: buildUpgradeCta?.({ requiresSignup: errorMeta.requiresSignup }) })) : isViewerAccessDenied ? (
5687
5791
  // A viewer's denial is expected state, not an alarm: a brief, calm,