Hide redeemed reset cards from the account menu
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# Task: Hide used reset cards from account sidebar menu
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260920-hide-used-reset-cards-7a391bd2
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260920-hide-used-reset-cards-7a391bd2-hide-used-reset-cards
|
||||
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260920-hide-used-reset-cards-7a391bd2
|
||||
- Base commit: c953126284eb81a3ac446c2213441db24e23929c
|
||||
- Owner: codex
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Hide redeemed reset cards from the lower-left account menu, including initial load and successful redemption, and show a suitable empty state.
|
||||
- Update focused Sidebar tests, reset-card Electron fixture and README.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- User explicitly requested already-used cards no longer appear. Keep available/expired inventory behavior and server card/entitlement records unchanged.
|
||||
- Retain explicit owner-authorized redemption, shared-wallet restrictions, confirmation semantics, and authoritative card/balance refresh.
|
||||
- Concurrent Task Gate and Planning Gate passed. Isolated feature from c953126; prior sharing-removal and QR-completion commits included, no peer reset-card ownership conflict identified.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Sidebar derives a visible list excluding cards with server status redeemed, used consistently for rows, loading/empty states, available count and shared-wallet notice.
|
||||
- Removed the redeemed badge/date presentation. Existing available and expired card behavior stays unchanged.
|
||||
- Confirmed redemption already updates card state before refreshing authoritative inventory and balance; the new projection removes the row immediately. When no visible cards remain, the drawer shows 暂无未使用的重置卡。
|
||||
- README describes the account-menu behavior. Reviewed the complete diff and the Electron screenshot of the empty drawer with successful redemption feedback.
|
||||
|
||||
## Verification
|
||||
|
||||
- Pinned pnpm 10.33.4 frozen install: passed, lockfile unchanged.
|
||||
- Sidebar token-point unit suite: 9 passed, covering mixed inventory, all-redeemed empty state, removal after redemption, refreshed balance, expired cards and shared-wallet restrictions.
|
||||
- Typecheck and scoped ESLint on Sidebar, its unit test and reset-card E2E: passed.
|
||||
- pnpm run build:vite: passed.
|
||||
- Electron reset-card-wallet.spec.ts: 1 passed. Confirms row and count disappear, empty state is visible, balance refreshes and exactly one redemption is submitted.
|
||||
- Visually inspected test-results/reset-card-wallet-Account--4d6fc-authoritative-point-balance/used-reset-card-hidden.png.
|
||||
- git diff --check and task-aware check_doc_drift.py: passed. Electron uses a local Host fixture; no actual account reset card was consumed.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Based on c953126 (QR completion), which includes e047877 (sharing removal). This task does not merge, package or deploy; the updated client is required to expose these changes.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Target: .project-docs/40-domain/business-rules.md and .project-docs/30-worklog/current-state.md.
|
||||
- Proposal: the account-menu reset-card inventory omits redeemed cards and removes a card on confirmed redemption, retaining authoritative card/balance refresh and owner/shared-wallet restrictions.
|
||||
- Evidence: explicit user request, 9 unit tests and passing Electron scenario. Future impact: keep used-card history out of this compact action menu.
|
||||
- Semantic conflict: none; this narrows display only, without deleting server history or changing redemption. Human confirmation is supplied by the request.
|
||||
@@ -27,7 +27,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
|
||||
- AI 编程核心对话运行时:Electron Main 按需启动一个长驻 Pi Agent Server,同一进程内为每条 Conversation 建立独立的 Runtime、Session、凭证视图、扩展上下文和 JSONL 通道,同时复用 Pi 模块与进程启动成本;Server 异常退出时所有旧通道统一失效,下一次恢复只重启一个 Server。正式包中的 Agent Server 从明确传入的 `resources/pi-runtime` 根定位依赖清单与包入口,不从脚本所在目录或应用 `node_modules` 猜测解析。Renderer 不直接启动或调用 Pi,也不读取其 wire 类型。项目与 Conversation 元数据先从本地读取,选中对话后才按需准备对应逻辑线程;输入框不等待运行时准备完成。同一助手工具批次中,会取得项目写租约的 `bash`、`edit`、`write` 与动态产品工具按顺序执行,避免批次在命令启动前自锁;纯只读工具仍可并行。子 Agent 仍使用独立的短生命周期进程,以保留并发配额、故障和写入边界;macOS 上两类后台进程都使用 Electron 自带的 Helper,不在 Dock 中创建额外应用图标。
|
||||
- AI 编程默认入口:进入 Code 后保持在 `/chat`。尚未选择本地项目时,页面以麦洛 M 标识和居中引导语建立焦点,下方提供独立的“新增项目”主操作以及可横向浏览、直接进入的已有项目卡片;项目创建和进入仍复用侧栏现有流程。已有项目的新 Conversation 在可编辑 Composer 上方沿用同一品牌引导,第一条消息出现后切换为正常时间线。
|
||||
- AI 编程历史与配额反馈:Pi 上下文压缩不删除用户可见的 active-branch 历史,压缩摘要保持 Main-private;Renderer 首次渲染最近 120 个节点,向上滚动时按 100 个节点追加更早内容并保持阅读位置。Main 精确识别 `token_point_balance_exhausted` / `词元点数余额不足`,只向 Renderer 投影不可重试的稳定配额错误和安全中文提示,重开会话后仍可见且不暴露上游原始错误或 request id。
|
||||
- 账号词元点数:左下角账号菜单只通过 Main-owned `/api/works/billing/points` 读取 Works Square V2 余额。可管理会员的账号看到套餐、本周剩余/额度、总可用点数与刷新时间;家庭成员和青少年自有权益只收到“可用/已用尽”等粗粒度状态,不向 Renderer 暴露套餐周期或精确余额。
|
||||
- 账号词元点数:左下角账号菜单只通过 Main-owned `/api/works/billing/points` 读取 Works Square V2 余额。可管理会员的账号看到套餐、本周剩余/额度、总可用点数与刷新时间;家庭成员和青少年自有权益只收到“可用/已用尽”等粗粒度状态,不向 Renderer 暴露套餐周期或精确余额。 账号菜单的重置卡列表隐藏已使用的卡;使用成功后立即移除该卡并刷新卡包和余额,没有剩余卡片时显示“暂无未使用的重置卡”。
|
||||
- 桌面性能策略:应用窗口使用不透明浅色表面并默认保留硬件合成;仅在显式安全模式或短时间内重复 GPU 进程崩溃时启用软件渲染,并把故障原因保存在用户数据目录。启动关键路径只创建本地应用壳,认证、代理、同步、更新和遥测在首帧后延迟初始化;开发版可通过 `app:performance` 快照观察 GPU、进程、WebContents、事件循环与 Renderer Long Task 聚合指标。
|
||||
- 后台生命周期:Main 统一维护模块活动状态与任务租约。隐藏窗口、离开模块和关闭开发浏览器会释放非必要连接;生成、Code 执行、下载与发布构建持有租约并在完成后释放。已写入 Pi 的 prompt 或上下文整理即使确认超过 10 秒,也会继续持有运行所有权与后台租约,直到迟到响应、权威事件、明确失败或用户清理使其收敛;此时同一 Conversation 不接受重叠 mutation,其他 Conversation 不受影响。各模块的后台连接、轮询和子进程必须通过同一生命周期入口登记。
|
||||
- 共享开发浏览器:AI 编程右侧提供项目级浏览器,用户与 Agent 查看并调试同一实时页面、Console 和 Network,支持本地与公网开发地址。
|
||||
|
||||
@@ -273,7 +273,8 @@ export function Sidebar({
|
||||
tokenPointBalance && !tokenPointBalance.can_manage_membership,
|
||||
);
|
||||
const nextRefreshLabel = getRefreshTimeLabel(tokenPointBalance?.next_refresh_at);
|
||||
const availableResetCardCount = resetCardState.cards.filter(
|
||||
const visibleResetCards = resetCardState.cards.filter((card) => card.status !== 'redeemed');
|
||||
const availableResetCardCount = visibleResetCards.filter(
|
||||
(card) => getEffectiveResetCardStatus(card) === 'available',
|
||||
).length;
|
||||
const resetCardRedemptionBlocked = tokenPointBalance?.entitlement_source === 'shared_group';
|
||||
@@ -1286,7 +1287,7 @@ export function Sidebar({
|
||||
className="ml-5 rounded-lg bg-surface-subtle/70 p-1 text-xs font-medium text-muted-foreground shadow-soft"
|
||||
innerClassName="grid gap-1"
|
||||
>
|
||||
{resetCardState.status === 'loading' && resetCardState.cards.length === 0 ? (
|
||||
{resetCardState.status === 'loading' && visibleResetCards.length === 0 ? (
|
||||
<p className="px-1.5 py-2 text-pretty text-[11px]">正在加载重置卡…</p>
|
||||
) : null}
|
||||
{resetCardState.status === 'error' ? (
|
||||
@@ -1301,20 +1302,19 @@ export function Sidebar({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{resetCardState.status === 'loaded' && resetCardState.cards.length === 0 ? (
|
||||
<p className="px-1.5 py-2 text-pretty text-[11px]">暂无赠送的重置卡。</p>
|
||||
{resetCardState.status === 'loaded' && visibleResetCards.length === 0 ? (
|
||||
<p className="px-1.5 py-2 text-pretty text-[11px]">暂无未使用的重置卡。</p>
|
||||
) : null}
|
||||
{resetCardRedemptionBlocked && resetCardState.cards.length > 0 ? (
|
||||
{resetCardRedemptionBlocked && visibleResetCards.length > 0 ? (
|
||||
<p className="rounded-md bg-background px-1.5 py-1.5 text-pretty text-[10px] leading-4 shadow-soft">
|
||||
当前使用家庭共享额度,请由家庭管理员使用自己的重置卡。
|
||||
</p>
|
||||
) : null}
|
||||
{resetCardState.cards.length > 0 ? (
|
||||
{visibleResetCards.length > 0 ? (
|
||||
<div className="grid max-h-64 gap-1 overflow-y-auto pr-0.5">
|
||||
{resetCardState.cards.map((card) => {
|
||||
{visibleResetCards.map((card) => {
|
||||
const status = getEffectiveResetCardStatus(card);
|
||||
const expiresAt = formatResetCardTime(card.expires_at) ?? '时间不可用';
|
||||
const redeemedAt = formatResetCardTime(card.redeemed_at);
|
||||
const redeeming = redeemingResetCardId === card.id;
|
||||
return (
|
||||
<article
|
||||
@@ -1328,17 +1328,13 @@ export function Sidebar({
|
||||
'rounded-full px-1.5 py-0.5 text-[10px] font-semibold',
|
||||
status === 'available' && 'bg-accent-soft text-accent-strong',
|
||||
status === 'expired' && 'bg-destructive/10 text-destructive',
|
||||
status === 'redeemed' && 'bg-surface-subtle text-muted-foreground',
|
||||
)}>
|
||||
{status === 'available' ? '可使用' : status === 'expired' ? '已过期' : '已使用'}
|
||||
{status === 'available' ? '可使用' : '已过期'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-pretty text-[10px] leading-4 tabular-nums">
|
||||
有效期至 {expiresAt}
|
||||
</p>
|
||||
{status === 'redeemed' && redeemedAt ? (
|
||||
<p className="text-pretty text-[10px] leading-4 tabular-nums">使用于 {redeemedAt}</p>
|
||||
) : null}
|
||||
{status === 'available' ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Account reset-card wallet', () => {
|
||||
test('redeems an available card and refreshes the authoritative point balance', async ({ launchElectronApp }) => {
|
||||
test('hides a redeemed card and refreshes the authoritative point balance', async ({ launchElectronApp }, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
@@ -108,7 +108,10 @@ test.describe('Account reset-card wallet', () => {
|
||||
await expect(page.getByTestId('sidebar-reset-card-card-e2e')).toContainText('可使用');
|
||||
await expect(page.getByTestId('sidebar-reset-card-card-e2e')).toContainText('有效期至 2099-09-15');
|
||||
await page.getByTestId('sidebar-reset-card-redeem-card-e2e').click();
|
||||
await expect(page.getByTestId('sidebar-reset-card-card-e2e')).toContainText('已使用');
|
||||
await expect(page.getByTestId('sidebar-reset-card-card-e2e')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-reset-cards-drawer')).toContainText('暂无未使用的重置卡。');
|
||||
await expect(page.getByTestId('sidebar-reset-card-available-count')).toHaveCount(0);
|
||||
await page.screenshot({ path: testInfo.outputPath('used-reset-card-hidden.png') });
|
||||
|
||||
await page.getByTestId('sidebar-account-usage-menuitem').click();
|
||||
await expect(page.getByTestId('sidebar-weekly-token-points')).toContainText('500 / 500 点');
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('Sidebar V2 token point balance', () => {
|
||||
expect(screen.queryByTestId('sidebar-account-upgrade-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows available, expired, and redeemed reset cards with explicit dates', async () => {
|
||||
it('shows available and expired reset cards while hiding redeemed cards', async () => {
|
||||
fetchTokenPointBalanceMock.mockResolvedValue(pointBalance());
|
||||
fetchResetCardsMock.mockResolvedValue([
|
||||
resetCard(),
|
||||
@@ -259,14 +259,26 @@ describe('Sidebar V2 token point balance', () => {
|
||||
await waitFor(() => expect(within(drawer).getByText('可使用')).toBeInTheDocument());
|
||||
expect(screen.getByTestId('sidebar-reset-card-available-count')).toHaveTextContent('1');
|
||||
expect(drawer).toHaveTextContent('已过期');
|
||||
expect(drawer).toHaveTextContent('已使用');
|
||||
expect(screen.queryByTestId('sidebar-reset-card-card-redeemed')).not.toBeInTheDocument();
|
||||
expect(drawer).toHaveTextContent('有效期至 2099-09-15');
|
||||
expect(drawer).toHaveTextContent('使用于 2026-09-09');
|
||||
expect(drawer).not.toHaveTextContent('使用于');
|
||||
expect(screen.getByTestId('sidebar-reset-card-redeem-card-1')).toHaveTextContent('立即使用');
|
||||
expect(screen.queryByTestId('sidebar-reset-card-redeem-card-expired')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redeems one card and refreshes both the card wallet and point balance', async () => {
|
||||
it('shows an empty wallet when all cards were already redeemed', async () => {
|
||||
fetchTokenPointBalanceMock.mockResolvedValue(pointBalance());
|
||||
fetchResetCardsMock.mockResolvedValue([resetCard({ status: 'redeemed', redeemed_at: '2026-09-09T00:00:00Z' })]);
|
||||
|
||||
const drawer = await renderResetCardDrawer();
|
||||
|
||||
await waitFor(() => expect(drawer).toHaveTextContent('暂无未使用的重置卡。'));
|
||||
expect(screen.queryByTestId('sidebar-reset-card-card-1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sidebar-reset-card-available-count')).not.toBeInTheDocument();
|
||||
expect(redeemResetCardMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides a redeemed card and refreshes both the card wallet and point balance', async () => {
|
||||
const available = resetCard();
|
||||
const redeemed = resetCard({
|
||||
status: 'redeemed',
|
||||
@@ -285,7 +297,9 @@ describe('Sidebar V2 token point balance', () => {
|
||||
fireEvent.click(screen.getByTestId('sidebar-reset-card-redeem-card-1'));
|
||||
|
||||
await waitFor(() => expect(redeemResetCardMock).toHaveBeenCalledWith('access-token', 'card-1'));
|
||||
await waitFor(() => expect(within(drawer).getByText('已使用')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByTestId('sidebar-reset-card-card-1')).not.toBeInTheDocument());
|
||||
await waitFor(() => expect(drawer).toHaveTextContent('暂无未使用的重置卡。'));
|
||||
expect(screen.queryByTestId('sidebar-reset-card-available-count')).not.toBeInTheDocument();
|
||||
expect(fetchResetCardsMock.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
expect(fetchTokenPointBalanceMock.mock.calls.length).toBeGreaterThan(balanceCallsBeforeRedeem);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user