chat panel reaches zero convertible (203 -> 181)
The remaining interpolated strings and the shared Cancel/Save/Delete labels. What is left in Chat is three thrown Errors, which the never-translate category already excludes. useAudioRecording used `t` as its media-track loop variable in four places, which the translation function then shadowed. Fourth panel with that collision.
This commit is contained in:
@@ -16,7 +16,7 @@ type AgentIdentityBarProps = { panel: UseAgentPanelType };
|
||||
* the two apart is to notice that the conversation did not reset.
|
||||
*/
|
||||
export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, tUi } = useTranslation();
|
||||
const [naming, setNaming] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -58,7 +58,7 @@ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => {
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => setNaming(false)}
|
||||
aria-label="Cancel"
|
||||
aria-label={tUi('button.cancel')}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -103,7 +103,7 @@ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => {
|
||||
<span className="font-medium">{panel.agent.name}</span>
|
||||
<span
|
||||
className="text-muted-foreground flex-1 truncate"
|
||||
title={peers.length ? `Peers: ${peers.map((p) => p.name).join(', ')}` : undefined}
|
||||
title={peers.length ? t('chat.agent.peers', { names: peers.map((p) => p.name).join(', ') }) : undefined}
|
||||
>
|
||||
{peers.length === 0
|
||||
? 'no peers yet'
|
||||
|
||||
@@ -34,7 +34,7 @@ export type ChatSessionSelection = {
|
||||
const title = (s: { title: string }) => s.title || '(untitled)';
|
||||
|
||||
export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, tUi } = useTranslation();
|
||||
const { workspace, cwd } = useWorkspace();
|
||||
|
||||
/**
|
||||
@@ -109,10 +109,15 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
||||
}}
|
||||
className="min-w-0 flex-1 rounded border border-duck-dark/20 bg-transparent px-1.5 py-0.5 text-xs"
|
||||
/>
|
||||
<button type="submit" className="p-0.5 cursor-pointer" title="Save">
|
||||
<button type="submit" className="p-0.5 cursor-pointer" title={t('common.save')}>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button type="button" onClick={() => setRenaming(null)} className="p-0.5 cursor-pointer" title="Cancel">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRenaming(null)}
|
||||
className="p-0.5 cursor-pointer"
|
||||
title={tUi('button.cancel')}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</form>
|
||||
@@ -138,11 +143,11 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
||||
<Pencil className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
title="Delete"
|
||||
title={t('common.delete')}
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (!confirm(`Delete "${title(s)}"?`)) return;
|
||||
if (!confirm(t('chat.deleteQ', { title: title(s) }))) return;
|
||||
// Deleting the one on screen leaves this panel pointing at nothing, so it starts a
|
||||
// new conversation rather than showing a transcript that no longer exists.
|
||||
if (s.id === sessionId) setSessionId(null);
|
||||
|
||||
@@ -68,7 +68,12 @@ export function AttachButton({
|
||||
const fence = text.includes('```') ? '````' : '```';
|
||||
onAttachText(`${fence} ${file.name}\n${text.replace(/\s+$/, '')}\n${fence}`);
|
||||
} catch (err) {
|
||||
toast.error(`Could not read ${file.name}: ${err instanceof Error ? err.message : 'unknown error'}`);
|
||||
toast.error(
|
||||
t('chat.attach.readFailed', {
|
||||
name: file.name,
|
||||
reason: err instanceof Error ? err.message : t('chat.attach.unknownError'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export const BackgroundJobDialog = ({
|
||||
isCommander = false,
|
||||
recordPaths,
|
||||
}: BackgroundJobDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, tUi } = useTranslation();
|
||||
const [mode, setMode] = useState<'command' | 'prompt'>('command');
|
||||
const [body, setBody] = useState('');
|
||||
const [watching, setWatching] = useState(false);
|
||||
@@ -220,7 +220,7 @@ export const BackgroundJobDialog = ({
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
{tUi('button.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={!canSubmit}>
|
||||
{t('chat.job.start')}
|
||||
|
||||
@@ -316,7 +316,7 @@ const TaskChip = ({ task, active, pinned, onClick, onTogglePin, onDismiss, onSto
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
title={t('chat.tasks.stopJob')}
|
||||
aria-label={`Stop ${label}`}
|
||||
aria-label={t('chat.tasks.stopNamed', { label })}
|
||||
className="cursor-pointer py-1 pl-0.5 pr-2 text-muted-foreground/50 transition-colors hover:text-destructive"
|
||||
>
|
||||
<Square className="h-3 w-3" />
|
||||
@@ -327,7 +327,7 @@ const TaskChip = ({ task, active, pinned, onClick, onTogglePin, onDismiss, onSto
|
||||
type="button"
|
||||
onClick={onDismiss}
|
||||
title={t('chat.tasks.dismiss')}
|
||||
aria-label={`Dismiss ${label}`}
|
||||
aria-label={t('chat.tasks.dismissNamed', { label })}
|
||||
className="cursor-pointer rounded-full py-1 pl-0.5 pr-2 text-muted-foreground/50 transition-colors hover:text-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
|
||||
@@ -25,7 +25,7 @@ type Started = { id: string; url: string; expiresInMs: number };
|
||||
type Props = { open: boolean; onOpenChange: (open: boolean) => void };
|
||||
|
||||
export const ClaudeLoginDialog = ({ open, onOpenChange }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, tUi } = useTranslation();
|
||||
const client = useClient();
|
||||
const [started, setStarted] = useState<Started | null>(null);
|
||||
const [code, setCode] = useState('');
|
||||
@@ -162,7 +162,7 @@ export const ClaudeLoginDialog = ({ open, onOpenChange }: Props) => {
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Cancel
|
||||
{tUi('button.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -179,7 +179,7 @@ export const CodeSurface = ({ code, lang = '', startLine = 1, maxLines, numbered
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="mt-1 cursor-pointer text-xs text-duck-teal hover:underline"
|
||||
>
|
||||
{expanded ? 'Show less' : `Show more (${hidden} more lines)`}
|
||||
{expanded ? 'Show less' : t('chat.message.showMore', { count: hidden })}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@ const CollapsibleBlock = ({ label, content }: { label: string; content: string }
|
||||
type TtsState = 'idle' | 'loading' | 'playing';
|
||||
|
||||
const ReadAloudButton = ({ id, text }: { id: string; text: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, tUi } = useTranslation();
|
||||
const [state, setState] = useState<TtsState>('idle');
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const api = useFilesAPI('user-data');
|
||||
@@ -122,7 +122,7 @@ const ReadAloudButton = ({ id, text }: { id: string; text: string }) => {
|
||||
// the icon comes back, the button looks broken rather than failed. Most often this is the TTS
|
||||
// service being down, which is worth saying out loud.
|
||||
setState('idle');
|
||||
toast.error(`Could not read this message aloud: ${errorText(err)}`);
|
||||
toast.error(t('chat.message.readAloudFailed', { reason: errorText(err) }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -343,6 +343,7 @@ export const MessageBubble = ({ message, onAnswer, onRetry, onDecidePlan, defaul
|
||||
* both sides discarded the taskId that relates them.
|
||||
*/
|
||||
const TaskActivity = ({ message }: { message: Extract<ChatMessage, { role: 'task' }> }) => {
|
||||
const { t, tUi } = useTranslation();
|
||||
const { status, summary, description, taskType } = message;
|
||||
const Icon = status === 'completed' ? Check : status === 'failed' ? X : status === 'stopped' ? CircleSlash : Clock;
|
||||
const tone: Tone =
|
||||
@@ -357,7 +358,7 @@ const TaskActivity = ({ message }: { message: Extract<ChatMessage, { role: 'task
|
||||
{taskType && <span className="ml-2 text-xs text-muted-foreground">{taskType}</span>}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{status ? summary || `Task ${status}.` : 'Running in the background…'}
|
||||
{status ? summary || t('chat.tasks.status', { status }) : 'Running in the background…'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,6 +385,7 @@ const formatTokens = (n: number): string => (n >= 1000 ? `${Math.round(n / 1000)
|
||||
* the sidecar measured — which is the one a replay in another tab gets.
|
||||
*/
|
||||
const CompactActivity = ({ message }: { message: Extract<ChatMessage, { role: 'compact' }> }) => {
|
||||
const { t, tUi } = useTranslation();
|
||||
const { done, trigger, preTokens, durationMs } = message;
|
||||
const startedAt = useRef(Date.now());
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
@@ -405,7 +407,11 @@ const CompactActivity = ({ message }: { message: Extract<ChatMessage, { role: 'c
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] font-medium tracking-wide uppercase">
|
||||
<FoldVertical className={`h-3 w-3 ${done ? '' : 'animate-pulse'}`} />
|
||||
{done ? `Context compacted${trigger === 'manual' ? '' : ' automatically'}` : 'Compacting the conversation…'}
|
||||
{done
|
||||
? trigger === 'manual'
|
||||
? t('chat.message.compacted')
|
||||
: t('chat.message.compactedAuto')
|
||||
: 'Compacting the conversation…'}
|
||||
{detail && <span className="normal-case opacity-70 tabular-nums">· {detail}</span>}
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
@@ -502,7 +508,7 @@ const PlanCard = ({
|
||||
message: Extract<ChatMessage, { role: 'plan' }>;
|
||||
onDecide?: (requestId: string, approve: boolean, feedback?: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, tUi } = useTranslation();
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [rejecting, setRejecting] = useState(false);
|
||||
const decided = message.approved !== undefined;
|
||||
@@ -554,7 +560,7 @@ const PlanCard = ({
|
||||
onClick={() => setRejecting(false)}
|
||||
className="cursor-pointer rounded px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Cancel
|
||||
{tUi('button.cancel')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -308,6 +308,7 @@ export const ToolActivity = ({ message, defaultOpen }: ToolActivityProps) => {
|
||||
const DIFF_MAX_LINES = 40;
|
||||
|
||||
const DiffView = ({ lines }: { lines: DiffLine[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hidden = lines.length - DIFF_MAX_LINES;
|
||||
const shown = expanded ? lines : lines.slice(0, DIFF_MAX_LINES);
|
||||
@@ -353,7 +354,7 @@ const DiffView = ({ lines }: { lines: DiffLine[] }) => {
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="mt-1 cursor-pointer text-xs text-duck-teal hover:underline"
|
||||
>
|
||||
{expanded ? 'Show less' : `Show more (${hidden} more lines)`}
|
||||
{expanded ? 'Show less' : t('chat.message.showMore', { count: hidden })}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -118,7 +118,12 @@ const Models = ({ account }: { account: UsageAccount }) => {
|
||||
<div
|
||||
key={model.name}
|
||||
className="relative flex items-center justify-between gap-2 overflow-hidden rounded-md px-2 py-1.5"
|
||||
title={`In ${formatTokens(model.input)} · out ${formatTokens(model.output)} · cache read ${formatTokens(model.cacheRead)} · cache write ${formatTokens(model.cacheWrite)}`}
|
||||
title={t('chat.usage.modelBreakdown', {
|
||||
input: formatTokens(model.input),
|
||||
output: formatTokens(model.output),
|
||||
cacheRead: formatTokens(model.cacheRead),
|
||||
cacheWrite: formatTokens(model.cacheWrite),
|
||||
})}
|
||||
>
|
||||
{/* The bar sits BEHIND the row rather than beside it — the model names are long and a separate
|
||||
track would leave no room for either. */}
|
||||
@@ -287,8 +292,10 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
|
||||
title: a.current
|
||||
? `${a.emailAccount} — signed in`
|
||||
: a.usable
|
||||
? `Switch to ${a.emailAccount}${a.orgName ? ` (${a.orgName})` : ''}`
|
||||
: `${a.emailAccount} — sign-in expired, run /login`,
|
||||
? a.orgName
|
||||
? t('chat.usage.switchToOrg', { account: a.emailAccount, org: a.orgName })
|
||||
: t('chat.usage.switchTo', { account: a.emailAccount })
|
||||
: t('chat.usage.signInExpired', { account: a.emailAccount }),
|
||||
})),
|
||||
...archived.map((a) => ({
|
||||
key: `archived:${a.email}`,
|
||||
|
||||
@@ -2,12 +2,14 @@ import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import type { Attachment } from './types';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
type UseAttachmentsParams = {
|
||||
sessionId?: string | null;
|
||||
};
|
||||
|
||||
export function useAttachments(params?: UseAttachmentsParams) {
|
||||
const { t } = useTranslation();
|
||||
const client = useClient();
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
|
||||
@@ -33,7 +35,7 @@ export function useAttachments(params?: UseAttachmentsParams) {
|
||||
);
|
||||
} catch {
|
||||
setAttachments((prev) => prev.filter((_, i) => i !== idx));
|
||||
toast.error('Failed to scrape webpage');
|
||||
toast.error(t('chat.attach.scrapeFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,7 +60,7 @@ export function useAttachments(params?: UseAttachmentsParams) {
|
||||
);
|
||||
} catch {
|
||||
setAttachments((prev) => prev.filter((_, i) => i !== idx));
|
||||
toast.error('Failed to upload image');
|
||||
toast.error(t('chat.attach.uploadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
export function useAudioRecording(onTranscription: (text: string) => void) {
|
||||
const { t } = useTranslation();
|
||||
const [recording, setRecording] = useState(false);
|
||||
const [transcribing, setTranscribing] = useState(false);
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
@@ -16,7 +18,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) {
|
||||
|
||||
try {
|
||||
if (recorder.state === 'inactive') {
|
||||
recorder.stream.getTracks().forEach((t) => t.stop());
|
||||
recorder.stream.getTracks().forEach((track) => track.stop());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,10 +32,10 @@ export function useAudioRecording(onTranscription: (text: string) => void) {
|
||||
recorder.stop();
|
||||
});
|
||||
|
||||
recorder.stream.getTracks().forEach((t) => t.stop());
|
||||
recorder.stream.getTracks().forEach((track) => track.stop());
|
||||
|
||||
if (blob.size === 0) {
|
||||
toast.error('No audio was captured');
|
||||
toast.error(t('chat.composer.noAudio'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,7 +63,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) {
|
||||
setTranscribing(false);
|
||||
}
|
||||
} catch (err) {
|
||||
recorder.stream?.getTracks().forEach((t) => t.stop());
|
||||
recorder.stream?.getTracks().forEach((track) => track.stop());
|
||||
chunksRef.current = [];
|
||||
toast.error(err instanceof Error ? err.message : 'Recording failed');
|
||||
}
|
||||
@@ -94,7 +96,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) {
|
||||
const recorder = mediaRecorderRef.current;
|
||||
if (!recorder) return;
|
||||
if (recorder.state !== 'inactive') recorder.stop();
|
||||
recorder.stream?.getTracks().forEach((t) => t.stop());
|
||||
recorder.stream?.getTracks().forEach((track) => track.stop());
|
||||
mediaRecorderRef.current = null;
|
||||
chunksRef.current = [];
|
||||
},
|
||||
|
||||
@@ -361,7 +361,9 @@
|
||||
"byModel": "Tokens by model",
|
||||
"title": "Usage and limits",
|
||||
"modelBreakdown": "In {{input}} · out {{output}} · cache read {{cacheRead}} · cache write {{cacheWrite}}",
|
||||
"switchTo": "Switch to {{account}}"
|
||||
"switchTo": "Switch to {{account}}",
|
||||
"switchToOrg": "Switch to {{account}} ({{org}})",
|
||||
"signInExpired": "{{account}} — sign-in expired, run /login"
|
||||
},
|
||||
"attach": {
|
||||
"image": "Image",
|
||||
|
||||
@@ -361,7 +361,9 @@
|
||||
"byModel": "Tokens por modelo",
|
||||
"title": "Consumo e limites",
|
||||
"modelBreakdown": "Entrada {{input}} · saída {{output}} · leitura de cache {{cacheRead}} · escrita de cache {{cacheWrite}}",
|
||||
"switchTo": "Mudar para {{account}}"
|
||||
"switchTo": "Mudar para {{account}}",
|
||||
"switchToOrg": "Mudar para {{account}} ({{org}})",
|
||||
"signInExpired": "{{account}} — sessão expirada, execute /login"
|
||||
},
|
||||
"attach": {
|
||||
"image": "Imagem",
|
||||
|
||||
Reference in New Issue
Block a user