@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.
- package/README.md +237 -1
- package/dist/command-metadata.d.ts.map +1 -1
- package/dist/command-metadata.js +9 -0
- package/dist/command-metadata.js.map +1 -1
- package/dist/components/ChatPanel.d.ts +5 -0
- package/dist/components/ChatPanel.d.ts.map +1 -1
- package/dist/components/ChatPanel.js +128 -24
- package/dist/components/ChatPanel.js.map +1 -1
- package/dist/components/ChatTimestamp.d.ts +33 -0
- package/dist/components/ChatTimestamp.d.ts.map +1 -0
- package/dist/components/ChatTimestamp.js +30 -0
- package/dist/components/ChatTimestamp.js.map +1 -0
- package/dist/components/PreviewPanel.d.ts.map +1 -1
- package/dist/components/PreviewPanel.js +21 -2
- package/dist/components/PreviewPanel.js.map +1 -1
- package/dist/components/chat-timestamps-utilities.d.ts +119 -0
- package/dist/components/chat-timestamps-utilities.d.ts.map +1 -0
- package/dist/components/chat-timestamps-utilities.js +173 -0
- package/dist/components/chat-timestamps-utilities.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/customEventCards.d.ts +7 -0
- package/dist/customEventCards.d.ts.map +1 -1
- package/dist/customEventCards.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useChatTimestampsVisible.d.ts +20 -0
- package/dist/hooks/useChatTimestampsVisible.d.ts.map +1 -0
- package/dist/hooks/useChatTimestampsVisible.js +74 -0
- package/dist/hooks/useChatTimestampsVisible.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/settings-metadata.d.ts +1 -1
- package/dist/settings-metadata.d.ts.map +1 -1
- package/dist/settings-metadata.js +6 -0
- package/dist/settings-metadata.js.map +1 -1
- 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(
|
|
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
|
-
}) }) })),
|
|
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,
|