Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions apps/desktop/src/renderer/locales/settings-tasks-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ export type SettingsTasksCopy = {
purgeUnverified: string;
noMatchTitle: string;
noMatchBody: string;
moreActions(name: string): string;
restore: string;
restoreTask(name: string): string;
unarchive: string;
unarchiveTask(name: string): string;
delete: string;
deleteTask(name: string): string;
emptyTitle: string;
emptyBody: string;
};
Expand All @@ -80,10 +80,10 @@ const SETTINGS_TASKS_COPY_BY_LOCALE = {
purgeUnverified: '任务已删除,但无法读取列表确认结果。请重新打开本页查看。',
noMatchTitle: '没有匹配的任务',
noMatchBody: '换个关键词试试。',
moreActions: (name: string) => `「${name}」的更多操作`,
restore: '恢复',
restoreTask: (name: string) => `恢复「${name}」`,
unarchive: '取消归档',
unarchiveTask: (name: string) => `取消归档「${name}」`,
delete: '彻底删除',
deleteTask: (name: string) => `彻底删除「${name}」`,
emptyTitle: '没有已归档的任务',
emptyBody: '在侧栏里归档一个任务后,可以在这里恢复或彻底删除它。',
},
Expand All @@ -107,10 +107,10 @@ const SETTINGS_TASKS_COPY_BY_LOCALE = {
purgeUnverified: '任務已刪除,但無法讀取列表確認結果。請重新開啟本頁檢視。',
noMatchTitle: '沒有符合的任務',
noMatchBody: '換個關鍵詞試試。',
moreActions: (name: string) => `「${name}」的更多操作`,
restore: '恢復',
restoreTask: (name: string) => `恢復「${name}」`,
unarchive: '取消歸檔',
unarchiveTask: (name: string) => `取消歸檔「${name}」`,
delete: '徹底刪除',
deleteTask: (name: string) => `徹底刪除「${name}」`,
emptyTitle: '沒有已歸檔的任務',
emptyBody: '在側欄裡歸檔一個任務後,可以在這裡恢復或徹底刪除它。',
},
Expand Down Expand Up @@ -142,10 +142,10 @@ const SETTINGS_TASKS_COPY_BY_LOCALE = {
purgeUnverified: 'The tasks were deleted, but the list could not be read back to confirm. Reopen this page to check.',
noMatchTitle: 'No matching tasks',
noMatchBody: 'Try a different search.',
moreActions: (name: string) => `More actions for ${name}`,
restore: 'Restore',
restoreTask: (name: string) => `Restore ${name}`,
unarchive: 'Unarchive',
unarchiveTask: (name: string) => `Unarchive ${name}`,
delete: 'Delete',
deleteTask: (name: string) => `Delete ${name}`,
emptyTitle: 'Nothing archived',
emptyBody: 'Archive a task from the rail to restore or permanently delete it here.',
},
Expand Down
26 changes: 14 additions & 12 deletions apps/desktop/src/renderer/settings/tasks-settings-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import { useCallback, useMemo, useState } from 'react';
import type { ProjectRecord } from '@maka/core/project';
import { formatCompactTimestamp } from '@maka/core/relative-time';
import { runtimeHostProfileUsesHostWorkspace } from '@maka/runtime-host/profile-kind';
import { Button, EmptyState, MoreMenu, useMountedRef, useToast, useUiLocale } from '@maka/ui';
import { Archive, ICON_SIZE, Search } from '@maka/ui/icons';
import { Button, EmptyState, IconButton, useMountedRef, useToast, useUiLocale } from '@maka/ui';
import { Archive, ICON_SIZE, Search, Trash2, Unarchive } from '@maka/ui/icons';
import { HStack, StackItem } from '@astryxdesign/core';
import { List, ListItem } from '@astryxdesign/core/List';
import { TextInput } from '@astryxdesign/core/TextInput';
Expand Down Expand Up @@ -236,25 +236,27 @@ export function TasksSettingsPage(props: ArchivedTasksBridge) {
startContent={<Archive size={ICON_SIZE.control} aria-hidden="true" />}
endContent={
<>
<Button
variant="secondary"
<IconButton
variant="ghost"
size="sm"
isDisabled={purging}
clickAction={() => props.onRestore(session.id)}
label={copy.restore}
// Every row's button reads 恢复; only the accessible
// name can say which task it restores.
aria-label={copy.restoreTask(session.name)}
label={copy.unarchiveTask(session.name)}
tooltip={copy.unarchive}
icon={<Unarchive size={ICON_SIZE.control} aria-hidden="true" />}
/>
{/* No 打开 here. An archived task has no rail row to
land on, and giving it one would make "the open task
is always visible in the rail" an invariant the rail
does not otherwise hold. Restore first. */}
<MoreMenu
label={copy.moreActions(session.name)}
does not otherwise hold. Unarchive first. */}
<IconButton
Comment thread
Phoenix500526 marked this conversation as resolved.
variant="ghost"
size="sm"
isDisabled={purging}
items={[{ label: copy.delete, onClick: () => props.onDelete(session.id) }]}
clickAction={() => props.onDelete(session.id)}
label={copy.deleteTask(session.name)}
tooltip={copy.delete}
icon={<Trash2 size={ICON_SIZE.control} aria-hidden="true" />}
/>
</>
}
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/stories/settings/settings-pages.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3073,6 +3073,22 @@ export const ArchivedTasks: Story = {
render: () => (
<SettingsStory section="archived-tasks" archivedTaskSessions={archivedTaskSessions} />
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// The newer revision represents this task family in the archived list.
const title = 'Single agent_spawn, second attempt';
const unarchive = await canvas.findByRole('button', { name: `取消归档「${title}」` });
const remove = await canvas.findByRole('button', { name: `彻底删除「${title}」` });
await expect(unarchive).toBeVisible();
await expect(remove).toBeVisible();
await expect(unarchive.querySelector('svg[aria-hidden="true"]')).not.toBeNull();
await expect(remove.querySelector('svg[aria-hidden="true"]')).not.toBeNull();
await expect(canvas.queryByRole('button', { name: `「${title}」的更多操作` })).toBeNull();
const row = unarchive.closest('li');
if (!row) throw new Error('archived task row is missing');
await userEvent.click(unarchive);
await waitFor(() => expect(row).not.toBeInTheDocument());
},
};

// Real path: 设置 → 导入任务 on a machine that has Codex installed.
Expand Down
2 changes: 1 addition & 1 deletion docs/astryx-surface-file-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
| `apps/desktop/src/renderer/settings/settings-status-summary-filter.tsx` | settings-module | Button | aligned — uses Astryx (Button) | aligned |
| `apps/desktop/src/renderer/settings/settings-surface.tsx` | settings-module | Badge, Banner, Button, IconButton, Layout, LayoutContent, LayoutHeader, LayoutPanel, Selector, SideNav, SideNavItem, SideNavSection | aligned — uses Astryx (Badge, Banner, Button, IconButton, Layout, LayoutContent, LayoutHeader, LayoutPanel) | aligned |
| `apps/desktop/src/renderer/settings/subagent-settings-page.tsx` | settings-page | Badge, Banner, Button, EmptyState, HStack, IconButton, Selector, Switch, TextArea, TextInput, VStack | aligned — uses Astryx (Badge, Banner, Button, EmptyState, HStack, IconButton, Selector, Switch) | aligned |
| `apps/desktop/src/renderer/settings/tasks-settings-page.tsx` | settings-page | Button, EmptyState, HStack, List, ListItem, MoreMenu, StackItem, TextInput | aligned — uses Astryx (Button, EmptyState, HStack, List, ListItem, MoreMenu, StackItem, TextInput) | aligned |
| `apps/desktop/src/renderer/settings/tasks-settings-page.tsx` | settings-page | Button, EmptyState, HStack, IconButton, List, ListItem, StackItem, TextInput | aligned — uses Astryx (Button, EmptyState, HStack, IconButton, List, ListItem, StackItem, TextInput) | aligned |
| `apps/desktop/src/renderer/settings/usage-settings-page.tsx` | settings-page | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `apps/desktop/src/renderer/settings/web-search-settings-page.tsx` | settings-page | Banner, Button, EmptyState, Link, Selector, StatusDot, Switch, TextInput | aligned — uses Astryx (Banner, Button, EmptyState, Link, Selector, StatusDot, Switch, TextInput) | aligned |
| `apps/desktop/src/renderer/styles.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned |
Expand Down
39 changes: 39 additions & 0 deletions packages/ui/src/__tests__/icons.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import test from 'node:test';
import type { ReactElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { parseHTML } from 'linkedom';
import { Archive, Unarchive } from '../icons.js';

function iconPathData(icon: ReactElement): string[] {
const { document } = parseHTML(renderToStaticMarkup(icon));
return [...document.querySelectorAll('path')].map((path) => path.getAttribute('d') ?? '');
}

test('archive and unarchive use one tray with inverse arrows', () => {
const archive = iconPathData(<Archive />);
const unarchive = iconPathData(<Unarchive />);

assert.deepEqual(archive.slice(0, 2), unarchive.slice(0, 2));
assert.deepEqual(archive.slice(2), ['M12 3v14', 'm7 12 5 5 5-5']);
assert.deepEqual(unarchive.slice(2), ['M12 17V3', 'm7 8 5-5 5 5']);
});
22 changes: 20 additions & 2 deletions packages/ui/src/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,28 @@
* UI icons.
*/

import { createLucideIcon, type IconNode } from 'lucide-react';

export type { LucideIcon, LucideProps } from 'lucide-react';

const ARCHIVE_TRAY_NODES: IconNode = [
['path', { d: 'M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7', key: 'tray' }],
['path', { d: 'M2 12h20', key: 'tray-edge' }],
];

/** A matched pair: the same tray with the arrow entering or leaving it. */
export const Archive = createLucideIcon('archive', [
Comment thread
Phoenix500526 marked this conversation as resolved.
...ARCHIVE_TRAY_NODES,
['path', { d: 'M12 3v14', key: 'arrow-stem' }],
['path', { d: 'm7 12 5 5 5-5', key: 'arrow-head' }],
]);

export const Unarchive = createLucideIcon('unarchive', [
...ARCHIVE_TRAY_NODES,
['path', { d: 'M12 17V3', key: 'arrow-stem' }],
['path', { d: 'm7 8 5-5 5 5', key: 'arrow-head' }],
]);

/**
* The five-rung icon scale. Pick by the role the glyph plays, not by eye.
*
Expand All @@ -54,8 +74,6 @@ export {
AlertCircle,
AlertOctagon,
AlertTriangle,
Archive,
ArchiveRestore,
ArrowDown,
ArrowLeft,
ArrowRight,
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/session-history-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ import {
ICON_SIZE,
AlertTriangle,
Archive,
ArchiveRestore,
FolderOpen,
Pencil,
Pin,
PinOff,
Plug,
SquarePen,
Unarchive,
} from './icons.js';
import { RelativeTime } from './relative-time.js';
import { formatAbsoluteTimestamp } from '@maka/core/relative-time';
Expand Down Expand Up @@ -1179,7 +1179,7 @@ function ProjectItemActions(props: {
? [
{
label: copy.projectRestore,
icon: ArchiveRestore,
icon: Unarchive,
onClick: () => runProjectAction('restore', () => actions.onRestore(project.id)),
},
]
Expand Down Expand Up @@ -1354,7 +1354,7 @@ function SessionItemActions(props: {
// that makes the intent deliberate.
{
label: props.session.isArchived ? copy.unarchive : copy.archive,
icon: props.session.isArchived ? ArchiveRestore : Archive,
icon: props.session.isArchived ? Unarchive : Archive,
onClick: () =>
runRowAction('archive', () =>
props.session.isArchived
Expand Down