Expand AITURK Turkish settings and desktop surfaces for beta 5
This commit is contained in:
1 parent
237d1e2060
commit
5e76f58637
108 files changed
+6675
-1034
No files matched your search
@@ -245,7 +245,7 @@ export function VoicePlaybackActivity() {
|
||||
variant="ghost"
|
||||
>
|
||||
<VolumeX className={iconSize.xs} />
|
||||
Stop
|
||||
{t.surfaces.stop}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { lazy, type ReactNode, Suspense } from 'react'
|
||||
|
||||
import { ContribBoundary, ContribRender } from '@/contrib/react/boundary'
|
||||
import { useContributions } from '@/contrib/react/use-contributions'
|
||||
import { translateNow, type Translations, useI18n } from '@/i18n'
|
||||
import { $routeTiles, closeRouteTile, type RouteTile } from '@/store/route-tiles'
|
||||
|
||||
import { ARTIFACTS_ROUTE, contributedRoutes, MESSAGING_ROUTE, ROUTES_AREA, SKILLS_ROUTE } from '../routes'
|
||||
@@ -22,10 +23,10 @@ const MessagingView = lazy(async () => ({ default: (await import('../messaging')
|
||||
const ArtifactsView = lazy(async () => ({ default: (await import('../artifacts')).ArtifactsView }))
|
||||
|
||||
// Built-in page views + their pane titles, keyed by route.
|
||||
const BUILTIN_PAGES: Record<string, { render: () => ReactNode; title: string }> = {
|
||||
[ARTIFACTS_ROUTE]: { render: () => <ArtifactsView />, title: 'Artifacts' },
|
||||
[MESSAGING_ROUTE]: { render: () => <MessagingView />, title: 'Messaging' },
|
||||
[SKILLS_ROUTE]: { render: () => <SkillsView />, title: 'Capabilities' }
|
||||
const BUILTIN_PAGES: Record<string, { render: () => ReactNode; titleKey: keyof Translations['sidebar']['nav'] }> = {
|
||||
[ARTIFACTS_ROUTE]: { render: () => <ArtifactsView />, titleKey: 'artifacts' },
|
||||
[MESSAGING_ROUTE]: { render: () => <MessagingView />, titleKey: 'messaging' },
|
||||
[SKILLS_ROUTE]: { render: () => <SkillsView />, titleKey: 'skills' }
|
||||
}
|
||||
|
||||
/** Humanize a route path into a tab title: `/my-atlas` → `My Atlas`. */
|
||||
@@ -41,13 +42,21 @@ const humanizePath = (path: string): string =>
|
||||
* else a humanized path — never the internal `${source}:${id}` key. */
|
||||
function routeTitle(path: string): string {
|
||||
if (BUILTIN_PAGES[path]) {
|
||||
return BUILTIN_PAGES[path].title
|
||||
return translateNow(`sidebar.nav.${BUILTIN_PAGES[path].titleKey}`)
|
||||
}
|
||||
|
||||
return contributedRoutes().find(r => r.path === path)?.title ?? humanizePath(path)
|
||||
}
|
||||
|
||||
function RouteTileTitle({ path }: { path: string }) {
|
||||
const { t } = useI18n()
|
||||
const builtin = BUILTIN_PAGES[path]
|
||||
|
||||
return <>{builtin ? t.sidebar.nav[builtin.titleKey] : routeTitle(path)}</>
|
||||
}
|
||||
|
||||
function RouteTilePane({ path }: { path: string }) {
|
||||
const { t } = useI18n()
|
||||
const builtin = BUILTIN_PAGES[path]
|
||||
|
||||
// Subscribe so a plugin page tile appears the moment its route registers.
|
||||
@@ -74,7 +83,7 @@ function RouteTilePane({ path }: { path: string }) {
|
||||
|
||||
return (
|
||||
<div className="grid h-full place-items-center font-mono text-[11px] text-(--ui-text-quaternary)">
|
||||
no page at {path}
|
||||
{t.surfaces.noPage(path)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -91,6 +100,7 @@ export const watchRouteTiles = paneMirror<RouteTile>({
|
||||
dir: t => t.dir,
|
||||
minWidth: '22rem',
|
||||
title: routeTitle,
|
||||
tabTitle: path => <RouteTileTitle path={path} />,
|
||||
render: path => <RouteTilePane path={path} />,
|
||||
close: closeRouteTile
|
||||
})
|
||||
@@ -333,6 +333,7 @@ function TileChat({
|
||||
}
|
||||
|
||||
export function SessionTilePane({ storedSessionId }: { storedSessionId: string }) {
|
||||
const { t } = useI18n()
|
||||
const tiles = useStore($sessionTiles)
|
||||
const tile = tiles.find(t => t.storedSessionId === storedSessionId)
|
||||
const ownerRoute = tile?.ownerRoute
|
||||
@@ -462,10 +463,10 @@ export function SessionTilePane({ storedSessionId }: { storedSessionId: string }
|
||||
return (
|
||||
<div className="grid h-full place-items-center p-4">
|
||||
<div className="max-w-[24rem] space-y-2 text-center font-mono text-[11px]">
|
||||
<div className="text-(--ui-danger,#f87171)">Couldn't open this session</div>
|
||||
<div className="text-(--ui-danger,#f87171)">{t.surfaces.sessionUnavailable}</div>
|
||||
<div className="break-words text-(--ui-text-quaternary)">{tile.error}</div>
|
||||
<Button onClick={() => patchSessionTile(storedSessionId, { error: undefined })} size="sm" variant="outline">
|
||||
Retry
|
||||
{t.common.retry}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,47 +72,6 @@ interface Option<T extends string = string> {
|
||||
label: string
|
||||
}
|
||||
|
||||
const GROUPINGS: Option<SidebarGrouping>[] = [
|
||||
{ icon: 'clock', id: 'date', label: 'Updated' },
|
||||
{ icon: 'root-folder', id: 'project', label: 'Project' },
|
||||
{ icon: 'pulse', id: 'status', label: 'Status' },
|
||||
{ icon: 'account', id: 'profile', label: 'Profile' }
|
||||
]
|
||||
|
||||
const ORDERINGS: Option<SidebarOrdering>[] = [
|
||||
{ icon: 'clock', id: 'updated', label: 'Updated' },
|
||||
{ icon: 'add', id: 'created', label: 'Created' },
|
||||
{ icon: 'pulse', id: 'status', label: 'Status' },
|
||||
{ icon: 'symbol-numeric', id: 'tokens', label: 'Tokens' },
|
||||
{ icon: 'credit-card', id: 'cost', label: 'Cost' },
|
||||
{ icon: 'list-ordered', id: 'manual', label: 'Manual' }
|
||||
]
|
||||
|
||||
const ROW_META: Option<SidebarRowMeta>[] = [
|
||||
{ icon: 'clock', id: 'updated', label: 'Updated' },
|
||||
{ icon: 'comment', id: 'preview', label: 'Preview' },
|
||||
{ icon: 'symbol-numeric', id: 'tokens', label: 'Tokens' },
|
||||
{ icon: 'credit-card', id: 'cost', label: 'Cost' },
|
||||
{ icon: 'git-pull-request', id: 'pr', label: 'PR' },
|
||||
{ icon: 'account', id: 'profile', label: 'Profile' }
|
||||
]
|
||||
|
||||
const PR_FILTERS: Option<PullRequestBucket>[] = [
|
||||
{ icon: 'git-pull-request', id: 'open', label: 'Open' },
|
||||
{ icon: 'git-pull-request-draft', id: 'draft', label: 'Draft' },
|
||||
{ icon: 'git-merge', id: 'merged', label: 'Merged' },
|
||||
{ icon: 'git-pull-request-closed', id: 'closed', label: 'Closed' },
|
||||
{ icon: 'circle-slash', id: 'none', label: 'No PR' }
|
||||
]
|
||||
|
||||
const STATUS_FILTERS: Option<SessionStatusBucket>[] = [
|
||||
{ dot: sessionDotClassName('needs-input'), id: 'needs-input', label: 'Needs input' },
|
||||
{ dot: sessionDotClassName('working'), id: 'working', label: 'Working' },
|
||||
{ dot: sessionDotClassName('unread'), id: 'unread', label: 'Unread' },
|
||||
{ dot: sessionDotClassName('draft'), id: 'draft', label: 'Draft' },
|
||||
{ dot: cn(sessionDotClassName('idle'), 'bg-(--ui-text-quaternary)'), id: 'idle', label: 'Idle' }
|
||||
]
|
||||
|
||||
function OptionGlyph({ option }: { option: Option }) {
|
||||
if (option.dot) {
|
||||
return <span aria-hidden="true" className={cn('shrink-0', option.dot)} />
|
||||
@@ -121,7 +80,7 @@ function OptionGlyph({ option }: { option: Option }) {
|
||||
return option.icon ? <Codicon className="text-(--ui-text-tertiary)" name={option.icon} size="0.8125rem" /> : null
|
||||
}
|
||||
|
||||
/** Every option row — single or multi select — leaves the menu open, so a whole
|
||||
/** Every option row — single or multi select — leaves the menu open, so a whole
|
||||
* view can be set up in one pass. Only the actions at the bottom dismiss it. */
|
||||
const keepOpen = (event: Event) => event.preventDefault()
|
||||
|
||||
@@ -151,6 +110,48 @@ function OptionRadio({ option }: { option: Option }) {
|
||||
|
||||
export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
const { t } = useI18n()
|
||||
|
||||
const GROUPINGS: Option<SidebarGrouping>[] = [
|
||||
{ icon: 'clock', id: 'date', label: t.surfaces.filters.updated },
|
||||
{ icon: 'root-folder', id: 'project', label: t.surfaces.filters.project },
|
||||
{ icon: 'pulse', id: 'status', label: t.surfaces.filters.status },
|
||||
{ icon: 'account', id: 'profile', label: t.surfaces.filters.profile }
|
||||
]
|
||||
|
||||
const ORDERINGS: Option<SidebarOrdering>[] = [
|
||||
{ icon: 'clock', id: 'updated', label: t.surfaces.filters.updated },
|
||||
{ icon: 'add', id: 'created', label: t.surfaces.filters.created },
|
||||
{ icon: 'pulse', id: 'status', label: t.surfaces.filters.status },
|
||||
{ icon: 'symbol-numeric', id: 'tokens', label: t.surfaces.filters.tokens },
|
||||
{ icon: 'credit-card', id: 'cost', label: t.surfaces.filters.cost },
|
||||
{ icon: 'list-ordered', id: 'manual', label: t.surfaces.filters.manual }
|
||||
]
|
||||
|
||||
const ROW_META: Option<SidebarRowMeta>[] = [
|
||||
{ icon: 'clock', id: 'updated', label: t.surfaces.filters.updated },
|
||||
{ icon: 'comment', id: 'preview', label: t.surfaces.filters.preview },
|
||||
{ icon: 'symbol-numeric', id: 'tokens', label: t.surfaces.filters.tokens },
|
||||
{ icon: 'credit-card', id: 'cost', label: t.surfaces.filters.cost },
|
||||
{ icon: 'git-pull-request', id: 'pr', label: t.surfaces.filters.pr },
|
||||
{ icon: 'account', id: 'profile', label: t.surfaces.filters.profile }
|
||||
]
|
||||
|
||||
const PR_FILTERS: Option<PullRequestBucket>[] = [
|
||||
{ icon: 'git-pull-request', id: 'open', label: t.surfaces.filters.open },
|
||||
{ icon: 'git-pull-request-draft', id: 'draft', label: t.surfaces.filters.draft },
|
||||
{ icon: 'git-merge', id: 'merged', label: t.surfaces.filters.merged },
|
||||
{ icon: 'git-pull-request-closed', id: 'closed', label: t.surfaces.filters.closed },
|
||||
{ icon: 'circle-slash', id: 'none', label: t.surfaces.filters.noPr }
|
||||
]
|
||||
|
||||
const STATUS_FILTERS: Option<SessionStatusBucket>[] = [
|
||||
{ dot: sessionDotClassName('needs-input'), id: 'needs-input', label: t.surfaces.filters.needsInput },
|
||||
{ dot: sessionDotClassName('working'), id: 'working', label: t.surfaces.filters.working },
|
||||
{ dot: sessionDotClassName('unread'), id: 'unread', label: t.surfaces.filters.unread },
|
||||
{ dot: sessionDotClassName('draft'), id: 'draft', label: t.surfaces.filters.draft },
|
||||
{ dot: cn(sessionDotClassName('idle'), 'bg-(--ui-text-quaternary)'), id: 'idle', label: t.surfaces.filters.idle }
|
||||
]
|
||||
|
||||
const grouping = useStore($sidebarGrouping)
|
||||
const ordering = useStore($sidebarOrdering)
|
||||
const rowMeta = useStore($sidebarRowMeta)
|
||||
@@ -170,7 +171,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
const projects = useStore($projectTree)
|
||||
const hasCost = useStore($sessionsHaveCost)
|
||||
const unreadIds = useStore($unreadFinishedSessionIds)
|
||||
// PR state comes from `gh` on whichever machine holds the checkout — Electron
|
||||
// PR state comes from `gh` on whichever machine holds the checkout — Electron
|
||||
// locally, the gateway's REST mirror remotely. Resolved per render, not once
|
||||
// at module load: switching to a remote profile swaps the bridge underneath.
|
||||
const prAvailable = Boolean(desktopGit()?.review?.prList)
|
||||
@@ -217,12 +218,12 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label="Filters"
|
||||
aria-label={t.surfaces.filters.filters}
|
||||
className={cn(
|
||||
className,
|
||||
'data-[state=open]:bg-(--ui-control-active-background) data-[state=open]:text-foreground data-[state=open]:opacity-100',
|
||||
// Active filters read as "this control is engaged", the same way the
|
||||
// open menu does — never as an accent, which the sidebar reserves
|
||||
// open menu does — never as an accent, which the sidebar reserves
|
||||
// for a session that is actually doing something.
|
||||
filtersActive && 'bg-(--ui-control-active-background) text-foreground opacity-100'
|
||||
)}
|
||||
@@ -238,7 +239,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger hideChevron>
|
||||
Grouping
|
||||
{t.surfaces.filters.grouping}
|
||||
<span className="ml-auto flex items-center gap-1 pl-4 text-(--ui-text-tertiary)">
|
||||
{groupingLabel}
|
||||
<Codicon name="chevron-right" size="1rem" />
|
||||
@@ -257,7 +258,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Ordering</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubTrigger>{t.surfaces.filters.ordering}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={value => setSidebarOrdering(value as SidebarOrdering)}
|
||||
@@ -271,7 +272,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Show</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubTrigger>{t.surfaces.filters.show}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{rowMetaOptions.map(option => (
|
||||
<OptionCheckbox
|
||||
@@ -284,22 +285,22 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
{/* A render variant, not a grouping: three-line cards (project · age /
|
||||
title / model · size) compose with whichever grouping is active. */}
|
||||
{/* A render variant, not a grouping: three-line cards (project · age /
|
||||
title / model · size) compose with whichever grouping is active. */}
|
||||
<OptionCheckbox
|
||||
checked={cardRows}
|
||||
onCheck={() => setSidebarCardRows(!cardRows)}
|
||||
option={{ icon: 'inbox', id: 'card-rows', label: 'Inbox style' }}
|
||||
option={{ icon: 'inbox', id: 'card-rows', label: t.surfaces.filters.inbox }}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Filters</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>{t.surfaces.filters.filters}</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Status</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubTrigger>{t.surfaces.filters.status}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{STATUS_FILTERS.map(option => (
|
||||
<OptionCheckbox
|
||||
@@ -316,7 +317,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
this submenu never appears rather than filtering everything out. */}
|
||||
{prAvailable && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Pull request</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubTrigger>{t.surfaces.filters.pullRequest}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{PR_FILTERS.map(option => (
|
||||
<OptionCheckbox
|
||||
@@ -331,7 +332,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
)}
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Profile</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubTrigger>{t.surfaces.filters.profile}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="max-h-80 overflow-y-auto">
|
||||
{/* Scoped to one profile the rail is already the filter, so the
|
||||
per-profile boxes only appear where they can narrow something.
|
||||
@@ -358,7 +359,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
|
||||
{projects.length > 1 && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Project</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubTrigger>{t.surfaces.filters.project}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="max-h-80 overflow-y-auto">
|
||||
{projects.map(project => (
|
||||
<OptionCheckbox
|
||||
@@ -378,7 +379,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
)}
|
||||
|
||||
{/* Off by default: one profile's sessions are what the rail selected.
|
||||
Nothing to widen to until a second profile exists — but stay
|
||||
Nothing to widen to until a second profile exists — but stay
|
||||
visible while it's on, or deleting your way back down to one
|
||||
profile would strand the sidebar in a mode nothing can leave (the
|
||||
rail hides its switcher at one profile too). */}
|
||||
@@ -393,23 +394,25 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
||||
<OptionCheckbox
|
||||
checked={showArchived}
|
||||
onCheck={() => setSidebarShowArchived(!showArchived)}
|
||||
option={{ id: 'archived', label: 'Archived' }}
|
||||
option={{ id: 'archived', label: t.surfaces.filters.archived }}
|
||||
/>
|
||||
|
||||
{/* One way back rather than two near-identical ones: this drops the
|
||||
grouping and sort too, which "clear filters" left behind. */}
|
||||
{viewCustomized && <DropdownMenuItem onSelect={resetSidebarView}>Reset to defaults</DropdownMenuItem>}
|
||||
{viewCustomized && (
|
||||
<DropdownMenuItem onSelect={resetSidebarView}>{t.surfaces.filters.reset}</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{foldIds.length > 0 && (
|
||||
<DropdownMenuItem onSelect={() => setWorkspaceNodesOpen(foldIds, foldCollapsed)}>
|
||||
{foldCollapsed ? 'Expand all' : 'Collapse all'}
|
||||
{foldCollapsed ? t.surfaces.filters.expand : t.surfaces.filters.collapse}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem disabled={unreadIds.length === 0} onSelect={markAllSessionsRead}>
|
||||
Mark all as read
|
||||
{t.surfaces.filters.markRead}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
Reference in new issue
Block a user