sidebar ui changes, loading state fixes (#2039)

* tons of sidebar ui cleanup, loading states etc

* lint fix

* fix archive store

* i18n

---------

Co-authored-by: Louis Knight-Webb <louis@bloop.ai>
This commit is contained in:
Theo Browne
2026-01-14 05:14:50 -08:00
committed by GitHub
parent add92d94f4
commit d54a46209b
12 changed files with 328 additions and 116 deletions

View File

@@ -7,9 +7,8 @@ import {
VirtuosoMessageListProps, VirtuosoMessageListProps,
} from '@virtuoso.dev/message-list'; } from '@virtuoso.dev/message-list';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SpinnerGapIcon } from '@phosphor-icons/react';
import { cn } from '@/lib/utils';
import NewDisplayConversationEntry from './NewDisplayConversationEntry'; import NewDisplayConversationEntry from './NewDisplayConversationEntry';
import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext'; import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
import { useEntries } from '@/contexts/EntriesContext'; import { useEntries } from '@/contexts/EntriesContext';
@@ -86,7 +85,6 @@ const computeItemKey: VirtuosoMessageListProps<
>['computeItemKey'] = ({ data }) => `conv-${data.patchKey}`; >['computeItemKey'] = ({ data }) => `conv-${data.patchKey}`;
export function ConversationList({ attempt, task }: ConversationListProps) { export function ConversationList({ attempt, task }: ConversationListProps) {
const { t } = useTranslation('common');
const [channelData, setChannelData] = const [channelData, setChannelData] =
useState<DataWithScrollModifier<PatchTypeWithKey> | null>(null); useState<DataWithScrollModifier<PatchTypeWithKey> | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -156,29 +154,33 @@ export function ConversationList({ attempt, task }: ConversationListProps) {
[attempt, task] [attempt, task]
); );
// Determine if content is ready to show (has data or finished loading)
const hasContent = !loading || (channelData?.data?.length ?? 0) > 0;
return ( return (
<ApprovalFormProvider> <ApprovalFormProvider>
<VirtuosoMessageListLicense <div
licenseKey={import.meta.env.VITE_PUBLIC_REACT_VIRTUOSO_LICENSE_KEY} className={cn(
'h-full transition-opacity duration-300',
hasContent ? 'opacity-100' : 'opacity-0'
)}
> >
<VirtuosoMessageList<PatchTypeWithKey, MessageListContext> <VirtuosoMessageListLicense
ref={messageListRef} licenseKey={import.meta.env.VITE_PUBLIC_REACT_VIRTUOSO_LICENSE_KEY}
className="h-full scrollbar-none" >
data={channelData} <VirtuosoMessageList<PatchTypeWithKey, MessageListContext>
initialLocation={INITIAL_TOP_ITEM} ref={messageListRef}
context={messageListContext} className="h-full scrollbar-none"
computeItemKey={computeItemKey} data={channelData}
ItemContent={ItemContent} initialLocation={INITIAL_TOP_ITEM}
Header={() => <div className="h-2" />} context={messageListContext}
Footer={() => <div className="h-2" />} computeItemKey={computeItemKey}
/> ItemContent={ItemContent}
</VirtuosoMessageListLicense> Header={() => <div className="h-2" />}
{loading && !channelData?.data?.length && ( Footer={() => <div className="h-2" />}
<div className="absolute inset-0 bg-primary flex flex-col gap-2 justify-center items-center"> />
<SpinnerGapIcon className="h-8 w-8 animate-spin" /> </VirtuosoMessageListLicense>
<p>{t('states.loadingHistory')}</p> </div>
</div>
)}
</ApprovalFormProvider> </ApprovalFormProvider>
); );
} }

View File

@@ -944,11 +944,7 @@ export type NavbarItem = ActionDefinition | typeof NavbarDivider;
// Navbar action groups define which actions appear in each section // Navbar action groups define which actions appear in each section
export const NavbarActionGroups = { export const NavbarActionGroups = {
left: [ left: [Actions.OpenInOldUI] as ActionDefinition[],
Actions.OpenInOldUI,
NavbarDivider,
Actions.ArchiveWorkspace,
] as ActionDefinition[],
right: [ right: [
Actions.ToggleDiffViewMode, Actions.ToggleDiffViewMode,
Actions.ToggleAllDiffs, Actions.ToggleAllDiffs,

View File

@@ -545,9 +545,43 @@ export function SessionChatBoxContainer({
localMessage, localMessage,
]); ]);
// Don't render if no session and not in new session mode // Render placeholder state if no session and not in new session mode
if (!session && !isNewSessionMode) { // This maintains the visual structure during workspace transitions
return null; const isPlaceholderMode = !session && !isNewSessionMode;
// In placeholder mode, render a disabled version to maintain visual structure
if (isPlaceholderMode) {
return (
<SessionChatBox
status="idle"
workspaceId={workspaceId}
projectId={projectId}
editor={{
value: '',
onChange: () => {},
}}
actions={{
onSend: () => {},
onQueue: () => {},
onCancelQueue: () => {},
onStop: () => {},
onPasteFiles: () => {},
}}
session={{
sessions: [],
selectedSessionId: undefined,
onSelectSession: () => {},
isNewSessionMode: false,
onNewSession: undefined,
}}
stats={{
filesChanged: 0,
linesAdded: 0,
linesRemoved: 0,
onViewCode: undefined,
}}
/>
);
} }
return ( return (

View File

@@ -40,6 +40,7 @@ import {
PERSIST_KEYS, PERSIST_KEYS,
useExpandedAll, useExpandedAll,
usePaneSize, usePaneSize,
usePersistedExpanded,
} from '@/stores/useUiPreferencesStore'; } from '@/stores/useUiPreferencesStore';
import { import {
useLayoutStore, useLayoutStore,
@@ -304,6 +305,10 @@ export function WorkspacesLayout() {
50 50
); );
const isRightMainPanelVisible = useIsRightMainPanelVisible(); const isRightMainPanelVisible = useIsRightMainPanelVisible();
const [showArchive, setShowArchive] = usePersistedExpanded(
PERSIST_KEYS.workspacesSidebarArchived,
false
);
const defaultLayout = (): Layout => { const defaultLayout = (): Layout => {
let layout = { 'left-main': 50, 'right-main': 50 }; let layout = { 'left-main': 50, 'right-main': 50 };
@@ -710,6 +715,23 @@ export function WorkspacesLayout() {
); );
}; };
// Action handlers for sidebar workspace actions
const { executeAction } = useActions();
const handleArchiveWorkspace = useCallback(
(workspaceId: string) => {
executeAction(Actions.ArchiveWorkspace, workspaceId);
},
[executeAction]
);
const handlePinWorkspace = useCallback(
(workspaceId: string) => {
executeAction(Actions.PinWorkspace, workspaceId);
},
[executeAction]
);
// Render sidebar with persisted draft title // Render sidebar with persisted draft title
const renderSidebar = () => ( const renderSidebar = () => (
<WorkspacesSidebar <WorkspacesSidebar
@@ -720,9 +742,13 @@ export function WorkspacesLayout() {
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={setSearchQuery} onSearchChange={setSearchQuery}
onAddWorkspace={navigateToCreate} onAddWorkspace={navigateToCreate}
onArchiveWorkspace={handleArchiveWorkspace}
onPinWorkspace={handlePinWorkspace}
isCreateMode={isCreateMode} isCreateMode={isCreateMode}
draftTitle={persistedDraftTitle} draftTitle={persistedDraftTitle}
onSelectCreate={navigateToCreate} onSelectCreate={navigateToCreate}
showArchive={showArchive}
onShowArchiveChange={setShowArchive}
/> />
); );

View File

@@ -1,12 +1,13 @@
import { import {
PushPinIcon, PushPinIcon,
DotsThreeIcon,
HandIcon, HandIcon,
TriangleIcon, TriangleIcon,
PlayIcon, PlayIcon,
FileIcon, FileIcon,
CircleIcon, CircleIcon,
GitPullRequestIcon, GitPullRequestIcon,
ArchiveIcon,
ListIcon,
} from '@phosphor-icons/react'; } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -30,6 +31,8 @@ interface WorkspaceSummaryProps {
latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed'; latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed';
prStatus?: 'open' | 'merged' | 'closed' | 'unknown'; prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
onClick?: () => void; onClick?: () => void;
onArchive?: () => void;
onPin?: () => void;
className?: string; className?: string;
summary?: boolean; summary?: boolean;
/** Whether this is a draft workspace (shows "Draft" instead of elapsed time) */ /** Whether this is a draft workspace (shows "Draft" instead of elapsed time) */
@@ -52,6 +55,8 @@ export function WorkspaceSummary({
latestProcessStatus, latestProcessStatus,
prStatus, prStatus,
onClick, onClick,
onArchive,
onPin,
className, className,
summary = false, summary = false,
isDraft = false, isDraft = false,
@@ -69,21 +74,41 @@ export function WorkspaceSummary({
}); });
}; };
const handleArchive = (e: React.MouseEvent) => {
e.stopPropagation();
onArchive?.();
};
const handlePin = (e: React.MouseEvent) => {
e.stopPropagation();
onPin?.();
};
return ( return (
<div className={cn('group relative', className)}> <div
className={cn(
'group relative rounded-sm transition-all duration-100 overflow-hidden',
isActive ? 'bg-tertiary' : '',
className
)}
>
{/* Selection indicator - thin colored tab on the left */}
<div
className={cn(
'absolute left-0 top-1 bottom-1 w-0.5 rounded-full transition-colors duration-100',
isActive ? 'bg-brand' : 'bg-transparent'
)}
/>
<button <button
onClick={onClick} onClick={onClick}
className={cn( className={cn(
'flex w-full cursor-pointer flex-col border-l-4 text-left text-low', 'flex w-full cursor-pointer flex-col text-left px-base py-half transition-all duration-150',
isActive ? 'border-normal pl-base' : 'border-none' isActive
? 'text-normal'
: 'text-low opacity-60 hover:opacity-100 hover:text-normal'
)} )}
> >
<div <div className={cn('truncate pr-double', !summary && 'text-normal')}>
className={cn(
'truncate group-hover:text-high pr-double',
!summary && 'text-normal'
)}
>
{name} {name}
</div> </div>
{(!summary || isActive) && ( {(!summary || isActive) && (
@@ -179,15 +204,54 @@ export function WorkspaceSummary({
)} )}
</button> </button>
{/* Right-side hover zone for action overlay */}
{workspaceId && ( {workspaceId && (
<div className="absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="absolute right-0 top-0 bottom-0 w-16 group/actions">
<button {/* Sliding action overlay - only appears when hovering this zone */}
onClick={handleOpenCommandBar} <div
onPointerDown={(e) => e.stopPropagation()} className={cn(
className="p-half rounded-sm hover:bg-tertiary text-low hover:text-high focus:outline-none" 'absolute right-0 top-0 bottom-0 flex items-center',
'translate-x-full group-hover/actions:translate-x-0',
'transition-transform duration-150 ease-out'
)}
> >
<DotsThreeIcon className="size-icon-sm" weight="bold" /> {/* Gradient fade from transparent to pill background */}
</button> <div className="h-full w-6 pointer-events-none bg-gradient-to-r from-transparent to-secondary" />
{/* Action pill */}
<div className="flex items-center gap-0.5 pr-base h-full bg-secondary">
<button
onClick={handlePin}
onPointerDown={(e) => e.stopPropagation()}
className={cn(
'p-1.5 rounded-sm transition-colors duration-100',
'hover:bg-tertiary',
isPinned ? 'text-brand' : 'text-low hover:text-normal'
)}
title={isPinned ? t('workspaces.unpin') : t('workspaces.pin')}
>
<PushPinIcon
className="size-icon-xs"
weight={isPinned ? 'fill' : 'regular'}
/>
</button>
<button
onClick={handleArchive}
onPointerDown={(e) => e.stopPropagation()}
className="p-1.5 rounded-sm text-low hover:text-normal hover:bg-tertiary transition-colors duration-100"
title={t('workspaces.archive')}
>
<ArchiveIcon className="size-icon-xs" />
</button>
<button
onClick={handleOpenCommandBar}
onPointerDown={(e) => e.stopPropagation()}
className="p-1.5 rounded-sm text-low hover:text-normal hover:bg-tertiary transition-colors duration-100"
title={t('workspaces.more')}
>
<ListIcon className="size-icon-xs" />
</button>
</div>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,11 +1,9 @@
import { PlusIcon } from '@phosphor-icons/react'; import { PlusIcon, ArrowLeftIcon, ArchiveIcon } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { Workspace } from '@/components/ui-new/hooks/useWorkspaces'; import type { Workspace } from '@/components/ui-new/hooks/useWorkspaces';
import { CollapsibleSection } from '@/components/ui-new/primitives/CollapsibleSection';
import { InputField } from '@/components/ui-new/primitives/InputField'; import { InputField } from '@/components/ui-new/primitives/InputField';
import { WorkspaceSummary } from '@/components/ui-new/primitives/WorkspaceSummary'; import { WorkspaceSummary } from '@/components/ui-new/primitives/WorkspaceSummary';
import { SectionHeader } from '../primitives/SectionHeader'; import { SectionHeader } from '../primitives/SectionHeader';
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
interface WorkspacesSidebarProps { interface WorkspacesSidebarProps {
workspaces: Workspace[]; workspaces: Workspace[];
@@ -13,6 +11,8 @@ interface WorkspacesSidebarProps {
selectedWorkspaceId: string | null; selectedWorkspaceId: string | null;
onSelectWorkspace: (id: string) => void; onSelectWorkspace: (id: string) => void;
onAddWorkspace?: () => void; onAddWorkspace?: () => void;
onArchiveWorkspace?: (id: string) => void;
onPinWorkspace?: (id: string) => void;
searchQuery: string; searchQuery: string;
onSearchChange: (value: string) => void; onSearchChange: (value: string) => void;
/** Whether we're in create mode */ /** Whether we're in create mode */
@@ -21,6 +21,10 @@ interface WorkspacesSidebarProps {
draftTitle?: string; draftTitle?: string;
/** Handler to navigate back to create mode */ /** Handler to navigate back to create mode */
onSelectCreate?: () => void; onSelectCreate?: () => void;
/** Whether to show archived workspaces */
showArchive?: boolean;
/** Handler for toggling archive view */
onShowArchiveChange?: (show: boolean) => void;
} }
export function WorkspacesSidebar({ export function WorkspacesSidebar({
@@ -29,11 +33,15 @@ export function WorkspacesSidebar({
selectedWorkspaceId, selectedWorkspaceId,
onSelectWorkspace, onSelectWorkspace,
onAddWorkspace, onAddWorkspace,
onArchiveWorkspace,
onPinWorkspace,
searchQuery, searchQuery,
onSearchChange, onSearchChange,
isCreateMode = false, isCreateMode = false,
draftTitle, draftTitle,
onSelectCreate, onSelectCreate,
showArchive = false,
onShowArchiveChange,
}: WorkspacesSidebarProps) { }: WorkspacesSidebarProps) {
const { t } = useTranslation(['tasks', 'common']); const { t } = useTranslation(['tasks', 'common']);
const searchLower = searchQuery.toLowerCase(); const searchLower = searchQuery.toLowerCase();
@@ -48,8 +56,11 @@ export function WorkspacesSidebar({
.filter((workspace) => workspace.name.toLowerCase().includes(searchLower)) .filter((workspace) => workspace.name.toLowerCase().includes(searchLower))
.slice(0, isSearching ? undefined : DISPLAY_LIMIT); .slice(0, isSearching ? undefined : DISPLAY_LIMIT);
const hasArchivedWorkspaces = archivedWorkspaces.length > 0;
return ( return (
<div className="w-full h-full bg-secondary flex flex-col"> <div className="w-full h-full bg-secondary flex flex-col">
{/* Header + Search */}
<div className="flex flex-col gap-base"> <div className="flex flex-col gap-base">
<SectionHeader <SectionHeader
title={t('common:workspaces.title')} title={t('common:workspaces.title')}
@@ -65,72 +76,109 @@ export function WorkspacesSidebar({
/> />
</div> </div>
</div> </div>
<div className="flex flex-col flex-1 overflow-y-auto">
<CollapsibleSection {/* Scrollable workspace list */}
persistKey={PERSIST_KEYS.workspacesSidebarActive} <div className="flex-1 overflow-y-auto p-base">
title={t('common:workspaces.active')} {showArchive ? (
defaultExpanded /* Archived workspaces view */
className="p-base" <div className="flex flex-col gap-base">
contentClassName="flex flex-col gap-base min-h-[50vh]" <span className="text-sm font-medium text-low">
> {t('common:workspaces.archived')}
{draftTitle && ( </span>
<WorkspaceSummary {filteredArchivedWorkspaces.length === 0 ? (
name={draftTitle} <span className="text-sm text-low opacity-60">
isActive={isCreateMode} {t('common:workspaces.noArchived')}
isDraft={true} </span>
onClick={onSelectCreate} ) : (
/> filteredArchivedWorkspaces.map((workspace) => (
)} <WorkspaceSummary
{filteredWorkspaces.map((workspace) => ( summary
<WorkspaceSummary key={workspace.id}
key={workspace.id} name={workspace.name}
name={workspace.name} workspaceId={workspace.id}
workspaceId={workspace.id} filesChanged={workspace.filesChanged}
filesChanged={workspace.filesChanged} linesAdded={workspace.linesAdded}
linesAdded={workspace.linesAdded} linesRemoved={workspace.linesRemoved}
linesRemoved={workspace.linesRemoved} isActive={selectedWorkspaceId === workspace.id}
isActive={selectedWorkspaceId === workspace.id} isRunning={workspace.isRunning}
isRunning={workspace.isRunning} isPinned={workspace.isPinned}
isPinned={workspace.isPinned} hasPendingApproval={workspace.hasPendingApproval}
hasPendingApproval={workspace.hasPendingApproval} hasRunningDevServer={workspace.hasRunningDevServer}
hasRunningDevServer={workspace.hasRunningDevServer} hasUnseenActivity={workspace.hasUnseenActivity}
hasUnseenActivity={workspace.hasUnseenActivity} latestProcessCompletedAt={workspace.latestProcessCompletedAt}
latestProcessCompletedAt={workspace.latestProcessCompletedAt} latestProcessStatus={workspace.latestProcessStatus}
latestProcessStatus={workspace.latestProcessStatus} prStatus={workspace.prStatus}
prStatus={workspace.prStatus} onClick={() => onSelectWorkspace(workspace.id)}
onClick={() => onSelectWorkspace(workspace.id)} onArchive={() => onArchiveWorkspace?.(workspace.id)}
/> onPin={() => onPinWorkspace?.(workspace.id)}
))} />
</CollapsibleSection> ))
<CollapsibleSection )}
persistKey={PERSIST_KEYS.workspacesSidebarArchived} </div>
title={t('common:workspaces.archived')} ) : (
defaultExpanded /* Active workspaces view */
className="px-base pb-half" <div className="flex flex-col gap-base">
> <span className="text-sm font-medium text-low">
{filteredArchivedWorkspaces.map((workspace) => ( {t('common:workspaces.active')}
<WorkspaceSummary </span>
summary {draftTitle && (
key={workspace.id} <WorkspaceSummary
name={workspace.name} name={draftTitle}
workspaceId={workspace.id} isActive={isCreateMode}
filesChanged={workspace.filesChanged} isDraft={true}
linesAdded={workspace.linesAdded} onClick={onSelectCreate}
linesRemoved={workspace.linesRemoved} />
isActive={selectedWorkspaceId === workspace.id} )}
isRunning={workspace.isRunning} {filteredWorkspaces.map((workspace) => (
isPinned={workspace.isPinned} <WorkspaceSummary
hasPendingApproval={workspace.hasPendingApproval} key={workspace.id}
hasRunningDevServer={workspace.hasRunningDevServer} name={workspace.name}
hasUnseenActivity={workspace.hasUnseenActivity} workspaceId={workspace.id}
latestProcessCompletedAt={workspace.latestProcessCompletedAt} filesChanged={workspace.filesChanged}
latestProcessStatus={workspace.latestProcessStatus} linesAdded={workspace.linesAdded}
prStatus={workspace.prStatus} linesRemoved={workspace.linesRemoved}
onClick={() => onSelectWorkspace(workspace.id)} isActive={selectedWorkspaceId === workspace.id}
/> isRunning={workspace.isRunning}
))} isPinned={workspace.isPinned}
</CollapsibleSection> hasPendingApproval={workspace.hasPendingApproval}
hasRunningDevServer={workspace.hasRunningDevServer}
hasUnseenActivity={workspace.hasUnseenActivity}
latestProcessCompletedAt={workspace.latestProcessCompletedAt}
latestProcessStatus={workspace.latestProcessStatus}
prStatus={workspace.prStatus}
onClick={() => onSelectWorkspace(workspace.id)}
onArchive={() => onArchiveWorkspace?.(workspace.id)}
onPin={() => onPinWorkspace?.(workspace.id)}
/>
))}
</div>
)}
</div> </div>
{/* Fixed footer toggle - only show if there are archived workspaces */}
{hasArchivedWorkspaces && (
<div className="border-t border-primary p-base">
<button
onClick={() => onShowArchiveChange?.(!showArchive)}
className="w-full flex items-center gap-base text-sm text-low hover:text-normal transition-colors duration-100"
>
{showArchive ? (
<>
<ArrowLeftIcon className="size-icon-xs" />
<span>{t('common:workspaces.backToActive')}</span>
</>
) : (
<>
<ArchiveIcon className="size-icon-xs" />
<span>{t('common:workspaces.viewArchive')}</span>
<span className="ml-auto text-xs bg-tertiary px-1.5 py-0.5 rounded">
{archivedWorkspaces.length}
</span>
</>
)}
</button>
</div>
)}
</div> </div>
); );
} }

View File

@@ -131,6 +131,13 @@
"loading": "Loading...", "loading": "Loading...",
"selectToStart": "Select a workspace to get started", "selectToStart": "Select a workspace to get started",
"draft": "Draft", "draft": "Draft",
"viewArchive": "View Archive",
"backToActive": "Back to Active",
"noArchived": "No archived workspaces",
"pin": "Pin",
"unpin": "Unpin",
"archive": "Archive",
"more": "More actions",
"rename": { "rename": {
"title": "Rename Workspace", "title": "Rename Workspace",
"description": "Enter a new name for this workspace.", "description": "Enter a new name for this workspace.",

View File

@@ -131,6 +131,13 @@
"loading": "Cargando...", "loading": "Cargando...",
"selectToStart": "Selecciona un espacio de trabajo para comenzar", "selectToStart": "Selecciona un espacio de trabajo para comenzar",
"draft": "Borrador", "draft": "Borrador",
"viewArchive": "Ver archivo",
"backToActive": "Volver a activos",
"noArchived": "No hay espacios de trabajo archivados",
"pin": "Fijar",
"unpin": "Desfijar",
"archive": "Archivar",
"more": "Más acciones",
"rename": { "rename": {
"title": "Renombrar espacio de trabajo", "title": "Renombrar espacio de trabajo",
"description": "Ingresa un nuevo nombre para este espacio de trabajo.", "description": "Ingresa un nuevo nombre para este espacio de trabajo.",

View File

@@ -131,6 +131,13 @@
"loading": "読み込み中...", "loading": "読み込み中...",
"selectToStart": "ワークスペースを選択して開始", "selectToStart": "ワークスペースを選択して開始",
"draft": "下書き", "draft": "下書き",
"viewArchive": "アーカイブを表示",
"backToActive": "アクティブに戻る",
"noArchived": "アーカイブされたワークスペースはありません",
"pin": "ピン留め",
"unpin": "ピン留め解除",
"archive": "アーカイブ",
"more": "その他の操作",
"rename": { "rename": {
"title": "ワークスペースの名前を変更", "title": "ワークスペースの名前を変更",
"description": "このワークスペースの新しい名前を入力してください。", "description": "このワークスペースの新しい名前を入力してください。",

View File

@@ -131,6 +131,13 @@
"loading": "로딩 중...", "loading": "로딩 중...",
"selectToStart": "워크스페이스를 선택하여 시작", "selectToStart": "워크스페이스를 선택하여 시작",
"draft": "초안", "draft": "초안",
"viewArchive": "보관함 보기",
"backToActive": "활성으로 돌아가기",
"noArchived": "보관된 워크스페이스 없음",
"pin": "고정",
"unpin": "고정 해제",
"archive": "보관",
"more": "더 많은 작업",
"rename": { "rename": {
"title": "워크스페이스 이름 변경", "title": "워크스페이스 이름 변경",
"description": "이 워크스페이스의 새 이름을 입력하세요.", "description": "이 워크스페이스의 새 이름을 입력하세요.",

View File

@@ -131,6 +131,13 @@
"loading": "加载中...", "loading": "加载中...",
"selectToStart": "选择一个工作区开始", "selectToStart": "选择一个工作区开始",
"draft": "草稿", "draft": "草稿",
"viewArchive": "查看归档",
"backToActive": "返回活跃",
"noArchived": "没有已归档的工作区",
"pin": "置顶",
"unpin": "取消置顶",
"archive": "归档",
"more": "更多操作",
"rename": { "rename": {
"title": "重命名工作区", "title": "重命名工作区",
"description": "输入此工作区的新名称。", "description": "输入此工作区的新名称。",

View File

@@ -131,6 +131,13 @@
"loading": "載入中...", "loading": "載入中...",
"selectToStart": "選擇一個工作區開始", "selectToStart": "選擇一個工作區開始",
"draft": "草稿", "draft": "草稿",
"viewArchive": "檢視封存",
"backToActive": "返回活躍",
"noArchived": "沒有已封存的工作區",
"pin": "釘選",
"unpin": "取消釘選",
"archive": "封存",
"more": "更多操作",
"rename": { "rename": {
"title": "重新命名工作區", "title": "重新命名工作區",
"description": "輸入此工作區的新名稱。", "description": "輸入此工作區的新名稱。",