chat history panel keyed (344 -> 283)
Three module-level tab/option tables carried labels, so they carry labelKey now and
the components resolve them.
The hidden-subfolder hint hand-rolled a plural with a ternary ("one is" / "ones are"),
which is a real plural entry now — verified at 0, 1 and 2 in both locales.
OpenCode and Claude are product names, wrapped rather than keyed.
Scanner: the jsx-text pattern now rejects a preceding `=`, so an arrow return type is
not read as prose — `(id: string) => Promise<unknown>` was being reported as the string
"Promise", seven times across the panels. Verified the delta is exactly those seven.
This commit is contained in:
@@ -52,7 +52,10 @@ const Q = String.raw`(?:"([^"\n]{2,120})"|'([^'\n]{2,120})')`;
|
|||||||
*/
|
*/
|
||||||
const PATTERNS: { kind: string; rx: RegExp; neverTranslate?: true }[] = [
|
const PATTERNS: { kind: string; rx: RegExp; neverTranslate?: true }[] = [
|
||||||
// >Some visible words<
|
// >Some visible words<
|
||||||
{ kind: 'jsx text', rx: />\s*([A-Z][A-Za-z0-9][^<>{}\n]{2,100}?)\s*</g },
|
//
|
||||||
|
// The lookbehind rejects `=>`, so an arrow return type does not read as JSX text: `(id: string) =>
|
||||||
|
// Promise<unknown>` was reported as the string "Promise".
|
||||||
|
{ kind: 'jsx text', rx: /(?<!=)>\s*([A-Z][A-Za-z0-9][^<>{}\n]{2,100}?)\s*</g },
|
||||||
// placeholder="..." and friends
|
// placeholder="..." and friends
|
||||||
{
|
{
|
||||||
kind: 'attributes',
|
kind: 'attributes',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useClaudeSessions } from 'state/useClaudeSessions';
|
|||||||
import type { NotifyPreference } from 'state/useClaudeSessions';
|
import type { NotifyPreference } from 'state/useClaudeSessions';
|
||||||
import { useChat, EmbeddableChat } from '../Chat';
|
import { useChat, EmbeddableChat } from '../Chat';
|
||||||
import type { ChatMessage } from '../Chat/types';
|
import type { ChatMessage } from '../Chat/types';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
export type SelectedSession = {
|
export type SelectedSession = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -71,6 +72,7 @@ function DetailBar({
|
|||||||
onDisconnect,
|
onDisconnect,
|
||||||
notifyPreference,
|
notifyPreference,
|
||||||
}: DetailBarProps) {
|
}: DetailBarProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
// The id to rename comes from the URL, not from `resumeSessionId`. They are the same for an ordinary
|
// The id to rename comes from the URL, not from `resumeSessionId`. They are the same for an ordinary
|
||||||
// conversation and not for a merged `/clear` chain, where the resume target is the tail transcript
|
// conversation and not for a merged `/clear` chain, where the resume target is the tail transcript
|
||||||
// while the list, the links and the server all address the chain by its head. Renaming the tail would
|
// while the list, the links and the server all address the chain by its head. Renaming the tail would
|
||||||
@@ -115,11 +117,11 @@ function DetailBar({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 px-3 text-center">
|
<div className="flex-1 min-w-0 px-3 text-center">
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
value={sessionTitle ?? 'New chat'}
|
value={sessionTitle ?? t('chatHistory.newChatTitle')}
|
||||||
onCommit={commitRename}
|
onCommit={commitRename}
|
||||||
readOnly={!sessionId}
|
readOnly={!sessionId}
|
||||||
ariaLabel="Session title"
|
ariaLabel={t('chatHistory.sessionTitle')}
|
||||||
hint="Click to rename this chat"
|
hint={t('chatHistory.renameHint')}
|
||||||
className="block w-full cursor-text truncate text-sm font-medium text-foreground/80 transition-opacity hover:opacity-70"
|
className="block w-full cursor-text truncate text-sm font-medium text-foreground/80 transition-opacity hover:opacity-70"
|
||||||
inputClassName="w-full border-b border-primary/40 bg-transparent text-center text-sm font-medium text-foreground/80 outline-none"
|
inputClassName="w-full border-b border-primary/40 bg-transparent text-center text-sm font-medium text-foreground/80 outline-none"
|
||||||
/>
|
/>
|
||||||
@@ -142,7 +144,7 @@ function DetailBar({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onDisconnect}
|
onClick={onDisconnect}
|
||||||
title="End session — kills any running turn and releases it so it can be resumed elsewhere"
|
title={t('chatHistory.endSession')}
|
||||||
className="ml-1 p-1 rounded hover:bg-muted hover:text-destructive transition-colors cursor-pointer"
|
className="ml-1 p-1 rounded hover:bg-muted hover:text-destructive transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<Unplug className="h-3.5 w-3.5" />
|
<Unplug className="h-3.5 w-3.5" />
|
||||||
@@ -177,6 +179,7 @@ type NewChatProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function NewChat(props: NewChatProps) {
|
function NewChat(props: NewChatProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { resumeSummary, resumeSessionId, initialMessages, total, initialOffset, sessionCwd, planMode } = props;
|
const { resumeSummary, resumeSessionId, initialMessages, total, initialOffset, sessionCwd, planMode } = props;
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const locationState = location.state as ChatLocationState;
|
const locationState = location.state as ChatLocationState;
|
||||||
@@ -254,6 +257,7 @@ function NewChat(props: NewChatProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ChatDetailPanel = () => {
|
export const ChatDetailPanel = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [selected] = useSelectedChatSession();
|
const [selected] = useSelectedChatSession();
|
||||||
|
|
||||||
// Name the page after the conversation, whenever the URL names a real one. Gated on the route param
|
// Name the page after the conversation, whenever the URL names a real one. Gated on the route param
|
||||||
@@ -284,7 +288,7 @@ export const ChatDetailPanel = () => {
|
|||||||
if (!selected) {
|
if (!selected) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">
|
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">
|
||||||
Select a session to view
|
{t('chatHistory.selectSession')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { Folder, ChevronRight, Loader2, FolderPlus, Check, Eye, EyeOff } from 'lucide-react';
|
import { Folder, ChevronRight, Loader2, FolderPlus, Check, Eye, EyeOff } from 'lucide-react';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { useFilesAPI, type DirEntry } from '../../hooks/useFilesAPI';
|
import { useFilesAPI, type DirEntry } from '../../hooks/useFilesAPI';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
type DirPickerModalProps = {
|
type DirPickerModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -13,6 +14,7 @@ type DirPickerModalProps = {
|
|||||||
// A simplified file-browser modal for picking a working directory (returns an absolute path).
|
// A simplified file-browser modal for picking a working directory (returns an absolute path).
|
||||||
// Navigates within the home root; dirs elsewhere are reachable via the selector's free-text field.
|
// Navigates within the home root; dirs elsewhere are reachable via the selector's free-text field.
|
||||||
export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps) => {
|
export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps) => {
|
||||||
|
const { t, tUi } = useTranslation();
|
||||||
const api = useFilesAPI('home');
|
const api = useFilesAPI('home');
|
||||||
const [path, setPath] = useState('/'); // root-relative, always starts with '/'
|
const [path, setPath] = useState('/'); // root-relative, always starts with '/'
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
@@ -54,7 +56,7 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
|||||||
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
||||||
<DialogContent className="flex h-[70vh] max-w-2xl flex-col gap-0 p-0">
|
<DialogContent className="flex h-[70vh] max-w-2xl flex-col gap-0 p-0">
|
||||||
<DialogHeader className="border-b border-border px-4 py-3">
|
<DialogHeader className="border-b border-border px-4 py-3">
|
||||||
<DialogTitle className="text-sm">Choose a working directory</DialogTitle>
|
<DialogTitle className="text-sm">{t('chatHistory.chooseDir')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
@@ -82,9 +84,7 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
|||||||
// Say when the folder isn't actually empty, just filtered — "No subfolders here" over a
|
// Say when the folder isn't actually empty, just filtered — "No subfolders here" over a
|
||||||
// directory holding thirty dot-folders is a lie the toggle below can't help you spot.
|
// directory holding thirty dot-folders is a lie the toggle below can't help you spot.
|
||||||
<div className="flex h-full items-center justify-center px-4 text-center text-sm opacity-40">
|
<div className="flex h-full items-center justify-center px-4 text-center text-sm opacity-40">
|
||||||
{hiddenCount > 0
|
{hiddenCount > 0 ? t('chatHistory.noSubfolders', { count: hiddenCount }) : 'No subfolders here'}
|
||||||
? `No visible subfolders — ${hiddenCount} hidden ${hiddenCount === 1 ? 'one is' : 'ones are'} filtered out`
|
|
||||||
: 'No subfolders here'}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
dirs.map((d) => (
|
dirs.map((d) => (
|
||||||
@@ -113,14 +113,14 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
|||||||
onKeyDown={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === 'Escape') setCreating(false);
|
if (ev.key === 'Escape') setCreating(false);
|
||||||
}}
|
}}
|
||||||
placeholder="New folder name"
|
placeholder={t('chatHistory.newFolderName')}
|
||||||
className="min-w-0 flex-1 rounded border border-border bg-transparent px-2 py-1 text-xs outline-none"
|
className="min-w-0 flex-1 rounded border border-border bg-transparent px-2 py-1 text-xs outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded px-2 py-1 text-xs text-duck-teal hover:bg-duck-teal/10 cursor-pointer"
|
className="rounded px-2 py-1 text-xs text-duck-teal hover:bg-duck-teal/10 cursor-pointer"
|
||||||
>
|
>
|
||||||
Create
|
{t('chatHistory.create')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
@@ -129,7 +129,7 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
|||||||
onClick={() => setCreating(true)}
|
onClick={() => setCreating(true)}
|
||||||
className="flex items-center gap-1.5 text-xs opacity-60 hover:opacity-100 cursor-pointer"
|
className="flex items-center gap-1.5 text-xs opacity-60 hover:opacity-100 cursor-pointer"
|
||||||
>
|
>
|
||||||
<FolderPlus className="h-3.5 w-3.5" /> New folder
|
<FolderPlus className="h-3.5 w-3.5" /> {t('chatHistory.newFolder')}
|
||||||
</button>
|
</button>
|
||||||
{/* Shown even at zero, so the control's state is always legible: a folder with nothing
|
{/* Shown even at zero, so the control's state is always legible: a folder with nothing
|
||||||
hidden and one with everything hidden must not look identical. */}
|
hidden and one with everything hidden must not look identical. */}
|
||||||
@@ -139,7 +139,11 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
|||||||
className="flex items-center gap-1.5 text-xs opacity-60 hover:opacity-100 cursor-pointer"
|
className="flex items-center gap-1.5 text-xs opacity-60 hover:opacity-100 cursor-pointer"
|
||||||
>
|
>
|
||||||
{showHidden ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
{showHidden ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||||
{showHidden ? 'Hide hidden' : `Show hidden${hiddenCount > 0 ? ` (${hiddenCount})` : ''}`}
|
{showHidden
|
||||||
|
? t('chatHistory.hideHidden')
|
||||||
|
: hiddenCount > 0
|
||||||
|
? t('chatHistory.showHiddenCount', { count: hiddenCount })
|
||||||
|
: t('chatHistory.showHidden')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -151,7 +155,7 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-md px-3 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
className="rounded-md px-3 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
||||||
>
|
>
|
||||||
Cancel
|
{tUi('button.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -161,7 +165,7 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
|||||||
disabled={!absCurrent}
|
disabled={!absCurrent}
|
||||||
className="flex items-center gap-1.5 rounded-md bg-duck-teal px-3 py-1.5 text-sm font-medium text-duck-yellow hover:bg-duck-teal/90 disabled:opacity-50 cursor-pointer"
|
className="flex items-center gap-1.5 rounded-md bg-duck-teal px-3 py-1.5 text-sm font-medium text-duck-yellow hover:bg-duck-teal/90 disabled:opacity-50 cursor-pointer"
|
||||||
>
|
>
|
||||||
<Check className="h-3.5 w-3.5" /> Select this folder
|
<Check className="h-3.5 w-3.5" /> {t('chatHistory.selectFolder')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Bell, BellOff, Check } from 'lucide-react';
|
import { Bell, BellOff, Check } from 'lucide-react';
|
||||||
import * as Dropdown from '@/components/ui/dropdown-menu';
|
import * as Dropdown from '@/components/ui/dropdown-menu';
|
||||||
import type { NotifyPreference } from 'state/useClaudeSessions';
|
import type { NotifyPreference } from 'state/useClaudeSessions';
|
||||||
|
import type { PanelTranslationKey } from '../../locales';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
const { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } = Dropdown;
|
const { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } = Dropdown;
|
||||||
|
|
||||||
const OPTIONS: { value: NotifyPreference; label: string }[] = [
|
const OPTIONS: { value: NotifyPreference; labelKey: PanelTranslationKey }[] = [
|
||||||
{ value: 'every_turn', label: 'Every end of turn' },
|
{ value: 'every_turn', labelKey: 'chatHistory.notify.everyTurn' },
|
||||||
{ value: 'input_needed', label: 'Only when input is needed' },
|
{ value: 'input_needed', labelKey: 'chatHistory.notify.inputNeeded' },
|
||||||
{ value: 'none', label: 'None' },
|
{ value: 'none', labelKey: 'chatHistory.notify.none' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +42,7 @@ type NotifyMenuProps = {
|
|||||||
* variant, which nothing here uses.
|
* variant, which nothing here uses.
|
||||||
*/
|
*/
|
||||||
export const NotifyMenu = ({ preference, onChange }: NotifyMenuProps) => {
|
export const NotifyMenu = ({ preference, onChange }: NotifyMenuProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const pick = (value: NotifyPreference) => {
|
const pick = (value: NotifyPreference) => {
|
||||||
onChange(value);
|
onChange(value);
|
||||||
// Lazy, first-opt-in request — the only place this fires. A denial or an already-decided answer is a
|
// Lazy, first-opt-in request — the only place this fires. A denial or an already-decided answer is a
|
||||||
@@ -55,8 +58,10 @@ export const NotifyMenu = ({ preference, onChange }: NotifyMenuProps) => {
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title={`Notifications: ${OPTIONS.find((o) => o.value === preference)?.label ?? 'None'}`}
|
title={t('chatHistory.notificationsLabel', {
|
||||||
aria-label="Notification preference"
|
choice: t(OPTIONS.find((o) => o.value === preference)?.labelKey ?? 'chatHistory.notify.none'),
|
||||||
|
})}
|
||||||
|
aria-label={t('chatHistory.notifyPreference')}
|
||||||
className={`rounded p-1 transition-colors hover:bg-muted cursor-pointer ${BELL_TONE[preference]}`}
|
className={`rounded p-1 transition-colors hover:bg-muted cursor-pointer ${BELL_TONE[preference]}`}
|
||||||
>
|
>
|
||||||
{preference === 'none' ? <BellOff className="h-3.5 w-3.5" /> : <Bell className="h-3.5 w-3.5" />}
|
{preference === 'none' ? <BellOff className="h-3.5 w-3.5" /> : <Bell className="h-3.5 w-3.5" />}
|
||||||
@@ -66,7 +71,7 @@ export const NotifyMenu = ({ preference, onChange }: NotifyMenuProps) => {
|
|||||||
{OPTIONS.map((opt) => (
|
{OPTIONS.map((opt) => (
|
||||||
<DropdownMenuItem key={opt.value} className="cursor-pointer" onClick={() => pick(opt.value)}>
|
<DropdownMenuItem key={opt.value} className="cursor-pointer" onClick={() => pick(opt.value)}>
|
||||||
<Check className={`mr-2 h-4 w-4 ${opt.value === preference ? 'opacity-100' : 'opacity-0'}`} />
|
<Check className={`mr-2 h-4 w-4 ${opt.value === preference ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
{opt.label}
|
{t(opt.labelKey)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { chatNewPath, chatSessionPath, cwdFromSplat } from './chat-routes';
|
|||||||
import { DirPickerModal } from './DirPickerModal';
|
import { DirPickerModal } from './DirPickerModal';
|
||||||
import { LiveMarker, SessionRow, shortenPath, useScrollActiveIntoView, useSessionRowActions } from './SessionRow';
|
import { LiveMarker, SessionRow, shortenPath, useScrollActiveIntoView, useSessionRowActions } from './SessionRow';
|
||||||
import { SessionTree } from './SessionTree';
|
import { SessionTree } from './SessionTree';
|
||||||
|
import type { PanelTranslationKey } from '../../locales';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The /chat sidebar: one panel, three views.
|
The /chat sidebar: one panel, three views.
|
||||||
@@ -30,10 +32,10 @@ import { SessionTree } from './SessionTree';
|
|||||||
|
|
||||||
type View = 'tree' | 'latest' | 'live';
|
type View = 'tree' | 'latest' | 'live';
|
||||||
|
|
||||||
const TABS: { value: View; label: string }[] = [
|
const TABS: { value: View; labelKey: PanelTranslationKey }[] = [
|
||||||
{ value: 'tree', label: 'Tree' },
|
{ value: 'tree', labelKey: 'chatHistory.tabs.tree' },
|
||||||
{ value: 'latest', label: 'Latest' },
|
{ value: 'latest', labelKey: 'chatHistory.tabs.latest' },
|
||||||
{ value: 'live', label: 'Live' },
|
{ value: 'live', labelKey: 'chatHistory.tabs.live' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -52,6 +54,7 @@ const LIVE_KIND_KEY = 'chat.sessions.liveKind';
|
|||||||
const EXPANDED_KEY = 'chat.sessions.expanded';
|
const EXPANDED_KEY = 'chat.sessions.expanded';
|
||||||
|
|
||||||
export const SessionList = () => {
|
export const SessionList = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Which session is open, and which project group we're browsing, both come from the route. Reading them
|
// Which session is open, and which project group we're browsing, both come from the route. Reading them
|
||||||
// there rather than from the selection channel means the highlight and the group are correct on a deep
|
// there rather than from the selection channel means the highlight and the group are correct on a deep
|
||||||
@@ -157,7 +160,7 @@ export const SessionList = () => {
|
|||||||
value={tab.value}
|
value={tab.value}
|
||||||
className="flex-1 cursor-pointer rounded-md border border-transparent px-2 py-1 text-xs font-medium leading-normal ring-0 transition-all hover:bg-foreground/5 hover:text-foreground focus-visible:ring-0 data-[state=active]:border-border data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
className="flex-1 cursor-pointer rounded-md border border-transparent px-2 py-1 text-xs font-medium leading-normal ring-0 transition-all hover:bg-foreground/5 hover:text-foreground focus-visible:ring-0 data-[state=active]:border-border data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
||||||
>
|
>
|
||||||
{tab.label}
|
{t(tab.labelKey)}
|
||||||
{/* The count that decides whether Live is worth a click. Only when non-zero — a permanent
|
{/* The count that decides whether Live is worth a click. Only when non-zero — a permanent
|
||||||
`0` is the same as no information. */}
|
`0` is the same as no information. */}
|
||||||
{tab.value === 'live' && liveSessions.length > 0 && (
|
{tab.value === 'live' && liveSessions.length > 0 && (
|
||||||
@@ -173,8 +176,8 @@ export const SessionList = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
className="cursor-pointer rounded p-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
className="cursor-pointer rounded p-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
title="Refresh"
|
title={t('chatHistory.refresh')}
|
||||||
aria-label="Refresh sessions"
|
aria-label={t('chatHistory.refreshSessions')}
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-3.5 w-3.5 ${latest.isLoading ? 'animate-spin' : ''}`} />
|
<RefreshCw className={`h-3.5 w-3.5 ${latest.isLoading ? 'animate-spin' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
@@ -184,10 +187,10 @@ export const SessionList = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setBrowsing(true)}
|
onClick={() => setBrowsing(true)}
|
||||||
className="flex cursor-pointer items-center gap-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-opacity hover:opacity-90"
|
className="flex cursor-pointer items-center gap-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-opacity hover:opacity-90"
|
||||||
title="New chat in a directory…"
|
title={t('chatHistory.newChatInDir')}
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
New Chat
|
{t('chatHistory.newChat')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,25 +212,25 @@ export const SessionList = () => {
|
|||||||
<>
|
<>
|
||||||
{latest.error ? (
|
{latest.error ? (
|
||||||
<ErrorBlock
|
<ErrorBlock
|
||||||
title="Could not load sessions"
|
title={t('chatHistory.loadFailed')}
|
||||||
message={errorText(latest.error)}
|
message={errorText(latest.error)}
|
||||||
action={
|
action={
|
||||||
<button
|
<button
|
||||||
onClick={() => void latest.refetch()}
|
onClick={() => void latest.refetch()}
|
||||||
className="cursor-pointer rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground"
|
className="cursor-pointer rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground"
|
||||||
>
|
>
|
||||||
Try again
|
{t('chatHistory.tryAgain')}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : latest.isLoading && latest.sessions.length === 0 ? (
|
) : latest.isLoading && latest.sessions.length === 0 ? (
|
||||||
// Loading and empty used to be the same grey line of text, so a slow read looked like an
|
// Loading and empty used to be the same grey line of text, so a slow read looked like an
|
||||||
// account with no history.
|
// account with no history.
|
||||||
<LoadingBlock label="Reading transcripts…" />
|
<LoadingBlock label={t('chatHistory.reading')} />
|
||||||
) : latest.sessions.length === 0 ? (
|
) : latest.sessions.length === 0 ? (
|
||||||
<EmptyBlock
|
<EmptyBlock
|
||||||
icon={MessageSquare}
|
icon={MessageSquare}
|
||||||
title="No sessions yet"
|
title={t('chatHistory.noSessions')}
|
||||||
hint="Every conversation you have had shows up here, newest first. Start one with New Chat."
|
hint="Every conversation you have had shows up here, newest first. Start one with New Chat."
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -277,12 +280,13 @@ export const SessionList = () => {
|
|||||||
*/
|
*/
|
||||||
type LiveKind = 'agents' | 'commanders';
|
type LiveKind = 'agents' | 'commanders';
|
||||||
|
|
||||||
const LIVE_TABS: { value: LiveKind; label: string }[] = [
|
const LIVE_TABS: { value: LiveKind; labelKey: PanelTranslationKey }[] = [
|
||||||
{ value: 'agents', label: 'Agents' },
|
{ value: 'agents', labelKey: 'chatHistory.tabs.agents' },
|
||||||
{ value: 'commanders', label: 'Commanders' },
|
{ value: 'commanders', labelKey: 'chatHistory.tabs.commanders' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LiveView = ({ activeId, home }: { activeId?: string; home: string | null }) => {
|
const LiveView = ({ activeId, home }: { activeId?: string; home: string | null }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { live: sessions, isLoading, error, refetch } = useLiveSessions();
|
const { live: sessions, isLoading, error, refetch } = useLiveSessions();
|
||||||
const [kind, setKind] = useLocalStorageState<LiveKind>(LIVE_KIND_KEY, 'agents');
|
const [kind, setKind] = useLocalStorageState<LiveKind>(LIVE_KIND_KEY, 'agents');
|
||||||
|
|
||||||
@@ -297,20 +301,20 @@ const LiveView = ({ activeId, home }: { activeId?: string; home: string | null }
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorBlock
|
<ErrorBlock
|
||||||
title="Could not read live sessions"
|
title={t('chatHistory.liveFailed')}
|
||||||
message={errorText(error)}
|
message={errorText(error)}
|
||||||
action={
|
action={
|
||||||
<button
|
<button
|
||||||
onClick={() => void refetch()}
|
onClick={() => void refetch()}
|
||||||
className="cursor-pointer rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground"
|
className="cursor-pointer rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground"
|
||||||
>
|
>
|
||||||
Try again
|
{t('chatHistory.tryAgain')}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isLoading && sessions.length === 0) return <LoadingBlock label="Asking the agent…" />;
|
if (isLoading && sessions.length === 0) return <LoadingBlock label={t('chatHistory.asking')} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
@@ -328,7 +332,7 @@ const LiveView = ({ activeId, home }: { activeId?: string; home: string | null }
|
|||||||
value={tab.value}
|
value={tab.value}
|
||||||
className="flex-1 cursor-pointer rounded-md border border-transparent px-2 py-1 text-xs font-medium leading-normal ring-0 transition-all hover:bg-foreground/5 hover:text-foreground focus-visible:ring-0 data-[state=active]:border-border data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
className="flex-1 cursor-pointer rounded-md border border-transparent px-2 py-1 text-xs font-medium leading-normal ring-0 transition-all hover:bg-foreground/5 hover:text-foreground focus-visible:ring-0 data-[state=active]:border-border data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
||||||
>
|
>
|
||||||
{tab.label}
|
{t(tab.labelKey)}
|
||||||
<span className={`ml-1 tabular-nums ${count > 0 ? 'text-success' : 'opacity-50'}`}>({count})</span>
|
<span className={`ml-1 tabular-nums ${count > 0 ? 'text-success' : 'opacity-50'}`}>({count})</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { DataRow, MetaItem, RelativeTime } from '@/components/Data';
|
|||||||
import { errorText } from 'helpers/error-text';
|
import { errorText } from 'helpers/error-text';
|
||||||
import type { ClaudeSessionSummary, NotifyPreference } from 'state/useClaudeSessions';
|
import type { ClaudeSessionSummary, NotifyPreference } from 'state/useClaudeSessions';
|
||||||
import { chatSessionPath } from './chat-routes';
|
import { chatSessionPath } from './chat-routes';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
import { Untranslated } from '@/components/Untranslated';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
One conversation, rendered the same way in all three tabs.
|
One conversation, rendered the same way in all three tabs.
|
||||||
@@ -26,9 +28,13 @@ import { chatSessionPath } from './chat-routes';
|
|||||||
*/
|
*/
|
||||||
export const HarnessBadge = ({ harness }: { harness?: 'claude' | 'opencode' }) =>
|
export const HarnessBadge = ({ harness }: { harness?: 'claude' | 'opencode' }) =>
|
||||||
harness === 'opencode' ? (
|
harness === 'opencode' ? (
|
||||||
<span className="rounded bg-info/10 px-1.5 py-0.5 font-medium text-info">OpenCode</span>
|
<span className="rounded bg-info/10 px-1.5 py-0.5 font-medium text-info">
|
||||||
|
<Untranslated reason="product-name">OpenCode</Untranslated>
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="rounded bg-primary/10 px-1.5 py-0.5 font-medium text-primary">Claude</span>
|
<span className="rounded bg-primary/10 px-1.5 py-0.5 font-medium text-primary">
|
||||||
|
<Untranslated reason="product-name">Claude</Untranslated>
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,6 +119,7 @@ export function useSessionRowActions(actions: {
|
|||||||
renameSession: (id: string, title: string) => Promise<unknown>;
|
renameSession: (id: string, title: string) => Promise<unknown>;
|
||||||
deleteSession: (id: string) => Promise<unknown>;
|
deleteSession: (id: string) => Promise<unknown>;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [editValue, setEditValue] = useState('');
|
const [editValue, setEditValue] = useState('');
|
||||||
const [confirmingId, setConfirmingId] = useState<string | null>(null);
|
const [confirmingId, setConfirmingId] = useState<string | null>(null);
|
||||||
@@ -134,7 +141,7 @@ export function useSessionRowActions(actions: {
|
|||||||
try {
|
try {
|
||||||
await actions.renameSession(id, title);
|
await actions.renameSession(id, title);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(`Could not rename the session: ${errorText(err)}`);
|
toast.error(t('chatHistory.renameFailed', { reason: errorText(err) }));
|
||||||
}
|
}
|
||||||
}, [editingId, editValue, actions]);
|
}, [editingId, editValue, actions]);
|
||||||
|
|
||||||
@@ -144,7 +151,7 @@ export function useSessionRowActions(actions: {
|
|||||||
try {
|
try {
|
||||||
await actions.deleteSession(id);
|
await actions.deleteSession(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(`Could not delete the session: ${errorText(err)}`);
|
toast.error(t('chatHistory.deleteFailed', { reason: errorText(err) }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[actions],
|
[actions],
|
||||||
@@ -188,6 +195,7 @@ export const SessionRow = ({
|
|||||||
actions,
|
actions,
|
||||||
activeRef,
|
activeRef,
|
||||||
}: SessionRowProps) => {
|
}: SessionRowProps) => {
|
||||||
|
const { t, tUi } = useTranslation();
|
||||||
const isActive = activeId === session.id;
|
const isActive = activeId === session.id;
|
||||||
const isEditing = actions.editingId === session.id;
|
const isEditing = actions.editingId === session.id;
|
||||||
const isConfirming = actions.confirmingId === session.id;
|
const isConfirming = actions.confirmingId === session.id;
|
||||||
@@ -209,15 +217,15 @@ export const SessionRow = ({
|
|||||||
if (ev.key === 'Escape') actions.setEditingId(null);
|
if (ev.key === 'Escape') actions.setEditingId(null);
|
||||||
}}
|
}}
|
||||||
onBlur={() => void actions.commitRename()}
|
onBlur={() => void actions.commitRename()}
|
||||||
aria-label="Session title"
|
aria-label={t('chatHistory.sessionTitle')}
|
||||||
className="min-w-0 flex-1 border-b border-primary/40 bg-transparent text-sm outline-none"
|
className="min-w-0 flex-1 border-b border-primary/40 bg-transparent text-sm outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
onMouseDown={(ev) => ev.preventDefault()}
|
onMouseDown={(ev) => ev.preventDefault()}
|
||||||
className="cursor-pointer p-1 text-success hover:opacity-80"
|
className="cursor-pointer p-1 text-success hover:opacity-80"
|
||||||
title="Save"
|
title={t('common.save')}
|
||||||
aria-label="Save title"
|
aria-label={t('chatHistory.saveTitle')}
|
||||||
>
|
>
|
||||||
<Check className="h-3.5 w-3.5" />
|
<Check className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -226,8 +234,8 @@ export const SessionRow = ({
|
|||||||
onMouseDown={(ev) => ev.preventDefault()}
|
onMouseDown={(ev) => ev.preventDefault()}
|
||||||
onClick={() => actions.setEditingId(null)}
|
onClick={() => actions.setEditingId(null)}
|
||||||
className="cursor-pointer p-1 opacity-50 hover:opacity-100"
|
className="cursor-pointer p-1 opacity-50 hover:opacity-100"
|
||||||
title="Cancel"
|
title={tUi('button.cancel')}
|
||||||
aria-label="Cancel rename"
|
aria-label={t('chatHistory.cancelRename')}
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -279,21 +287,23 @@ export const SessionRow = ({
|
|||||||
{/* Say the size of it. Deleting a merged chain removes every part, and a row that looks like one
|
{/* Say the size of it. Deleting a merged chain removes every part, and a row that looks like one
|
||||||
conversation giving no warning that it is three files is exactly the surprise this prevents. */}
|
conversation giving no warning that it is three files is exactly the surprise this prevents. */}
|
||||||
<span className="text-xs text-destructive">
|
<span className="text-xs text-destructive">
|
||||||
{session.partCount && session.partCount > 1 ? `Delete all ${session.partCount}?` : 'Delete?'}
|
{session.partCount && session.partCount > 1
|
||||||
|
? t('chatHistory.deleteAll', { count: session.partCount })
|
||||||
|
: 'Delete?'}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => void actions.handleDelete(session.id)}
|
onClick={() => void actions.handleDelete(session.id)}
|
||||||
className="cursor-pointer rounded p-1 text-destructive hover:bg-destructive/10"
|
className="cursor-pointer rounded p-1 text-destructive hover:bg-destructive/10"
|
||||||
title="Confirm delete"
|
title={t('chatHistory.confirmDelete')}
|
||||||
aria-label={`Confirm deleting ${session.title}`}
|
aria-label={t('chatHistory.confirmDeletingNamed', { name: session.title })}
|
||||||
>
|
>
|
||||||
<Check className="h-3.5 w-3.5" />
|
<Check className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => actions.setConfirmingId(null)}
|
onClick={() => actions.setConfirmingId(null)}
|
||||||
className="cursor-pointer rounded p-1 opacity-50 hover:opacity-100"
|
className="cursor-pointer rounded p-1 opacity-50 hover:opacity-100"
|
||||||
title="Cancel"
|
title={tUi('button.cancel')}
|
||||||
aria-label="Cancel delete"
|
aria-label={t('chatHistory.cancelDelete')}
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -304,16 +314,16 @@ export const SessionRow = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => actions.startRename(session.id, session.title)}
|
onClick={() => actions.startRename(session.id, session.title)}
|
||||||
className="cursor-pointer rounded p-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
className="cursor-pointer rounded p-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||||
title="Rename"
|
title={t('chatHistory.rename')}
|
||||||
aria-label={`Rename ${session.title}`}
|
aria-label={t('chatHistory.renameNamed', { name: session.title })}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => actions.setConfirmingId(session.id)}
|
onClick={() => actions.setConfirmingId(session.id)}
|
||||||
className="cursor-pointer rounded p-1.5 text-muted-foreground transition-colors hover:text-destructive"
|
className="cursor-pointer rounded p-1.5 text-muted-foreground transition-colors hover:text-destructive"
|
||||||
title="Delete session"
|
title={t('chatHistory.deleteSession')}
|
||||||
aria-label={`Delete ${session.title}`}
|
aria-label={t('chatHistory.deleteNamed', { name: session.title })}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { LiveSession } from 'state/useLiveSessions';
|
|||||||
import { chatNewPath } from './chat-routes';
|
import { chatNewPath } from './chat-routes';
|
||||||
import { SessionRow, shortenPath, type SessionRowActions, useScrollActiveIntoView } from './SessionRow';
|
import { SessionRow, shortenPath, type SessionRowActions, useScrollActiveIntoView } from './SessionRow';
|
||||||
import { useSelectedChatSession } from '../../channels';
|
import { useSelectedChatSession } from '../../channels';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Directories as an accordion, each expanding to its own conversations.
|
Directories as an accordion, each expanding to its own conversations.
|
||||||
@@ -36,6 +37,7 @@ type DirectoryNodeProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DirectoryNode = ({ pwd, home, expanded, onToggle, live, activeId, actions }: DirectoryNodeProps) => {
|
const DirectoryNode = ({ pwd, home, expanded, onToggle, live, activeId, actions }: DirectoryNodeProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [, setSelected] = useSelectedChatSession();
|
const [, setSelected] = useSelectedChatSession();
|
||||||
const activeRef = useScrollActiveIntoView();
|
const activeRef = useScrollActiveIntoView();
|
||||||
@@ -104,8 +106,8 @@ const DirectoryNode = ({ pwd, home, expanded, onToggle, live, activeId, actions
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={startHere}
|
onClick={startHere}
|
||||||
title={`New chat in ${shortenPath(pwd.cwd, home)}`}
|
title={t('chatHistory.newChatIn', { path: shortenPath(pwd.cwd, home) })}
|
||||||
aria-label={`New chat in ${shortenPath(pwd.cwd, home)}`}
|
aria-label={t('chatHistory.newChatIn', { path: shortenPath(pwd.cwd, home) })}
|
||||||
className="mr-2 shrink-0 cursor-pointer rounded p-1.5 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/dir:opacity-100"
|
className="mr-2 shrink-0 cursor-pointer rounded p-1.5 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/dir:opacity-100"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
@@ -123,11 +125,11 @@ const DirectoryNode = ({ pwd, home, expanded, onToggle, live, activeId, actions
|
|||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="bg-background/40 py-2 pl-8">
|
<div className="bg-background/40 py-2 pl-8">
|
||||||
{isLoading && sessions.length === 0 ? (
|
{isLoading && sessions.length === 0 ? (
|
||||||
<LoadingBlock label="Reading transcripts…" />
|
<LoadingBlock label={t('chatHistory.reading')} />
|
||||||
) : sessions.length === 0 ? (
|
) : sessions.length === 0 ? (
|
||||||
// Reachable: `/chat/pwds` counts transcripts per group, and a group can hold only files this
|
// Reachable: `/chat/pwds` counts transcripts per group, and a group can hold only files this
|
||||||
// reader skips. Better than an accordion that opens onto nothing.
|
// reader skips. Better than an accordion that opens onto nothing.
|
||||||
<div className="py-3 text-xs text-muted-foreground">No readable conversations here.</div>
|
<div className="py-3 text-xs text-muted-foreground">{t('chatHistory.noReadable')}</div>
|
||||||
) : (
|
) : (
|
||||||
<DataList className="!h-auto !overflow-visible">
|
<DataList className="!h-auto !overflow-visible">
|
||||||
{sessions.map((session) => (
|
{sessions.map((session) => (
|
||||||
@@ -167,11 +169,12 @@ export const SessionTree = ({
|
|||||||
activeId,
|
activeId,
|
||||||
actions,
|
actions,
|
||||||
}: SessionTreeProps) => {
|
}: SessionTreeProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
if (!pwds.length) {
|
if (!pwds.length) {
|
||||||
return (
|
return (
|
||||||
<EmptyBlock
|
<EmptyBlock
|
||||||
icon={MessageSquare}
|
icon={MessageSquare}
|
||||||
title="No conversations yet"
|
title={t('chatHistory.noConversations')}
|
||||||
hint="Every directory you have run a chat in shows up here. Start one with New Chat."
|
hint="Every directory you have run a chat in shows up here. Start one with New Chat."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -257,5 +257,63 @@
|
|||||||
"startHint": "It will look around this directory, then ask you what to watch for.",
|
"startHint": "It will look around this directory, then ask you what to watch for.",
|
||||||
"startBlocked": "{{missing}} — then this starts it.",
|
"startBlocked": "{{missing}} — then this starts it.",
|
||||||
"confirmDeletingNamed": "Confirm deleting {{name}}"
|
"confirmDeletingNamed": "Confirm deleting {{name}}"
|
||||||
|
},
|
||||||
|
"chatHistory": {
|
||||||
|
"newChat": "New Chat",
|
||||||
|
"tryAgain": "Try again",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"refreshSessions": "Refresh sessions",
|
||||||
|
"newChatInDir": "New chat in a directory…",
|
||||||
|
"loadFailed": "Could not load sessions",
|
||||||
|
"reading": "Reading transcripts…",
|
||||||
|
"noSessions": "No sessions yet",
|
||||||
|
"liveFailed": "Could not read live sessions",
|
||||||
|
"asking": "Asking the agent…",
|
||||||
|
"tabs": {
|
||||||
|
"tree": "Tree",
|
||||||
|
"latest": "Latest",
|
||||||
|
"live": "Live",
|
||||||
|
"agents": "Agents",
|
||||||
|
"commanders": "Commanders"
|
||||||
|
},
|
||||||
|
"notifyPreference": "Notification preference",
|
||||||
|
"notify": {
|
||||||
|
"everyTurn": "Every end of turn",
|
||||||
|
"inputNeeded": "Only when input is needed",
|
||||||
|
"none": "None"
|
||||||
|
},
|
||||||
|
"notificationsLabel": "Notifications: {{choice}}",
|
||||||
|
"chooseDir": "Choose a working directory",
|
||||||
|
"create": "Create",
|
||||||
|
"newFolder": "New folder",
|
||||||
|
"selectFolder": "Select this folder",
|
||||||
|
"newFolderName": "New folder name",
|
||||||
|
"noSubfolders": {
|
||||||
|
"one": "No visible subfolders — {{count}} hidden one is filtered out",
|
||||||
|
"other": "No visible subfolders — {{count}} hidden ones are filtered out"
|
||||||
|
},
|
||||||
|
"showHidden": "Show hidden",
|
||||||
|
"showHiddenCount": "Show hidden ({{count}})",
|
||||||
|
"selectSession": "Select a session to view",
|
||||||
|
"endSession": "End session — kills any running turn and releases it so it can be resumed elsewhere",
|
||||||
|
"noReadable": "No readable conversations here.",
|
||||||
|
"noConversations": "No conversations yet",
|
||||||
|
"newChatIn": "New chat in {{path}}",
|
||||||
|
"sessionTitle": "Session title",
|
||||||
|
"saveTitle": "Save title",
|
||||||
|
"cancelRename": "Cancel rename",
|
||||||
|
"confirmDelete": "Confirm delete",
|
||||||
|
"cancelDelete": "Cancel delete",
|
||||||
|
"rename": "Rename",
|
||||||
|
"deleteSession": "Delete session",
|
||||||
|
"renameFailed": "Could not rename the session: {{reason}}",
|
||||||
|
"deleteFailed": "Could not delete the session: {{reason}}",
|
||||||
|
"deleteAll": "Delete all {{count}}?",
|
||||||
|
"confirmDeletingNamed": "Confirm deleting {{name}}",
|
||||||
|
"renameNamed": "Rename {{name}}",
|
||||||
|
"deleteNamed": "Delete {{name}}",
|
||||||
|
"newChatTitle": "New chat",
|
||||||
|
"renameHint": "Click to rename this chat",
|
||||||
|
"hideHidden": "Hide hidden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,5 +257,63 @@
|
|||||||
"startHint": "Vai observar este diretório e depois perguntar-lhe o que deve vigiar.",
|
"startHint": "Vai observar este diretório e depois perguntar-lhe o que deve vigiar.",
|
||||||
"startBlocked": "{{missing}} — depois é isto que o inicia.",
|
"startBlocked": "{{missing}} — depois é isto que o inicia.",
|
||||||
"confirmDeletingNamed": "Confirmar a eliminação de {{name}}"
|
"confirmDeletingNamed": "Confirmar a eliminação de {{name}}"
|
||||||
|
},
|
||||||
|
"chatHistory": {
|
||||||
|
"newChat": "Novo chat",
|
||||||
|
"tryAgain": "Tentar novamente",
|
||||||
|
"refresh": "Atualizar",
|
||||||
|
"refreshSessions": "Atualizar sessões",
|
||||||
|
"newChatInDir": "Novo chat num diretório…",
|
||||||
|
"loadFailed": "Não foi possível carregar as sessões",
|
||||||
|
"reading": "A ler transcrições…",
|
||||||
|
"noSessions": "Ainda não há sessões",
|
||||||
|
"liveFailed": "Não foi possível ler as sessões ativas",
|
||||||
|
"asking": "A perguntar ao agente…",
|
||||||
|
"tabs": {
|
||||||
|
"tree": "Árvore",
|
||||||
|
"latest": "Recentes",
|
||||||
|
"live": "Ativas",
|
||||||
|
"agents": "Agentes",
|
||||||
|
"commanders": "Comandantes"
|
||||||
|
},
|
||||||
|
"notifyPreference": "Preferência de notificações",
|
||||||
|
"notify": {
|
||||||
|
"everyTurn": "Ao fim de cada turno",
|
||||||
|
"inputNeeded": "Apenas quando é preciso responder",
|
||||||
|
"none": "Nenhuma"
|
||||||
|
},
|
||||||
|
"notificationsLabel": "Notificações: {{choice}}",
|
||||||
|
"chooseDir": "Escolha um diretório de trabalho",
|
||||||
|
"create": "Criar",
|
||||||
|
"newFolder": "Nova pasta",
|
||||||
|
"selectFolder": "Selecionar esta pasta",
|
||||||
|
"newFolderName": "Nome da nova pasta",
|
||||||
|
"noSubfolders": {
|
||||||
|
"one": "Sem subpastas visíveis — {{count}} oculta foi filtrada",
|
||||||
|
"other": "Sem subpastas visíveis — {{count}} ocultas foram filtradas"
|
||||||
|
},
|
||||||
|
"showHidden": "Mostrar ocultas",
|
||||||
|
"showHiddenCount": "Mostrar ocultas ({{count}})",
|
||||||
|
"selectSession": "Selecione uma sessão para ver",
|
||||||
|
"endSession": "Terminar sessão — mata qualquer turno em execução e liberta-a para ser retomada noutro lugar",
|
||||||
|
"noReadable": "Não há conversas legíveis aqui.",
|
||||||
|
"noConversations": "Ainda não há conversas",
|
||||||
|
"newChatIn": "Novo chat em {{path}}",
|
||||||
|
"sessionTitle": "Título da sessão",
|
||||||
|
"saveTitle": "Guardar título",
|
||||||
|
"cancelRename": "Cancelar renomeação",
|
||||||
|
"confirmDelete": "Confirmar eliminação",
|
||||||
|
"cancelDelete": "Cancelar eliminação",
|
||||||
|
"rename": "Renomear",
|
||||||
|
"deleteSession": "Eliminar sessão",
|
||||||
|
"renameFailed": "Não foi possível renomear a sessão: {{reason}}",
|
||||||
|
"deleteFailed": "Não foi possível eliminar a sessão: {{reason}}",
|
||||||
|
"deleteAll": "Eliminar todas as {{count}}?",
|
||||||
|
"confirmDeletingNamed": "Confirmar a eliminação de {{name}}",
|
||||||
|
"renameNamed": "Renomear {{name}}",
|
||||||
|
"deleteNamed": "Eliminar {{name}}",
|
||||||
|
"newChatTitle": "Novo chat",
|
||||||
|
"renameHint": "Clique para renomear este chat",
|
||||||
|
"hideHidden": "Ocultar ocultas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user