feat(composer): context chip previews with inline comment editing

Hovering (or tapping) a context chip opens a stacked preview of its
pending items above the composer: numbered entries with a muted header
band, the captured selection, and the user's comment, which can be
edited in place (save/cancel) or removed before sending. The chips
component now subscribes to the draft store itself; the per-kind count
plumbing in ChatInput is gone.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 00:16:26 +03:00
parent c3f89e0106
commit 538b532fb1
13 changed files with 388 additions and 190 deletions
@@ -752,55 +752,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
[inlineDraftKey]
)
);
const draftSourceKey = useInlineCommentDraftStore(
React.useCallback(
(state) => {
const drafts = inlineDraftKey ? (state.drafts[inlineDraftKey] ?? []) : [];
let previewAnnotation = 0;
let review = 0;
let terminal = 0;
let prComment = 0;
let prCheck = 0;
let chatQuote = 0;
for (const draft of drafts) {
if (draft.source === 'preview-annotation') previewAnnotation += 1;
else if (draft.source === 'terminal') terminal += 1;
else if (draft.source === 'pr-comment') prComment += 1;
else if (draft.source === 'pr-check') prCheck += 1;
else if (draft.source === 'chat-quote') chatQuote += 1;
else review += 1;
}
return `${previewAnnotation}:${review}:${terminal}:${prComment}:${prCheck}:${chatQuote}`;
},
[inlineDraftKey]
)
);
const consumeDrafts = useInlineCommentDraftStore((state) => state.consumeDrafts);
const removeInlineCommentDraft = useInlineCommentDraftStore((state) => state.removeDraft);
const hasDrafts = draftCount > 0;
const [previewAnnotationCount, reviewCount, terminalContextCount, prCommentCount, prCheckCount, chatQuoteCount] = draftSourceKey.split(':').map((entry) => Number(entry) || 0);
const terminalContextDrafts = terminalContextCount > 0
? (inlineDraftKey ? useInlineCommentDraftStore.getState().drafts[inlineDraftKey] ?? [] : []).filter((draft) => draft.source === 'terminal')
: [];
const removePreviewDrafts = React.useCallback((source: 'preview-annotation' | 'pr-comment' | 'pr-check' | 'chat-quote') => {
if (!inlineDraftTarget) return;
const drafts = useInlineCommentDraftStore.getState().getDrafts(inlineDraftTarget);
for (const draft of drafts) {
if (draft.source === source) {
removeInlineCommentDraft(inlineDraftTarget, draft.id);
}
}
}, [inlineDraftTarget, removeInlineCommentDraft]);
// Review comments are the inline-comment drafts that aren't preview sources.
const removeReviewDrafts = React.useCallback(() => {
if (!inlineDraftTarget) return;
const drafts = useInlineCommentDraftStore.getState().getDrafts(inlineDraftTarget);
for (const draft of drafts) {
if (draft.source !== 'preview-annotation' && draft.source !== 'terminal' && draft.source !== 'pr-comment' && draft.source !== 'pr-check' && draft.source !== 'chat-quote') {
removeInlineCommentDraft(inlineDraftTarget, draft.id);
}
}
}, [inlineDraftTarget, removeInlineCommentDraft]);
// User message history for up/down arrow navigation.
// Keep this on a narrow hook instead of full session message records.
@@ -2661,16 +2614,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
<AutoReviewBanner />
{hasDrafts ? (
<ComposerContextChips
terminalDrafts={terminalContextDrafts}
reviewCount={reviewCount}
prCommentCount={prCommentCount}
prCheckCount={prCheckCount}
previewAnnotationCount={previewAnnotationCount}
chatQuoteCount={chatQuoteCount}
draftTarget={inlineDraftTarget}
onRemoveDraft={removeInlineCommentDraft}
onRemoveReviewDrafts={removeReviewDrafts}
onRemovePreviewDrafts={removePreviewDrafts}
colors={currentTheme.colors}
/>
) : null}
@@ -2,165 +2,375 @@
* Context chips above the composer.
*
* Each chip stands for context that will be attached to the next message but
* is not part of its text: review comments left in a diff, captured dev-server
* logs, preview annotations, terminal selections. They are shown so the user
* knows what is riding along and can drop any of it before sending.
* is not part of its text: review comments left in a diff, preview
* annotations, terminal selections, PR context, chat quotes. Hovering (or
* tapping) a chip opens a stacked preview of its items above the composer,
* where a comment the user wrote can be edited in place and any item removed
* before sending.
*/
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import type { IconName } from '@/components/icon/icons';
import { useI18n } from '@/lib/i18n';
import type { InlineCommentDraft, InlineCommentDraftTarget } from '@/stores/useInlineCommentDraftStore';
import { getRuntimeKey } from '@/lib/runtime-switch';
import {
EMPTY_INLINE_COMMENT_DRAFTS,
getInlineCommentDraftKey,
useInlineCommentDraftStore,
type InlineCommentDraft,
type InlineCommentDraftTarget,
type InlineCommentSource,
} from '@/stores/useInlineCommentDraftStore';
import type { Theme } from '@/types/theme';
export interface ComposerContextChipsProps {
/** Terminal selections, which show their own label and line range. */
terminalDrafts: readonly InlineCommentDraft[];
reviewCount: number;
prCommentCount: number;
prCheckCount: number;
previewAnnotationCount: number;
chatQuoteCount: number;
draftTarget: InlineCommentDraftTarget | null;
onRemoveDraft: (target: InlineCommentDraftTarget, draftId: string) => void;
onRemoveReviewDrafts: () => void;
onRemovePreviewDrafts: (source: 'preview-annotation' | 'pr-comment' | 'pr-check' | 'chat-quote') => void;
colors: Theme['colors'];
}
/** A chip showing how many items of one kind are attached, with a clear action. */
function CountChip(props: {
/** Chip groups: every terminal selection is its own chip; the rest group by kind. */
type ChipGroup = {
key: string;
icon: IconName;
iconClassName?: string;
label: string;
count: number;
removeLabel: string;
drafts: InlineCommentDraft[];
};
const REVIEW_SOURCES: readonly InlineCommentSource[] = ['diff', 'file', 'plan'];
/** Sources whose drafts carry a user-written comment that can be edited. */
const editableSource = (source: InlineCommentSource): boolean => source !== 'terminal';
/** Captured code/output kinds read better monospaced; quoted prose does not. */
const monoSource = (source: InlineCommentSource): boolean =>
source !== 'chat-quote' && source !== 'preview-annotation';
const basename = (path: string): string => {
const segments = path.split('/').filter(Boolean);
return segments[segments.length - 1] ?? path;
};
const ENTRY_ACTION_CLASS = 'inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[var(--surface-mutedForeground)] hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)]';
const ENTRY_LABEL_CLASS = 'text-[10px] font-medium uppercase tracking-wide text-[var(--surface-mutedForeground)] opacity-60';
const DraftPreviewEntry: React.FC<{
draft: InlineCommentDraft;
index: number;
title: string;
editing: boolean;
onStartEdit: () => void;
onEndEdit: () => void;
onRemove: () => void;
colors: Theme['colors'];
icon?: React.ReactNode;
}) {
return (
<div
className="inline-flex items-center gap-1.5 rounded-xl border px-2.5 py-1"
style={{
backgroundColor: props.colors?.surface?.elevated,
borderColor: props.colors?.interactive?.border,
}}
>
{props.icon}
<span className="text-xs font-medium text-muted-foreground">{props.label}</span>
<span className="text-xs font-semibold" style={{ color: props.colors?.status?.info }}>
{props.count}
</span>
<button
type="button"
className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-full text-muted-foreground hover:bg-interactive-hover hover:text-foreground"
style={{ minHeight: 0, minWidth: 0 }}
onClick={props.onRemove}
aria-label={props.removeLabel}
title={props.removeLabel}
>
<Icon name="close" className="h-3 w-3" />
</button>
</div>
);
}
export function ComposerContextChips(props: ComposerContextChipsProps) {
onSaveComment: ((text: string) => void) | null;
}> = ({ draft, index, title, editing, onStartEdit, onEndEdit, onRemove, onSaveComment }) => {
const { t } = useI18n();
const {
terminalDrafts,
reviewCount,
prCommentCount,
prCheckCount,
previewAnnotationCount,
chatQuoteCount,
draftTarget,
onRemoveDraft,
onRemoveReviewDrafts,
onRemovePreviewDrafts,
colors,
} = props;
const [editText, setEditText] = React.useState(draft.text);
const editRef = React.useRef<HTMLTextAreaElement>(null);
React.useEffect(() => {
if (!editing) return;
setEditText(draft.text);
queueMicrotask(() => {
const element = editRef.current;
if (element) {
element.focus();
element.setSelectionRange(element.value.length, element.value.length);
}
});
// The draft text at edit start is the baseline; later store updates are
// our own saves.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editing]);
const commitEdit = () => {
if (onSaveComment && editText !== draft.text) {
onSaveComment(editText);
}
onEndEdit();
};
const cancelEdit = () => {
setEditText(draft.text);
onEndEdit();
};
// Keep focus in the textarea while a header button is pressed: without
// this the textarea's blur commits first, the header re-renders under the
// pointer, and the click lands on the button that replaced the pressed one
// (save punches through to edit, cancel to remove).
const keepEditorFocus = (event: React.PointerEvent) => {
if (editing) event.preventDefault();
};
return (
<div className="flex flex-wrap items-center gap-2 pb-2">
{terminalDrafts.map((draft) => (
<div
key={draft.id}
className="inline-flex max-w-full items-center gap-1.5 rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-2.5 py-1"
title={draft.code}
>
<Icon name="terminal" className="h-3.5 w-3.5" />
<span className="truncate text-xs font-medium text-[var(--surface-mutedForeground)]">
{t('chat.chatInput.terminalContext', {
terminal: draft.fileLabel,
start: draft.startLine,
end: draft.endLine,
})}
</span>
<div>
<div className="flex items-center gap-1.5 px-3 py-1.5"
style={{ backgroundColor: 'color-mix(in srgb, var(--surface-mutedForeground) 8%, transparent)' }}>
<span className="text-xs font-medium text-[var(--surface-mutedForeground)]">{index + 1}.</span>
<span className="min-w-0 flex-1 truncate text-xs font-medium text-[var(--surface-foreground)]" title={title}>
{title}
</span>
{onSaveComment ? (
<button
type="button"
className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-full text-[var(--surface-mutedForeground)] hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)]"
onClick={() => draftTarget && onRemoveDraft(draftTarget, draft.id)}
aria-label={t('chat.chatInput.terminalContextRemove')}
title={t('chat.chatInput.terminalContextRemove')}
className={ENTRY_ACTION_CLASS}
style={{ minHeight: 0, minWidth: 0 }}
onPointerDown={keepEditorFocus}
onClick={editing ? commitEdit : onStartEdit}
aria-label={t('chat.chatInput.contextPreview.edit')}
title={t('chat.chatInput.contextPreview.edit')}
>
<Icon name="close" className="h-3 w-3" />
<Icon name={editing ? 'check' : 'pencil'} className="h-3 w-3" />
</button>
) : null}
<button
type="button"
className={ENTRY_ACTION_CLASS}
style={{ minHeight: 0, minWidth: 0 }}
onPointerDown={keepEditorFocus}
onClick={editing ? cancelEdit : onRemove}
aria-label={t('chat.chatInput.contextPreview.remove')}
title={t('chat.chatInput.contextPreview.remove')}
>
<Icon name="close" className="h-3 w-3" />
</button>
</div>
<div className="space-y-2 px-3 py-2">
{draft.code.trim() ? (
<div>
<div className={ENTRY_LABEL_CLASS}>{t('chat.chatInput.contextPreview.selectedLabel')}</div>
<div
className={
monoSource(draft.source)
? 'mt-0.5 whitespace-pre-wrap break-words font-mono text-xs text-[var(--surface-foreground)]'
: 'mt-0.5 whitespace-pre-wrap break-words text-sm text-[var(--surface-foreground)]'
}
>
{draft.code}
</div>
</div>
) : null}
{onSaveComment && (editing || draft.text.trim()) ? (
<div>
<div className={ENTRY_LABEL_CLASS}>{t('chat.chatInput.contextPreview.commentLabel')}</div>
{editing ? (
<textarea
ref={editRef}
rows={2}
value={editText}
onChange={(event) => setEditText(event.target.value)}
onBlur={commitEdit}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
commitEdit();
} else if (event.key === 'Escape') {
event.preventDefault();
setEditText(draft.text);
onEndEdit();
}
}}
placeholder={t('chat.textSelection.comment.placeholder')}
className="mt-0.5 w-full resize-none rounded-md border border-[var(--interactive-border)] bg-[var(--surface-background)] px-2 py-1 text-sm text-[var(--surface-foreground)] outline-none placeholder:text-[var(--surface-mutedForeground)]"
style={{ minHeight: 0 }}
/>
) : (
<div className="mt-0.5 whitespace-pre-wrap break-words text-sm text-[var(--surface-foreground)]">{draft.text}</div>
)}
</div>
) : null}
</div>
</div>
);
};
export function ComposerContextChips({ draftTarget, colors }: ComposerContextChipsProps) {
const { t } = useI18n();
const draftKey = draftTarget
? getInlineCommentDraftKey(getRuntimeKey(), draftTarget.directory, draftTarget.sessionKey)
: null;
const drafts = useInlineCommentDraftStore(
React.useCallback(
(state) => (draftKey ? state.drafts[draftKey] ?? EMPTY_INLINE_COMMENT_DRAFTS : EMPTY_INLINE_COMMENT_DRAFTS),
[draftKey],
),
);
const removeDraft = useInlineCommentDraftStore((state) => state.removeDraft);
const updateDraft = useInlineCommentDraftStore((state) => state.updateDraft);
const [openGroupKey, setOpenGroupKey] = React.useState<string | null>(null);
const [editingDraftId, setEditingDraftId] = React.useState<string | null>(null);
const editingRef = React.useRef<string | null>(null);
editingRef.current = editingDraftId;
const containerRef = React.useRef<HTMLDivElement>(null);
const closeTimerRef = React.useRef<number | null>(null);
const cancelClose = React.useCallback(() => {
if (closeTimerRef.current !== null) {
window.clearTimeout(closeTimerRef.current);
closeTimerRef.current = null;
}
}, []);
// Hover-away close. Suspended while a comment is being edited: entering or
// leaving edit mode reflows the panel under the pointer, and a synthetic
// mouseleave from that reflow must not tear the editor down.
const scheduleClose = React.useCallback(() => {
if (editingRef.current) return;
cancelClose();
closeTimerRef.current = window.setTimeout(() => {
closeTimerRef.current = null;
setOpenGroupKey(null);
}, 150);
}, [cancelClose]);
React.useEffect(() => cancelClose, [cancelClose]);
// Clicking outside the chips + panel closes the preview even when a reflow
// swallowed the mouseleave (e.g. right after finishing an edit).
React.useEffect(() => {
if (!openGroupKey) return;
const handlePointerDown = (event: PointerEvent) => {
// SAFETY: a pointer event target inside the document is always a
// Node; `contains` only needs that.
if (containerRef.current?.contains(event.target as Node)) return;
setOpenGroupKey(null);
setEditingDraftId(null);
};
document.addEventListener('pointerdown', handlePointerDown);
return () => document.removeEventListener('pointerdown', handlePointerDown);
}, [openGroupKey]);
const titleFor = React.useCallback((draft: InlineCommentDraft): string => {
switch (draft.source) {
case 'terminal':
return t('chat.chatInput.terminalContext', {
terminal: draft.fileLabel,
start: draft.startLine,
end: draft.endLine,
});
case 'preview-annotation':
return t('chat.message.context.browserAnnotation', { page: draft.fileLabel });
case 'pr-comment':
return t('chat.message.context.prComment', { label: draft.fileLabel });
case 'pr-check':
return t('chat.message.context.prCheck', { label: draft.fileLabel });
case 'chat-quote':
return t('chat.message.context.chatQuote');
default:
return draft.startLine === draft.endLine
? t('chat.message.context.codeCommentLine', { file: basename(draft.fileLabel), line: draft.startLine })
: t('chat.message.context.codeComment', { file: basename(draft.fileLabel), start: draft.startLine, end: draft.endLine });
}
}, [t]);
const groups = React.useMemo<ChipGroup[]>(() => {
const result: ChipGroup[] = [];
const byKind = (
key: string,
icon: IconName,
label: string,
match: (draft: InlineCommentDraft) => boolean,
iconClassName?: string,
) => {
const matched = drafts.filter(match);
if (matched.length > 0) {
result.push({ key, icon, iconClassName, label, count: matched.length, drafts: matched });
}
};
for (const draft of drafts) {
if (draft.source !== 'terminal') continue;
result.push({
key: `terminal-${draft.id}`,
icon: 'terminal',
label: t('chat.chatInput.terminalContext', {
terminal: draft.fileLabel,
start: draft.startLine,
end: draft.endLine,
}),
count: 0,
drafts: [draft],
});
}
byKind('review', 'chat-1', t('chat.chatInput.reviewComments'), (draft) => REVIEW_SOURCES.includes(draft.source));
byKind('pr-comment', 'git-pull-request', t('chat.chatInput.prCommentContext'), (draft) => draft.source === 'pr-comment');
byKind('pr-check', 'close-circle', t('chat.chatInput.prCheckContext'), (draft) => draft.source === 'pr-check', 'text-[var(--status-error)]');
byKind('chat-quote', 'chat-1', t('chat.chatInput.chatQuoteContext'), (draft) => draft.source === 'chat-quote');
byKind('annotation', 'global', t('chat.chatInput.previewAnnotations'), (draft) => draft.source === 'preview-annotation');
return result;
}, [drafts, t]);
React.useEffect(() => {
if (openGroupKey && !groups.some((group) => group.key === openGroupKey)) {
setOpenGroupKey(null);
setEditingDraftId(null);
}
}, [groups, openGroupKey]);
if (!draftTarget || drafts.length === 0) return null;
const openGroup = openGroupKey ? groups.find((group) => group.key === openGroupKey) ?? null : null;
return (
<div className="relative" ref={containerRef}>
{openGroup ? (
<div
className="absolute bottom-full left-0 z-30 mb-1.5 w-full max-w-[480px] overflow-hidden rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)] shadow-[0_4px_16px_-4px_rgb(0_0_0_/_0.12)]"
onMouseEnter={cancelClose}
onMouseLeave={scheduleClose}
>
<div className="max-h-[min(50vh,420px)] divide-y divide-[var(--interactive-border)] overflow-y-auto">
{openGroup.drafts.map((draft, index) => (
<DraftPreviewEntry
key={draft.id}
draft={draft}
index={index}
title={titleFor(draft)}
editing={editingDraftId === draft.id}
onStartEdit={() => setEditingDraftId(draft.id)}
onEndEdit={() => setEditingDraftId((current) => (current === draft.id ? null : current))}
onRemove={() => removeDraft(draftTarget, draft.id)}
onSaveComment={editableSource(draft.source)
? (text) => updateDraft(draftTarget, draft.id, { text })
: null}
/>
))}
</div>
</div>
))}
{reviewCount > 0 ? (
<CountChip
label={t('chat.chatInput.reviewComments')}
count={reviewCount}
removeLabel={t('chat.chatInput.reviewCommentsRemove')}
onRemove={onRemoveReviewDrafts}
colors={colors}
/>
) : null}
{prCommentCount > 0 ? (
<CountChip
label={t('chat.chatInput.prCommentContext')}
count={prCommentCount}
removeLabel={t('chat.chatInput.prCommentContextRemove')}
onRemove={() => onRemovePreviewDrafts('pr-comment')}
colors={colors}
icon={<Icon name="git-pull-request" className="h-3.5 w-3.5 text-muted-foreground" />}
/>
) : null}
{prCheckCount > 0 ? (
<CountChip
label={t('chat.chatInput.prCheckContext')}
count={prCheckCount}
removeLabel={t('chat.chatInput.prCheckContextRemove')}
onRemove={() => onRemovePreviewDrafts('pr-check')}
colors={colors}
icon={<Icon name="close-circle" className="h-3.5 w-3.5 text-[var(--status-error)]" />}
/>
) : null}
{chatQuoteCount > 0 ? (
<CountChip
label={t('chat.chatInput.chatQuoteContext')}
count={chatQuoteCount}
removeLabel={t('chat.chatInput.chatQuoteContextRemove')}
onRemove={() => onRemovePreviewDrafts('chat-quote')}
colors={colors}
icon={<Icon name="chat-1" className="h-3.5 w-3.5 text-muted-foreground" />}
/>
) : null}
{previewAnnotationCount > 0 ? (
<CountChip
label={t('chat.chatInput.previewAnnotations')}
count={previewAnnotationCount}
removeLabel={t('chat.chatInput.previewContextRemove')}
onRemove={() => onRemovePreviewDrafts('preview-annotation')}
colors={colors}
/>
) : null}
<div className="flex flex-wrap items-center gap-2 pb-2">
{groups.map((group) => (
<button
key={group.key}
type="button"
className="inline-flex max-w-full items-center gap-1.5 rounded-xl border px-2.5 py-1 text-left"
style={{
backgroundColor: colors?.surface?.elevated,
borderColor: colors?.interactive?.border,
}}
onMouseEnter={() => {
cancelClose();
setOpenGroupKey(group.key);
}}
onMouseLeave={scheduleClose}
onClick={() => {
if (editingRef.current) return;
setOpenGroupKey((current) => (current === group.key ? null : group.key));
}}
aria-expanded={openGroupKey === group.key}
>
<Icon name={group.icon} className={`h-3.5 w-3.5 shrink-0 text-muted-foreground ${group.iconClassName ?? ''}`} />
<span className="truncate text-xs font-medium text-muted-foreground">{group.label}</span>
{group.count > 0 ? (
<span className="text-xs font-semibold" style={{ color: colors?.status?.info }}>
{group.count}
</span>
) : null}
</button>
))}
</div>
</div>
);
}
+4
View File
@@ -2909,6 +2909,10 @@ export const dict = {
'chat.message.context.chatQuote': 'Zitat aus einer früheren Nachricht',
'chat.chatInput.chatQuoteContext': 'Chat-Zitate',
'chat.chatInput.chatQuoteContextRemove': 'Chat-Zitate entfernen',
'chat.chatInput.contextPreview.selectedLabel': 'Ausgewählter Text',
'chat.chatInput.contextPreview.commentLabel': 'Nutzerkommentar',
'chat.chatInput.contextPreview.edit': 'Kommentar bearbeiten',
'chat.chatInput.contextPreview.remove': 'Entfernen',
'chat.message.context.browserAnnotation': 'Browser-Anmerkung ({page})',
'chat.message.context.prComment': 'GitHub-PR-Kommentar ({label})',
'chat.message.context.prCheck': 'Fehlgeschlagener GitHub-PR-Check ({label})',
+4
View File
@@ -10,6 +10,10 @@ export const dict = {
'chat.message.context.chatQuote': 'Quoted from an earlier message',
'chat.chatInput.chatQuoteContext': 'Chat quotes',
'chat.chatInput.chatQuoteContextRemove': 'Remove chat quotes',
'chat.chatInput.contextPreview.selectedLabel': 'Selected text',
'chat.chatInput.contextPreview.commentLabel': 'User comment',
'chat.chatInput.contextPreview.edit': 'Edit comment',
'chat.chatInput.contextPreview.remove': 'Remove',
'chat.message.context.browserAnnotation': 'Browser annotation ({page})',
'chat.message.context.prComment': 'GitHub PR comment ({label})',
'chat.message.context.prCheck': 'Failed GitHub PR check ({label})',
+4
View File
@@ -11,6 +11,10 @@ export const dict: Record<I18nKey, string> = {
'chat.message.context.chatQuote': 'Cita de un mensaje anterior',
'chat.chatInput.chatQuoteContext': 'Citas del chat',
'chat.chatInput.chatQuoteContextRemove': 'Quitar citas del chat',
'chat.chatInput.contextPreview.selectedLabel': 'Texto seleccionado',
'chat.chatInput.contextPreview.commentLabel': 'Comentario del usuario',
'chat.chatInput.contextPreview.edit': 'Editar comentario',
'chat.chatInput.contextPreview.remove': 'Quitar',
'chat.message.context.browserAnnotation': 'Anotación del navegador ({page})',
'chat.message.context.prComment': 'Comentario de PR de GitHub ({label})',
'chat.message.context.prCheck': 'Verificación de PR de GitHub fallida ({label})',
+4
View File
@@ -10,6 +10,10 @@ export const dict = {
'chat.message.context.chatQuote': 'Citation dun message précédent',
'chat.chatInput.chatQuoteContext': 'Citations du chat',
'chat.chatInput.chatQuoteContextRemove': 'Supprimer les citations du chat',
'chat.chatInput.contextPreview.selectedLabel': 'Texte sélectionné',
'chat.chatInput.contextPreview.commentLabel': 'Commentaire de lutilisateur',
'chat.chatInput.contextPreview.edit': 'Modifier le commentaire',
'chat.chatInput.contextPreview.remove': 'Supprimer',
'chat.message.context.browserAnnotation': 'Annotation du navigateur ({page})',
'chat.message.context.prComment': 'Commentaire de PR GitHub ({label})',
'chat.message.context.prCheck': 'Vérification de PR GitHub échouée ({label})',
+4
View File
@@ -11,6 +11,10 @@ export const dict: Record<I18nKey, string> = {
'chat.message.context.chatQuote': '以前のメッセージからの引用',
'chat.chatInput.chatQuoteContext': 'チャット引用',
'chat.chatInput.chatQuoteContextRemove': 'チャット引用を削除',
'chat.chatInput.contextPreview.selectedLabel': '選択したテキスト',
'chat.chatInput.contextPreview.commentLabel': 'ユーザーのコメント',
'chat.chatInput.contextPreview.edit': 'コメントを編集',
'chat.chatInput.contextPreview.remove': '削除',
'chat.message.context.browserAnnotation': 'ブラウザ注釈({page}',
'chat.message.context.prComment': 'GitHub PR コメント({label}',
'chat.message.context.prCheck': '失敗した GitHub PR チェック({label}',
+4
View File
@@ -11,6 +11,10 @@ export const dict: Record<I18nKey, string> = {
'chat.message.context.chatQuote': '이전 메시지에서 인용',
'chat.chatInput.chatQuoteContext': '채팅 인용',
'chat.chatInput.chatQuoteContextRemove': '채팅 인용 제거',
'chat.chatInput.contextPreview.selectedLabel': '선택한 텍스트',
'chat.chatInput.contextPreview.commentLabel': '사용자 댓글',
'chat.chatInput.contextPreview.edit': '댓글 편집',
'chat.chatInput.contextPreview.remove': '제거',
'chat.message.context.browserAnnotation': '브라우저 주석 ({page})',
'chat.message.context.prComment': 'GitHub PR 댓글 ({label})',
'chat.message.context.prCheck': '실패한 GitHub PR 검사 ({label})',
+4
View File
@@ -11,6 +11,10 @@ export const dict: Record<I18nKey, string> = {
'chat.message.context.chatQuote': 'Cytat z wcześniejszej wiadomości',
'chat.chatInput.chatQuoteContext': 'Cytaty z czatu',
'chat.chatInput.chatQuoteContextRemove': 'Usuń cytaty z czatu',
'chat.chatInput.contextPreview.selectedLabel': 'Zaznaczony tekst',
'chat.chatInput.contextPreview.commentLabel': 'Komentarz użytkownika',
'chat.chatInput.contextPreview.edit': 'Edytuj komentarz',
'chat.chatInput.contextPreview.remove': 'Usuń',
'chat.message.context.browserAnnotation': 'Adnotacja przeglądarki ({page})',
'chat.message.context.prComment': 'Komentarz PR GitHub ({label})',
'chat.message.context.prCheck': 'Nieudane sprawdzenie PR GitHub ({label})',
@@ -11,6 +11,10 @@ export const dict: Record<I18nKey, string> = {
'chat.message.context.chatQuote': 'Citação de uma mensagem anterior',
'chat.chatInput.chatQuoteContext': 'Citações do chat',
'chat.chatInput.chatQuoteContextRemove': 'Remover citações do chat',
'chat.chatInput.contextPreview.selectedLabel': 'Texto selecionado',
'chat.chatInput.contextPreview.commentLabel': 'Comentário do usuário',
'chat.chatInput.contextPreview.edit': 'Editar comentário',
'chat.chatInput.contextPreview.remove': 'Remover',
'chat.message.context.browserAnnotation': 'Anotação do navegador ({page})',
'chat.message.context.prComment': 'Comentário de PR do GitHub ({label})',
'chat.message.context.prCheck': 'Verificação de PR do GitHub com falha ({label})',
+4
View File
@@ -11,6 +11,10 @@ export const dict: Record<I18nKey, string> = {
'chat.message.context.chatQuote': 'Цитата з попереднього повідомлення',
'chat.chatInput.chatQuoteContext': 'Цитати з чату',
'chat.chatInput.chatQuoteContextRemove': 'Прибрати цитати з чату',
'chat.chatInput.contextPreview.selectedLabel': 'Виділений текст',
'chat.chatInput.contextPreview.commentLabel': 'Коментар користувача',
'chat.chatInput.contextPreview.edit': 'Редагувати коментар',
'chat.chatInput.contextPreview.remove': 'Прибрати',
'chat.message.context.browserAnnotation': 'Анотація браузера ({page})',
'chat.message.context.prComment': 'Коментар PR GitHub ({label})',
'chat.message.context.prCheck': 'Невдала перевірка PR GitHub ({label})',
@@ -11,6 +11,10 @@ export const dict: Record<I18nKey, string> = {
'chat.message.context.chatQuote': '引用自先前的消息',
'chat.chatInput.chatQuoteContext': '聊天引用',
'chat.chatInput.chatQuoteContextRemove': '移除聊天引用',
'chat.chatInput.contextPreview.selectedLabel': '所选文本',
'chat.chatInput.contextPreview.commentLabel': '用户评论',
'chat.chatInput.contextPreview.edit': '编辑评论',
'chat.chatInput.contextPreview.remove': '移除',
'chat.message.context.browserAnnotation': '浏览器标注({page}',
'chat.message.context.prComment': 'GitHub PR 评论({label}',
'chat.message.context.prCheck': '失败的 GitHub PR 检查({label}',
@@ -11,6 +11,10 @@ export const dict: Record<I18nKey, string> = {
'chat.message.context.chatQuote': '引用自先前的訊息',
'chat.chatInput.chatQuoteContext': '聊天引用',
'chat.chatInput.chatQuoteContextRemove': '移除聊天引用',
'chat.chatInput.contextPreview.selectedLabel': '所選文字',
'chat.chatInput.contextPreview.commentLabel': '使用者留言',
'chat.chatInput.contextPreview.edit': '編輯留言',
'chat.chatInput.contextPreview.remove': '移除',
'chat.message.context.browserAnnotation': '瀏覽器標註({page}',
'chat.message.context.prComment': 'GitHub PR 留言({label}',
'chat.message.context.prCheck': '失敗的 GitHub PR 檢查({label}',