Praetor UI 重建 Playbook(給 Codex 執行用)

Status: historical execution playbook · v1 · 2026-05-12

> This file is no longer the source of truth for future UI direction. It records

> the 2026-05 React rebuild plan. For current product direction, follow

> docs/ADR-002-praetor-as-ai-company.md and

> docs/PRAETOR_PAPERCLIP_MATURITY_ROADMAP.zh-CN.md.

>

> In particular, outdated instructions such as "do not do board view" or

> hiding Agents under Runtime/Settings are superseded by the maturity roadmap:

> Praetor now prioritizes Agent/org-chart management, Paperclip-like work

> boards, and company-feeling operational surfaces.

這份文件是 Praetor v1 UI 重建的歷史腳本。若與 ADR-002 或 maturity roadmap 衝突,以新文件為準。

---

0. Context(給 Codex 的快速 onboarding)

Praetor 是一個 local-first AI company OS。當前狀態:

- 12 個 Jinja server-rendered 頁面(在 apps/api/praetor_api/ui.py + apps/api/praetor_api/templates/)

- 一個極簡 React SPA(只有 /office 一頁,在 apps/web/frontend/src/main.tsx,1,071 行 single-file)

不要做的事

---

1. Design DNA

> 「平靜的入口 + 密集的工作面」

/office 是董事長每天打開 Praetor 的「早安畫面」,氣質要平靜、留白多、prompt 為中心(參考:AI design generator dashboards、Linear inbox)。其它頁面是 command center,密集但乾淨(參考:Linear、ClickUp dashboards 的 metric + table 結構)。

1.1 配色 (CSS tokens)

放在 apps/web/frontend/src/lib/tokens.css:

:root {
  /* Surfaces */
  --bg-app: #F7F7F8;
  --bg-card: #FFFFFF;
  --bg-subtle: #F1F2F4;
  --bg-hover: #ECECEF;

  /* Borders & dividers */
  --border-hairline: rgba(15, 17, 21, 0.08);
  --border-focus: #3B5BFD;

  /* Text */
  --fg-default: #0A0A0A;
  --fg-muted: #5C5F66;
  --fg-subtle: #8A8D94;
  --fg-on-accent: #FFFFFF;

  /* Accent */
  --accent: #3B5BFD;
  --accent-hover: #2F4DE0;
  --accent-soft: #EEF1FF;

  /* Status pills (bg / fg pairs) */
  --status-active-bg: #E7F5EB;     --status-active-fg: #126B2C;
  --status-running-bg: #E5EEFF;    --status-running-fg: #234CD6;
  --status-waiting-bg: #FFF3D6;    --status-waiting-fg: #8A5B00;
  --status-blocked-bg: #FCE6E6;    --status-blocked-fg: #B0271A;
  --status-done-bg: #ECECEF;       --status-done-fg: #3D4148;
  --status-failed-bg: #FCE6E6;     --status-failed-fg: #B0271A;
  --status-paused-bg: #F1ECFF;     --status-paused-fg: #5A37C8;

  /* Shadows */
  --shadow-card: 0 1px 2px rgba(15, 17, 21, 0.04), 0 0 0 1px var(--border-hairline);
  --shadow-popover: 0 8px 24px rgba(15, 17, 21, 0.12), 0 0 0 1px var(--border-hairline);

  /* Radius */
  --radius-card: 16px;
  --radius-input: 12px;
  --radius-pill: 9999px;
  --radius-button: 10px;

  /* Type */
  --font-sans: "Inter", "Noto Sans TC", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
}

[data-theme="dark"] {
  --bg-app: #0B0C0E;
  --bg-card: #131418;
  --bg-subtle: #1B1D22;
  --bg-hover: #22252B;

  --border-hairline: rgba(255, 255, 255, 0.08);

  --fg-default: #F5F6F8;
  --fg-muted: #9CA0A8;
  --fg-subtle: #6E727A;

  --accent: #6B85FF;
  --accent-hover: #8197FF;
  --accent-soft: #1A1F36;

  --status-active-bg: #0F2C18;     --status-active-fg: #6BCB87;
  --status-running-bg: #0F1D3D;    --status-running-fg: #8DA3FF;
  --status-waiting-bg: #2E2204;    --status-waiting-fg: #FFD27A;
  --status-blocked-bg: #2E0F0E;    --status-blocked-fg: #FF8A80;
  --status-done-bg: #1B1D22;       --status-done-fg: #B0B4BC;
  --status-failed-bg: #2E0F0E;     --status-failed-fg: #FF8A80;
  --status-paused-bg: #1F1838;     --status-paused-fg: #B59AFF;

  --shadow-card: 0 0 0 1px var(--border-hairline);
  --shadow-popover: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--border-hairline);
}

1.2 Type scale

| Token | Size | Line | Weight | Use |

|---|---|---|---|---|

| text-display | 32px | 1.2 | 600 | /office Good morning headline, login title |

| text-h1 | 24px | 1.3 | 600 | page title |

| text-h2 | 18px | 1.4 | 600 | section title |

| text-body | 14px | 1.5 | 400 | default |

| text-small | 13px | 1.4 | 400 | secondary |

| text-meta | 12px | 1.4 | 500 | uppercase labels, timestamps |

| text-code | 13px | 1.5 | 400 | monospace for IDs, JSON |

Headings always use letter-spacing -0.01em; uppercase meta uses +0.06em.

1.3 Spacing

8px base grid. Common values: 4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 / 56.

Card padding: 24px (desktop), 16px (tablet).

Section gap: 24px between cards in a column, 16px in a row.

1.4 Motion

1.5 Accessibility

---

2. Information Architecture

2.1 Top-level routes(5 條)

| Route | 名稱 | 取代的 Jinja 頁面 |

|---|---|---|

| /office | Office | /app/praetor, /app/inbox, 現有的 /office |

| /missions + /missions/:id | Missions | /app/tasks, /app/overview, /app/missions/:id, /app/activity |

| /memory | Memory | /app/memory, /app/decisions, /app/meetings |

| /runtime | Runtime | /app/models, /app/agents(admin tabs 部分) |

| /settings | Settings | /app/settings, /app/agents(org tabs 部分) |

預設 landing 是 /office。

2.2 App shell

┌─────────────────────────────────────────────────────────────────┐
│  TopBar: [Workspace name]  [⌘K omnibox]            [● runtime]  │
├──────┬──────────────────────────────────────────┬───────────────┤
│ Rail │  Page content                            │ Right panel   │
│ 56px │  (max-width 1280px, centered)            │ (optional,    │
│ ↔    │                                          │  per-page)    │
│ 240  │                                          │ 320px         │
└──────┴──────────────────────────────────────────┴───────────────┘

2.3 Sidebar groups

✦  Office
▤  Missions
☷  Memory
⚡  Runtime
⚙  Settings
────────────────
👤 Owner avatar / menu

展開時群組標籤:Workspace (上 5 個) / Account (下 1 個)。

2.4 Keyboard shortcuts (Linear-style)

| Combo | Action |

|---|---|

| ⌘K | Open command palette |

| ⌘\ | Toggle sidebar |

| g o | Go to Office |

| g m | Go to Missions |

| g r | Go to Runtime |

| g k | Go to Memory (knowledge) |

| g s | Go to Settings |

| c | New mission (anywhere) |

| / | Focus search in current page |

| ? | Show keyboard shortcuts overlay |

| esc | Close any modal/popover |

實作用 tinykeys(或自己寫的 keymap hook)。

2.5 Command palette (⌘K)

固定區塊:

1. Quick actions: New mission · New decision · Open CEO chat

2. Pages: Office / Missions / Memory / Runtime / Settings

3. Recent missions: 最近 5 個 mission

4. Settings shortcuts: Switch language · Toggle dark mode · Logout

Fuzzy search 用 cmdk library。

---

3. Page specs(細到 Codex 能直接寫)

3.1 /office

佈局:兩欄。主欄 + 右側 rail。

<AppShell rightPanel={<NeedsDecisionPanel />}>
  <OfficePage>
    <GreetingHeader />                  {/* "Good morning, {owner}" + briefing line */}
    <CEOChat />                         {/* big prompt input */}
    <Section title="Tasks in flight">
      <MissionCardGrid missions={runningMissions} />
    </Section>
    <Section title="Recent memory">
      <MemoryTimeline events={recentMemoryEvents} />
    </Section>
  </OfficePage>
</AppShell>

GreetingHeader:

CEOChat:

MissionCardGrid:

MemoryTimeline:

NeedsDecisionPanel (right rail):

- 每項:mission title、reason、[Approve] [Reject] button

資料來源:單一 GET /api/office/snapshot call(已存在),TanStack Query key ["office", "snapshot"],stale 30s。

---

3.2 /missions(列表)

┌──────────────────────────────────────────────────────────────────┐
│  Missions                              [+ New mission]           │
│  ───────────                                                      │
│  [All ▾]  [Status: any ▾]  [Domain: any ▾]   🔍 search           │
│                                                                   │
│  Status  Title                Owner   Priority  Updated   Progress│
│  ●●     Draft contract Acme  PM      high      2 hours   ────░80%│
│  ○○     Stop me              CEO     normal    1 day     done    │
│  ...                                                              │
└──────────────────────────────────────────────────────────────────┘

3.3 /missions/:id

頂部 banner:title + status pill + priority pill + [Run] [Pause] [Stop] [Enqueue]

Tab 列(用 URL 子路徑而非 query 參數):

| Tab | Content | API |

|---|---|---|

| Briefing (default) | summary / requested_outputs / governance / board_briefings | GET /missions/{id}, GET /api/missions/{id}/board-briefings |

| Tasks | task list + run_attempts + jobs queue | GET /api/missions/{id}/run-attempts, GET /api/missions/{id}/jobs |

| Decisions | approvals + escalations + completion contract | GET /approvals, GET /api/missions/{id}/completion-contract |

| Files | bridge_runs.changed_files + workspace scope | GET /api/missions/{id}/workspace-scope, GET /api/missions/{id}/timeline |

| Activity | timeline + agent_messages + work_sessions | GET /api/missions/{id}/timeline, GET /api/missions/{id}/agent-messages, GET /api/missions/{id}/work-sessions |

Jobs queue 顯示:

New mission modal:

---

3.4 /memory

┌──────────────────────────────┬───────────────────────────────────┐
│ TREE (220px)                 │ DETAIL                            │
│                              │                                   │
│ ▾ Company Memory (8)         │ # Company Wiki.md                 │
│   · Company Wiki.md          │                                   │
│ ▾ CEO Operating Memory       │ # CEO Operating Memory.md         │
│ ▾ Company Playbook           │                                   │
│   · Company DNA.md           │ ## 2026-05-12                     │
│   · ...                      │ Stop spending on cosmetic AI...   │
│ ▾ Standing orders (5)        │                                   │
│   · 對外通訊一律升級         │ ─── Retrieval Preview ───────────│
│   · ...                      │ 過去 7 天被 CEO chat 引用 3 次    │
│ ▾ Decisions (12)             │ 最近一次:今天 09:42              │
│   · Ship MVP without...      │ Prompt context score: 0.78        │
│ ▾ Open questions (3)         │                                   │
└──────────────────────────────┴───────────────────────────────────┘

---

3.5 /runtime

┌──────────────────────────────────────────────────────────────────┐
│  Runtime                                                          │
│                                                                   │
│  ┌─ Health Hero ────────────────────────────────────────────┐   │
│  │  ● Healthy                                                │   │
│  │  API mode · openai · gpt-4.1-mini · last sync 12s ago     │   │
│  │  [Test connection]   [Switch provider]                    │   │
│  └───────────────────────────────────────────────────────────┘   │
│                                                                   │
│  [Models] [Executors] [Recent runs] [Usage]                       │
│                                                                   │
│  Tab content...                                                   │
└──────────────────────────────────────────────────────────────────┘

---

3.6 /settings

縱向 tab,左寬 200px / 右 form 區。

| Tab | Form / Section |

|---|---|

| Owner | name / email / language / change password |

| Governance | autonomy_mode / require_approval categories / run_budget defaults |

| Workspace | workspace_root (read-only) / permissions allow/deny lists |

| Roles | list agent_roles + agent_permission_profiles(v1 read-only) |

| AI | re-link to /runtime(避免重複設定) |

| Telegram | bot_token / webhook_secret / allowed_user_id / pairing code |

| Advanced | export schemas / debug routes / reset (危險區域) |

API:GET /settings(read),其它 form post 暫時仍指向既有的 Jinja routes,等 backend 補 JSON endpoint(這部分 v1 可保留 Jinja form post,因為 settings 是低頻使用面)。

---

3.7 /app/login 和 onboarding(保留 Jinja,但翻新視覺)

這兩頁是唯一允許保留 Jinja 的入口。但視覺要翻新成「Image 1 天空漸層」氣質:

Onboarding wizard 步驟視覺也用同一張卡片,每步切換用淡入。

---

4. Component primitives

放在 apps/web/frontend/src/components/ui/。全部複製 shadcn/ui 的 source 後改 tokens,不要 install MUI/Chakra。

| 元件 | 檔名 | 仿照 |

|---|---|---|

| Button (variants: primary / secondary / ghost / danger / gradient) | Button.tsx | shadcn |

| Card | Card.tsx | shadcn |

| Badge | Badge.tsx | shadcn |

| StatusPill | StatusPill.tsx | 自製(綁定 status → token 配色) |

| Input / Textarea | Input.tsx, Textarea.tsx | shadcn |

| Select | Select.tsx | radix-ui/react-select |

| Dialog (modal) | Dialog.tsx | radix-ui/react-dialog |

| Popover | Popover.tsx | radix-ui/react-popover |

| Tooltip | Tooltip.tsx | radix-ui/react-tooltip |

| Tabs | Tabs.tsx | radix-ui/react-tabs |

| DataTable | DataTable.tsx | 自製,支援 sort / hover row / sticky header |

| Skeleton | Skeleton.tsx | 自製 div + shimmer |

| Avatar | Avatar.tsx | radix-ui/react-avatar |

| CommandPalette | CommandPalette.tsx | cmdk |

| EmptyState | EmptyState.tsx | 自製:icon + headline + action |

4.1 StatusPill API

type MissionStatus =
  | "planned" | "staffing" | "active" | "review" | "reviewing"
  | "ready_for_ceo" | "needs_decision" | "waiting_approval"
  | "paused" | "resumed" | "completed" | "archived" | "failed";

type StatusKind = "active" | "running" | "waiting" | "blocked" | "done" | "failed" | "paused";

function statusKind(s: MissionStatus): StatusKind { /* lookup */ }

<StatusPill kind={statusKind(mission.status)}>{label(mission.status)}</StatusPill>

Pill 是 --radius-pill、12px 8px padding、text-meta 大小、bg/fg 用對應 token pair。

4.2 Button gradient variant 只給 CEO Send 用

.btn-gradient {
  background: linear-gradient(135deg, #3B5BFD 0%, #6B85FF 100%);
  color: white;
}

別處的 primary button 一律 bg: #0A0A0A、color: white。

---

5. Tech stack(已確認,不要改)

| Concern | Choice | 理由 |

|---|---|---|

| Build | Vite (已存在) | 不變 |

| Framework | React 19 (已存在) | 不變 |

| Router | react-router-dom@6 | 與 React 18+ 兼容、社群最大 |

| Server state | @tanstack/react-query@5 | 取代手寫 useEffect+fetch |

| Forms | react-hook-form@7 + zod@3 | type-safe form validation |

| Icons | lucide-react (已存在) | 不變 |

| Styling | Tailwind v3 + CSS tokens | 不要 styled-components |

| UI primitives | shadcn/ui pattern + radix-ui | 沒有 vendor 鎖死 |

| Cmd palette | cmdk@1 | Linear/Vercel 都用這個 |

| Keyboard | tinykeys | 輕量 |

| Date | date-fns@3 (tree-shakable) | 不要 moment |

| Markdown render | react-markdown@9 + remark-gfm | for /memory |

| Charts | recharts@2 | for /runtime usage |

| Dark mode | 自寫 ThemeProvider + data-theme attribute | 不需要 next-themes |

5.1 npm install 命令

cd apps/web
npm install --save \
  react-router-dom@^6 \
  @tanstack/react-query@^5 \
  react-hook-form@^7 zod@^3 \
  @radix-ui/react-dialog@^1 @radix-ui/react-popover@^1 \
  @radix-ui/react-tooltip@^1 @radix-ui/react-tabs@^1 \
  @radix-ui/react-select@^2 @radix-ui/react-avatar@^1 \
  @radix-ui/react-checkbox@^1 @radix-ui/react-dropdown-menu@^2 \
  cmdk@^1 tinykeys@^3 date-fns@^3 \
  react-markdown@^9 remark-gfm@^4 recharts@^2 \
  clsx@^2 class-variance-authority@^0.7 tailwind-merge@^2

npm install --save-dev \
  tailwindcss@^3 postcss@^8 autoprefixer@^10 \
  @types/node

5.2 Tailwind 設定

apps/web/tailwind.config.ts:

import type { Config } from "tailwindcss";

export default {
  content: ["./frontend/index.html", "./frontend/src/**/*.{ts,tsx}"],
  darkMode: ["class", "[data-theme='dark']"],
  theme: {
    extend: {
      colors: {
        app: "var(--bg-app)",
        card: "var(--bg-card)",
        subtle: "var(--bg-subtle)",
        hover: "var(--bg-hover)",
        accent: "var(--accent)",
        "accent-hover": "var(--accent-hover)",
        "accent-soft": "var(--accent-soft)",
        fg: "var(--fg-default)",
        muted: "var(--fg-muted)",
        "border-hairline": "var(--border-hairline)",
      },
      fontFamily: {
        sans: "var(--font-sans)",
        mono: "var(--font-mono)",
      },
      borderRadius: {
        card: "var(--radius-card)",
        input: "var(--radius-input)",
        pill: "var(--radius-pill)",
        button: "var(--radius-button)",
      },
      boxShadow: {
        card: "var(--shadow-card)",
        popover: "var(--shadow-popover)",
      },
      transitionTimingFunction: {
        ease: "cubic-bezier(0.2, 0, 0.1, 1)",
      },
    },
  },
  plugins: [],
} satisfies Config;

---

6. Backend API contract(不變,不要新增)

所有 endpoint 都需要 cookie session + X-CSRF-Token header(CSRF token 從 GET /auth/session 取,存進 TanStack Query cache)。

6.1 Auth & onboarding

| Method | Path | 用在 |

|---|---|---|

| GET | /auth/session | App boot, 取 CSRF token + ui_language |

| POST | /auth/login | Login form |

| POST | /auth/logout | Logout menu |

| POST | /onboarding/preview | Onboarding wizard 預覽 |

| POST | /onboarding/complete | Onboarding 完成(需要 X-Praetor-Setup-Token header) |

| GET | /settings | 讀目前 settings |

6.2 Office

| GET | /api/office/snapshot | /office 主頁所有資料 |

| GET | /api/office/conversation | CEO chat history |

| POST | /api/office/conversation | 送 CEO 訊息(body: {body, related_mission_id?}) |

6.3 Missions

| GET | /missions | mission list |

| POST | /missions | create mission |

| GET | /missions/{id} | mission detail |

| POST | /missions/{id}/run | run mission (synchronous, blocks) |

| POST | /api/missions/{id}/enqueue | enqueue mission job (非阻塞,新建議用法) |

| POST | /missions/{id}/pause | |

| POST | /missions/{id}/continue | |

| POST | /missions/{id}/stop | |

| POST | /api/missions/{id}/executor-control | pause/resume executor |

6.4 Mission detail tabs

| GET | /api/missions/{id}/timeline | events |

| GET | /api/missions/{id}/agent-messages | AI internal conversation |

| GET | /api/missions/{id}/work-sessions | work sessions |

| GET | /api/missions/{id}/run-attempts | run attempts |

| GET | /api/missions/{id}/jobs | mission queue jobs (NEW) |

| GET | /api/missions/{id}/work-trace | stages + work events + contracts |

| GET | /api/missions/{id}/knowledge | mission knowledge snapshot |

| GET | /api/missions/{id}/workspace-scope | scope |

| GET | /api/missions/{id}/completion-contract | contract |

| GET | /api/missions/{id}/board-briefings | board briefings |

| POST | /api/missions/{id}/board-briefings | create briefing |

| GET | /api/missions/{id}/memory-promotion | promotion reviews |

| POST | /api/missions/{id}/memory-promotion | create promotion review |

| POST | /missions/{id}/meeting | create review meeting |

6.5 Jobs

| GET | /api/mission-jobs/{job_id} | poll a single job status |

6.6 Approvals & governance

| GET | /approvals | list pending |

| POST | /approvals | create approval |

| POST | /approvals/{id}/approved | approve |

| POST | /approvals/{id}/rejected | reject |

| GET | /api/governance/review | latest review |

| POST | /api/governance/review | run review |

6.7 Knowledge / Memory

| GET | /api/knowledge | full snapshot |

| GET | /api/workflow | workflow contract |

6.8 Organization

| GET | /api/organization | agents/teams/delegations/escalations |

| POST | /api/organization/skill-sources | add Company Playbook source |

| POST | /api/organization/skill-sources/{id}/import | import playbook entries |

| POST | /api/organization/skills/{id}/status | activate/reject playbook entry |

6.9 TanStack Query keys 標準

// apps/web/frontend/src/lib/queryKeys.ts
export const qk = {
  session: ["session"] as const,
  settings: ["settings"] as const,
  officeSnapshot: ["office", "snapshot"] as const,
  conversation: ["office", "conversation"] as const,
  missions: ["missions"] as const,
  mission: (id: string) => ["missions", id] as const,
  missionJobs: (id: string) => ["missions", id, "jobs"] as const,
  missionTimeline: (id: string) => ["missions", id, "timeline"] as const,
  missionAgentMessages: (id: string) => ["missions", id, "agent-messages"] as const,
  missionWorkSessions: (id: string) => ["missions", id, "work-sessions"] as const,
  missionRunAttempts: (id: string) => ["missions", id, "run-attempts"] as const,
  missionWorkTrace: (id: string) => ["missions", id, "work-trace"] as const,
  missionKnowledge: (id: string) => ["missions", id, "knowledge"] as const,
  missionBoardBriefings: (id: string) => ["missions", id, "board-briefings"] as const,
  approvals: ["approvals"] as const,
  knowledge: ["knowledge"] as const,
  organization: ["organization"] as const,
  governanceReview: ["governance", "review"] as const,
  workflow: ["workflow"] as const,
  job: (jobId: string) => ["mission-jobs", jobId] as const,
};

6.10 CSRF + fetch wrapper

// apps/web/frontend/src/lib/api.ts
let csrfToken: string | null = null;

export function setCsrfToken(token: string) {
  csrfToken = token;
}

export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
  const method = (init.method ?? "GET").toUpperCase();
  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    ...(init.headers as Record<string, string>),
  };
  if (csrfToken && method !== "GET" && method !== "HEAD") {
    headers["X-CSRF-Token"] = csrfToken;
  }
  const res = await fetch(path, { ...init, credentials: "same-origin", headers });
  const env = await res.json();
  if (!res.ok || !env.ok) {
    throw new ApiError(env.error?.code ?? "http_error", env.error?.message ?? res.statusText, res.status);
  }
  return env.data as T;
}

export class ApiError extends Error {
  constructor(public code: string, message: string, public status: number) {
    super(message);
  }
}

---

7. Migration plan (commit by commit)

每個 commit 都要:

Commit 1 — Wipe legacy SPA and install foundation

動作:

1. 砍掉 apps/web/frontend/src/main.tsx(保留 git history,但檔案重寫)

2. 砍掉 apps/web/frontend/src/styles.css、api.ts、types.ts

3. 安裝第 5.1 節的所有 npm packages

4. 建立 apps/web/tailwind.config.ts、apps/web/postcss.config.js

5. 建立 apps/web/frontend/src/lib/tokens.css(複製第 1.1 節)

6. 建立 apps/web/frontend/src/index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
@import "./lib/tokens.css";

html, body { background: var(--bg-app); color: var(--fg-default); font-family: var(--font-sans); }
body { font-size: 14px; line-height: 1.5; }
* { box-sizing: border-box; }

7. 確保 frontend/index.html 載入 Inter 與 Noto Sans TC(preconnect + font-display swap)

Acceptance:

---

Commit 2 — App shell + routing + theme

動作:

1. 建立 apps/web/frontend/src/lib/ 模組:

- api.ts(第 6.10 節)

- queryKeys.ts(第 6.9 節)

- auth.ts(useSession() hook,從 /auth/session 取 session + csrf)

- theme.ts(useTheme() hook,read/write localStorage("praetor-theme") → set data-theme on <html>)

- i18n.ts(兩個 dict:en / zh-TW,shell-only 用語;長文還是給後端產)

2. 建立 components/AppShell.tsx、Sidebar.tsx、TopBar.tsx、CommandPalette.tsx、ThemeToggle.tsx

3. 建立 5 個空白 page:Office.tsx、Missions.tsx、Memory.tsx、Runtime.tsx、Settings.tsx(每個都用 <EmptyState> 顯示 placeholder)

4. 建立 App.tsx + 新版 main.tsx:

- QueryClientProvider

- BrowserRouter

- <AuthGate> (未登入導向 /app/login)

- <AppShell> 包 5 條 Route

5. 全域鍵盤:⌘K, ⌘\, g o/m/k/r/s

6. Command palette 內容先放:pages + dark mode toggle + logout

Acceptance:

---

Commit 3 — /office 上線(取代既有 React /office)

動作:

1. 實作 pages/Office.tsx 完整版(第 3.1 節)

2. 子元件:GreetingHeader.tsx、CEOChat.tsx、MissionCardGrid.tsx、MemoryTimeline.tsx、NeedsDecisionPanel.tsx

3. CEO chat 的「latest exchange」氣泡:送出後 50ms 內 optimistic 顯示 chairman 訊息,CEO 回應到了再淡入

4. 中文語音輸入:保留現有的 webkitSpeechRecognition,再加一條 fallback 路徑:點麥克風 → 錄音 → 上傳到 POST /app/ceo/transcribe(Jinja 路徑,先共用)

5. NeedsDecisionPanel 的 approve/reject 按鈕直接 hit POST /approvals/{id}/{status},成功後 invalidate qk.officeSnapshot

Acceptance:

---

Commit 4 — /missions + /missions/:id(刪掉 Jinja /app/tasks 和 /app/missions/:id)

動作:

1. 實作 pages/Missions.tsx(list,第 3.2 節)

2. 實作 pages/MissionDetail.tsx(tabs,第 3.3 節)

3. 子元件:MissionTable.tsx、NewMissionDialog.tsx、MissionTabBriefing.tsx、MissionTabTasks.tsx、MissionTabDecisions.tsx、MissionTabFiles.tsx、MissionTabActivity.tsx

4. [Run] 改用 POST /api/missions/{id}/enqueue,並 poll /api/missions/{id}/jobs 每 2s 直到最新 job status 是 terminal

5. 刪掉 Jinja:

- apps/api/praetor_api/ui.py 中的 /app/tasks、/app/overview、/app/activity、/app/missions/{mission_id}、/app/missions/{mission_id}/events、/app/missions/{mission_id}/run、/app/missions/{mission_id}/pause、/app/missions/{mission_id}/continue、/app/missions/{mission_id}/stop、/app/missions/{mission_id}/executor-control、/app/missions/{mission_id}/meeting、/app/missions/{mission_id}/memory-promotion、/app/missions/{mission_id}/board-briefing、/app/missions/{mission_id}/approval、/app/approvals/{approval_id}/{status} 全部刪除

- 對應 templates:tasks.html、overview.html、activity.html、mission_detail.html 刪除

- _translations.py 中只屬於這幾頁的 key 刪除(其它頁仍會用到的保留)

Acceptance:

---

Commit 5 — /memory(刪掉 /app/memory, /app/decisions, /app/meetings, /app/inbox)

動作:

1. 實作 pages/Memory.tsx(第 3.4 節)

2. 子元件:MemoryTree.tsx、MemoryDetail.tsx、StandingOrderForm.tsx

3. 用 react-markdown + remark-gfm render

4. 刪掉 Jinja:/app/memory、/app/decisions、/app/meetings、/app/inbox、/app/inbox/governance-review,對應 templates 與 i18n key 刪除

Acceptance:

---

Commit 6 — /runtime(刪掉 /app/models)

動作:

1. 實作 pages/Runtime.tsx(第 3.5 節)

2. 子元件:RuntimeHealthCard.tsx、ModelsTab.tsx、ExecutorsTab.tsx、RecentRunsTab.tsx、UsageChart.tsx

3. 後端只允許這個新增:在 apps/api/praetor_api/main.py 加一個 POST /api/settings/runtime(payload: RuntimeSelection),內部呼叫既有的 service.update_runtime()

4. 刪掉 Jinja:/app/models、/app/settings/runtime(form post path)、/app/settings/runtime/test,對應 template 與 i18n key 刪除

Acceptance:

---

Commit 7 — /settings(縮減 Jinja settings 為僅 form post)

動作:

1. 實作 pages/Settings.tsx(第 3.6 節)

2. 各 tab 顯示 read-only 資料 + 「Edit」按鈕,按 Edit 開 Dialog 顯示既有 Jinja form(用 iframe)— v1 妥協方案,v2 才把 settings form 全 JSON 化

3. 例外:Telegram 的「Create pairing code」按鈕直接 hit POST /app/settings/telegram/pairing-code 並顯示回傳碼

4. 刪掉 Jinja:/app/agents、/app/agents/skill-sources、/app/agents/skill-sources/{id}/import、/app/agents/skills/{id}/status,對應 template + i18n。這些舊 route 的 product label 必須顯示為 Company Playbook,不再顯示為獨立 Skill Registry。

5. apps/api/praetor_api/ui.py 至此應 < 600 行(只剩 /app/login, /app/welcome, /app/praetor (onboarding), /app/settings/* form posts, /app/onboarding, /app/login//app/logout, /app/set-language/*, /m/briefing(mobile))

Acceptance:

---

Commit 8 — Onboarding & login 視覺翻新

動作:

1. 更新 apps/api/praetor_api/static/praetor.css 中 /app/login、/app/welcome、/app/praetor 三頁 CSS,套上第 3.7 節的天空漸層

2. 字體切到 Inter + Noto Sans TC

3. 表單視覺對齊 SPA token

Acceptance:

---

Commit 9 — i18n 統一

動作:

1. 寫一個 build-time script apps/web/scripts/extract-translations.mjs:讀 apps/api/praetor_api/_translations.py 用 regex 抓 dict,輸出 apps/web/frontend/src/translations.json

2. 在 package.json 加 "prebuild": "node scripts/extract-translations.mjs"

3. 把 lib/i18n.ts 改成從 translations.json 讀取,不再內嵌 dict

4. 跟後端共用同一份 zh-TW / en key

Acceptance:

---

8. 完成定義(整個重建結束時)

---

9. Codex 執行注意事項

1. 每個 commit 都要先讀這份 playbook 對應段落,不要從 git log 推測意圖。

2. 遇到設計選擇衝突:以這份文件為準;若文件未涵蓋,選 Linear 的做法。

3. 不要新增後端 endpoint,除非 commit 6 的「POST /api/settings/runtime」這條已明確列出的例外。

4. 不要修改既有 Pydantic models(除非 commit 期間發現 type bug)。

5. 每次刪 Jinja 頁面前:先確認 SPA 對應頁能完整取代它,再刪。

6. smoke tests 是 source of truth:跑 pixi run app-smoke 失敗就要修,不要 skip。

7. commit message 用英文(與既有 commit history 一致),但程式碼註解和文件用 zh-TW 可。

8. branch 策略:每個 commit 直接 push 到 claude/crazy-taussig-d75255 worktree branch;要開 PR 時再合到 main。

9. 無頭測試:每個 commit 結束跑一輪 npm run build && pixi run app-import-check && pixi run app-smoke 三連。

10. 不確定就停:如果 Codex 在某個 commit 改了 > 30 個檔案、或刪掉的程式碼 > 500 行還沒進到刪 Jinja 階段,停下來等人工 review。

---

10. Reference 圖片的氣質

兩張參考圖的 spirit 對應到 Praetor 的哪一面:

如果 Codex 在某個視覺決策上猶豫:

---

End of playbook.