From 12e7d4d667f226999d600b3ae8a00f87945c46e9 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Fri, 21 Aug 2026 12:37:29 +0800 Subject: [PATCH 01/34] ux(empty,card): the two shared pieces every other phase 3 item needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Starting phase 3 with the primitives, so the eighteen items that follow do not each invent their own. **Empty states get their third part.** Every empty view was a grey circle and two lines of grey text with no way out — "Screenshots will appear here when captured" tells an operator exactly what they had already worked out from the empty screen. §5.4 wants what will appear, *why there is nothing yet*, and one button. The middle one is where the work is: "no screenshots yet" is the observation they just made; "periodic capture is off and you have not taken one by hand" is the reason, and it implies the fix. Screenshots, Loot, Targets, Marks and Transcript convert here. **Cards move their colour to the left.** The 2px bar across the top made the card itself the status indicator, so a screen of cards read as a wall of colour with nothing standing out — including the one that was actually shouting. Colour goes into a block on the left wrapped around the icon, 12% tint like the badges, solid only for danger. The card's background stays `surface` and never changes with state. Also fixes a live `ReferenceError` that predates this branch: the transcript's ↗ button called `onNavigate` inside App, where only `setView` exists. That is §7's transcript ↔ timeline link — one of the five cross-view routes phase 3 is here to complete — and it threw on every click. `test/tailwind-classes.test.ts` is new because I shipped two dead classes within an hour writing the card: `border-redlog-border-strong` (a token the phase 1 palette collapsed away) and `text-wrap-pretty` (the CSS property name; Tailwind spells it `text-pretty`). Tailwind drops what it does not recognise without a warning, so both looked right in review and did nothing on screen. Co-Authored-By: Claude Opus 5 --- src/renderer/src/App.tsx | 29 +++++--- src/renderer/src/components/Card.tsx | 68 +++++++++++++++++++ src/renderer/src/components/EmptyState.tsx | 63 +++++++++++++++++ src/renderer/src/components/FindingsView.tsx | 17 ++++- src/renderer/src/components/LootPanel.tsx | 13 ++-- src/renderer/src/components/TargetView.tsx | 8 ++- .../src/components/TranscriptView.tsx | 8 ++- src/renderer/src/i18n/en.json | 8 ++- src/renderer/src/i18n/zh-TW.json | 8 ++- test/tailwind-classes.test.ts | 45 ++++++++++++ test/truncation.test.ts | 38 +++++++++++ 11 files changed, 281 insertions(+), 24 deletions(-) create mode 100644 src/renderer/src/components/Card.tsx create mode 100644 src/renderer/src/components/EmptyState.tsx create mode 100644 test/tailwind-classes.test.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 32aa8018..4f8fc6d5 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -24,6 +24,7 @@ import { loadSidebarOrder, onSidebarOrderChanged, type SidebarViewId } from './l import { appShortcuts } from './lib/shortcuts' import logoUrl from './assets/logo.svg' import { Image } from 'lucide-react' +import { EmptyState } from './components/EmptyState' import { formatTime } from './lib/time' type View = SidebarViewId | 'settings' | 'search' @@ -245,10 +246,15 @@ export default function App(): JSX.Element { {view === 'transcript' && ( { setFocusEvent({ id, ts }); onNavigate('timeline') }} + // `onNavigate` is not in scope here — App switches views with + // `setView`. This threw a ReferenceError on every use of the + // transcript's ↗ button, which is §7's transcript ↔ timeline + // link and one of the five cross-view routes phase 3 is meant + // to be completing. + onOpenInTimeline={(id, ts) => { setFocusEvent({ id, ts }); setView('timeline') }} /> )} - {view === 'screenshots' && } + {view === 'screenshots' && setView(v as View)} />} {view === 'targets' && { setFocusEvent({ id: '', ts }); setView('timeline') }} />} {view === 'scope' && { setFocusEvent({ id: '', ts }); setView('timeline') }} />} {view === 'loot' && { setFocusEvent({ id, ts }); setView('timeline') }} />} @@ -940,7 +946,7 @@ function StatCard({ label, value, sub, tone = 'neutral' }: { ) } -function ScreenshotsView(): JSX.Element { +function ScreenshotsView({ onNavigate }: { onNavigate: (v: string) => void }): JSX.Element { const [screenshots, setScreenshots] = useState([]) const [thumbs, setThumbs] = useState>({}) const [expanded, setExpanded] = useState(null) @@ -1013,13 +1019,16 @@ function ScreenshotsView(): JSX.Element { {screenshots.length === 0 ? ( -
-
- -
-

{t('screenshots.empty')}

-

{t('screenshots.emptyDesc')}

-
+ { void window.redlog.screenshot.capture() } + }} + secondary={{ label: t('screenshots.emptyEnable'), onClick: () => onNavigate('settings') }} + /> ) : (() => { // Trigger filter (audit #32) — all captures land in one grid mixing // periodic / manual / mark-triggered. Chip toggles narrow the view. diff --git a/src/renderer/src/components/Card.tsx b/src/renderer/src/components/Card.tsx new file mode 100644 index 00000000..2d52ef73 --- /dev/null +++ b/src/renderer/src/components/Card.tsx @@ -0,0 +1,68 @@ +import type { LucideIcon } from 'lucide-react' +import type { ReactNode } from 'react' + +// The card (docs/UIUX-STANDARD.md §4). +// +// Cards used to carry a 2px colour bar across the top and, in some places, +// tint their whole background by status. Both are wrong for the same reason: +// they make the card itself the status indicator, so a screen of cards reads +// as a wall of colour with no hierarchy, and the operator cannot tell which +// one is actually shouting. +// +// The replacement puts the colour in a block on the left, wrapped around the +// icon. Danger fills it; everything else uses a 12% tint, matching the badge +// rule so one vocabulary covers both. The card's own background is always +// `surface` and never changes with state — status lives in one place on the +// card, not in the card. + +export type CardTone = 'neutral' | 'safe' | 'warn' | 'danger' | 'info' + +const TONE: Record = { + neutral: { block: 'bg-redlog-elevated', icon: 'text-redlog-text-dim' }, + safe: { block: 'bg-emerald-500/12', icon: 'text-emerald-400' }, + warn: { block: 'bg-amber-500/12', icon: 'text-amber-400' }, + info: { block: 'bg-cyan-500/12', icon: 'text-cyan-400' }, + // The one solid fill, and the one the standard allows once per screen. + danger: { block: 'bg-redlog-danger', icon: 'text-white' } +} + +export interface CardProps { + icon: LucideIcon + tone?: CardTone + label: string + children: ReactNode + /** Turns the whole card into a button. */ + onClick?: () => void + title?: string +} + +export function Card({ + icon: Icon, tone = 'neutral', label, children, onClick, title +}: CardProps): JSX.Element { + const t = TONE[tone] + const body = ( + <> + + + +
+

{label}

+ {children} +
+ + ) + const shell = 'flex items-stretch bg-redlog-surface border border-redlog-border rounded-lg overflow-hidden text-left w-full' + if (!onClick) return
{body}
+ return ( + + ) +} diff --git a/src/renderer/src/components/EmptyState.tsx b/src/renderer/src/components/EmptyState.tsx new file mode 100644 index 00000000..5eb896c9 --- /dev/null +++ b/src/renderer/src/components/EmptyState.tsx @@ -0,0 +1,63 @@ +import type { LucideIcon } from 'lucide-react' + +// The empty state, in three parts (docs/UIUX-STANDARD.md §5.4). +// +// Every empty view in the app was a grey circle and two lines of grey text +// with no way out — "Screenshots will appear here when captured" tells an +// operator nothing they had not already worked out from the empty screen. +// §5.4 asks for three things, and the third is the one that was always +// missing: +// +// what will appear here so the view has a purpose before it has content +// why there is nothing yet the actual reason, not a restatement of "empty" +// one button the next step, taken from here +// +// The `why` is the part worth writing carefully. "No screenshots yet" is the +// observation the operator just made themselves; "periodic capture is off and +// you have not taken one manually" is the reason, and it implies the fix. + +export interface EmptyStateProps { + icon: LucideIcon + /** What will show up here, once there is something. */ + title: string + /** Why there is nothing right now. The specific cause, not "it is empty". */ + reason: string + /** The one next step. Omitted only when there genuinely isn't one. */ + action?: { label: string; onClick: () => void } + /** A second, quieter route — usually "turn on the thing that fills this". */ + secondary?: { label: string; onClick: () => void } +} + +export function EmptyState({ + icon: Icon, title, reason, action, secondary +}: EmptyStateProps): JSX.Element { + return ( +
+ +

{title}

+

+ {reason} +

+ {(action || secondary) && ( +
+ {action && ( + + )} + {secondary && ( + + )} +
+ )} +
+ ) +} diff --git a/src/renderer/src/components/FindingsView.tsx b/src/renderer/src/components/FindingsView.tsx index 9b21cc8f..d6bc39dd 100644 --- a/src/renderer/src/components/FindingsView.tsx +++ b/src/renderer/src/components/FindingsView.tsx @@ -5,6 +5,8 @@ import { toast } from './Toast' import { DEFAULT_CDP_PORT } from '../lib/defaults' import { useListKeyboard } from '../lib/useListKeyboard' import { formatDateTime } from '../lib/time' +import { EmptyState } from './EmptyState' +import { Flag } from 'lucide-react' const TAG_COLORS = [ { bg: 'bg-red-500/20', text: 'text-red-400', dot: 'bg-red-400' }, @@ -182,9 +184,18 @@ export function QuickMarksView({ onOpenInTimeline }: { onOpenInTimeline?: (ts: n ) })} {filteredMarks.length === 0 && !creating && ( -
- {marks.length === 0 ? t('marks.empty') : t('marks.noSearchMatches', { query: search })} -
+ marks.length === 0 ? ( + setCreating(true) }} + /> + ) : ( +
+ {t('marks.noSearchMatches', { query: search })} +
+ ) )} diff --git a/src/renderer/src/components/LootPanel.tsx b/src/renderer/src/components/LootPanel.tsx index 53100daf..73a5f52f 100644 --- a/src/renderer/src/components/LootPanel.tsx +++ b/src/renderer/src/components/LootPanel.tsx @@ -3,6 +3,7 @@ import { useI18n } from '../i18n' import { LoadingSpinner } from './Feedback' import { toast } from './Toast' import { Gem } from 'lucide-react' +import { EmptyState } from './EmptyState' import { formatDateTime } from '../lib/time' import { useListKeyboard } from '../lib/useListKeyboard' @@ -151,13 +152,11 @@ export function LootPanel({ onOpenInTimeline }: { onOpenInTimeline?: (eventId: s })()} {lootEvents.length === 0 ? ( -
-
- -
-

{t('loot.empty')}

-

{t('loot.emptyDesc')}

-
+ ) : ( // v0.7.1 P1: rendering uses the same `visibleList` that feeds the // header count, so what you see and what the header says can never diff --git a/src/renderer/src/components/TargetView.tsx b/src/renderer/src/components/TargetView.tsx index 411ebcdf..d9c7ca0c 100644 --- a/src/renderer/src/components/TargetView.tsx +++ b/src/renderer/src/components/TargetView.tsx @@ -3,6 +3,8 @@ import { useI18n } from '../i18n' import { formatTime } from '../lib/time' import { toastDeferred } from './Toast' import { useListKeyboard } from '../lib/useListKeyboard' +import { EmptyState } from './EmptyState' +import { Crosshair } from 'lucide-react' interface TargetEntry { target: string @@ -182,7 +184,11 @@ export function TargetView({ onOpenInTimeline }: TargetViewProps = {}): JSX.Elem {filtered.length === 0 ? ( -

{t('targets.empty')}

+ ) : (
{filtered.map((tgt, i) => { diff --git a/src/renderer/src/components/TranscriptView.tsx b/src/renderer/src/components/TranscriptView.tsx index dffb6c97..7847b727 100644 --- a/src/renderer/src/components/TranscriptView.tsx +++ b/src/renderer/src/components/TranscriptView.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react' import { useI18n } from '../i18n/I18nContext' import { toast } from './Toast' import { formatTime } from '../lib/time' +import { EmptyState } from './EmptyState' +import { AlignLeft } from 'lucide-react' /** * v0.11.2 (design note T5): the Timeline read vertically. @@ -330,7 +332,11 @@ export default function TranscriptView({ onOpenInTimeline }: {
{loading &&

{t('transcript.loading')}

} {!loading && shown.length === 0 && ( -

{t('transcript.empty')}

+ )} {shown.map((b) => { const revealed = expanded.has(b.id) diff --git a/src/renderer/src/i18n/en.json b/src/renderer/src/i18n/en.json index b45f6b5c..0caf5653 100644 --- a/src/renderer/src/i18n/en.json +++ b/src/renderer/src/i18n/en.json @@ -960,5 +960,11 @@ "statusBar.tierCountTitle": "{{chained}} chained (the audit chain) · {{logged}} logged (supporting evidence). Click to hide the logged rows on the Timeline — open the Timeline first, or the click does nothing.", "statusBar.tierCountLabel": "{{chained}} chained events, {{logged}} logged events", "targets.listLabel": "Targets, {{count}} entries", - "scope.violationsLabel": "Scope violations, {{count}} entries" + "scope.violationsLabel": "Scope violations, {{count}} entries", + "screenshots.emptyReason": "Periodic capture is off and nothing has been captured by hand yet. Every screenshot carries a SHA-256 and the reason it was taken.", + "screenshots.emptyEnable": "Turn on periodic capture", + "loot.emptyReason": "Credentials and keys are detected in command output and clipboard content as they go past. Nothing has matched yet — which is the expected state at the start of an engagement.", + "targets.emptyReason": "Hosts and addresses are extracted from commands, HTTP traffic and DNS queries as they are captured. Nothing has been seen yet — run something against a host and it will appear.", + "marks.emptyReason": "Markers are the notes you leave for the write-up: what you found, and when. ⌘⇧M drops one from anywhere, including while another application is in front.", + "transcript.emptyReason": "This is what you typed and what came back, in order. It fills from shell commands, agent turns and HTTP exchanges as they are captured." } diff --git a/src/renderer/src/i18n/zh-TW.json b/src/renderer/src/i18n/zh-TW.json index 7d1804af..4a29c01a 100644 --- a/src/renderer/src/i18n/zh-TW.json +++ b/src/renderer/src/i18n/zh-TW.json @@ -960,5 +960,11 @@ "statusBar.tierCountTitle": "{{chained}} 筆已上鏈(證據鏈)· {{logged}} 筆已記錄(佐證資料)。點擊可在時間軸隱藏已記錄的列——請先開啟時間軸,否則點了不會有反應。", "statusBar.tierCountLabel": "{{chained}} 筆已上鏈事件,{{logged}} 筆已記錄事件", "targets.listLabel": "目標清單,{{count}} 筆", - "scope.violationsLabel": "範圍違規清單,{{count}} 筆" + "scope.violationsLabel": "範圍違規清單,{{count}} 筆", + "screenshots.emptyReason": "定時擷取目前關閉,而你尚未手動擷取過。每張截圖都帶 SHA-256 與觸發原因。", + "screenshots.emptyEnable": "開啟定時擷取", + "loot.emptyReason": "憑證與金鑰會在指令輸出與剪貼簿內容經過時被偵測。目前還沒有命中——在行動剛開始時這是正常的。", + "targets.emptyReason": "主機與位址會在指令、HTTP 流量與 DNS 查詢被擷取時自動抽出。目前還沒有看到任何一個——對某台主機跑點東西,它就會出現。", + "marks.emptyReason": "標記是你為報告留下的筆記:發現了什麼、在什麼時候。⌘⇧M 可以從任何地方落一個,包括其他應用程式在前景時。", + "transcript.emptyReason": "這裡按順序呈現你輸入了什麼、回來了什麼。它會隨著 shell 指令、代理對話與 HTTP 往返被擷取而填滿。" } diff --git a/test/tailwind-classes.test.ts b/test/tailwind-classes.test.ts new file mode 100644 index 00000000..d0e72cd2 --- /dev/null +++ b/test/tailwind-classes.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest' +import fs from 'fs' +import path from 'path' +import glob from 'fast-glob' +import { repoRelative } from './helpers/repo-path' + +// Tailwind drops a class it does not recognise. It does not warn, and the +// build does not fail — the element simply renders without that style, which +// on a hover state or a text colour is invisible until someone looks at the +// right pixel in the right state. +// +// Two of these shipped inside an hour of each other while writing the phase 3 +// card: `border-redlog-border-strong` (a token that existed before the phase 1 +// palette collapsed the border ramp) and `text-wrap-pretty` (the CSS property +// name; Tailwind spells it `text-pretty`). Both looked right in review. + +const ROOT = path.join(__dirname, '..') + +function redlogTokens(): Set { + const config = fs.readFileSync(path.join(ROOT, 'tailwind.config.js'), 'utf-8') + const block = /redlog: \{([\s\S]*?)\n \}/.exec(config) + if (!block) throw new Error('redlog token block not found') + return new Set([...block[1].matchAll(/'?([a-z][a-z-]*)'?:/g)].map((m) => m[1])) +} + +describe('tailwind classes', () => { + it('only references redlog tokens that exist', () => { + const tokens = redlogTokens() + const bad: string[] = [] + for (const file of glob.sync('src/renderer/src/**/*.{ts,tsx}', { cwd: ROOT, absolute: true })) { + const src = fs.readFileSync(file, 'utf-8') + for (const m of src.matchAll(/\b(?:[a-z-]+:)*(?:bg|text|border|ring|fill|stroke|divide|placeholder|outline|from|to|via|accent|caret|shadow)-redlog-([a-z][a-z-]*)/g)) { + // Longest-match first: `text-redlog-text-dim` must resolve as + // `text-dim`, not as `text` with a stray suffix. + const name = m[1] + if (tokens.has(name)) continue + // A trailing opacity or state suffix is not part of the token name. + const base = name.replace(/-(hover|focus|active|disabled)$/, '') + if (tokens.has(base)) continue + bad.push(`${repoRelative(ROOT, file)}: ${m[0]}`) + } + } + expect([...new Set(bad)]).toEqual([]) + }) +}) diff --git a/test/truncation.test.ts b/test/truncation.test.ts index 155e8669..b316a1c4 100644 --- a/test/truncation.test.ts +++ b/test/truncation.test.ts @@ -65,3 +65,41 @@ describe('truncated text', () => { expect(stale).toEqual([]) }) }) + +// §5.4: an empty state has three parts, and the third — a way out — is the +// one that was missing from every one of them. "Screenshots will appear here +// when captured" restates the empty screen the operator is already looking at. +describe('empty states', () => { + it('every view that can be empty uses the shared three-part component', () => { + const VIEWS = [ + ['App.tsx', 'screenshots'], + ['components/LootPanel.tsx', 'loot'], + ['components/TargetView.tsx', 'targets'], + ['components/FindingsView.tsx', 'marks'], + ['components/TranscriptView.tsx', 'transcript'] + ] as const + const missing = VIEWS.filter(([file]) => { + const src = fs.readFileSync(path.join(ROOT, 'src/renderer/src', file), 'utf-8') + return !/ file) + expect(missing).toEqual([]) + }) + + it('gives each one a reason distinct from its title', () => { + // The failure mode is a `reason` that restates the title in more words. + // Comparing the strings catches the laziest version of that. + const en = JSON.parse( + fs.readFileSync(path.join(ROOT, 'src/renderer/src/i18n/en.json'), 'utf-8') + ) as Record + const pairs = Object.keys(en) + .filter((k) => k.endsWith('.emptyReason')) + .map((k) => [k.replace('.emptyReason', '.empty'), k]) + expect(pairs.length, 'no empty-state reasons found at all').toBeGreaterThan(2) + for (const [titleKey, reasonKey] of pairs) { + expect(en[reasonKey], `${reasonKey} is missing`).toBeTruthy() + expect(en[reasonKey], `${reasonKey} just restates the title`).not.toBe(en[titleKey]) + // A reason short enough to be a title is not explaining anything. + expect(en[reasonKey].length, `${reasonKey} is too short to be a reason`).toBeGreaterThan(40) + } + }) +}) From 40a4d856b1d9382eeb123a4b9308daddc1d78111 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Fri, 21 Aug 2026 12:53:00 +0800 Subject: [PATCH 02/34] =?UTF-8?q?ux(sidebar,status):=20fix=20the=20nav=20o?= =?UTF-8?q?rder,=20print=20the=20numbers,=20drop=20the=20=E2=8F=B1=20butto?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **The sidebar order is fixed and the numbers are printed** (§5.3). Those two changes depend on each other: while rows could be dragged, the number was a property of the operator's current arrangement rather than of the view, so printing it would have taught the wrong thing — and ⌘2 had already drifted off the sidebar once, which is why the shared order module was written in the first place. Fixing the order is what lets the number be shown, and showing it is what makes the shortcut learnable without opening the ? panel. That deletes the drag gesture, the localStorage persistence, the change-subscription, and the state and effect in App that consumed it — about seventy lines, and with them a class of bug where what ⌘2 did and what the sidebar showed were maintained separately. **The status bar's ⏱ button goes** (§10). It wrote to the same table as a marker with neither a title nor a severity, so what it produced was a marker that says nothing — the operator has to remember what they meant by that timestamp, which is the job the marker dialog already does properly. ⌘⇧M reaches that from anywhere, including from another application. One note on verification: a full e2e run failed on `cli-smoke` after these changes, taking 7.4 minutes for a spec that takes 6 seconds. It passed in isolation and in two subsequent full runs, and nothing in this diff touches the CLI or the API server. I could not reproduce it and am not claiming to have explained it. Co-Authored-By: Claude Opus 5 --- src/renderer/src/App.tsx | 12 ++-- src/renderer/src/components/Sidebar.tsx | 86 ++++++----------------- src/renderer/src/components/StatusBar.tsx | 15 ---- src/renderer/src/i18n/en.json | 4 -- src/renderer/src/i18n/zh-TW.json | 4 -- src/renderer/src/lib/sidebarOrder.ts | 67 +++++------------- 6 files changed, 44 insertions(+), 144 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4f8fc6d5..29e4843f 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -20,7 +20,7 @@ import { ConfirmDialogContainer, confirm as confirmDialog } from './components/C import { toast } from './components/Toast' import { computeCaptureReadiness, primaryCaptureAction, type CaptureAction } from './lib/captureReadiness' import { useI18n } from './i18n' -import { loadSidebarOrder, onSidebarOrderChanged, type SidebarViewId } from './lib/sidebarOrder' +import { DEFAULT_ORDER, type SidebarViewId } from './lib/sidebarOrder' import { appShortcuts } from './lib/shortcuts' import logoUrl from './assets/logo.svg' import { Image } from 'lucide-react' @@ -44,7 +44,7 @@ type View = SidebarViewId | 'settings' | 'search' const SETTINGS_SHORTCUT_INDEX = 9 function currentShortcutOrder(): View[] { - return loadSidebarOrder().slice(0, SETTINGS_SHORTCUT_INDEX - 1) as View[] + return DEFAULT_ORDER.slice(0, SETTINGS_SHORTCUT_INDEX - 1) as View[] } function viewForShortcut(num: number): View | null { @@ -684,10 +684,10 @@ function DashboardView({ onNavigate }: { onNavigate: (v: string) => void }): JSX // a stalled OTS submission at a glance (e.g. "last anchor: 3h ago" vs "26h ago"). const [lastAnchor, setLastAnchor] = useState<{ createdAt: number; status: string } | null>(null) const [loading, setLoading] = useState(true) - // Shortcut order — kept in state + subscribed so a drag-reorder in the sidebar - // updates the cheatsheet immediately, no view switch required. - const [shortcutOrder, setShortcutOrder] = useState(currentShortcutOrder) - useEffect(() => onSidebarOrderChanged(() => setShortcutOrder(currentShortcutOrder())), []) + // Fixed since §5.3 — no state, no subscription. It was both when the + // sidebar could be dragged. + const shortcutOrder = currentShortcutOrder() + const { t } = useI18n() useEffect(() => { diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 4138a307..138c14af 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -28,11 +28,10 @@ const NAV_ICON_STROKE = 1.5 // Shared with App.tsx's ⌘1..9 shortcut handler so the two orders can't // drift. See src/renderer/src/lib/sidebarOrder.ts. -import { DEFAULT_ORDER, loadSidebarOrder, saveSidebarOrder } from '../lib/sidebarOrder' +import { DEFAULT_ORDER } from '../lib/sidebarOrder' // Aliased so existing call sites don't need to change; the persistence lives // in the shared module now so App.tsx's ⌘1..9 handler sees the same order. -const loadOrder = loadSidebarOrder // Vertical travel before a press counts as a reorder instead of a click. Kept // comfortably above the few px a normal click/tap drifts on a trackpad — a @@ -45,14 +44,6 @@ const isMac = (window as { redlog?: { platform?: string } }).redlog?.platform != export default function Sidebar({ active, onNavigate }: SidebarProps): JSX.Element { const [lootCount, setLootCount] = useState(0) const [scopeViolations, setScopeViolations] = useState(0) - const [order, setOrder] = useState(loadOrder) - const [draggingId, setDraggingId] = useState(null) - // HTML5 `draggable` hands the browser control of the gesture, and any press - // it decides was a drag never produces a click — so navigation would silently - // fail. Own the gesture instead: a press is a click until it travels far - // enough to be a reorder. - const press = useRef<{ id: string; y: number; from: number } | null>(null) - const didDrag = useRef(false) const { t } = useI18n() useEffect(() => { @@ -84,49 +75,9 @@ export default function Sidebar({ active, onNavigate }: SidebarProps): JSX.Eleme marks: { id: 'marks', label: t('sidebar.marks'), icon: Flag } } - const items = order.map((id) => itemMap[id]).filter(Boolean) + const items = DEFAULT_ORDER.map((id) => itemMap[id]).filter(Boolean) - const onPointerDown = useCallback((e: React.PointerEvent, id: string) => { - if (e.button !== 0) return - press.current = { id, y: e.clientY, from: order.indexOf(id) } - didDrag.current = false - }, [order]) - - const onPointerMove = useCallback((e: React.PointerEvent) => { - const p = press.current - if (!p) return - const dy = e.clientY - p.y - if (!didDrag.current && Math.abs(dy) < DRAG_THRESHOLD_PX) return - - if (!didDrag.current) { - didDrag.current = true - setDraggingId(p.id) - e.currentTarget.setPointerCapture(e.pointerId) - } - - const target = Math.max(0, Math.min(order.length - 1, p.from + Math.round(dy / ITEM_STRIDE_PX))) - const current = order.indexOf(p.id) - if (target === current) return - const next = [...order] - next.splice(current, 1) - next.splice(target, 0, p.id) - setOrder(next) - }, [order]) - - const onPointerUp = useCallback(() => { - if (didDrag.current) saveSidebarOrder(order as import('../lib/sidebarOrder').SidebarViewId[]) - press.current = null - setDraggingId(null) - }, [order]) - - const onItemClick = useCallback((id: string) => { - // A press that turned into a reorder should not also navigate. - if (didDrag.current) { - didDrag.current = false - return - } - onNavigate(id) - }, [onNavigate]) + const onItemClick = useCallback((id: string) => onNavigate(id), [onNavigate]) return (
diff --git a/src/renderer/src/components/StatusBar.tsx b/src/renderer/src/components/StatusBar.tsx index e099129e..02081f0e 100644 --- a/src/renderer/src/components/StatusBar.tsx +++ b/src/renderer/src/components/StatusBar.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react' import { useI18n } from '../i18n' import { toast, toastUndo } from './Toast' import { Gem } from 'lucide-react' -import { formatTime } from '../lib/time' import { useIssues, raiseIssue, clearIssue } from '../lib/issues' import { appShortcuts } from '../lib/shortcuts' @@ -20,7 +19,6 @@ export default function StatusBar(): JSX.Element { const [scopeViolations, setScopeViolations] = useState(0) const [uptime, setUptime] = useState(0) const [recording, setRecording] = useState(true) - const [stamped, setStamped] = useState(false) const [overlayVisible, setOverlayVisible] = useState(true) const [captureVerdict, setCaptureVerdict] = useState<'healthy' | 'partial' | 'dark' | null>(null) const { t } = useI18n() @@ -307,19 +305,6 @@ export default function StatusBar(): JSX.Element { {t('statusBar.events', { count: eventCount })} )} -
{showMarker && { setShowMarker(false); setMarkerAtTs(undefined) }} atTimestamp={markerAtTs} />} + setPaletteOpen(false)} + onNavigate={(v) => setView(v as View)} + onOpenEvent={(id, ts) => { setFocusEvent({ id, ts }); setView('timeline') }} + recording={recordingOn} + /> diff --git a/src/renderer/src/components/CommandPalette.tsx b/src/renderer/src/components/CommandPalette.tsx new file mode 100644 index 00000000..0f34badf --- /dev/null +++ b/src/renderer/src/components/CommandPalette.tsx @@ -0,0 +1,288 @@ +import { useState, useEffect, useMemo, useRef, useCallback } from 'react' +import { + Gauge, ChevronRight, Rows3, AlignLeft, Image, Crosshair, Ban, Gem, Flag, + Settings as SettingsIcon, Search, Play, Pause, FolderOpen, Rows2, type LucideIcon +} from 'lucide-react' +import { useI18n } from '../i18n' +import { useFocusTrap } from '../lib/useFocusTrap' +import { DEFAULT_ORDER } from '../lib/sidebarOrder' +import { applyDensity, resolveDensity, storedDensity, DENSITY_KEY } from '../lib/density' +import { formatTime } from '../lib/time' +import { toast } from './Toast' + +// ⌘K, the one way in (docs/UIUX-STANDARD.md §10). +// +// The sidebar had a Search page: a whole navigation slot, reachable by ⌘/, +// that did one thing — full-text over events. Meanwhile navigation, project +// switching, density and every app-level action had no keyboard route at all, +// and the Timeline had its own separate palette on the same ⌘K that did not +// exist anywhere else. An operator had to know which surface they were on to +// know what a key would do. +// +// §10 collapses that: ⌘K is global and covers navigation, fuzzy search over +// events/markers/operators/hosts, actions, density, and project switching. +// In-page filtering moves to ⌘F, which is the convention every other desktop +// app already taught the operator. + +type Section = 'nav' | 'action' | 'view' | 'project' | 'search' + +interface Item { + id: string + section: Section + icon: LucideIcon + label: string + /** Right-hand detail: a shortcut, a timestamp, a path. */ + hint?: string + run: () => void +} + +// ⌘9 is Settings', so the sidebar's numbered run stops at eight. +const NUMBERED_VIEWS = 8 +// Same detection App uses; read defensively because this module is imported +// in tests where the preload bridge may be absent. +const MOD = (window as { redlog?: { platform?: string } }).redlog?.platform === 'win32' ? 'Ctrl+' : '⌘' + +const NAV_ICONS: Record = { + dashboard: Gauge, terminal: ChevronRight, timeline: Rows3, transcript: AlignLeft, + screenshots: Image, targets: Crosshair, scope: Ban, loot: Gem, marks: Flag +} + +const SECTION_KEY: Record = { + nav: 'palette.sectionNav', + action: 'palette.sectionAction', + view: 'palette.sectionView', + project: 'palette.sectionProject', + search: 'palette.sectionSearch' +} + +/** Case-insensitive subsequence-free substring score, same rule the Timeline's + * palette uses: earlier match wins, then shorter haystack. Deliberately not a + * fuzzy matcher — these are literal identifiers (view names, hosts, commands) + * and gap tolerance only adds noise. */ +function score(haystack: string, needle: string): number { + if (!needle) return 1 + const i = haystack.toLowerCase().indexOf(needle.toLowerCase()) + if (i < 0) return 0 + return 1000 - i * 10 - Math.min(haystack.length, 99) / 100 +} + +export interface CommandPaletteProps { + open: boolean + onClose: () => void + onNavigate: (view: string) => void + onOpenEvent: (id: string, ts: number) => void + recording: boolean +} + +export function CommandPalette({ + open, onClose, onNavigate, onOpenEvent, recording +}: CommandPaletteProps): JSX.Element | null { + const { t } = useI18n() + const [query, setQuery] = useState('') + const [cursor, setCursor] = useState(0) + const [events, setEvents] = useState([]) + const [projects, setProjects] = useState([]) + const panel = useRef(null) + const field = useRef(null) + const debounce = useRef | null>(null) + + useFocusTrap(panel, open, field) + + // Reset on every open: a palette that remembers last time's query makes the + // operator delete something before they can type. + useEffect(() => { + if (!open) return + setQuery('') + setCursor(0) + setEvents([]) + window.redlog.project.list().then(setProjects).catch(() => {}) + }, [open]) + + // Event search is the only part that costs anything, so it is the only part + // that waits. + useEffect(() => { + if (!open) return + if (debounce.current) clearTimeout(debounce.current) + const q = query.trim() + if (q.length < 2) { setEvents([]); return } + debounce.current = setTimeout(() => { + window.redlog.events.search(q, 40).then(setEvents).catch(() => setEvents([])) + }, 140) + return () => { if (debounce.current) clearTimeout(debounce.current) } + }, [query, open]) + + const items = useMemo(() => { + const q = query.trim() + const out: Item[] = [] + + for (const [i, view] of DEFAULT_ORDER.entries()) { + const label = t(`sidebar.${view === 'screenshots' ? 'screens' : view}`) + out.push({ + id: `nav:${view}`, section: 'nav', icon: NAV_ICONS[view] ?? Gauge, label, + // Only the first eight are numbered. ⌘9 belongs to Settings, which is + // pinned outside the reorderable run — the ninth sidebar row has no + // chord, and claiming one here would advertise a key that does + // something else. + hint: i < NUMBERED_VIEWS ? `${MOD}${i + 1}` : undefined, + run: () => onNavigate(view) + }) + } + out.push({ + id: 'nav:settings', section: 'nav', icon: SettingsIcon, + label: t('sidebar.settings'), hint: `${MOD}9`, run: () => onNavigate('settings') + }) + + out.push({ + id: 'action:recording', section: 'action', icon: recording ? Pause : Play, + label: recording ? t('palette.pauseRecording') : t('palette.resumeRecording'), + hint: `${MOD}.`, + run: () => { void window.redlog.recording.toggle() } + }) + out.push({ + id: 'action:screenshot', section: 'action', icon: Image, + label: t('screenshots.captureNow'), + run: () => { + void window.redlog.screenshot.capture().then(() => toast(t('palette.screenshotTaken'), 'success')) + } + }) + + // The Timeline keeps a palette scoped to the events it has loaded, which + // knows about lanes, operators and hosts in a way a database search does + // not. ⌘K used to open it; now that ⌘K is global, this is how it stays + // reachable — rather than leaving a working feature with no way in. + out.push({ + id: 'action:timelinePalette', section: 'action', icon: Search, + label: t('palette.timelineScoped'), + run: () => { + onNavigate('timeline') + // After the view switch, so the Timeline is mounted to hear it. + setTimeout(() => window.dispatchEvent(new CustomEvent('redlog-timeline-palette')), 0) + } + }) + + const density = resolveDensity(1, storedDensity()) + out.push({ + id: 'view:density', section: 'view', icon: Rows2, + label: density === 'tight' ? t('palette.densityComfortable') : t('palette.densityTight'), + run: () => { + const next = density === 'tight' ? 'comfortable' : 'tight' + try { localStorage.setItem(DENSITY_KEY, next) } catch { /* quota */ } + applyDensity(next) + } + }) + + for (const p of projects.slice(0, 8)) { + out.push({ + id: `project:${p.id}`, section: 'project', icon: FolderOpen, + label: p.name, hint: formatTime(p.lastOpened), + run: () => { void window.redlog.project.open(p.id).then(() => window.location.reload()) } + }) + } + + for (const e of events) { + const d = e.data as Record | undefined + const label = String(d?.command ?? d?.title ?? d?.url ?? d?.description ?? e.agentType) + out.push({ + id: `search:${e.id}`, section: 'search', icon: Search, + label, hint: formatTime(e.timestamp, { seconds: true }), + run: () => onOpenEvent(e.id, e.timestamp) + }) + } + + if (!q) { + // With no query, the search section is empty and the rest is a menu. + return out.filter((i) => i.section !== 'search') + } + return out + .map((i) => ({ i, s: i.section === 'search' ? 900 : score(i.label, q) })) + .filter((x) => x.s > 0) + .sort((a, b) => b.s - a.s) + .map((x) => x.i) + }, [query, events, projects, recording, t, onNavigate, onOpenEvent]) + + useEffect(() => { setCursor(0) }, [query]) + + const activate = useCallback((item: Item | undefined) => { + if (!item) return + onClose() + item.run() + }, [onClose]) + + if (!open) return null + + const onKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === 'Escape') { e.preventDefault(); onClose(); return } + if (e.key === 'ArrowDown') { e.preventDefault(); setCursor((c) => Math.min(items.length - 1, c + 1)); return } + if (e.key === 'ArrowUp') { e.preventDefault(); setCursor((c) => Math.max(0, c - 1)); return } + if (e.key === 'Enter') { e.preventDefault(); activate(items[cursor]) } + } + + let lastSection: Section | null = null + + return ( +
+
e.stopPropagation()} + onKeyDown={onKeyDown} + > +
+ + setQuery(e.target.value)} + placeholder={t('palette.placeholder')} + aria-label={t('palette.placeholder')} + autoComplete="off" + spellCheck={false} + className="flex-1 bg-transparent py-3 text-sm text-redlog-text placeholder-redlog-muted outline-none" + /> +
+ +
+ {items.length === 0 && ( +

+ {t('palette.noMatches', { query })} +

+ )} + {items.map((item, i) => { + const header = item.section !== lastSection ? item.section : null + lastSection = item.section + return ( +
+ {header && ( +

+ {t(SECTION_KEY[header])} +

+ )} + +
+ ) + })} +
+
+
+ ) +} diff --git a/src/renderer/src/components/SearchPanel.tsx b/src/renderer/src/components/SearchPanel.tsx deleted file mode 100644 index 85343bd4..00000000 --- a/src/renderer/src/components/SearchPanel.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { useState, useCallback, useRef } from 'react' -import { useListKeyboard } from '../lib/useListKeyboard' -import { useI18n } from '../i18n' -import { formatTime } from '../lib/time' - -const TYPE_COLORS: Record = { - shell: 'text-green-400', - screenshot: 'text-blue-400', - clipboard: 'text-yellow-400', - file_transfer: 'text-purple-400', - marker: 'text-red-400', - loot: 'text-orange-400', - system: 'text-redlog-text-dim' -} - -function eventSummary(e: RedLogEvent): string { - const d = e.data - if (e.agentType === 'shell') return `$ ${(d.command as string)?.slice(0, 120) || ''}` - if (e.agentType === 'screenshot') return `Screenshot (${d.trigger})` - if (e.agentType === 'clipboard') return `Clipboard: ${(d.content as string)?.slice(0, 80) || ''}` - if (e.agentType === 'marker') return `[${d.severity}] ${d.title}` - if (e.agentType === 'file_transfer') return `${d.direction}: ${d.filename || d.localPath || d.remotePath}` - if (e.agentType === 'loot') return `Loot: ${d.type} (${d.confidence})` - return `${e.agentType}: ${d.subtype || JSON.stringify(d).slice(0, 60)}` -} - -interface SearchPanelProps { - onOpenInTimeline?: (eventId: string, ts: number) => void -} - -export function SearchPanel({ onOpenInTimeline }: SearchPanelProps = {}): JSX.Element { - const [query, setQuery] = useState('') - const [results, setResults] = useState([]) - const [searching, setSearching] = useState(false) - const [searched, setSearched] = useState(false) - // Type-filter chips: null = show all, non-null = only that agentType. Audit - // finding #57 — before this, "192.168" matching 200 shell events left no - // way to say "only screenshots". Kept client-side because the search - // backend uses full-text LIKE that doesn't take a WHERE agent_type filter. - const [typeFilter, setTypeFilter] = useState(null) - const debounceRef = useRef | null>(null) - const { t } = useI18n() - - // Hoisted out of the render IIFE it used to live in so the keyboard hook can - // count it. Same keys as every other list (§9); a result row's only action - // is "show me this on the Timeline", so Enter and ⌘↩ agree. - const filtered = typeFilter ? results.filter((e) => e.agentType === typeFilter) : results - const listNav = useListKeyboard({ - count: filtered.length, - onActivate: (i) => { const e = filtered[i]; if (e) onOpenInTimeline?.(e.id, e.timestamp) }, - onJumpToTimeline: (i) => { const e = filtered[i]; if (e) onOpenInTimeline?.(e.id, e.timestamp) }, - onEscape: () => setTypeFilter(null) - }) - - const doSearch = useCallback((q: string) => { - // Single-char search intents are real (IP octet, short tag) — down from - // the prior q<2 gate. 0-char still shows the placeholder hint. Audit P1 #27. - if (q.length < 1) { - setResults([]) - setSearched(false) - return - } - setSearching(true) - window.redlog.events.search(q, 200).then((r) => { - setResults(r) - setSearching(false) - setSearched(true) - }) - }, []) - - const onChange = useCallback((val: string) => { - setQuery(val) - if (debounceRef.current) clearTimeout(debounceRef.current) - debounceRef.current = setTimeout(() => doSearch(val), 300) - }, [doSearch]) - - return ( -
-
- onChange(e.target.value)} - placeholder={t('search.placeholder')} - autoFocus - className="w-full bg-redlog-elevated border border-redlog-border rounded-lg px-4 py-2.5 text-sm text-redlog-text font-mono focus:outline-none focus:border-red-500 placeholder-redlog-text-faint" - /> - {searching && ( - ... - )} -
- -
- {!searched && !searching && ( -
- {t('search.hint')} -
- )} - {searched && results.length === 0 && ( -
- {t('search.noResults', { query })} -
- )} - {results.length > 0 && (() => { - // Bucket by agentType so the filter chips can show counts inline. - const byType = new Map() - for (const e of results) byType.set(e.agentType, (byType.get(e.agentType) ?? 0) + 1) - const types = [...byType.entries()].sort((a, b) => b[1] - a[1]) - return ( - <> -
- {t('search.results', { count: filtered.length })} - {typeFilter && <> · } -
- {types.length > 1 && ( -
- {types.map(([type, count]) => ( - - ))} -
- )} -
- {filtered.map((e, i) => { - const rowProps = listNav.itemProps(i) - return ( - - ) - })} -
- - )})()} -
-
- ) -} diff --git a/src/renderer/src/components/Timeline.tsx b/src/renderer/src/components/Timeline.tsx index 2607c529..4a254a60 100644 --- a/src/renderer/src/components/Timeline.tsx +++ b/src/renderer/src/components/Timeline.tsx @@ -966,6 +966,18 @@ export default function TimelinePanel({ focusEventId, focusTs, onDropMarker }: { window.addEventListener('redlog-timeline-palette', onOpen) return () => window.removeEventListener('redlog-timeline-palette', onOpen) }, []) + + // ⌘F focuses the in-page filter (§5.7, §10). `/` still does too — it is the + // chord this view taught first and there is no reason to take it away — but + // ⌘F is the one an operator arrives already knowing. + useEffect(() => { + const onFind = (): void => { + searchInputRef.current?.focus() + searchInputRef.current?.select() + } + window.addEventListener('redlog:find-in-page', onFind) + return () => window.removeEventListener('redlog:find-in-page', onFind) + }, []) useEffect(() => { if (paletteOpen) { // Autofocus after paint — the modal renders inside a portal-like fixed diff --git a/src/renderer/src/i18n/en.json b/src/renderer/src/i18n/en.json index 04a77047..1083b550 100644 --- a/src/renderer/src/i18n/en.json +++ b/src/renderer/src/i18n/en.json @@ -21,7 +21,6 @@ "time.dAgo": "{{d}}d ago", "sidebar.dashboard": "Dashboard", "sidebar.terminal": "Terminal", - "sidebar.search": "Search (⌘/)", "sidebar.ops": "OPS", "sidebar.intel": "INTEL", "sidebar.timeline": "Timeline", @@ -76,7 +75,7 @@ "dashboard.shortcuts": "Keyboard Shortcuts", "dashboard.addMarker": "Add Marker", "dashboard.search": "Search", - "dashboard.palette": "Fuzzy palette (Timeline) / search", + "dashboard.palette": "Command palette — go anywhere, run anything, search events", "dashboard.toggleRecording": "Pause / resume recording", "dashboard.hudCorner": "Snap the HUD to a screen corner", "dashboard.terminalNewTab": "New terminal tab", @@ -245,12 +244,6 @@ "marker.saving": "Saving...", "marker.submit": "Mark + Screenshot", "marker.defaultTitle": "Mark @ {{time}}", - "search.placeholder": "Search commands, targets, clipboard, loot...", - "search.hint": "Type at least 2 characters to search across all events", - "search.noResults": "No results for \"{{query}}\"", - "search.results": "{{count}} results", - "search.openInTimeline": "Open in timeline", - "search.clearFilter": "clear filter", "statusBar.rec": "REC", "statusBar.paused": "PAUSED", "statusBar.clickToPause": "Click to pause recording", @@ -927,7 +920,6 @@ "issues.captureDetail": "Events are still being written, but at least one source has stopped producing — the gap will be in the timeline, not in an error.", "issues.chainBroken": "The evidence chain does not verify", "issues.chainBrokenDetail": "A row between the anchor and now does not hash to its successor. Export a bundle before doing anything else.", - "search.resultsLabel": "Search results, {{count}} events", "timeline.dropMarkerHere": "Drop a marker at {{time}}", "fatal.title": "{{view}} stopped working", "fatal.thisView": "This view", @@ -962,5 +954,20 @@ "loot.emptyReason": "Credentials and keys are detected in command output and clipboard content as they go past. Nothing has matched yet — which is the expected state at the start of an engagement.", "targets.emptyReason": "Hosts and addresses are extracted from commands, HTTP traffic and DNS queries as they are captured. Nothing has been seen yet — run something against a host and it will appear.", "marks.emptyReason": "Markers are the notes you leave for the write-up: what you found, and when. ⌘⇧M drops one from anywhere, including while another application is in front.", - "transcript.emptyReason": "This is what you typed and what came back, in order. It fills from shell commands, agent turns and HTTP exchanges as they are captured." + "transcript.emptyReason": "This is what you typed and what came back, in order. It fills from shell commands, agent turns and HTTP exchanges as they are captured.", + "palette.title": "Command palette", + "palette.placeholder": "Go to a view, run an action, or search events…", + "palette.noMatches": "Nothing matches “{{query}}”", + "palette.sectionNav": "Go to", + "palette.sectionAction": "Actions", + "palette.sectionView": "View", + "palette.sectionProject": "Projects", + "palette.sectionSearch": "Events", + "palette.pauseRecording": "Pause recording", + "palette.resumeRecording": "Resume recording", + "palette.screenshotTaken": "Screenshot captured", + "palette.densityTight": "Switch to tight density", + "palette.densityComfortable": "Switch to comfortable density", + "dashboard.findInPage": "Filter what is on screen", + "palette.timelineScoped": "Search within the loaded timeline (lanes, operators, hosts)" } diff --git a/src/renderer/src/i18n/zh-TW.json b/src/renderer/src/i18n/zh-TW.json index 9855d812..90adf1ce 100644 --- a/src/renderer/src/i18n/zh-TW.json +++ b/src/renderer/src/i18n/zh-TW.json @@ -21,7 +21,6 @@ "time.dAgo": "{{d}} 天前", "sidebar.dashboard": "儀表板", "sidebar.terminal": "終端機", - "sidebar.search": "搜尋 (⌘/)", "sidebar.ops": "作戰", "sidebar.intel": "情報", "sidebar.timeline": "時間軸", @@ -76,7 +75,7 @@ "dashboard.shortcuts": "快捷鍵", "dashboard.addMarker": "新增標記", "dashboard.search": "搜尋", - "dashboard.palette": "模糊搜尋面板(時間軸)/搜尋", + "dashboard.palette": "命令面板——前往任何畫面、執行任何動作、搜尋事件", "dashboard.toggleRecording": "暫停/恢復錄製", "dashboard.hudCorner": "將 HUD 貼齊螢幕角落", "dashboard.terminalNewTab": "新增終端機分頁", @@ -245,12 +244,6 @@ "marker.saving": "儲存中...", "marker.submit": "標記 + 截圖", "marker.defaultTitle": "Mark @ {{time}}", - "search.placeholder": "搜尋指令、目標、剪貼簿、戰利品...", - "search.hint": "輸入至少 2 個字元以跨所有事件搜尋", - "search.noResults": "「{{query}}」沒有結果", - "search.results": "{{count}} 項結果", - "search.openInTimeline": "在時間軸開啟", - "search.clearFilter": "清除篩選", "statusBar.rec": "錄製", "statusBar.paused": "已暫停", "statusBar.clickToPause": "點擊暫停錄製", @@ -927,7 +920,6 @@ "issues.captureDetail": "事件仍在寫入,但至少有一個來源已停止產出——缺口會出現在時間軸上,不會出現在錯誤訊息裡。", "issues.chainBroken": "證據鏈驗證失敗", "issues.chainBrokenDetail": "錨定點與現在之間有一列的雜湊接不上下一列。做任何其他事之前先匯出證據包。", - "search.resultsLabel": "搜尋結果,{{count}} 筆事件", "timeline.dropMarkerHere": "在 {{time}} 落一個標記", "fatal.title": "{{view}} 停止運作", "fatal.thisView": "這個畫面", @@ -962,5 +954,20 @@ "loot.emptyReason": "憑證與金鑰會在指令輸出與剪貼簿內容經過時被偵測。目前還沒有命中——在行動剛開始時這是正常的。", "targets.emptyReason": "主機與位址會在指令、HTTP 流量與 DNS 查詢被擷取時自動抽出。目前還沒有看到任何一個——對某台主機跑點東西,它就會出現。", "marks.emptyReason": "標記是你為報告留下的筆記:發現了什麼、在什麼時候。⌘⇧M 可以從任何地方落一個,包括其他應用程式在前景時。", - "transcript.emptyReason": "這裡按順序呈現你輸入了什麼、回來了什麼。它會隨著 shell 指令、代理對話與 HTTP 往返被擷取而填滿。" + "transcript.emptyReason": "這裡按順序呈現你輸入了什麼、回來了什麼。它會隨著 shell 指令、代理對話與 HTTP 往返被擷取而填滿。", + "palette.title": "命令面板", + "palette.placeholder": "前往畫面、執行動作,或搜尋事件…", + "palette.noMatches": "沒有符合「{{query}}」的項目", + "palette.sectionNav": "前往", + "palette.sectionAction": "動作", + "palette.sectionView": "檢視", + "palette.sectionProject": "專案", + "palette.sectionSearch": "事件", + "palette.pauseRecording": "暫停錄製", + "palette.resumeRecording": "恢復錄製", + "palette.screenshotTaken": "已擷取截圖", + "palette.densityTight": "切換為緊密密度", + "palette.densityComfortable": "切換為寬鬆密度", + "dashboard.findInPage": "篩選目前畫面上的內容", + "palette.timelineScoped": "在已載入的時間軸內搜尋(泳道、operator、host)" } diff --git a/src/renderer/src/lib/shortcuts.ts b/src/renderer/src/lib/shortcuts.ts index 4904ed76..cebe151e 100644 --- a/src/renderer/src/lib/shortcuts.ts +++ b/src/renderer/src/lib/shortcuts.ts @@ -106,16 +106,19 @@ export function appShortcuts(order: readonly string[], isMac: boolean): Shortcut return [ ...nav, { - id: 'app:search', keys: `${m}/`, label: 'dashboard.search', scope: 'app', + // §10: one palette, every view. ⌘/ used to open a separate Search page + // that has been deleted; it stays bound here as an alias so the muscle + // memory still lands somewhere useful rather than nowhere. + id: 'app:palette', keys: `${m}K`, label: 'dashboard.palette', scope: 'app', guardTyping: true, - // ⌘/ was the original chord, but macOS sends `Unidentified` as `key` for - // it (the system Help-menu grab), so `code` is what actually matches. - match: (e) => mod(e) && (e.key === '/' || e.code === 'Slash') + // macOS sends `Unidentified` as `key` for ⌘/ (the system Help-menu + // grab), so `code` is what actually matches for that alias. + match: (e) => mod(e) && (e.key === 'k' || e.key === 'K' || e.key === '/' || e.code === 'Slash') }, { - id: 'app:palette', keys: `${m}K`, label: 'dashboard.palette', scope: 'app', + id: 'app:findInPage', keys: `${m}F`, label: 'dashboard.findInPage', scope: 'app', guardTyping: true, - match: (e) => mod(e) && (e.key === 'k' || e.key === 'K') + match: (e) => mod(e) && (e.key === 'f' || e.key === 'F') }, { id: 'app:toggleRecording', keys: `${m}.`, label: 'dashboard.toggleRecording', scope: 'app', diff --git a/test/command-palette.test.tsx b/test/command-palette.test.tsx new file mode 100644 index 00000000..28b622e8 --- /dev/null +++ b/test/command-palette.test.tsx @@ -0,0 +1,128 @@ +// @vitest-environment jsdom +// +// ⌘K (UIUX-STANDARD §10). The palette replaced a whole sidebar page, so the +// things that page did — and the things it never did — both have to work here. + +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' +import { render, cleanup, screen, fireEvent, waitFor, act } from '@testing-library/react' +import { I18nProvider } from '../src/renderer/src/i18n' +import { CommandPalette } from '../src/renderer/src/components/CommandPalette' + +const bridge = { + project: { list: vi.fn(async () => []), open: vi.fn(async () => null) }, + events: { search: vi.fn(async () => []) }, + recording: { toggle: vi.fn(async () => false) }, + screenshot: { capture: vi.fn(async () => null) } +} + +beforeEach(() => { + ;(window as unknown as { redlog: typeof bridge }).redlog = bridge + vi.clearAllMocks() +}) +afterEach(cleanup) + +function open(props: Partial[0]> = {}): { + onNavigate: ReturnType + onClose: ReturnType +} { + const onNavigate = vi.fn() + const onClose = vi.fn() + render( + + + + ) + return { onNavigate, onClose } +} + +const type = (value: string): void => { + fireEvent.change(screen.getByRole('textbox'), { target: { value } }) +} + +describe('command palette', () => { + it('lists every view with its number, so the shortcut is learnable from here', async () => { + open() + const options = await screen.findAllByRole('option') + expect(options.length).toBeGreaterThan(9) + // The numbers are the point: the palette teaches ⌘N while being used. + expect(screen.getByText('⌘1')).toBeTruthy() + // ⌘9 is Settings'. The ninth sidebar row must not claim it — advertising + // a key that does something else is worse than advertising none. + expect(screen.getAllByText('⌘9')).toHaveLength(1) + const marks = screen.getByText(/marks/i).closest('[role="option"]') + expect(marks?.textContent).not.toMatch(/⌘\d/) + }) + + it('navigates and closes on Enter', async () => { + const { onNavigate, onClose } = open() + await screen.findAllByRole('option') + fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Enter' }) + expect(onNavigate).toHaveBeenCalledOnce() + expect(onClose).toHaveBeenCalledOnce() + }) + + it('filters as you type and reports an honest miss', async () => { + open() + type('zzzznotathing') + await waitFor(() => expect(screen.queryAllByRole('option')).toHaveLength(0)) + expect(screen.getByText(/zzzznotathing/)).toBeTruthy() + }) + + it('waits before hitting the database, and not at all for one character', async () => { + vi.useFakeTimers() + open() + type('a') + await act(async () => { vi.advanceTimersByTime(500) }) + expect(bridge.events.search, 'one character is not a search').not.toHaveBeenCalled() + + type('adm') + expect(bridge.events.search, 'not on the keystroke').not.toHaveBeenCalled() + await act(async () => { vi.advanceTimersByTime(200) }) + expect(bridge.events.search).toHaveBeenCalledWith('adm', 40) + vi.useRealTimers() + }) + + it('starts empty every time it opens', async () => { + // A palette that remembers last time's query makes the operator delete + // something before they can type. + const { onClose } = open() + type('leftover') + expect((screen.getByRole('textbox') as HTMLInputElement).value).toBe('leftover') + cleanup() + void onClose + open() + expect((screen.getByRole('textbox') as HTMLInputElement).value).toBe('') + }) + + it('closes on Escape', async () => { + const { onClose } = open() + await screen.findAllByRole('option') + fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' }) + expect(onClose).toHaveBeenCalledOnce() + }) + + it('offers pause or resume to match the current state', async () => { + open({ recording: true }) + expect(await screen.findByText(/pause recording/i)).toBeTruthy() + cleanup() + open({ recording: false }) + expect(await screen.findByText(/resume recording/i)).toBeTruthy() + }) + + it('keeps the timeline-scoped palette reachable now that ⌘K is global', async () => { + // It lost its chord when ⌘K became app-wide. A working feature with no + // way in is worse than one that was removed on purpose. + const { onNavigate } = open() + type('loaded timeline') + const option = await screen.findByRole('option') + fireEvent.click(option) + expect(onNavigate).toHaveBeenCalledWith('timeline') + }) +}) diff --git a/test/list-keyboard.test.tsx b/test/list-keyboard.test.tsx index bf523464..92a53f68 100644 --- a/test/list-keyboard.test.tsx +++ b/test/list-keyboard.test.tsx @@ -125,10 +125,12 @@ describe('list keyboard', () => { // Scope violations — the two lists an operator reaches for under time pressure // — did not, and nothing said so. describe('every list view uses the shared contract', () => { + // SearchPanel was on this list until §10 deleted the Search page and folded + // it into ⌘K. The palette has its own keyboard handling — it is a dialog, + // not a list view — so it is not a sixth entry here. const LIST_VIEWS = [ 'FindingsView.tsx', 'LootPanel.tsx', - 'SearchPanel.tsx', 'TargetView.tsx', 'ScopeStatus.tsx' ] diff --git a/test/renderer-smoke.test.tsx b/test/renderer-smoke.test.tsx index 3f7a252f..8cbf3e63 100644 --- a/test/renderer-smoke.test.tsx +++ b/test/renderer-smoke.test.tsx @@ -22,7 +22,6 @@ import IPStatusCard from '../src/renderer/src/components/IPStatusCard' import { TargetView } from '../src/renderer/src/components/TargetView' import { ScopeStatus } from '../src/renderer/src/components/ScopeStatus' import { LootPanel } from '../src/renderer/src/components/LootPanel' -import { SearchPanel } from '../src/renderer/src/components/SearchPanel' import { QuickMarksView } from '../src/renderer/src/components/FindingsView' const AGENT_TYPES = [ @@ -217,7 +216,6 @@ describe('renderer views render without throwing', () => { ['TargetView', () => ], ['ScopeStatus', () => ], ['LootPanel', () => ], - ['SearchPanel', () => ], ['QuickMarksView', () => ], ['OverlayApp', () => ] ] diff --git a/test/shortcuts.test.ts b/test/shortcuts.test.ts index 20511de5..12a7c5ea 100644 --- a/test/shortcuts.test.ts +++ b/test/shortcuts.test.ts @@ -81,7 +81,7 @@ describe('the cheatsheet table', () => { it('documents the app- and terminal-scoped bindings', () => { const rows = appShortcuts(SIDEBAR_ORDER, true).filter((r) => r.scope !== 'nav') expect(rows.map((r) => r.keys)).toEqual([ - '⌘/', '⌘K', '⌘.', '⌘⇧M', '⌘⇧⌥↑↓←→', '⌘⇧P', '⌘T', '⌘W', '⌘⇧[ ]' + '⌘K', '⌘F', '⌘.', '⌘⇧M', '⌘⇧⌥↑↓←→', '⌘⇧P', '⌘T', '⌘W', '⌘⇧[ ]' ]) }) }) @@ -128,7 +128,7 @@ describe('the table drives the handler, not just the cheatsheet', () => { expect(r.guardTyping, r.id).toBeFalsy() } // The ones a field may reasonably want do yield. - for (const id of ['app:search', 'app:palette', 'app:hudCorner']) { + for (const id of ['app:palette', 'app:findInPage', 'app:hudCorner']) { expect(rows.find((r) => r.id === id)?.guardTyping, id).toBe(true) } }) From 12daf45f4b64dfbe658bc9da83d5cfe78b74e28d Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Fri, 21 Aug 2026 13:26:25 +0800 Subject: [PATCH 04/34] fix(platform): ask whether it is a Mac, not whether it is not Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five modules had grown their own copy of the platform check and four wrote it as `platform !== 'win32'`. On Linux that is true, so every Linux operator was shown `⌘` — a key their keyboard does not have — in the sidebar tooltips, the status bar, the Timeline and the `?` panel. `shortcuts.ts` was never wrong: it takes `isMac` as a parameter, which is exactly why its tests passed throughout. The error was in what every caller handed it, and no test looked there. The bug also survives being fixed in one place. I wrote the palette's version an hour ago as `=== 'win32' ? 'Ctrl+' : '⌘'` while removing a hardcoded `⌘`, which moves the mistake to the other side of the same check and still shows Linux a Command key. The question is not "is this Windows" — it is "is this a Mac", and only `darwin` answers yes. One module answers it now, and a test asserts nobody re-derives it. Operator search lands in the palette with it. §10 lists operator among what ⌘K covers, and unlike host it needs no aggregation and no loaded timeline — `operators:list` is a plain registry read, so this half had no reason to wait for the other. Choosing one filters the Timeline to what that person did. Revoked operators are left out: a revoked key cannot produce new events, so offering it as a filter promises a view that can only ever shrink. Host stays owed, and belongs with the SQL aggregation rather than with the palette — `data.host` lives inside event JSON, so only the Timeline can currently count it, and the global answer is the same `GROUP BY` the Targets page needs. They are one query with two consumers. Also drops a comment in `shortcuts.ts` still describing ⌘1..8 as following a drag-reorder that no longer exists. Co-Authored-By: Claude Opus 5 --- src/renderer/src/App.tsx | 4 +- .../src/components/CommandPalette.tsx | 27 +++++++++-- src/renderer/src/components/Sidebar.tsx | 2 +- src/renderer/src/components/StatusBar.tsx | 3 +- src/renderer/src/components/Timeline.tsx | 14 +++++- src/renderer/src/i18n/en.json | 4 +- src/renderer/src/i18n/zh-TW.json | 4 +- src/renderer/src/lib/platform.ts | 25 ++++++++++ src/renderer/src/lib/shortcuts.ts | 6 ++- test/command-palette.test.tsx | 46 +++++++++++++++++-- test/shortcuts.test.ts | 33 +++++++++++++ 11 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 src/renderer/src/lib/platform.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4fcfe689..98bd0457 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -26,6 +26,7 @@ import logoUrl from './assets/logo.svg' import { Image } from 'lucide-react' import { EmptyState } from './components/EmptyState' import { formatTime } from './lib/time' +import { isMac } from './lib/platform' type View = SidebarViewId | 'settings' @@ -53,9 +54,6 @@ function viewForShortcut(num: number): View | null { return num >= 1 && num <= order.length ? order[num - 1] : null } -// Read defensively — this runs at module load, before the preload bridge is -// guaranteed present (e.g. in tests). Default to mac styling. -const isMac = (window as { redlog?: { platform?: string } }).redlog?.platform !== 'win32' export default function App(): JSX.Element { const [project, setProject] = useState<{ id: string; name: string } | null>(null) diff --git a/src/renderer/src/components/CommandPalette.tsx b/src/renderer/src/components/CommandPalette.tsx index 0f34badf..1d4c7270 100644 --- a/src/renderer/src/components/CommandPalette.tsx +++ b/src/renderer/src/components/CommandPalette.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useMemo, useRef, useCallback } from 'react' import { Gauge, ChevronRight, Rows3, AlignLeft, Image, Crosshair, Ban, Gem, Flag, - Settings as SettingsIcon, Search, Play, Pause, FolderOpen, Rows2, type LucideIcon + Settings as SettingsIcon, Search, Play, Pause, FolderOpen, Rows2, UserRound, type LucideIcon } from 'lucide-react' import { useI18n } from '../i18n' import { useFocusTrap } from '../lib/useFocusTrap' @@ -9,6 +9,7 @@ import { DEFAULT_ORDER } from '../lib/sidebarOrder' import { applyDensity, resolveDensity, storedDensity, DENSITY_KEY } from '../lib/density' import { formatTime } from '../lib/time' import { toast } from './Toast' +import { MOD } from '../lib/platform' // ⌘K, the one way in (docs/UIUX-STANDARD.md §10). // @@ -24,7 +25,7 @@ import { toast } from './Toast' // In-page filtering moves to ⌘F, which is the convention every other desktop // app already taught the operator. -type Section = 'nav' | 'action' | 'view' | 'project' | 'search' +type Section = 'nav' | 'action' | 'view' | 'project' | 'operator' | 'search' interface Item { id: string @@ -40,7 +41,7 @@ interface Item { const NUMBERED_VIEWS = 8 // Same detection App uses; read defensively because this module is imported // in tests where the preload bridge may be absent. -const MOD = (window as { redlog?: { platform?: string } }).redlog?.platform === 'win32' ? 'Ctrl+' : '⌘' + const NAV_ICONS: Record = { dashboard: Gauge, terminal: ChevronRight, timeline: Rows3, transcript: AlignLeft, @@ -52,6 +53,7 @@ const SECTION_KEY: Record = { action: 'palette.sectionAction', view: 'palette.sectionView', project: 'palette.sectionProject', + operator: 'palette.sectionOperator', search: 'palette.sectionSearch' } @@ -82,6 +84,7 @@ export function CommandPalette({ const [cursor, setCursor] = useState(0) const [events, setEvents] = useState([]) const [projects, setProjects] = useState([]) + const [operators, setOperators] = useState([]) const panel = useRef(null) const field = useRef(null) const debounce = useRef | null>(null) @@ -96,6 +99,10 @@ export function CommandPalette({ setCursor(0) setEvents([]) window.redlog.project.list().then(setProjects).catch(() => {}) + // §10 lists operator among what ⌘K covers. It needs no aggregation and no + // loaded timeline — `operators:list` is a plain registry read, which is + // why this half could land ahead of host search. + window.redlog.operators.list().then(setOperators).catch(() => {}) }, [open]) // Event search is the only part that costs anything, so it is the only part @@ -179,6 +186,18 @@ export function CommandPalette({ }) } + for (const op of operators) { + if (op.revokedAt) continue + out.push({ + id: `operator:${op.id}`, section: 'operator', icon: UserRound, + label: op.name, + hint: op.isPrimary ? t('palette.operatorPrimary') : undefined, + // Filtering the timeline by operator is the question worth asking of + // one: "what did this person do". + run: () => { onNavigate('timeline'); setTimeout(() => window.dispatchEvent(new CustomEvent('redlog:filter-operator', { detail: op.name })), 0) } + }) + } + for (const e of events) { const d = e.data as Record | undefined const label = String(d?.command ?? d?.title ?? d?.url ?? d?.description ?? e.agentType) @@ -198,7 +217,7 @@ export function CommandPalette({ .filter((x) => x.s > 0) .sort((a, b) => b.s - a.s) .map((x) => x.i) - }, [query, events, projects, recording, t, onNavigate, onOpenEvent]) + }, [query, events, projects, operators, recording, t, onNavigate, onOpenEvent]) useEffect(() => { setCursor(0) }, [query]) diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 138c14af..f8942343 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -29,6 +29,7 @@ const NAV_ICON_STROKE = 1.5 // Shared with App.tsx's ⌘1..9 shortcut handler so the two orders can't // drift. See src/renderer/src/lib/sidebarOrder.ts. import { DEFAULT_ORDER } from '../lib/sidebarOrder' +import { isMac } from '../lib/platform' // Aliased so existing call sites don't need to change; the persistence lives // in the shared module now so App.tsx's ⌘1..9 handler sees the same order. @@ -39,7 +40,6 @@ import { DEFAULT_ORDER } from '../lib/sidebarOrder' // navigation (the "first click does nothing" bug). const DRAG_THRESHOLD_PX = 12 const ITEM_STRIDE_PX = 34 // h-8 button + space-y-0.5 gap -const isMac = (window as { redlog?: { platform?: string } }).redlog?.platform !== 'win32' export default function Sidebar({ active, onNavigate }: SidebarProps): JSX.Element { const [lootCount, setLootCount] = useState(0) diff --git a/src/renderer/src/components/StatusBar.tsx b/src/renderer/src/components/StatusBar.tsx index 02081f0e..390276c3 100644 --- a/src/renderer/src/components/StatusBar.tsx +++ b/src/renderer/src/components/StatusBar.tsx @@ -4,10 +4,11 @@ import { toast, toastUndo } from './Toast' import { Gem } from 'lucide-react' import { useIssues, raiseIssue, clearIssue } from '../lib/issues' import { appShortcuts } from '../lib/shortcuts' +import { isMac } from '../lib/platform' // The ⌘. chord, drawn the way this platform writes it. Read from the one // shortcut table so the toast cannot drift from the binding (§11). -const isMacPlatform = (window as { redlog?: { platform?: string } }).redlog?.platform !== 'win32' +const isMacPlatform = isMac const recordingChord = appShortcuts([], isMacPlatform).find((r) => r.id === 'app:toggleRecording')?.keys ?? '' diff --git a/src/renderer/src/components/Timeline.tsx b/src/renderer/src/components/Timeline.tsx index 4a254a60..d697b7e3 100644 --- a/src/renderer/src/components/Timeline.tsx +++ b/src/renderer/src/components/Timeline.tsx @@ -11,6 +11,7 @@ import { Rows3 } from 'lucide-react' import { formatTime, formatTs, type TzMode, type TsStyle } from '../lib/time' import { timelineShortcuts } from '../lib/shortcuts' import { nextSelection } from '../lib/timelineSelection' +import { isMac } from '../lib/platform' const MIN_LANE_H = 36 const LABEL_W = 92 @@ -66,7 +67,7 @@ const EXTERNAL_ONLY_LANES: Set = new Set(['credential_use', 'c2_checkin' // (#6e6e78) and the palette is a function, not a table. // Read defensively — this runs at module load, before the preload bridge // is guaranteed present (e.g. in tests). Default to mac styling, as App does. -const isMacPlatform = (window as { redlog?: { platform?: string } }).redlog?.platform !== 'win32' +const isMacPlatform = isMac const LANE_COLOR = '#6e6e78' const LANE_COLORS: Record = Object.fromEntries( @@ -978,6 +979,17 @@ export default function TimelinePanel({ focusEventId, focusTs, onDropMarker }: { window.addEventListener('redlog:find-in-page', onFind) return () => window.removeEventListener('redlog:find-in-page', onFind) }, []) + + // ⌘K → an operator → filter this view to what that person did. The palette + // cannot reach into the Timeline's filter state, so it asks by event. + useEffect(() => { + const onFilterOperator = (e: Event): void => { + const name = (e as CustomEvent).detail + if (name) setFilterQuery(name) + } + window.addEventListener('redlog:filter-operator', onFilterOperator) + return () => window.removeEventListener('redlog:filter-operator', onFilterOperator) + }, []) useEffect(() => { if (paletteOpen) { // Autofocus after paint — the modal renders inside a portal-like fixed diff --git a/src/renderer/src/i18n/en.json b/src/renderer/src/i18n/en.json index 1083b550..cd864b07 100644 --- a/src/renderer/src/i18n/en.json +++ b/src/renderer/src/i18n/en.json @@ -969,5 +969,7 @@ "palette.densityTight": "Switch to tight density", "palette.densityComfortable": "Switch to comfortable density", "dashboard.findInPage": "Filter what is on screen", - "palette.timelineScoped": "Search within the loaded timeline (lanes, operators, hosts)" + "palette.timelineScoped": "Search within the loaded timeline (lanes, operators, hosts)", + "palette.sectionOperator": "Operators", + "palette.operatorPrimary": "primary" } diff --git a/src/renderer/src/i18n/zh-TW.json b/src/renderer/src/i18n/zh-TW.json index 90adf1ce..4f5e3f06 100644 --- a/src/renderer/src/i18n/zh-TW.json +++ b/src/renderer/src/i18n/zh-TW.json @@ -969,5 +969,7 @@ "palette.densityTight": "切換為緊密密度", "palette.densityComfortable": "切換為寬鬆密度", "dashboard.findInPage": "篩選目前畫面上的內容", - "palette.timelineScoped": "在已載入的時間軸內搜尋(泳道、operator、host)" + "palette.timelineScoped": "在已載入的時間軸內搜尋(泳道、operator、host)", + "palette.sectionOperator": "Operator", + "palette.operatorPrimary": "主要" } diff --git a/src/renderer/src/lib/platform.ts b/src/renderer/src/lib/platform.ts new file mode 100644 index 00000000..e373b303 --- /dev/null +++ b/src/renderer/src/lib/platform.ts @@ -0,0 +1,25 @@ +// Which keyboard the operator is looking at (docs/UIUX-STANDARD.md §5.7). +// +// Five modules had grown their own copy of this, and four of them wrote it as +// `platform !== 'win32'`. On Linux that is true, so every Linux operator was +// shown `⌘` — a key their keyboard does not have — in the sidebar tooltips, +// the status bar, the Timeline and the `?` panel. The test that pinned the +// shortcut table never caught it because `shortcuts.ts` correctly takes +// `isMac` as a parameter; the error was in what every caller passed it. +// +// The bug survives being fixed in one place, too: rewriting a call site as +// `=== 'win32' ? 'Ctrl+' : '⌘'` moves the mistake to the other side of the +// same check and still shows Linux a Command key. The question is not "is +// this Windows" — it is "is this a Mac", and only `darwin` answers yes. +// +// Read defensively: this module is imported in tests where the preload bridge +// is absent. Defaulting to non-Mac is the safer wrong answer — `Ctrl+` is at +// least a key that exists everywhere. + +const platform = (window as { redlog?: { platform?: string } }).redlog?.platform + +/** True only on macOS. Everything else — Windows and Linux — uses Ctrl. */ +export const isMac = platform === 'darwin' + +/** The modifier prefix as this platform writes it: `⌘` or `Ctrl+`. */ +export const MOD = isMac ? '⌘' : 'Ctrl+' diff --git a/src/renderer/src/lib/shortcuts.ts b/src/renderer/src/lib/shortcuts.ts index cebe151e..eda0533d 100644 --- a/src/renderer/src/lib/shortcuts.ts +++ b/src/renderer/src/lib/shortcuts.ts @@ -82,8 +82,10 @@ export function formatAccelerator(accelerator: string, isMac: boolean): string { } /** Every shortcut the app binds, in the order the cheatsheet shows them. - * `order` is the live sidebar order so the ⌘1..8 rows follow a drag-reorder; - * Settings is pinned to ⌘9 and is not part of that order. */ + * `order` is the navigation order — fixed since §5.3, though this still takes + * it as a parameter rather than importing it, so a test can pass a short list + * and assert the numbering without standing up the whole sidebar. Settings is + * pinned to ⌘9 and is not part of it. */ export function appShortcuts(order: readonly string[], isMac: boolean): ShortcutRow[] { const m = isMac ? '⌘' : 'Ctrl+' const nav: ShortcutRow[] = order.map((view, i) => ({ diff --git a/test/command-palette.test.tsx b/test/command-palette.test.tsx index 28b622e8..de8ae005 100644 --- a/test/command-palette.test.tsx +++ b/test/command-palette.test.tsx @@ -11,6 +11,11 @@ import { CommandPalette } from '../src/renderer/src/components/CommandPalette' const bridge = { project: { list: vi.fn(async () => []), open: vi.fn(async () => null) }, events: { search: vi.fn(async () => []) }, + operators: { list: vi.fn(async () => [ + { id: 'op1', name: 'alice', isPrimary: true, createdAt: 0, revokedAt: null }, + { id: 'op2', name: 'bob', isPrimary: false, createdAt: 0, revokedAt: null }, + { id: 'op3', name: 'carol-revoked', isPrimary: false, createdAt: 0, revokedAt: 1 } + ]) }, recording: { toggle: vi.fn(async () => false) }, screenshot: { capture: vi.fn(async () => null) } } @@ -51,13 +56,19 @@ describe('command palette', () => { open() const options = await screen.findAllByRole('option') expect(options.length).toBeGreaterThan(9) - // The numbers are the point: the palette teaches ⌘N while being used. - expect(screen.getByText('⌘1')).toBeTruthy() + // The numbers are the point: the palette teaches the chord while being + // used. Asserted without the glyph — under jsdom there is no preload + // bridge, so `lib/platform` correctly answers "not a Mac" and the hints + // read `Ctrl+N`. Pinning `⌘` here would only pin the test machine. + const hints = options + .map((o) => o.querySelector('.font-mono')?.textContent ?? '') + .filter(Boolean) + expect(hints.filter((h) => /(?:⌘|Ctrl\+)1$/.test(h))).toHaveLength(1) // ⌘9 is Settings'. The ninth sidebar row must not claim it — advertising // a key that does something else is worse than advertising none. - expect(screen.getAllByText('⌘9')).toHaveLength(1) + expect(hints.filter((h) => /(?:⌘|Ctrl\+)9$/.test(h))).toHaveLength(1) const marks = screen.getByText(/marks/i).closest('[role="option"]') - expect(marks?.textContent).not.toMatch(/⌘\d/) + expect(marks?.textContent).not.toMatch(/(?:⌘|Ctrl\+)\d/) }) it('navigates and closes on Enter', async () => { @@ -126,3 +137,30 @@ describe('command palette', () => { expect(onNavigate).toHaveBeenCalledWith('timeline') }) }) + +describe('operator search', () => { + it('offers live operators and filters the timeline to one', async () => { + // §10 lists operator among what ⌘K covers, and it needs no aggregation and + // no loaded timeline — `operators:list` is a plain registry read. + const { onNavigate } = open() + type('alice') + const option = await screen.findByRole('option') + expect(option.textContent).toMatch(/alice/) + + const dispatched: string[] = [] + window.addEventListener('redlog:filter-operator', (e) => { + dispatched.push((e as CustomEvent).detail) + }) + fireEvent.click(option) + expect(onNavigate).toHaveBeenCalledWith('timeline') + await waitFor(() => expect(dispatched).toEqual(['alice'])) + }) + + it('leaves revoked operators out', async () => { + // A revoked key cannot produce new events, so offering it as a filter + // promises a view that can only ever shrink. + open() + type('carol') + await waitFor(() => expect(screen.queryAllByRole('option')).toHaveLength(0)) + }) +}) diff --git a/test/shortcuts.test.ts b/test/shortcuts.test.ts index 12a7c5ea..255999c8 100644 --- a/test/shortcuts.test.ts +++ b/test/shortcuts.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest' import * as fs from 'fs' import * as path from 'path' +import glob from 'fast-glob' import { QUICK_MARK_ACCELERATOR as MAIN_ACCEL, HUD_PASSTHROUGH_ACCELERATOR as MAIN_HUD_ACCEL @@ -153,3 +154,35 @@ describe('the table drives the handler, not just the cheatsheet', () => { expect(timelineShortcuts(false)[0].rows[1].keys).toBe('Ctrl+K') }) }) + +// Every module that draws a chord asks the same question, and four of the five +// asked it backwards: `platform !== 'win32'` is true on Linux, so a Linux +// operator was shown `⌘` — a key their keyboard does not have — in the sidebar +// tooltips, the status bar, the Timeline and the `?` panel. `shortcuts.ts` +// itself was always correct; the error was in what each caller passed it, +// which is why the table's own tests never saw it. +describe('platform detection', () => { + it('lives in one module, and asks whether it is a Mac', () => { + const raw = fs.readFileSync( + path.join(__dirname, '../src/renderer/src/lib/platform.ts'), 'utf-8' + ) + // Comments only, stripped — the module's header quotes the buggy form in + // order to explain it, and matching that would be matching the prose. + const code = raw.split('\n').filter((l) => !l.trimStart().startsWith('//')).join('\n') + expect(code).toMatch(/platform === 'darwin'/) + expect(code, 'the inverted form is the bug this module exists to remove') + .not.toMatch(/platform !== 'win32'/) + }) + + it('is not re-derived anywhere else', () => { + const offenders = glob.sync('src/renderer/src/**/*.{ts,tsx}', { + cwd: path.join(__dirname, '..'), absolute: true + }) + .filter((f) => !f.endsWith('lib/platform.ts')) + .filter((f) => /redlog\?\.platform\s*[!=]==/.test(fs.readFileSync(f, 'utf-8'))) + .map((f) => path.relative(path.join(__dirname, '..'), f).split(path.sep).join('/')) + // Settings legitimately branches on the OS for OS-specific features + // (macOS location services, Windows WSL) rather than for a keyboard glyph. + expect(offenders.filter((f) => !f.endsWith('Settings.tsx'))).toEqual([]) + }) +}) From 90e1cfba31447ffb04cd8d66316f8d0f122c8d0c Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Fri, 21 Aug 2026 13:30:54 +0800 Subject: [PATCH 05/34] ux(sidebar,empty): the 1c deviations, and the residue the drag removal left MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five things from the audit against 1c. **The sidebar is 186px.** It was 140px when it held an icon and a label; printing the number left a few pixels between label and number, and a loot badge squeezed all three. The whole value of printing the number is that it can be read, so the column has to fit. **Count badges stop filling.** Loot was `bg-amber-500 text-white` and scope violations solid too, so the sidebar carried two solid blocks — exactly the competition "one solid red per screen" exists to prevent. §4's badge rule is 12% tint plus same-colour text, and only danger fills. A count is not danger. **One red per state.** The active row drew its text as `red-400` (#d75f63) and its indicator bar as `red-500` (#cf5459) — two reds for one state, five units apart, which reads as a rendering artefact rather than a decision. Both are `redlog-accent` now. The raw `red-*` classes are legitimate elsewhere (the `soften` map remaps them), so this is about the inconsistency, not the namespace. **Empty states take screen-title weight** — 19px/15px rather than 15px/13px. Sized down they read as a caption for content that is not there. The action becomes §4's primary: solid accent, 34px, 8px radius. An empty state has exactly one button, which makes it the primary one by definition. **Residue from deleting the drag gesture**: `DRAG_THRESHOLD_PX`, `ITEM_STRIDE_PX` and a comment claiming persistence still lives in the shared module, none of which had any remaining referent. The Settings row also gains the `title`, `aria-label` and `aria-current` every other row already had — it had been the one row a screen reader could not place. Co-Authored-By: Claude Opus 5 --- src/renderer/src/components/EmptyState.tsx | 14 +++-- src/renderer/src/components/Sidebar.tsx | 65 +++++++++++----------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/src/renderer/src/components/EmptyState.tsx b/src/renderer/src/components/EmptyState.tsx index 5eb896c9..347a7eb9 100644 --- a/src/renderer/src/components/EmptyState.tsx +++ b/src/renderer/src/components/EmptyState.tsx @@ -34,8 +34,11 @@ export function EmptyState({ return (
-

{title}

-

+ {/* An empty state is the whole screen, so it takes screen-title weight + rather than the size a list row would use — 19px/15px, not 15px/13px. + Sized down, it reads as a caption for content that is not there. */} +

{title}

+

{reason}

{(action || secondary) && ( @@ -43,7 +46,10 @@ export function EmptyState({ {action && ( @@ -51,7 +57,7 @@ export function EmptyState({ {secondary && ( diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index f8942343..ccded455 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -26,21 +26,11 @@ interface NavItem { const NAV_ICON_SIZE = 16 const NAV_ICON_STROKE = 1.5 -// Shared with App.tsx's ⌘1..9 shortcut handler so the two orders can't -// drift. See src/renderer/src/lib/sidebarOrder.ts. +// Shared with App.tsx's ⌘1..9 shortcut handler so the printed number and the +// key that works can never disagree. See src/renderer/src/lib/sidebarOrder.ts. import { DEFAULT_ORDER } from '../lib/sidebarOrder' import { isMac } from '../lib/platform' -// Aliased so existing call sites don't need to change; the persistence lives -// in the shared module now so App.tsx's ⌘1..9 handler sees the same order. - -// Vertical travel before a press counts as a reorder instead of a click. Kept -// comfortably above the few px a normal click/tap drifts on a trackpad — a -// smaller value reclassified ordinary clicks as drags and swallowed the first -// navigation (the "first click does nothing" bug). -const DRAG_THRESHOLD_PX = 12 -const ITEM_STRIDE_PX = 34 // h-8 button + space-y-0.5 gap - export default function Sidebar({ active, onNavigate }: SidebarProps): JSX.Element { const [lootCount, setLootCount] = useState(0) const [scopeViolations, setScopeViolations] = useState(0) @@ -56,9 +46,13 @@ export default function Sidebar({ active, onNavigate }: SidebarProps): JSX.Eleme return unsub }, []) - const badge = (count: number, color: string): JSX.Element | null => + // §4: 12% tint + same-colour text, the same vocabulary the badges elsewhere + // use. Only danger fills, and a count is not danger — two solid blocks on + // one sidebar is exactly the competition "one solid red per screen" exists + // to prevent. + const badge = (count: number, tone: string): JSX.Element | null => count > 0 ? ( - + {count > 99 ? '99+' : count} ) : null @@ -70,8 +64,8 @@ export default function Sidebar({ active, onNavigate }: SidebarProps): JSX.Eleme transcript: { id: 'transcript', label: t('sidebar.transcript'), icon: AlignLeft }, screenshots: { id: 'screenshots', label: t('sidebar.screens'), icon: Image }, targets: { id: 'targets', label: t('sidebar.targets'), icon: Crosshair }, - scope: { id: 'scope', label: t('sidebar.scope'), icon: Ban, badge: scopeViolations, badgeColor: 'bg-redlog-danger' }, - loot: { id: 'loot', label: t('sidebar.loot'), icon: Gem, badge: lootCount, badgeColor: 'bg-amber-500' }, + scope: { id: 'scope', label: t('sidebar.scope'), icon: Ban, badge: scopeViolations, badgeColor: 'bg-redlog-danger/12 text-redlog-danger' }, + loot: { id: 'loot', label: t('sidebar.loot'), icon: Gem, badge: lootCount, badgeColor: 'bg-amber-500/12 text-amber-400' }, marks: { id: 'marks', label: t('sidebar.marks'), icon: Flag } } @@ -80,7 +74,7 @@ export default function Sidebar({ active, onNavigate }: SidebarProps): JSX.Eleme const onItemClick = useCallback((id: string) => onNavigate(id), [onNavigate]) return ( -
)} diff --git a/src/renderer/src/components/EventMarker.tsx b/src/renderer/src/components/EventMarker.tsx index 5b2296fc..5156c963 100644 --- a/src/renderer/src/components/EventMarker.tsx +++ b/src/renderer/src/components/EventMarker.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react' import { useI18n } from '../i18n' import { useFocusTrap } from '../lib/useFocusTrap' import { formatTime } from '../lib/time' +import { Button } from './Button' const SEVERITIES = ['info', 'important', 'critical'] as const const CATEGORIES = [ @@ -145,13 +146,9 @@ export default function EventMarker({ onClose, atTimestamp }: EventMarkerProps): - + diff --git a/src/renderer/src/components/FindingsView.tsx b/src/renderer/src/components/FindingsView.tsx index d6bc39dd..5a9fe71b 100644 --- a/src/renderer/src/components/FindingsView.tsx +++ b/src/renderer/src/components/FindingsView.tsx @@ -7,6 +7,7 @@ import { useListKeyboard } from '../lib/useListKeyboard' import { formatDateTime } from '../lib/time' import { EmptyState } from './EmptyState' import { Flag } from 'lucide-react' +import { Button } from './Button' const TAG_COLORS = [ { bg: 'bg-red-500/20', text: 'text-red-400', dot: 'bg-red-400' }, @@ -286,9 +287,9 @@ function QuickMarkForm({ browserTab, onSave, onCancel, initial }: {
- + diff --git a/src/renderer/src/components/ProjectPicker.tsx b/src/renderer/src/components/ProjectPicker.tsx index b12ecec1..ff6d36ff 100644 --- a/src/renderer/src/components/ProjectPicker.tsx +++ b/src/renderer/src/components/ProjectPicker.tsx @@ -5,6 +5,7 @@ import { formatFreshness } from '../lib/time' import { confirmChainImpact } from './ConfirmDialog' import { toast } from './Toast' import logoUrl from '../assets/logo.svg' +import { Button } from './Button' interface ProjectPickerProps { onProjectOpen: (project: { id: string; name: string }) => void @@ -178,13 +179,13 @@ export default function ProjectPicker({ onProjectOpen }: ProjectPickerProps): JS autoFocus className="flex-1 bg-redlog-bg border border-redlog-border rounded-lg px-3 py-2 text-sm text-redlog-text font-mono focus:outline-none focus:border-red-500/50 focus:ring-1 focus:ring-red-500/20 placeholder-redlog-muted transition-all" /> - +
{/* Advanced toggle — opens a modal instead of expanding inline. The diff --git a/test/buttons.test.ts b/test/buttons.test.ts new file mode 100644 index 00000000..02c19730 --- /dev/null +++ b/test/buttons.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest' +import fs from 'fs' +import path from 'path' +import glob from 'fast-glob' +import { repoRelative } from './helpers/repo-path' + +// §4's four levels, and the one hard constraint that came with the §1 ruling. +// +// The original §1 said brand red may only draw text or a hairline. That drew +// the line in the wrong place — it made the strongest control on a screen look +// like the weakest, and an outline button cannot carry "Install the shell +// hook", which is the only way out of the dashboard's main question. The line +// that works is verb versus state: a filled #d75f63 "Create project" reads as +// something you press, a filled #ff4d4f "IP exposed" reads as something being +// reported. +// +// What remains true is the case those two reds really are indistinguishable +// in: side by side, filled, five luminance units apart. That is a destructive +// dialog — which holds Cancel and the destructive verb and never a primary. + +const ROOT = path.join(__dirname, '..') +const read = (rel: string): string => fs.readFileSync(path.join(ROOT, rel), 'utf-8') + +describe('button levels', () => { + it('defines all four in one place', () => { + const mod = read('src/renderer/src/components/Button.tsx') + for (const level of ['primary', 'secondary', 'quiet', 'danger']) { + expect(mod, `missing level: ${level}`).toMatch(new RegExp(`\\b${level}:`)) + } + // Only two of the four fill, and they are the two the standard names. + expect(mod).toMatch(/primary: 'bg-redlog-accent/) + expect(mod).toMatch(/danger: 'bg-redlog-danger/) + }) + + it('never puts a filled primary next to a filled danger', () => { + // The confirm dialog is the only surface that fills a button with danger, + // so it is the only place the constraint can be broken. + const dialog = read('src/renderer/src/components/ConfirmDialog.tsx') + expect(dialog).toMatch(/bg-redlog-danger/) + expect(dialog, 'a primary fill would sit beside the destructive verb') + .not.toMatch(/bg-redlog-accent(?![-/])/) + }) + + it('keeps at most one primary per component', () => { + // §4: at most one primary action per screen. Per file is the closest + // static stand-in, and it catches the real regression — a second primary + // added to a view that already had one. + const offenders: string[] = [] + for (const file of glob.sync('src/renderer/src/**/*.tsx', { cwd: ROOT, absolute: true })) { + const src = fs.readFileSync(file, 'utf-8') + if (file.endsWith('Button.tsx')) continue + const count = (src.match(/level="primary"/g) ?? []).length + if (count > 1) offenders.push(`${repoRelative(ROOT, file)}: ${count}`) + } + expect(offenders).toEqual([]) + }) + + it('leaves no hand-rolled primary styling behind', () => { + // Four call sites carried their own class strings, and two still had dead + // classes from an earlier revision fighting the ones beside them — + // `text-white` under a `text-redlog-accent`, a `hover:bg-red-700` under an + // accent hover. One definition is what stops that recurring. + const offenders: string[] = [] + for (const file of glob.sync('src/renderer/src/**/*.tsx', { cwd: ROOT, absolute: true })) { + if (file.endsWith('Button.tsx') || file.endsWith('EmptyState.tsx')) continue + const src = fs.readFileSync(file, 'utf-8') + for (const m of src.matchAll(/className="[^"]*bg-redlog-accent(?![-/])[^"]*"/g)) { + // A 2px active-indicator bar is a hairline, which §1 still allows — + // the rule is that brand red fills *command buttons* and nothing + // else. Padding is what distinguishes a control from a line. + if (!/\bp[xy]?-/.test(m[0])) continue + offenders.push(`${repoRelative(ROOT, file)}: ${m[0].slice(0, 60)}`) + } + } + expect(offenders).toEqual([]) + }) +}) From 2d692ba9ab631d90deee79abc66091259b0ead25 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Fri, 21 Aug 2026 14:02:27 +0800 Subject: [PATCH 07/34] a11y(fills): dark text on every fill, because white fails on all of them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The danger button was the case I flagged, but it was never the case. Every accent in this palette is mid-luminance, so white fails AA on all of them — and the two I had not measured are the worst: #d75f63 + white 3.68:1 #16090a 5.3:1 #ff4d4f + white 3.27:1 #1a0303 6.1:1 #d4ac5a + white 2.59:1 #1a1203 7.2:1 #5ecf9c + white 1.93:1 #03150d 9.7:1 #3fc7d6 + white 2.03:1 #031215 9.4:1 At 1.9:1 that is not low-contrast text, it is barely text. The rule that follows is simpler than the one it replaces and needs no per-component judgement: fill anything, and the text on it goes dark. Buttons, badges, chips, progress marks — one rule. Seventeen fills convert, and five `on-*` tokens land so the value is named rather than repeated. The remaining four `text-white` uses were plain text on a dark surface, where white was only ever an approximation of `text` (#ececf0) from outside the token namespace; they take the token too, which leaves the renderer with no `text-white` at all — an easier invariant to hold than "no white on a fill". Deliberately not: darkening #ff4d4f, whose luminance is exactly what separates it from the brand red, and not a third red, which is the problem §1 was written to end. The test asserts both halves — that each dark clears 4.5:1, and that the white it replaced did not — so the rule keeps its justification attached rather than becoming a number someone later rounds off. Phase 3 gains the wordmark as item 20; the app icon needs a vector original and is not in any phase. Co-Authored-By: Claude Opus 5 --- docs/UIUX-STANDARD.md | 19 ++++++- src/renderer/src/components/Button.tsx | 10 ++-- src/renderer/src/components/Card.tsx | 2 +- src/renderer/src/components/ConfirmDialog.tsx | 2 +- src/renderer/src/components/EmptyState.tsx | 2 +- src/renderer/src/components/LootPanel.tsx | 2 +- src/renderer/src/components/ScopeStatus.tsx | 2 +- src/renderer/src/components/Settings.tsx | 30 +++++----- src/renderer/src/components/TargetView.tsx | 4 +- src/renderer/src/components/WslPanel.tsx | 2 +- tailwind.config.js | 12 +++- test/buttons.test.ts | 55 +++++++++++++++++++ 12 files changed, 109 insertions(+), 33 deletions(-) diff --git a/docs/UIUX-STANDARD.md b/docs/UIUX-STANDARD.md index 2df0fe94..28fb5c2a 100644 --- a/docs/UIUX-STANDARD.md +++ b/docs/UIUX-STANDARD.md @@ -23,19 +23,31 @@ | `text-faint` | — | `#7a7a84` | 分區標題、次要說明 | | `muted` | `#71717a` | `#6a6a74` | placeholder、停用 | | `accent`(品牌紅) | `#d75f63` | `#d75f63` | 導航選取、區塊標題、主要按鈕 | -| `danger`(危險紅) | 與品牌紅同色 | `#ff4d4f` | 實心底 + 白字,僅危險 | +| `danger`(危險紅) | 與品牌紅同色 | `#ff4d4f` | 實心底 + `on-danger` 深色字,僅危險 | | `lane` | 18 種色相 | `#6e6e78` | 全部泳道統一 | | `safe` / `warn` / `info` | `#5ecf9c` / `#d4ac5a` / `#3fc7d6` | 不變 | 狀態 | **紅色語意收緊**:品牌紅只在**命令按鈕**上填色,其餘一律只做文字或細線;危險紅只填色,且可以填在任何地方。區別不靠「有沒有底色」,而靠「這是可以按的動詞,還是在報告狀態」。硬限制:**同一個動作區(對話框底部、工具列、卡片頁腳)不得同時出現實心品牌紅與實心危險紅**。搭配「一屏最多一顆主要按鈕」與「全 app 同時只允許一處危險紅」,這個情境不會發生。 +**填色一律配深色字**:這個色盤的彩色全是中亮度,白字在每一個上面都不過 AA——`#d75f63` 3.68:1、`#ff4d4f` 3.27:1、`#5ecf9c` 1.93:1、`#3fc7d6` 2.03:1。深色字全部過 5:1 以上。所以規則不分顏色、不分元件:**填色就配深色字**,涵蓋按鈕、徽章、chip、進度標示。 + +| 填色 | 配的字色 | 對比 | +|---|---|---| +| `accent` `#d75f63` | `on-accent` `#16090a` | 5.3:1 | +| `danger` `#ff4d4f` | `on-danger` `#1a0303` | 6.1:1 | +| `warn` `#d4ac5a` | `on-warn` `#1a1203` | 7.2:1 | +| `safe` `#5ecf9c` | `on-safe` `#03150d` | 9.7:1 | +| `info` `#3fc7d6` | `on-info` `#031215` | 9.4:1 | + +不調暗 `#ff4d4f`——它的亮度正是與品牌紅拉開距離的手段。也不另開第三個紅。 + **狀態唯一真相表** | 狀態 | 色彩 | 唯一允許的表現 | |---|---|---| | 正常/安全 | `#5ecf9c` | 圓點 + 文字,無實心底、無動畫 | | 未知/閒置 | `#d4ac5a` | 圓點 + 文字 + 一句「怎麼補」 | -| 危險/違規 | `#ff4d4f` | 實心徽章或實心按鈕,白字 | +| 危險/違規 | `#ff4d4f` | 實心徽章或實心按鈕,`on-danger` 深色字 | | 錄製中 | `#d75f63` | 閃爍圓點,僅狀態列與 HUD | | 資訊/連線 | `#3fc7d6` | HUD 邊框、拓樸、minimap;不用於狀態判定 | @@ -66,7 +78,7 @@ - **標誌**:單一字標 `REDL(●)G`,O 為錄製指示環。介面內不再並列「R」方塊與字標。環全部以 em 表示(換字級不必重算):外徑 `0.72em`(= Noto Sans TC 大寫字高)、環寬 `0.115em`(外徑 16%)、內點 `0.216em`(外徑 30%)、上移 `0.02em`;環必須 `box-sizing: border-box`,用 content-box 會讓外徑多出兩倍環寬。字標下限 12px。切角符號只用於 app 圖示、Dock、系統列、favicon,切角 = 邊長 22%(固定左上/右下;HUD 面板用的是 `lib/hud.ts` 的固定 11px/5px,因為面板尺寸不一,標誌是等比縮放所以改用比例),16px 以下環收成實心點。標誌永遠 `#d75f63`,不用危險紅、不動畫;單色場合用 `#ececf0` 或純黑;留白 ≥ 環直徑 - **按鈕**四級,高 34px(緊密 30px),圓角 8px,focus 環 2px `#d75f6366` 外偏移 2px。同一畫面最多一個主要 - 主要 = 實心 `accent` + 深色字(`#16090a`)。這是 §1 允許品牌紅填色的唯一場合 - - 次要 = `elevated` 底 + 1px 邊框;安靜 = 純文字;危險 = 實心 `danger` + 白字 + - 次要 = `elevated` 底 + 1px 邊框;安靜 = 純文字;危險 = 實心 `danger` + `on-danger` 深色字 - **卡片**:取消頂部 2px 色條,改為左側色塊包住圖示(危險用實心,其餘用 12% 底色,與徽章規則一致)。卡片底色永遠是 `surface`,不因狀態改變 - **徽章**:12% 底色 + 同色文字 + 圓點;只有危險用實心 - **chip**:選取時 `elevated` + 1px 邊框,不用紅色 @@ -220,5 +232,6 @@ - [ ] 外掛信任流程改逐一信任 + 權限清單 + 指紋 - [ ] 卡片左側色塊改版(影響所有卡片) - [ ] 移除狀態列〈⏱ 時戳〉 +- [ ] 字標改單一 `REDL(●)G`(標題列 + Project Picker);app 圖示/`.icns`/`.ico` 需向量原稿,不屬本期 **驗收**:同上 + 資訊架構走查(每個設定項 ≤2 層可達)· 虛擬列表在 28k 事件下的捲動效能 · 全畫面截圖比對 diff --git a/src/renderer/src/components/Button.tsx b/src/renderer/src/components/Button.tsx index 5648795e..2c495e5b 100644 --- a/src/renderer/src/components/Button.tsx +++ b/src/renderer/src/components/Button.tsx @@ -29,15 +29,13 @@ const BASE = 'focus-visible:ring-offset-redlog-bg disabled:opacity-40 disabled:cursor-not-allowed' const LEVEL: Record = { - // The verb. At most one per screen (§4). Dark text, not white: white on - // #d75f63 is 3.68:1 and fails AA, while #16090a is 5.3:1. The standard - // specifies the exact value, so it is written here rather than approximated - // with a Tailwind shade. - primary: 'bg-redlog-accent text-[#16090a] hover:bg-redlog-accent-dim focus-visible:ring-redlog-accent/40', + // The verb. At most one per screen (§4). Dark text, like every fill in this + // palette — see the `on-*` tokens for why white fails on all of them. + primary: 'bg-redlog-accent text-redlog-on-accent hover:bg-redlog-accent-dim focus-visible:ring-redlog-accent/40', secondary: 'bg-redlog-elevated text-redlog-text hover:bg-redlog-elevated-hover focus-visible:ring-redlog-text-dim/40', quiet: 'text-redlog-text-dim hover:text-redlog-text hover:bg-redlog-elevated focus-visible:ring-redlog-text-dim/40', // The only other fill in the system. - danger: 'bg-redlog-danger text-white hover:bg-redlog-danger-hover focus-visible:ring-redlog-danger/40' + danger: 'bg-redlog-danger text-redlog-on-danger hover:bg-redlog-danger-hover focus-visible:ring-redlog-danger/40' } export interface ButtonProps extends ButtonHTMLAttributes { diff --git a/src/renderer/src/components/Card.tsx b/src/renderer/src/components/Card.tsx index 2d52ef73..dcac3f1e 100644 --- a/src/renderer/src/components/Card.tsx +++ b/src/renderer/src/components/Card.tsx @@ -23,7 +23,7 @@ const TONE: Record = { warn: { block: 'bg-amber-500/12', icon: 'text-amber-400' }, info: { block: 'bg-cyan-500/12', icon: 'text-cyan-400' }, // The one solid fill, and the one the standard allows once per screen. - danger: { block: 'bg-redlog-danger', icon: 'text-white' } + danger: { block: 'bg-redlog-danger', icon: 'text-redlog-on-danger' } } export interface CardProps { diff --git a/src/renderer/src/components/ConfirmDialog.tsx b/src/renderer/src/components/ConfirmDialog.tsx index d49fdf58..c6003afd 100644 --- a/src/renderer/src/components/ConfirmDialog.tsx +++ b/src/renderer/src/components/ConfirmDialog.tsx @@ -192,7 +192,7 @@ function ConfirmDialogInner({ state, close, t }: { className={`px-3 py-1.5 text-xs rounded-md font-medium focus-visible:outline-none focus-visible:ring-2 transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${ level === 'plain' ? 'bg-redlog-elevated-hover text-redlog-text focus-visible:ring-redlog-text-dim' - : 'bg-redlog-danger text-white hover:bg-redlog-danger-hover focus-visible:ring-redlog-danger' + : 'bg-redlog-danger text-redlog-on-danger hover:bg-redlog-danger-hover focus-visible:ring-redlog-danger' }`} > {state.confirmLabel || (level === 'plain' ? t('confirm.confirm') : t('confirm.delete'))} diff --git a/src/renderer/src/components/EmptyState.tsx b/src/renderer/src/components/EmptyState.tsx index 3e52d116..250701f2 100644 --- a/src/renderer/src/components/EmptyState.tsx +++ b/src/renderer/src/components/EmptyState.tsx @@ -49,7 +49,7 @@ export function EmptyState({ // §4's primary: this is the only action on the screen, so it is // the primary one by definition. `h-[34px]`, 8px radius, and the // focus ring the standard specifies. - className="h-[34px] px-4 text-sm font-medium rounded-lg bg-redlog-accent text-[#16090a] hover:bg-redlog-accent-dim focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-redlog-accent/40 focus-visible:ring-offset-2 focus-visible:ring-offset-redlog-bg transition-colors" + className="h-[34px] px-4 text-sm font-medium rounded-lg bg-redlog-accent text-redlog-on-accent hover:bg-redlog-accent-dim focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-redlog-accent/40 focus-visible:ring-offset-2 focus-visible:ring-offset-redlog-bg transition-colors" > {action.label} diff --git a/src/renderer/src/components/LootPanel.tsx b/src/renderer/src/components/LootPanel.tsx index 73a5f52f..f09d7b42 100644 --- a/src/renderer/src/components/LootPanel.tsx +++ b/src/renderer/src/components/LootPanel.tsx @@ -108,7 +108,7 @@ export function LootPanel({ onOpenInTimeline }: { onOpenInTimeline?: (eventId: s return (
-

{t('loot.title', { count: visibleMatchCount })}

+

{t('loot.title', { count: visibleMatchCount })}

{lootEvents.length > 0 && ( @@ -841,7 +841,7 @@ function HooksPanel({ hooks, setHooks, hookLoading, setHookLoading, t }: { className={`px-3 py-1 text-xs rounded ml-3 transition-colors ${ hook.installed ? 'bg-redlog-elevated text-redlog-text-dim hover:bg-red-900/30 hover:text-red-400' - : 'bg-redlog-danger text-white hover:bg-redlog-danger-hover' + : 'bg-redlog-danger text-redlog-on-danger hover:bg-redlog-danger-hover' } ${hookLoading === hook.id ? 'opacity-50' : ''}`} > {hookLoading === hook.id ? '...' : hook.installed ? t('settings.hookDisable') : t('settings.hookEnable')} @@ -1048,7 +1048,7 @@ function PluginsPanel({ t }: { t: (key: string, vars?: Record toggle(p, p.status === 'disabled')} className={`px-3 py-1 text-xs rounded ${ - p.status === 'disabled' ? 'bg-redlog-danger text-white hover:bg-redlog-danger-hover' : 'bg-redlog-elevated text-redlog-text-dim hover:bg-redlog-elevated-hover' + p.status === 'disabled' ? 'bg-redlog-danger text-redlog-on-danger hover:bg-redlog-danger-hover' : 'bg-redlog-elevated text-redlog-text-dim hover:bg-redlog-elevated-hover' }`} > {busy === p.id ? '…' : p.status === 'disabled' ? t('plugins.enable') : t('plugins.disable')} @@ -1059,7 +1059,7 @@ function PluginsPanel({ t }: { t: (key: string, vars?: Record setConfirmGrant(p)} - className="px-3 py-1 text-xs rounded bg-amber-600/80 text-white hover:bg-amber-600" + className="px-3 py-1 text-xs rounded bg-amber-600/80 text-redlog-on-warn hover:bg-amber-600" > {t('plugins.review')} @@ -1100,7 +1100,7 @@ function PluginsPanel({ t }: { t: (key: string, vars?: Record setConfirmGrant(null)} className="px-3 py-1 text-xs rounded bg-redlog-elevated text-redlog-text hover:bg-redlog-elevated-hover"> {t('common.cancel')} -
@@ -1361,7 +1361,7 @@ function IntegrityPanel({ t }: { t: (key: string) => string }): JSX.Element { @@ -1552,7 +1552,7 @@ function McpPanel({ t }: { t: (key: string, vars?: Record {busy ? '…' : t('settings.mcpSetup')} @@ -1868,7 +1868,7 @@ function OperatorsPanel({ t }: { t: (key: string) => string }): JSX.Element { @@ -1898,7 +1898,7 @@ function OperatorsPanel({ t }: { t: (key: string) => string }): JSX.Element { @@ -2446,7 +2446,7 @@ function CloudSharePanel({ t }: { t: (key: string, vars?: Record
@@ -2684,7 +2684,7 @@ function MarketplacePanel({ t }: { t: (key: string, vars?: Record void trustOne(p)} disabled={trustingId === p.id} - className="shrink-0 px-2.5 py-1 text-xs bg-amber-600/80 hover:bg-amber-600 text-white rounded disabled:opacity-50" + className="shrink-0 px-2.5 py-1 text-xs bg-amber-600/80 hover:bg-amber-600 text-redlog-on-warn rounded disabled:opacity-50" > {trustingId === p.id ? '…' : t('marketplace.trustThisPublisher')} @@ -2717,7 +2717,7 @@ function MarketplacePanel({ t }: { t: (key: string, vars?: Record @@ -2820,7 +2820,7 @@ function PublisherEditor({ t, publishers, api, onReload }: { className="w-full bg-redlog-surface border border-redlog-border rounded px-2 py-1 text-xs text-redlog-text font-mono resize-y" />
diff --git a/src/renderer/src/components/TargetView.tsx b/src/renderer/src/components/TargetView.tsx index d9c7ca0c..46ce7815 100644 --- a/src/renderer/src/components/TargetView.tsx +++ b/src/renderer/src/components/TargetView.tsx @@ -163,7 +163,7 @@ export function TargetView({ onOpenInTimeline }: TargetViewProps = {}): JSX.Elem
-

{t('targets.title', { count: targets.length })}

+

{t('targets.title', { count: targets.length })}

{t('targets.subtitle')}

@@ -204,7 +204,7 @@ export function TargetView({ onOpenInTimeline }: TargetViewProps = {}): JSX.Elem }`} >
- {tgt.target} + {tgt.target}
{t('targets.cmds', { count: tgt.eventCount })} {tgt.inScope === false && ( diff --git a/src/renderer/src/components/WslPanel.tsx b/src/renderer/src/components/WslPanel.tsx index 525fe7cd..73545537 100644 --- a/src/renderer/src/components/WslPanel.tsx +++ b/src/renderer/src/components/WslPanel.tsx @@ -237,7 +237,7 @@ export default function WslPanel({ t }: WslPanelProps): JSX.Element { diff --git a/tailwind.config.js b/tailwind.config.js index 083df359..4614d09e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -78,7 +78,17 @@ module.exports = { 'text-dim': '#9a9aa4', 'text-faint': '#7a7a84', muted: '#6a6a74', - // Brand red — text and hairlines only. + // Text that sits *on* a fill. Every accent in this palette is + // mid-luminance, so white fails AA on all of them — #d75f63 gives + // 3.68:1, #ff4d4f 3.27:1, and the green and cyan are down at 1.9 + // and 2.0. Dark text clears 5:1 on every one. So the rule is not + // per-colour: fill anything, and the text on it goes dark. + 'on-accent': '#16090a', // 5.3:1 on accent + 'on-danger': '#1a0303', // 6.1:1 on danger + 'on-warn': '#1a1203', // 7.2:1 on amber, 5.1:1 at 80% over surface + 'on-safe': '#03150d', // 9.7:1 on emerald + 'on-info': '#031215', // 9.4:1 on cyan + // Brand red — fills command buttons, otherwise text and hairlines. accent: '#d75f63', 'accent-dim': '#b84d51', // Danger red — solid fills only, white on top. diff --git a/test/buttons.test.ts b/test/buttons.test.ts index 02c19730..830a27d1 100644 --- a/test/buttons.test.ts +++ b/test/buttons.test.ts @@ -75,3 +75,58 @@ describe('button levels', () => { expect(offenders).toEqual([]) }) }) + +// Every accent in this palette is mid-luminance, so white text fails AA on +// all of them — #d75f63 gives 3.68:1, #ff4d4f 3.27:1, and the emerald and +// cyan are down at 1.9 and 2.0, which is barely text at all. Dark text clears +// 5:1 on every one. The rule is therefore not per-colour and not per- +// component: fill anything, and the text on it goes dark. +describe('text on a fill', () => { + const luminance = (hex: string): number => { + const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255) + const f = (x: number): number => (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4) + const [r, g, b] = c.map(f) + return 0.2126 * r + 0.7152 * g + 0.0722 * b + } + const ratio = (a: string, b: string): number => { + const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x) + return (hi + 0.05) / (lo + 0.05) + } + + function tokens(): Record { + const config = read('tailwind.config.js') + const block = /redlog: \{([\s\S]*?)\n \}/.exec(config) + if (!block) throw new Error('redlog token block not found') + const out: Record = {} + for (const m of block[1].matchAll(/'?([a-z][a-z-]*)'?: '(#[0-9a-fA-F]{6})'/g)) out[m[1]] = m[2] + return out + } + + it('pairs every fill with a dark that clears 4.5:1', () => { + const t = tokens() + const pairs: Array<[string, string]> = [ + ['accent', 'on-accent'], + ['danger', 'on-danger'] + ] + for (const [fill, on] of pairs) { + expect(t[on], `missing token: ${on}`).toBeTruthy() + const r = ratio(t[on], t[fill]) + expect(r, `${on} on ${fill} is ${r.toFixed(2)}:1`).toBeGreaterThanOrEqual(4.5) + // And prove the thing being replaced really was failing, so the rule + // keeps its justification attached. + expect(ratio('#ffffff', t[fill]), `white on ${fill} should be the bad option`).toBeLessThan(4.5) + } + }) + + it('leaves no white text on a coloured fill anywhere', () => { + const offenders: string[] = [] + for (const file of glob.sync('src/renderer/src/**/*.tsx', { cwd: ROOT, absolute: true })) { + const src = fs.readFileSync(file, 'utf-8') + if (/text-white/.test(src)) offenders.push(repoRelative(ROOT, file)) + } + // Not "no white on a fill" but "no white at all": the four remaining uses + // were plain text on a dark surface, where white was only ever an + // approximation of `text` (#ececf0) from outside the token namespace. + expect(offenders).toEqual([]) + }) +}) From 6512c2204c2ea48a2a6b18026471693942baac31 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Fri, 21 Aug 2026 14:23:25 +0800 Subject: [PATCH 08/34] ux(settings): a left list of thirteen categories, and plugins flattened MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight tabs in a single 13px row was already hard to scan, and two of them — "Integrations" and "Data" — had drifted into meaning roughly the same thing. Underneath, Plugins held sub-tabs and those held publisher and revocation lists: three levels below the second level, when §10 allows two. The container had to come first. Thirteen categories fit down a side and do not fit across a top, so the content could not be split until there was somewhere to put it — which is why this lands before the pages themselves are filled out. A left list also does what a tab row cannot: the categories stay visible while a page is read, it takes group headings, and it has somewhere to put the search box §10 asks for. The eight blocks route onto the thirteen pages without being rewritten — the content was already separated into named panels, so this is wiring. "Data" becomes retention, chain, sharing and appearance, which is the split it had needed for a while. Plugins loses its sub-tabs. "What is installed" and "where to get more" are one task, so they are two sections of one page rather than two destinations. Two things caught in the process. The e2e specs reached settings by visible tab text (`/^data$/i`, `/^plugins$/i`), which cannot survive a category being split — they use the `data-settings-page` hook and an `openSettingsPage` helper now, the same shape as `openView`. And making the root a flex row turned the save bar, previously a sibling below the content, into a third column: it took 822px and squeezed the content pane to 35px. The right pane is a column of its own now. Co-Authored-By: Claude Opus 5 --- e2e/cloud-share-flow.spec.ts | 11 +- e2e/helpers.ts | 14 ++ e2e/marketplace-flow.spec.ts | 11 +- src/renderer/src/components/Settings.tsx | 239 ++++++++++++++++------- src/renderer/src/i18n/en.json | 23 ++- src/renderer/src/i18n/zh-TW.json | 23 ++- test/settings-ia.test.ts | 67 +++++++ 7 files changed, 301 insertions(+), 87 deletions(-) create mode 100644 test/settings-ia.test.ts diff --git a/e2e/cloud-share-flow.spec.ts b/e2e/cloud-share-flow.spec.ts index d018e096..5953810d 100644 --- a/e2e/cloud-share-flow.spec.ts +++ b/e2e/cloud-share-flow.spec.ts @@ -2,7 +2,7 @@ import { test, expect, type ElectronApplication, type Page } from '@playwright/t import { existsSync, mkdirSync, readFileSync, openSync, readSync, closeSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { join } from 'node:path' -import { launchWithTempHome, openTestProject } from './helpers' +import { launchWithTempHome, openTestProject, openSettingsPage } from './helpers' const SCREENSHOT_DIR = join(__dirname, 'screenshots') @@ -72,10 +72,9 @@ test.describe.serial('cloud share', () => { const viewRoot = page.locator('[data-testid="view-root"]') await expect(viewRoot).toHaveAttribute('data-view', 'settings', { timeout: 2_000 }) - // The Data tab hosts the Cloud share panel. The tab bar is a row of - // - ))} - {saved && {t('settings.saved')}} -
+
+ -
+ {/* The right pane is a column: content scrolls, the save bar stays put + under it. Making the root a row without this turned that bar into a + third column and squeezed the content to 35px. */} +
+
{tab === 'general' && ( <> @@ -184,14 +280,14 @@ export default function Settings(): JSX.Element { )} - {tab === 'integrations' && ( + {(tab === 'agents' || tab === 'network' || tab === 'deconfliction' || tab === 'operators') && ( <> - - - - - - + {tab === 'agents' && } + {tab === 'agents' && } + {tab === 'operators' && } + {tab === 'deconfliction' && } + {tab === 'network' && } + {tab === 'network' &&

{t('settings.cdpHint', { port: String(config.browser?.cdpPort ?? DEFAULT_CDP_PORT) })}

@@ -217,7 +313,7 @@ export default function Settings(): JSX.Element { > {t('settings.testConnection')} -
+
} )} @@ -436,12 +532,12 @@ export default function Settings(): JSX.Element { )} - {tab === 'capture' && ( + {(tab === 'hooks' || tab === 'captureControl') && ( <> {isWindows && } - + {tab === 'captureControl' && {t('settings.qualityHint')}

-
- + } + {tab === 'captureControl' && - + } + {tab === 'captureControl' && - + } + {tab === 'captureControl' && +
} )} - {tab === 'data' && ( + {(tab === 'export' || tab === 'integrity' || tab === 'cloud' || tab === 'general') && ( <> - + {tab === 'export' &&
{[ { v: 0, k: 'settings.screenshot.interval.off' }, @@ -555,8 +651,8 @@ export default function Settings(): JSX.Element { ))}

{t('settings.screenshot.intervalHint')}

-
- + } + {tab === 'general' &&

{t('settings.checkUpdateHint')}

-
- + } + {tab === 'export' &&
-
+
+
) } @@ -909,23 +1006,19 @@ interface PluginView { * between them constantly while installing something. Sub-tabs keep both a * click away without spending two of the eight top-level slots. */ function PluginsTab({ t }: { t: (key: string, vars?: Record) => string }): JSX.Element { - const [sub, setSub] = useState<'installed' | 'marketplace'>('installed') + // §10: two levels, not four. This page used to hold sub-tabs for installed + // and marketplace, and the marketplace held its own for publishers and + // revocations — three levels below a second-level tab, when the standard + // allows two. + // + // "What is installed" and "where to get more" are one task, so they are two + // sections of one page rather than two destinations. Publisher trust and + // revocation stop being lists you navigate to and become state on the row + // they describe, which is where an operator was looking anyway. return ( <> -
- {([['installed', t('settings.plugins')], ['marketplace', t('settings.marketplace')]] as const).map(([id, label]) => ( - - ))} -
- {sub === 'installed' ? : } + + ) } diff --git a/src/renderer/src/i18n/en.json b/src/renderer/src/i18n/en.json index cd864b07..0154e583 100644 --- a/src/renderer/src/i18n/en.json +++ b/src/renderer/src/i18n/en.json @@ -971,5 +971,26 @@ "dashboard.findInPage": "Filter what is on screen", "palette.timelineScoped": "Search within the loaded timeline (lanes, operators, hosts)", "palette.sectionOperator": "Operators", - "palette.operatorPrimary": "primary" + "palette.operatorPrimary": "primary", + "settings.categories": "Settings categories", + "settings.searchPages": "Search settings…", + "settings.noPageMatches": "No category matches “{{query}}”", + "settings.groupCapture": "Capture sources", + "settings.groupScope": "Scope", + "settings.groupEvidence": "Evidence", + "settings.groupCollab": "Collaboration", + "settings.groupApp": "Application", + "settings.pageHooks": "Hooks & terminals", + "settings.pageAgents": "AI agents", + "settings.pageCaptureControl": "What gets captured", + "settings.pageScope": "Scope rules", + "settings.pageNetwork": "Network & IP", + "settings.pageDeconfliction": "Deconfliction", + "settings.pageIntegrity": "Chain & anchoring", + "settings.pageExport": "Retention & export", + "settings.pageOperators": "Operators & team", + "settings.pageCloud": "Sharing & blue-team", + "settings.pagePlugins": "Plugins & marketplace", + "settings.pageGeneral": "Appearance & language", + "settings.pageHud": "HUD" } diff --git a/src/renderer/src/i18n/zh-TW.json b/src/renderer/src/i18n/zh-TW.json index 4f5e3f06..d9a47441 100644 --- a/src/renderer/src/i18n/zh-TW.json +++ b/src/renderer/src/i18n/zh-TW.json @@ -971,5 +971,26 @@ "dashboard.findInPage": "篩選目前畫面上的內容", "palette.timelineScoped": "在已載入的時間軸內搜尋(泳道、operator、host)", "palette.sectionOperator": "Operator", - "palette.operatorPrimary": "主要" + "palette.operatorPrimary": "主要", + "settings.categories": "設定分類", + "settings.searchPages": "搜尋設定…", + "settings.noPageMatches": "沒有符合「{{query}}」的分類", + "settings.groupCapture": "擷取來源", + "settings.groupScope": "範圍", + "settings.groupEvidence": "證據保全", + "settings.groupCollab": "協作", + "settings.groupApp": "應用程式", + "settings.pageHooks": "Hooks 與終端機", + "settings.pageAgents": "AI 代理", + "settings.pageCaptureControl": "控制抓取", + "settings.pageScope": "範圍規則", + "settings.pageNetwork": "網路與 IP", + "settings.pageDeconfliction": "去衝突", + "settings.pageIntegrity": "證據鏈與錨定", + "settings.pageExport": "保存與匯出", + "settings.pageOperators": "操作員與團隊", + "settings.pageCloud": "分享與藍隊通報", + "settings.pagePlugins": "外掛與市集", + "settings.pageGeneral": "外觀與語言", + "settings.pageHud": "HUD" } diff --git a/test/settings-ia.test.ts b/test/settings-ia.test.ts new file mode 100644 index 00000000..413746a2 --- /dev/null +++ b/test/settings-ia.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest' +import fs from 'fs' +import path from 'path' + +// §10 and the phase-3 acceptance gate: every setting reachable in ≤2 levels. +// +// Eight tabs in one 13px row was already hard to scan, and two of them — +// "Integrations" and "Data" — had drifted into meaning roughly the same thing. +// Below that, Plugins held sub-tabs and those held publisher and revocation +// lists: three levels underneath the second level. +// +// The container had to come first. Thirteen categories fit down a side and do +// not fit across a top, so the content could not be split until there was +// somewhere to put it. + +const ROOT = path.join(__dirname, '..') +const SRC = fs.readFileSync(path.join(ROOT, 'src/renderer/src/components/Settings.tsx'), 'utf-8') + +const PAGES = [ + 'hooks', 'agents', 'captureControl', + 'scope', 'network', 'deconfliction', + 'integrity', 'export', + 'operators', 'cloud', 'plugins', + 'general', 'hud' +] + +describe('settings information architecture', () => { + it('declares thirteen pages as a union', () => { + const block = /type SettingsPage =([\s\S]*?)\n\n/.exec(SRC) + expect(block, 'SettingsPage union not found').not.toBeNull() + for (const page of PAGES) { + expect(block![1], `missing page: ${page}`).toContain(`'${page}'`) + } + }) + + it('routes every declared page to content', () => { + // A page in the list with no `tab === 'x'` anywhere renders an empty pane, + // which looks like a bug in the setting rather than a missing route. + const unrouted = PAGES.filter((page) => !new RegExp(`tab === '${page}'`).test(SRC)) + expect(unrouted).toEqual([]) + }) + + it('offers every declared page in the left list', () => { + // And the reverse: content reachable by no list entry is content nobody + // finds. + const listed = [...SRC.matchAll(/id: '(\w+)' as SettingsPage|\{ id: '(\w+)',/g)] + .map((m) => m[1] ?? m[2]) + .filter((id) => PAGES.includes(id)) + expect([...new Set(listed)].sort()).toEqual([...PAGES].sort()) + }) + + it('has a search box over the categories', () => { + expect(SRC).toMatch(/settings\.searchPages/) + expect(SRC).toMatch(/pageQuery/) + }) + + it('flattens plugins to two levels', () => { + // The marker of the old shape was a second `useState` for a sub-tab + // inside the page. Installed and marketplace are sections now, not + // destinations. + const tab = SRC.slice(SRC.indexOf('function PluginsTab')) + const body = tab.slice(0, tab.indexOf('\n}')) + expect(body, 'a sub-tab selector is the third level').not.toMatch(/useState<'installed'/) + expect(body).toMatch(/ Date: Fri, 21 Aug 2026 14:29:58 +0800 Subject: [PATCH 09/34] model(capture): group sources by what they capture, drop the false sequence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CORE_SOURCE_ORDER` was an ordered triple — shell hook, agent tailer, built-in terminal — with everything else declared "enrichment" and excluded from the dark→recording path outright. Two things were wrong, and the second is a bug rather than a modelling preference. The ordering claimed a sequence that does not exist. An operator running a proxied web assessment wires mitmproxy first and may never install a shell hook. And because mitmproxy was outside the path, the model reported `dark` while HTTP and DNS events were landing on the timeline. "Dark" means the timeline will stay empty; it was saying that about an app that was recording. A source that produces events is a source that produces events, whatever the onboarding copy calls it. So: three groups — commands, traffic, artefacts — with no order inside one, because the useful distinction was never rank but *what each source captures*, which is the choice an operator is actually making. All nine sources are in the model now, and `recording` means any of them is live. `nextStep` follows from state rather than position, since "first in the array" would be an arbitrary answer dressed as a recommendation. A `wired` source needs an event; a `todo` source needs an installation first. The wired one is the shorter route out of dark, which is the only thing this model is for — so it now points at the installed-but-quiet hook rather than skipping past it to the next unset source. The Dashboard drops the numbered `
    `. Numbering was the visual claim that the sequence existed. Three readiness tests and two render tests change to assert the new contract rather than being deleted — the old behaviour was real and tested, and the diff should show which properties were traded. Co-Authored-By: Claude Opus 5 --- src/renderer/src/App.tsx | 52 +++++++---- src/renderer/src/i18n/en.json | 5 +- src/renderer/src/i18n/zh-TW.json | 5 +- src/renderer/src/lib/captureReadiness.ts | 109 +++++++++++++++-------- test/capture-onboarding-render.test.tsx | 28 ++++-- test/capture-readiness.test.ts | 38 ++++++-- 6 files changed, 169 insertions(+), 68 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 98bd0457..d383492d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -307,7 +307,13 @@ function CaptureOnboarding({ readiness, sources, busy, onInstall, onEnable, onNa const STEP_LABEL: Record = { 'shell-hook': t('capture.shellHook'), 'agent-tailer': t('capture.agentTailer'), - 'builtin-terminal': t('capture.builtinTerminal') + 'builtin-terminal': t('capture.builtinTerminal'), + 'mitmproxy': t('capture.mitmproxy'), + 'browser-console': t('capture.browserConsole'), + 'screenshot': t('capture.screenshot'), + 'clipboard': t('capture.clipboard'), + 'file-watcher': t('capture.fileWatcher'), + 'process-monitor': t('capture.processMonitor') } const glyph = (status: string): { mark: string; cls: string } => status === 'active' ? { mark: '●', cls: 'text-emerald-500' } @@ -338,21 +344,35 @@ function CaptureOnboarding({ readiness, sources, busy, onInstall, onEnable, onNa

    {readiness.level === 'dark' ? t('capture.setupIntro') : t('capture.setupAlmost')}

    -
      - {readiness.steps.map((s, i) => { - const g = glyph(s.status) - return ( -
    1. - {g.mark} - {i + 1}. - - {STEP_LABEL[s.id] ?? s.id} - - {t(`capture.step.${s.status}`)} -
    2. - ) - })} -
    + {/* Grouped, and no longer numbered. The numbers described a sequence + that does not exist — an operator on a proxied assessment starts with + traffic and may never install a shell hook. What the groups say is + what each source captures, which is the choice actually being made. */} +
    + {readiness.groups.map((group) => ( +
    +

    + {t(`capture.group.${group.id}`)} +

    +
      + {group.steps.map((s) => { + const g = glyph(s.status) + return ( +
    • + {g.mark} + + {STEP_LABEL[s.id] ?? s.id} + + + {t(`capture.step.${s.status}`)} + +
    • + ) + })} +
    +
    + ))} +
    {cta && (
    + {/* §6: the three view modes. The toolbar used to carry eight + independent toggles side by side with equal weight — 256 states, + and no way for an operator who landed in one to tell whether it + was a sensible place to be. They are not really independent: they + fall into three postures (working, audit, debug), and the modes + name them. The individual chips stay for the working and debug + postures; audit locks them, because a claim you can quietly tweak + is not a claim. */} + + {/* v0.9.3 U3: collapse-agent-turns chip. Off by default (existing operators don't lose visibility on upgrade). When on, per-turn agent subtypes are dropped from the render pipeline — the @@ -2538,6 +2595,7 @@ export default function TimelinePanel({ focusEventId, focusTs, onDropMarker }: { like a bug. Same visual weight as the other filter chips. */} } +
+ {types.length > 1 && ( +
+ {types.map(([type, count]) => ( + + ))} +
+ )} +
+ {filtered.map((e, i) => { + const rowProps = listNav.itemProps(i) + return ( + + ) + })} +
+ + )})()} +
+
+ ) +} diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index ccded455..48ea2492 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { Gauge, ChevronRight, Rows3, AlignLeft, Image, Crosshair, - Ban, Gem, Flag, Settings as SettingsIcon, type LucideIcon + Ban, Gem, Flag, Search, Settings as SettingsIcon, type LucideIcon } from 'lucide-react' import { useI18n } from '../i18n' @@ -66,7 +66,8 @@ export default function Sidebar({ active, onNavigate }: SidebarProps): JSX.Eleme targets: { id: 'targets', label: t('sidebar.targets'), icon: Crosshair }, scope: { id: 'scope', label: t('sidebar.scope'), icon: Ban, badge: scopeViolations, badgeColor: 'bg-redlog-danger/12 text-redlog-danger' }, loot: { id: 'loot', label: t('sidebar.loot'), icon: Gem, badge: lootCount, badgeColor: 'bg-amber-500/12 text-amber-400' }, - marks: { id: 'marks', label: t('sidebar.marks'), icon: Flag } + marks: { id: 'marks', label: t('sidebar.marks'), icon: Flag }, + search: { id: 'search', label: t('sidebar.search'), icon: Search } } const items = DEFAULT_ORDER.map((id) => itemMap[id]).filter(Boolean) diff --git a/src/renderer/src/components/Timeline.tsx b/src/renderer/src/components/Timeline.tsx index 4ab61f82..d697b7e3 100644 --- a/src/renderer/src/components/Timeline.tsx +++ b/src/renderer/src/components/Timeline.tsx @@ -9,10 +9,6 @@ import { getLastVerifyResult, VERIFY_UPDATED_EVENT, type FullVerifyResult } from import { resolveTimelineKey } from '../lib/timelineKeys' import { Rows3 } from 'lucide-react' import { formatTime, formatTs, type TzMode, type TsStyle } from '../lib/time' -import { - MODE_SETTINGS, modeFor, isLocked, - type TimelineMode, type TimelineSettings -} from '../lib/timelineModes' import { timelineShortcuts } from '../lib/shortcuts' import { nextSelection } from '../lib/timelineSelection' import { isMac } from '../lib/platform' @@ -884,7 +880,15 @@ export default function TimelinePanel({ focusEventId, focusTs, onDropMarker }: { localStorage.setItem(`redlog-timeline-auditor-view:${projectIdForKeys}`, auditorView ? '1' : '0') } catch { /* ignore */ } }, [auditorView, projectIdForKeys]) - + useEffect(() => { + // v0.14 §9.4: the StatusBar's tier counter dispatches this event when + // clicked. Toggle here so the click "opens" the auditor view exactly + // like clicking the chip would. Only wired when Timeline is mounted; + // a click from Dashboard is a documented silent no-op (tooltip warns). + const onToggle = (): void => setAuditorView((v) => !v) + window.addEventListener('redlog:auditor-view:toggle', onToggle) + return () => window.removeEventListener('redlog:auditor-view:toggle', onToggle) + }, []) // v0.6.91 W1: inline `/` search — dims events whose title / command / URL / // host / operator doesn't substring-match the query. Persisted so the @@ -940,38 +944,6 @@ export default function TimelinePanel({ focusEventId, focusTs, onDropMarker }: { return raw === 'utc' || raw === 'project' ? raw : 'local' } catch { return 'local' } }) - - // §6. The settings are still individual state — they have to be, since - // working and debug remain adjustable — so the mode is derived rather than - // stored. That way the label can never disagree with what is on screen, - // which is the failure that makes an audit view worthless. - const modeSettings: TimelineSettings = { - collapseAgentTurns, compressGaps, auditorView, anomalyFilter, sessionDividers, tz - } - const currentMode = modeFor(modeSettings) - const modeLocked = currentMode !== null && isLocked(currentMode) - const applyMode = (mode: TimelineMode): void => { - const preset = MODE_SETTINGS[mode] - setCollapseAgentTurns(preset.collapseAgentTurns) - setCompressGaps(preset.compressGaps) - setAuditorView(preset.auditorView) - setAnomalyFilter(preset.anomalyFilter) - setSessionDividers(preset.sessionDividers) - setTz(preset.tz) - } - - useEffect(() => { - // The StatusBar's tier counter dispatches this when clicked. §7 folds the - // standalone auditor chip into audit *mode*: flipping one flag used to - // leave the view in a state that hid the logged tier while still - // compressing time and folding agent turns — which looks like an audit - // view and is not one. Clicking it now enters audit properly, and - // clicking again returns to working. - const onToggle = (): void => applyMode(currentMode === 'audit' ? 'working' : 'audit') - window.addEventListener('redlog:auditor-view:toggle', onToggle) - return () => window.removeEventListener('redlog:auditor-view:toggle', onToggle) - }, [currentMode]) - useEffect(() => { try { localStorage.setItem('redlog-timeline-tz', tz) } catch { /* ignore */ } }, [tz]) @@ -2559,35 +2531,6 @@ export default function TimelinePanel({ focusEventId, focusTs, onDropMarker }: { >+
- {/* §6: the three view modes. The toolbar used to carry eight - independent toggles side by side with equal weight — 256 states, - and no way for an operator who landed in one to tell whether it - was a sensible place to be. They are not really independent: they - fall into three postures (working, audit, debug), and the modes - name them. The individual chips stay for the working and debug - postures; audit locks them, because a claim you can quietly tweak - is not a claim. */} - - {/* v0.9.3 U3: collapse-agent-turns chip. Off by default (existing operators don't lose visibility on upgrade). When on, per-turn agent subtypes are dropped from the render pipeline — the @@ -2595,7 +2538,6 @@ export default function TimelinePanel({ focusEventId, focusTs, onDropMarker }: { like a bug. Same visual weight as the other filter chips. */}
+ {/* §10: one export control, in the shell rather than six places. + Its scope is an option, not a location. */} + setView(v as View)} /> -
+

+ {t('dashboard.sessionStats')} +

{/* Events + chain length were two cards showing the same number — every event is one chain entry so they moved in lockstep. Merged diff --git a/src/renderer/src/components/ExportMenu.tsx b/src/renderer/src/components/ExportMenu.tsx new file mode 100644 index 00000000..55fe898c --- /dev/null +++ b/src/renderer/src/components/ExportMenu.tsx @@ -0,0 +1,152 @@ +import { useState, useRef, useEffect } from 'react' +import { Download, ChevronDown } from 'lucide-react' +import { useI18n } from '../i18n' +import { useFocusTrap } from '../lib/useFocusTrap' +import { toast } from './Toast' +import { useViewExport } from '../lib/exportScope' + +// One export control (docs/UIUX-STANDARD.md §10). +// +// Exporting was reachable from six places — the dashboard, three separate +// groups in Settings, the transcript's own button, and the timeline's slice +// button — each with its own wording and its own idea of what "export" meant. +// An operator writing an engagement up had to already know which of the six +// produced the thing they wanted. +// +// The scope of an export is a property of the export, not of where the button +// happens to live, so it becomes an option rather than a location: everything, +// what is currently on screen, or the visible time slice. Each option says +// what it will produce before it produces it — a wrong export is not a +// disaster, but discovering it was wrong after opening the file is a wasted +// round trip during the one task this record exists for. +// +// The evidence bundle stays, at the bottom, separated. It is a different kind +// of thing — a signed archive with a verifier for someone challenging the +// record — and after the 2026-08-21 core revision that is the exceptional +// path, not the everyday one (see DESIGN-core-and-capture.md §1). + +export type ExportScope = 'all' | 'view' | 'slice' +export type ExportFormat = 'json' | 'markdown' + +export interface ExportMenuProps { + /** How many events "everything" would carry, for the preview line. */ + totalCount?: number +} + +/** Rough bytes per exported event. Deliberately approximate — the preview + * exists to catch "I meant the slice, not all 28,000", which a rounded + * megabyte answers just as well as an exact one. */ +const BYTES_PER_EVENT = 520 + +function humanSize(events: number): string { + const bytes = events * BYTES_PER_EVENT + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} + +export function ExportMenu({ totalCount }: ExportMenuProps): JSX.Element { + const { t } = useI18n() + // Whatever the view on screen contributed, if anything. + const viewExport = useViewExport() + const [open, setOpen] = useState(false) + const [busy, setBusy] = useState(false) + const panel = useRef(null) + useFocusTrap(panel, open) + + useEffect(() => { + if (!open) return + const onKey = (e: KeyboardEvent): void => { if (e.key === 'Escape') setOpen(false) } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [open]) + + const run = async (label: string, fn: () => Promise): Promise => { + setBusy(true) + try { + const path = await fn() + if (path) toast(t('export.done', { label }), { type: 'success', why: path }) + else toast(t('export.failed', { label }), { type: 'error', why: t('toast.exportFailedWhy') }) + } catch (e) { + toast(t('export.failed', { label }), { + type: 'error', + why: t('toast.exportFailedWhy'), + detail: String((e as Error)?.message ?? e) + }) + } finally { + setBusy(false) + setOpen(false) + } + } + + const Option = ({ label, count, onPick }: { + label: string; count?: number; onPick: () => void + }): JSX.Element => ( + + ) + + return ( +
+ + + {open && ( + <> +
setOpen(false)} /> +
+ {viewExport && ( +
+ ) +} diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index c6d2d050..f3c7b273 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -79,7 +79,7 @@ const LOCALE_LABELS: Record = { type SettingsPage = | 'hooks' | 'agents' | 'captureControl' | 'scope' | 'network' | 'deconfliction' - | 'integrity' | 'export' + | 'integrity' | 'operators' | 'cloud' | 'plugins' | 'general' | 'hud' @@ -142,9 +142,12 @@ export default function Settings(): JSX.Element { // // A left list solves the part a tab row cannot: the categories stay visible // while you read a page, it takes group headings, and it has somewhere to - // put a search box. Thirteen pages fit down the side and would not fit + // put a search box. Twelve pages fit down the side and would not fit // across the top, which is why the content could not be split before the // container existed. + // + // The "export" page is deliberately absent: exporting is an action, not a + // setting, and it now lives in the shell's one export control (§10). const groups: Array<{ heading: string; pages: Array<{ id: SettingsPage; label: string }> }> = [ { heading: t('settings.groupCapture'), @@ -165,8 +168,7 @@ export default function Settings(): JSX.Element { { heading: t('settings.groupEvidence'), pages: [ - { id: 'integrity', label: t('settings.pageIntegrity') }, - { id: 'export', label: t('settings.pageExport') } + { id: 'integrity', label: t('settings.pageIntegrity') } ] }, { @@ -629,9 +631,9 @@ export default function Settings(): JSX.Element { )} - {(tab === 'export' || tab === 'integrity' || tab === 'cloud' || tab === 'general') && ( + {(tab === 'captureControl' || tab === 'integrity' || tab === 'cloud' || tab === 'general') && ( <> - {tab === 'export' && + {tab === 'captureControl' &&
{[ { v: 0, k: 'settings.screenshot.interval.off' }, @@ -664,39 +666,8 @@ export default function Settings(): JSX.Element {

{t('settings.checkUpdateHint')}

} - {tab === 'export' && - - {exportResult &&

{exportResult}

} -

- {t('settings.exportHint')} -

-
} - {tab === 'export' && } - {tab === 'export' && - -

- {t('settings.scopeExportHint')} -

-
} + + {tab === 'cloud' && } {tab === 'integrity' && } {tab === 'general' && @@ -1285,87 +1256,9 @@ interface FullVerifyResult { badSignatureAtEventId?: string | null } -// v0.6.94 D: renderer-side wrapper around data:exportBundle. Builds a sanitized -// evidence pack via src/core/bundle-export.ts (same code path as the CLI + -// MCP tool) and surfaces the resulting directory with a Reveal button so the -// operator can zip it up and hand-deliver. Building is synchronous inside the -// main process (walks events.jsonl + hashes screenshots), so the button -// disables itself with a "Building..." label until IPC returns. -function ExportBundlePanel({ t }: { t: (key: string, vars?: Record) => string }): JSX.Element { - const [busy, setBusy] = useState(false) - const [result, setResult] = useState<{ path: string } | null>(null) - const [error, setError] = useState(null) - - const handleExport = async (): Promise => { - setBusy(true) - setError(null) - setResult(null) - try { - const dataApi = window.redlog.data as { exportBundle?: () => Promise<{ ok?: boolean; outDir?: string; error?: string } | null> } - if (!dataApi.exportBundle) { - setError(t('settings.exportFailed')) - setBusy(false) - return - } - const r = await dataApi.exportBundle() - if (!r) { - setError(t('settings.exportFailed')) - } else if (r.ok === false || !r.outDir) { - setError(r.error || t('settings.exportFailed')) - } else { - setResult({ path: r.outDir }) - toast(t('toast.bundleExported'), 'success') - } - } catch (e) { - setError((e as Error)?.message ?? String(e)) - } finally { - setBusy(false) - } - } - - const handleReveal = async (): Promise => { - if (!result?.path) return - const dataApi = window.redlog.data as { revealPath?: (p: string) => Promise } - if (dataApi.revealPath) await dataApi.revealPath(result.path) - } - - return ( - -
- - {result && ( - - )} -
- {result && ( -

- {t('settings.savedTo', { path: result.path })} -

- )} - {error && ( -
- {error} -
- )} -

- {t('settings.exportBundleHint')} -

-
- ) -} - +// Chain state: anchors, the two-tier counter, and verification. Read-only — +// building the evidence pack itself moved to the shell's export control (§10), +// because it is an action and this page is where you look at the chain. function IntegrityPanel({ t }: { t: (key: string) => string }): JSX.Element { const [anchors, setAnchors] = useState([]) const [busy, setBusy] = useState(false) @@ -1987,7 +1880,7 @@ function OperatorsPanel({ t }: { t: (key: string) => string }): JSX.Element { onClick={() => { void window.redlog.data.revealPath?.(pendingToken.path as string) }} className="px-2 py-1.5 text-xs bg-redlog-elevated text-redlog-text rounded hover:bg-redlog-elevated-hover whitespace-nowrap" > - {t('settings.exportBundleReveal')} + {t('settings.revealInFolder')} {hiddenLanes.size > 0 && (
diff --git a/src/renderer/src/i18n/en.json b/src/renderer/src/i18n/en.json index 203b8845..fdba26ad 100644 --- a/src/renderer/src/i18n/en.json +++ b/src/renderer/src/i18n/en.json @@ -404,16 +404,13 @@ "settings.cdpPort": "CDP Port", "settings.testConnection": "Test Connection", "settings.cdpHint": "Launch Chrome: google-chrome --remote-debugging-port={{port}}", - "settings.exportAll": "Export All Data", "settings.exportJson": "Export JSON Dump", "settings.exportHint": "Exports all events, quickmarks, and config as a single JSON file.", "settings.exportFailed": "Export failed", "settings.savedTo": "Saved to: {{path}}", "settings.exportBundle": "Export Bundle", - "settings.exportBundleTitle": "Evidence Bundle", "settings.exportBundleHint": "Full sanitized evidence pack (events.jsonl, chain anchors, screenshots, casts, operators) plus a standalone Python chain verifier. Hand-deliver or attach.", "settings.exportBundleBuilding": "Building bundle...", - "settings.exportBundleReveal": "Show in Finder / Explorer", "toast.repeat": "×{{count}}", "toast.dismiss": "Dismiss", "toast.undo": "Undo", @@ -621,7 +618,6 @@ "timeline.tz.utc": "UTC", "timeline.tz.project": "Project", "timeline.tz.tooltip": "Display timestamps in this timezone", - "dashboard.exportData": "Export Data", "overlay.mark": "Mark", "overlay.rec": "REC", "overlay.paused": "PAUSED", @@ -761,7 +757,6 @@ "marketplace.revokedPublishers": "Revoked publishers", "marketplace.sizeKb": "{{size}} KB", "marketplace.rolledBackFrom": "Previous version snapshotted to {path}", - "settings.scopeExport": "Scope-Filtered Export", "settings.exportScopeJson": "Export Scope-Only JSON", "settings.scopeExportHint": "Exports only events matching configured scope targets. Use this when uploading logs to a remote server — out-of-scope data is excluded.", "timeline.detail.ioOnDisk": "output recorded — {{size}} of session capture, expand below to read it", @@ -817,7 +812,6 @@ "transcript.title": "Transcript", "transcript.count": "{{n}} exchanges", "transcript.filter": "Filter transcript…", - "transcript.exportMd": "Copy as Markdown", "transcript.copied": "Transcript copied as Markdown", "transcript.copyFailed": "Copy failed", "transcript.loading": "Loading…", @@ -987,7 +981,6 @@ "settings.pageNetwork": "Network & IP", "settings.pageDeconfliction": "Deconfliction", "settings.pageIntegrity": "Chain & anchoring", - "settings.pageExport": "Retention & export", "settings.pageOperators": "Operators & team", "settings.pageCloud": "Sharing & blue-team", "settings.pagePlugins": "Plugins & marketplace", @@ -1003,5 +996,15 @@ "search.results": "{{count}} results", "search.openInTimeline": "Open in timeline", "search.clearFilter": "clear filter", - "search.resultsLabel": "Search results, {{count}} events" + "search.resultsLabel": "Search results, {{count}} events", + "export.title": "Export", + "export.all": "Everything", + "export.view": "This view — {{view}}", + "export.slice": "The visible time range", + "export.bundle": "Evidence bundle (with verifier)", + "export.preview": "{{count}} events · about {{size}}", + "export.done": "Exported {{label}}", + "export.failed": "Could not export {{label}}", + "settings.revealInFolder": "Show in Finder / Explorer", + "transcript.copyMd": "Copy as Markdown" } diff --git a/src/renderer/src/i18n/zh-TW.json b/src/renderer/src/i18n/zh-TW.json index c942d43b..293e7797 100644 --- a/src/renderer/src/i18n/zh-TW.json +++ b/src/renderer/src/i18n/zh-TW.json @@ -404,16 +404,13 @@ "settings.cdpPort": "CDP 連接埠", "settings.testConnection": "測試連線", "settings.cdpHint": "啟動 Chrome:google-chrome --remote-debugging-port={{port}}", - "settings.exportAll": "匯出所有資料", "settings.exportJson": "匯出 JSON", "settings.exportHint": "將所有事件、快速標記和設定匯出為單一 JSON 檔案。", "settings.exportFailed": "匯出失敗", "settings.savedTo": "已儲存至:{{path}}", "settings.exportBundle": "匯出證據包", - "settings.exportBundleTitle": "證據包(Evidence Bundle)", "settings.exportBundleHint": "完整消毒後的證據包(events.jsonl、鏈錨、截圖、cast、operators),並附獨立 Python 鏈驗證器。可直接交付或作為附件。", "settings.exportBundleBuilding": "打包中...", - "settings.exportBundleReveal": "在 Finder / 檔案總管開啟", "toast.repeat": "×{{count}}", "toast.dismiss": "關閉", "toast.undo": "復原", @@ -621,7 +618,6 @@ "timeline.tz.utc": "UTC", "timeline.tz.project": "專案", "timeline.tz.tooltip": "以此時區顯示時間戳記", - "dashboard.exportData": "匯出資料", "overlay.mark": "標記", "overlay.rec": "錄製", "overlay.paused": "已暫停", @@ -761,7 +757,6 @@ "marketplace.revokedPublishers": "撤銷的發行者", "marketplace.sizeKb": "{{size}} KB", "marketplace.rolledBackFrom": "上一版已備份至 {path}", - "settings.scopeExport": "範圍過濾匯出", "settings.exportScopeJson": "匯出範圍內 JSON", "settings.scopeExportHint": "僅匯出符合已設定範圍目標的事件。適用於上傳日誌至遠端伺服器時——排除範圍外資料。", "timeline.detail.ioOnDisk": "已記錄輸出 — 工作階段錄影 {{size}},展開下方即可閱讀", @@ -817,7 +812,6 @@ "transcript.title": "逐字稿", "transcript.count": "{{n}} 段往返", "transcript.filter": "篩選逐字稿…", - "transcript.exportMd": "複製為 Markdown", "transcript.copied": "逐字稿已複製為 Markdown", "transcript.copyFailed": "複製失敗", "transcript.loading": "載入中…", @@ -987,7 +981,6 @@ "settings.pageNetwork": "網路與 IP", "settings.pageDeconfliction": "去衝突", "settings.pageIntegrity": "證據鏈與錨定", - "settings.pageExport": "保存與匯出", "settings.pageOperators": "操作員與團隊", "settings.pageCloud": "分享與藍隊通報", "settings.pagePlugins": "外掛與市集", @@ -1003,5 +996,15 @@ "search.results": "{{count}} 項結果", "search.openInTimeline": "在時間軸開啟", "search.clearFilter": "清除篩選", - "search.resultsLabel": "搜尋結果,{{count}} 筆事件" + "search.resultsLabel": "搜尋結果,{{count}} 筆事件", + "export.title": "匯出", + "export.all": "全部", + "export.view": "目前畫面——{{view}}", + "export.slice": "可視時間範圍", + "export.bundle": "證據包(含驗證器)", + "export.preview": "{{count}} 筆事件 · 約 {{size}}", + "export.done": "已匯出{{label}}", + "export.failed": "無法匯出{{label}}", + "settings.revealInFolder": "在 Finder / 檔案總管開啟", + "transcript.copyMd": "複製為 Markdown" } diff --git a/src/renderer/src/lib/exportScope.ts b/src/renderer/src/lib/exportScope.ts new file mode 100644 index 00000000..c867aca4 --- /dev/null +++ b/src/renderer/src/lib/exportScope.ts @@ -0,0 +1,67 @@ +import { useEffect } from 'react' +import { useSyncExternalStore } from 'react' + +// Who can export what, kept outside React so the shell's one export control +// (docs/UIUX-STANDARD.md §10) can offer a view's own scope without the shell +// having to know what views exist. +// +// The alternative was the shape this replaced: each view grew its own export +// button, worded its own way, because the view was the only place that knew +// what "this" meant. That put the same verb in six places and made the scope +// a property of where you clicked. Here the view contributes only the part it +// alone knows — how to serialise its current subset — and the menu stays the +// single place the verb appears. + +export interface ViewExport { + /** What the operator will recognise this subset as, in the menu. */ + label: string + /** Resolves to the written path, or null if the operator cancelled. */ + run: () => Promise + /** Events in this subset, for the preview line. Omit if not countable. */ + count?: number +} + +let current: ViewExport | null = null +const listeners = new Set<() => void>() + +function emit(): void { + for (const l of listeners) l() +} + +export function subscribe(fn: () => void): () => void { + listeners.add(fn) + return () => listeners.delete(fn) +} + +export function getViewExport(): ViewExport | null { + return current +} + +/** Read the currently contributed view export, in the shell. */ +export function useViewExport(): ViewExport | null { + return useSyncExternalStore(subscribe, getViewExport, () => null) +} + +/** + * Contribute this view's export while it is mounted. + * + * Registration is cleared on unmount, so navigating away cannot leave the menu + * offering a subset that is no longer on screen — an export labelled "this + * view" that quietly means the previous one is worse than no option at all. + */ +export function useContributeExport(entry: ViewExport | null): void { + const label = entry?.label + const count = entry?.count + const run = entry?.run + useEffect(() => { + if (!label || !run) { + return + } + current = { label, run, count } + emit() + return () => { + current = null + emit() + } + }, [label, count, run]) +} diff --git a/test/export-single-control.test.ts b/test/export-single-control.test.ts new file mode 100644 index 00000000..b02c849a --- /dev/null +++ b/test/export-single-control.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest' +import fs from 'fs' +import path from 'path' + +// §10 and phase-3 item 13: one export control. +// +// Exporting was reachable from six places, each worded its own way — the +// dashboard, three separate groups in Settings, the timeline's slice button, +// and the transcript's. An operator writing an engagement up had to already +// know which one produced the thing they wanted, and the answer was not +// guessable from the labels. +// +// The fix was not to pick a winner but to notice that the scope of an export +// is a property of the export, not of where the button sits. Once scope is an +// option, six locations collapse to one control with three options — and the +// only thing a view still contributes is the part it alone knows. + +const ROOT = path.join(__dirname, '..') +const R = (p: string): string => fs.readFileSync(path.join(ROOT, p), 'utf-8') + +/** Every renderer file, so a seventh entry point cannot appear unnoticed. */ +function rendererFiles(): string[] { + const out: string[] = [] + const walk = (dir: string): void => { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, e.name) + if (e.isDirectory()) walk(full) + else if (/\.tsx?$/.test(e.name)) out.push(full) + } + } + walk(path.join(ROOT, 'src/renderer/src')) + return out +} + +describe('one export control', () => { + it('calls the file-writing export APIs from exactly one component', () => { + const callers = rendererFiles().filter((f) => + /window\.redlog\.data\.export(Json|Bundle|ScopeFiltered)\b/.test(fs.readFileSync(f, 'utf-8')) + ) + expect(callers.map((f) => path.basename(f))).toEqual(['ExportMenu.tsx']) + }) + + it('lets a view contribute its own scope instead of its own button', () => { + // The timeline knows what "the visible range" means and nothing else + // does. It contributes that; it does not render a second export button. + const timeline = R('src/renderer/src/components/Timeline.tsx') + expect(timeline).toMatch(/useContributeExport/) + expect(timeline).not.toMatch(/timeline\.exportSlice['"]\)\}<\/button>/) + }) + + it('clears the view contribution on unmount', () => { + // An option labelled "the visible time range" that still means the + // previous view's range is worse than no option at all. + const store = R('src/renderer/src/lib/exportScope.ts') + expect(store).toMatch(/return \(\) => \{\s*current = null/) + }) + + it('previews what each option will produce', () => { + // A wrong export is cheap; discovering it was wrong after opening the + // file is a wasted round trip during the one task this record exists for. + const menu = R('src/renderer/src/components/ExportMenu.tsx') + expect(menu).toMatch(/export\.preview/) + expect(menu).toMatch(/humanSize/) + }) + + it('keeps copy-to-clipboard out of the file-export menu', () => { + // The transcript's Markdown button writes the clipboard, not a file. It + // was counted as one of the six and is not one of them; folding it in + // would have made the count tidier and the action worse. + const transcript = R('src/renderer/src/components/TranscriptView.tsx') + expect(transcript).toMatch(/copyAsMarkdown/) + expect(transcript).toMatch(/clipboard\.writeText/) + expect(transcript).not.toMatch(/window\.redlog\.data\.export/) + }) +}) diff --git a/test/settings-ia.test.ts b/test/settings-ia.test.ts index 413746a2..10936e2a 100644 --- a/test/settings-ia.test.ts +++ b/test/settings-ia.test.ts @@ -9,9 +9,13 @@ import path from 'path' // Below that, Plugins held sub-tabs and those held publisher and revocation // lists: three levels underneath the second level. // -// The container had to come first. Thirteen categories fit down a side and do +// The container had to come first. Twelve categories fit down a side and do // not fit across a top, so the content could not be split until there was // somewhere to put it. +// +// There is no "export" page. Exporting is an action, not a setting, and it +// was three separate groups here before §10 collapsed every entry point into +// one control in the shell. This list is the guard against it drifting back. const ROOT = path.join(__dirname, '..') const SRC = fs.readFileSync(path.join(ROOT, 'src/renderer/src/components/Settings.tsx'), 'utf-8') @@ -19,13 +23,13 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/renderer/src/components/Setting const PAGES = [ 'hooks', 'agents', 'captureControl', 'scope', 'network', 'deconfliction', - 'integrity', 'export', + 'integrity', 'operators', 'cloud', 'plugins', 'general', 'hud' ] describe('settings information architecture', () => { - it('declares thirteen pages as a union', () => { + it('declares twelve pages as a union', () => { const block = /type SettingsPage =([\s\S]*?)\n\n/.exec(SRC) expect(block, 'SettingsPage union not found').not.toBeNull() for (const page of PAGES) { @@ -49,6 +53,14 @@ describe('settings information architecture', () => { expect([...new Set(listed)].sort()).toEqual([...PAGES].sort()) }) + it('keeps exporting out of settings', () => { + // The three groups that used to live here — export all, export the + // filtered scope, build an evidence bundle — were actions wearing a + // settings page. Each was also a fourth place to look for the same verb. + expect(SRC).not.toMatch(/data\.export(Json|Bundle|ScopeFiltered)/) + expect(SRC).not.toMatch(/tab === 'export'/) + }) + it('has a search box over the categories', () => { expect(SRC).toMatch(/settings\.searchPages/) expect(SRC).toMatch(/pageQuery/) From 5603ae9860bc918f1bb12f5188fd1187359dac59 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Sat, 22 Aug 2026 11:48:28 +0800 Subject: [PATCH 15/34] feat(plugins)!: remove the marketplace, keep what is installed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browsing a registry, pinning publishers by Ed25519 fingerprint, and reading revocation lists are the machinery of distributing capture code. The core is "nothing missing, findable afterwards" (docs/DESIGN-core-and-capture.md §1); distribution serves extensibility, which is a different product. What stays is the installed list, because "is anything capturing that I did not put there" is a question §1 does have to answer. Removed: MarketplacePanel and PublisherEditor (381 lines of Settings), the core modules marketplace.ts and publisher-trust.ts, eleven IPC handlers and their preload surface, config.marketplace.defaultRegistryUrl, 46 i18n keys per locale, and examples/registry. The redlog-sign CLI goes too. It existed to produce signed registry entries; with no registry there are no entries, and a redlog-sign on someone's PATH that signs for nothing is worse than its absence. Two handlers were exposed and never consumed: marketplace:listVersions and marketplace:rollback. Versions only exist for registry-installed plugins, so they had no reachable caller either way. Revocation lists were only ever displayed in the marketplace panel — the plugin loader does not consult them. Removing the panel removes the whole feature rather than orphaning a check, which is the outcome to want here but worth stating rather than discovering later. Settings: 31 field groups to 30, 12 pages unchanged. One group understates it — that group held a registry browser, a trust editor, and a revocation viewer stacked inside it. Noted in the design doc, because the group count is the metric positioning risk #2 is measured by. npm test 771 passed · npm run e2e 43 passed Co-Authored-By: Claude Opus 5 --- README.md | 2 +- cli/redlog-sign.js | 179 -------- docs/ARCHITECTURE.md | 7 +- docs/DESIGN-core-and-capture.md | 36 +- docs/DESIGN-two-tier-chain.md | 4 +- docs/PLUGIN_MARKETPLACE.md | 153 ------- docs/ROADMAP.md | 1 - docs/plugin-development.md | 1 - docs/screenshots/marketplace.jpg | Bin 261681 -> 0 bytes e2e/cloud-share-flow.spec.ts | 1 - e2e/helpers.ts | 5 +- e2e/marketplace-flow.spec.ts | 405 ----------------- examples/registry/.gitignore | 5 - examples/registry/README.md | 61 --- examples/registry/codex-hook-0.1.0.tar.gz | Bin 5267 -> 0 bytes examples/registry/index.json | 47 -- examples/registry/opencode-hook-0.1.0.tar.gz | Bin 4777 -> 0 bytes examples/registry/recon-pack-0.1.0.tar.gz | Bin 1753 -> 0 bytes package.json | 3 +- src/core/config.ts | 12 - src/core/paths.ts | 4 +- src/core/plugins/marketplace.ts | 430 ------------------- src/core/plugins/publisher-trust.ts | 135 ------ src/main/index.ts | 116 ----- src/preload/index.ts | 22 - src/renderer/src/components/Settings.tsx | 418 +----------------- src/renderer/src/i18n/en.json | 46 -- src/renderer/src/i18n/zh-TW.json | 46 -- test/marketplace.test.ts | 236 ---------- test/publisher-trust.test.ts | 196 --------- test/redlog-sign.test.ts | 146 ------- test/settings-ia.test.ts | 33 +- 32 files changed, 83 insertions(+), 2667 deletions(-) delete mode 100755 cli/redlog-sign.js delete mode 100644 docs/PLUGIN_MARKETPLACE.md delete mode 100644 docs/screenshots/marketplace.jpg delete mode 100644 e2e/marketplace-flow.spec.ts delete mode 100644 examples/registry/.gitignore delete mode 100644 examples/registry/README.md delete mode 100644 examples/registry/codex-hook-0.1.0.tar.gz delete mode 100644 examples/registry/index.json delete mode 100644 examples/registry/opencode-hook-0.1.0.tar.gz delete mode 100644 examples/registry/recon-pack-0.1.0.tar.gz delete mode 100644 src/core/plugins/marketplace.ts delete mode 100644 src/core/plugins/publisher-trust.ts delete mode 100644 test/marketplace.test.ts delete mode 100644 test/publisher-trust.test.ts delete mode 100644 test/redlog-sign.test.ts diff --git a/README.md b/README.md index e27e11fd..d045878e 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ notice and choose **Open Anyway**. Timeline
Attack Timeline — swim-lane view across Shell / Agent / HTTP / DNS / Pivot / Screenshots / Loot / Marks / System with cluster popups, per-command replay, and full-session `.cast` replay -Marketplace
Plugin Marketplace — fetch a signed registry, install with sha256 + Ed25519 verification, one-click trust publishers advertised in the index Cloud share
Cloud share bundle — preview review (events / sanitize / screenshots / casts, raw + zipped estimate), hard review-gate checkbox, expiry picker, stub / HTTPS backend switch + diff --git a/cli/redlog-sign.js b/cli/redlog-sign.js deleted file mode 100755 index 9662493e..00000000 --- a/cli/redlog-sign.js +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env node -// redlog-sign — sign a plugin tarball for the RedLog marketplace registry. -// -// Publishers run this locally after tarring their plugin. It: -// 1. Reads the tarball, computes sha256. -// 2. Signs "sha256:" with an Ed25519 private key. -// 3. Emits a JSON snippet suitable for pasting into the registry index. -// -// Usage: -// redlog-sign keygen -> writes keypair.json (pub/priv b64) -// redlog-sign sign --key [--id ] [--version ] -// -> prints the signed index entry -// -// Design intent: -// - Ed25519 only (spec §5). No knobs. -// - No network. No touching ~/.redlog. Operator-side utility. -// - Same trust model as publisher-trust.ts: SPKI b64 public key, base64 -// signature over the ASCII string "sha256:". - -const fs = require('fs') -const path = require('path') -const crypto = require('crypto') - -function fail(msg) { - console.error(`redlog-sign: ${msg}`) - process.exit(1) -} - -function parseArgs(argv) { - const args = argv.slice(2) - const cmd = args[0] - const rest = args.slice(1) - const positional = [] - const flags = {} - for (let i = 0; i < rest.length; i++) { - const a = rest[i] - if (a.startsWith('--')) { - const key = a.slice(2) - const val = rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[++i] : true - flags[key] = val - } else { - positional.push(a) - } - } - return { cmd, positional, flags } -} - -function keygen(flags) { - const out = flags.out || 'redlog-plugin-keypair.json' - if (fs.existsSync(out) && !flags.force) fail(`refuse to overwrite ${out} — pass --force to override`) - const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519') - const pubDer = publicKey.export({ format: 'der', type: 'spki' }) - const privDer = privateKey.export({ format: 'der', type: 'pkcs8' }) - const record = { - algorithm: 'ed25519', - publicKey: pubDer.toString('base64'), - privateKey: privDer.toString('base64'), - createdAt: new Date().toISOString(), - label: flags.label || undefined - } - fs.writeFileSync(out, JSON.stringify(record, null, 2), { mode: 0o600 }) - const fpHex = crypto.createHash('sha256').update(pubDer).digest('hex') - const fp = fpHex.match(/.{2}/g).slice(0, 16).join(':') - console.log(`wrote keypair → ${out} (mode 0600)`) - console.log(`publicKey fingerprint: ${fp}`) - console.log() - console.log('Share the publicKey field with the RedLog registry maintainer;') - console.log('keep privateKey local — anyone with it can sign plugins as you.') -} - -function sign(positional, flags) { - const tarballPath = positional[0] - if (!tarballPath) fail('sign: missing tarball path') - const keyPath = flags.key - if (!keyPath) fail('sign: --key is required') - if (!fs.existsSync(tarballPath)) fail(`sign: no such file: ${tarballPath}`) - if (!fs.existsSync(keyPath)) fail(`sign: no such key: ${keyPath}`) - - const bytes = fs.readFileSync(tarballPath) - const hexHash = crypto.createHash('sha256').update(bytes).digest('hex') - const message = Buffer.from(`sha256:${hexHash}`, 'utf-8') - - const keypair = JSON.parse(fs.readFileSync(keyPath, 'utf-8')) - if (keypair.algorithm && keypair.algorithm !== 'ed25519') { - fail(`sign: unsupported key algorithm: ${keypair.algorithm}`) - } - const privKey = crypto.createPrivateKey({ - key: Buffer.from(keypair.privateKey, 'base64'), - format: 'der', - type: 'pkcs8' - }) - const signature = crypto.sign(null, message, privKey).toString('base64') - - // Try to sniff id + version from the tarball's plugin.json if the flags - // weren't supplied. Best-effort — a signer with an unusual tarball layout - // can still pass them explicitly. - let id = flags.id - let version = flags.version - if ((!id || !version) && !flags['no-sniff']) { - try { - const parsed = sniffManifest(tarballPath) - if (!id) id = parsed?.id - if (!version) version = parsed?.version - } catch { /* leave fields blank */ } - } - - const entry = { - id: id || '', - version: version || '', - publisher: flags.publisher || '', - tarball: flags.url || '', - sha256: hexHash, - signature, - sizeKb: Math.round(bytes.length / 1024) - } - if (flags.name) entry.name = flags.name - if (flags.description) entry.description = flags.description - if (flags.homepage) entry.homepage = flags.homepage - if (flags.tags) entry.tags = String(flags.tags).split(',').map((s) => s.trim()).filter(Boolean) - - // Order the keys so the JSON is stable and easy to diff. - const orderedKeys = ['id', 'name', 'description', 'homepage', 'publisher', 'version', 'tarball', 'sha256', 'signature', 'sizeKb', 'tags'] - const ordered = {} - for (const k of orderedKeys) if (k in entry) ordered[k] = entry[k] - - console.log(JSON.stringify(ordered, null, 2)) -} - -// Peek inside a .tar.gz to pull { id, version } out of plugin.json without -// extracting to disk. Falls back gracefully if node's tar isn't around — we -// just use gunzip + streaming tar parse via headers. -function sniffManifest(tarballPath) { - const zlib = require('zlib') - const buf = fs.readFileSync(tarballPath) - const tar = zlib.gunzipSync(buf) - // Tar block size is 512. - for (let off = 0; off + 512 <= tar.length;) { - const nameEnd = tar.indexOf(0, off) - const name = tar.slice(off, nameEnd < 0 || nameEnd > off + 100 ? off + 100 : nameEnd).toString('utf-8').replace(/\0+$/, '') - if (!name) break - // Octal size at offset +124 (12 bytes ASCII). - const sizeStr = tar.slice(off + 124, off + 136).toString('ascii').trim().replace(/\0/g, '') - const size = parseInt(sizeStr, 8) - off += 512 - if (isNaN(size)) break - if (name.endsWith('plugin.json')) { - const body = tar.slice(off, off + size).toString('utf-8') - try { return JSON.parse(body) } catch { return null } - } - off += Math.ceil(size / 512) * 512 - } - return null -} - -function help() { - console.log(`redlog-sign — sign a plugin tarball for the RedLog marketplace. - - keygen [--out ] [--label ] [--force] - Generate an Ed25519 keypair. Default: ./redlog-plugin-keypair.json (0600). - - sign --key - [--id ] [--version ] - [--publisher ] [--url ] - [--name ] [--description ] [--homepage ] - [--tags foo,bar] - Print a registry index entry with sha256 + Ed25519 signature. - - help - This message.`) -} - -const { cmd, positional, flags } = parseArgs(process.argv) -switch (cmd) { - case 'keygen': keygen(flags); break - case 'sign': sign(positional, flags); break - case 'help': - case undefined: help(); break - default: fail(`unknown command: ${cmd}`) -} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 06c01113..f0f8f56e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,7 +2,7 @@ Current as of **v0.9.3**. This page is the map the README's ASCII diagram stopped being (that diagram predates the tailer host, plugin host, -marketplace and cloud share). Read `event-schema.md` for what lands on the +cloud share). Read `event-schema.md` for what lands on the timeline and `audit-trail.md` for why it can't be quietly edited. ## 1. Process / layer model @@ -268,11 +268,6 @@ signing keys. Trust is pinned to a content hash covering the manifest plus every privileged code file; changing either the code or the requested capabilities revokes it automatically. -Marketplace install: revocation check → HTTPS fetch (5 MB cap, one redirect, -20 s timeout) → sha256 match → Ed25519 verify against a pinned publisher key -→ tar-escape pre-check → extract to scratch → manifest re-validation → tier -post-check → atomic rename with the previous version snapshotted. - `tailers` is the exception and currently does **not** follow this path — see `AUDIT-2026-08-08.md` §2 (P1-3). diff --git a/docs/DESIGN-core-and-capture.md b/docs/DESIGN-core-and-capture.md index 77761f29..6b7077e8 100644 --- a/docs/DESIGN-core-and-capture.md +++ b/docs/DESIGN-core-and-capture.md @@ -342,12 +342,40 @@ recordings. ## 6. Still open - Windows script-block logging (§2.3) and queryable tool output (§2.4). -- Settings: 13 pages / 34 field groups, against positioning risk #2 which +- Settings: **12 pages / 30 field groups**, against positioning risk #2 which named "8 tabs / 34 groups" as the symptom of breadth outrunning the persona. - The page count went **up**. + The left list took the page count *up* to 13 before two rounds of removal + brought it to 12 and the groups from 34 to 30. That is a dent, not a + resolution — the remaining test is the one below, applied to each group in + turn, and most of the 30 have not been through it yet. - Timeline view modes — the audit-mode shape does not survive §1, but the eight flat toggles are still a real problem. - 18 lanes. -- The plugin marketplace, publisher trust and revocation lists — an ecosystem, - which is enterprise shape; neither core answers for it. +- ~~The plugin marketplace, publisher trust and revocation lists.~~ + **Removed 2026-08-22.** Browsing a registry, pinning publishers by Ed25519 + fingerprint, and reading revocation lists are the machinery of *distributing* + capture code. §1 is about not missing anything and finding it afterwards; + distribution serves extensibility, which is a different product. Managing + what is installed stays, because "is anything capturing that I did not put + there" is a question §1 does have to answer. The `redlog-sign` CLI went with + it — it existed only to produce registry entries. - Rewriting `PRODUCT-POSITIONING.md` to match §1. + +### The test being applied to Settings + +For each field group: **does the operator in §1 have to understand this to +answer "what happened, and can I show it"?** If not, it is either a default +this product should pick, or something that belongs to a different product. + +Two rounds so far, worth 4 groups and 1 page between them. + +The first removed three groups that were not settings at all — export is an +action, and it now lives in the shell's one export control (§10). That also +emptied the "Retention & export" page down to a screenshot interval, which had +been filed under export while being a capture setting; it moved, and the page +went. + +The second removed the marketplace. Only one group, but the group was a +registry browser, a publisher trust editor, and a revocation viewer stacked +inside it — the group count understated it, which is worth noting because the +count is the metric positioning risk #2 uses. diff --git a/docs/DESIGN-two-tier-chain.md b/docs/DESIGN-two-tier-chain.md index 840ede55..75172ea5 100644 --- a/docs/DESIGN-two-tier-chain.md +++ b/docs/DESIGN-two-tier-chain.md @@ -51,7 +51,7 @@ Prior-art guidance: this is not a performance PR. v0.12.1 cached Ed25519 keys an - Every logged-tier row lands in every export bundle whether the recipient asked for full evidence footprint or not. - Every logged-tier row that touches the chain widens the window during which `chain_sample_broken` can misfire under real-world clock skew. -Fixing this at v0.13 also unblocks the plugin-marketplace roadmap ([PLUGIN_MARKETPLACE.md](PLUGIN_MARKETPLACE.md)), because a 🟢 trust-tier plugin that wants to log its own high-frequency observations can be pointed at the logged tier by default, without every plugin author having to think about anchor cadence. +Fixing this at v0.13 also helps plugins, because a 🟢 trust-tier plugin that wants to log its own high-frequency observations can be pointed at the logged tier by default, without every plugin author having to think about anchor cadence. --- @@ -777,7 +777,7 @@ Release note draft (for CHANGELOG.md): ### 13.1 Plugin-contributed event types default to which tier? -**Recommendation: chained.** A 🟢-trust plugin (see [PLUGIN_MARKETPLACE.md](PLUGIN_MARKETPLACE.md) and [MEMORY.md#redlog-plugin-system](file:///Users/guantou/.claude/projects/-Users-guantou-Desktop-redlog/memory/redlog-plugin-system.md)) that emits `insertEvent('my-plugin', {...})` should land chained by default. Plugin authors can opt into logged tier by extending `LOGGED_TIER` in a plugin manifest field: +**Recommendation: chained.** A 🟢-trust plugin that emits `insertEvent('my-plugin', {...})` should land chained by default. Plugin authors can opt into logged tier by extending `LOGGED_TIER` in a plugin manifest field: ```yaml # plugin.yaml diff --git a/docs/PLUGIN_MARKETPLACE.md b/docs/PLUGIN_MARKETPLACE.md deleted file mode 100644 index 16688b29..00000000 --- a/docs/PLUGIN_MARKETPLACE.md +++ /dev/null @@ -1,153 +0,0 @@ -# RedLog Plugin Marketplace — Design Spec (draft) - -Status: **shipped** since v0.6.68 (core) / v0.6.69 (UI). The threat model in §9 was corrected in v0.11.0 to describe what the code actually does — two of its claims described controls that were never built. - -## 1. Goal - -Today plugins are installed by dropping a directory into `~/.redlog/plugins//`. That works for the operator who wrote the plugin, but nothing else — no discoverability, no version updates, no way to distinguish "the RedLog team wrote this" from "a random archive I pulled off the internet." The marketplace is the answer to three concrete problems: (a) an operator can browse and install a curated set of loot patterns, capture integrations, and MCP tools without a shell, (b) declarative-tier plugins can auto-update so pattern lists stay current, and (c) privileged-tier plugins can be signed by a publisher key the operator has explicitly trusted, so a compromised or malicious archive can't silently obtain `write:events` or `net:outbound`. The existing manifest schema, trust store (`~/.redlog/plugins/trust.json`), and content-hash pinning already do the heavy lifting — the marketplace is a distribution and consent layer on top. - -## 2. Non-goals (v1) - -- No paid plugins, no billing, no license enforcement. -- No user reviews, ratings, or comments — trivially poisoned, and irrelevant for an audit tool. -- No cross-vendor identity or SSO — publishers are keypairs, nothing more. -- No in-app plugin authoring or hot-reload UI (that stays in the dev guide). -- No mobile / web client — desktop Electron only. -- No telemetry on install counts back to the RedLog project. - -## 3. Distribution model — recommendation: **A. Git-repo-as-registry** - -Use a public GitHub repo (`redlog-plugins/registry`) as the index. It contains a single `index.json` listing plugin id, latest version, publisher key id, tarball URL, sha256, and category. Tarballs live wherever the publisher wants — GitHub Releases, S3, a personal server — as long as the sha256 matches. RedLog fetches `index.json` over HTTPS, verifies the tarball hash, and (for privileged plugins) the publisher signature. - -Why not the others: **NPM (B)** drags in a package manager and a namespace we don't control, and NPM's own supply-chain history is exactly what we're trying to avoid. **Self-hosted service (C)** means we run infrastructure, handle abuse reports, and become a single point of failure for a security tool. **Hybrid (D)** is where we end up eventually if the git index grows too large, but v1 doesn't need it. - -Git-as-registry gives us: free hosting, PR-based moderation with a full audit trail, easy forks for air-gapped shops, and signed commits from the RedLog maintainers as the root of trust for the index itself. - -## 4. Plugin package format - -A gzipped tarball (`.tar.gz`), because tar preserves file modes and macOS/Linux operators already have it. Structure: - -``` --.tar.gz -└── / - ├── plugin.json # manifest, required - ├── README.md # rendered in the marketplace UI - ├── LICENSE # optional but shown - └── ... (contributed files) -``` - -Rules enforced during unpack: - -- Reject symlinks, hardlinks, absolute paths, and any entry containing `..`. -- Reject entries outside the single top-level `/` directory. -- Reject files with executable bits set except inside a `bin/` subdir (we don't spawn them anyway; this is belt-and-braces). -- Size cap: 5 MiB uncompressed in v1. Bump later if a real use case appears. - -The tarball's sha256 is what the index pins. RedLog re-computes it before extraction and rejects a mismatch. - -## 5. Signing & trust flow - -Two tiers, two flows. - -**🟢 Declarative plugins** (loot patterns, redaction, tags, extractors, event types, capture) — signature optional. Install prompt shows a "Publisher: unverified" badge if unsigned, or "Publisher: `redlog-official` (trusted)" if signed by a key in the trust store. No capability prompt because these can't call anything. - -**🔴 Privileged plugins** (`mcpTools`, `exporters`, `monitors`, `tailers`) — signature required. The publisher signs the tarball's sha256 with an Ed25519 key; the public key + a human name lives in `~/.redlog/trusted-publishers.json`: - -> **`tailers` (v0.8.2)** — transcript adapters for AI coding agents. A tailer module exports `{ adapter }` where `adapter` implements the `TailerAdapter` interface from `src/main/services/tailer-host.ts`. **v0.8.2 restriction:** only bundled tailer plugins (shipped inside the RedLog resources dir) run; user-installed plugins that declare `tailers` produce an advisory in `~/.redlog/log` and are otherwise skipped. Third-party `tailers` require isolation work landing in v0.8.3+ — the current tailer-host would run plugin code in-process at line-rate against the transcript stream, which needs either a faster IPC path than `utilityProcess` or a per-plugin JS sandbox before we can safely open it up. - -```json -{ - "redlog-official": { "pubkey": "ed25519:...", "addedAt": 1706... }, - "acme-redteam": { "pubkey": "ed25519:...", "addedAt": 1706... } -} -``` - -First install of a plugin from a new publisher shows a two-step consent dialog: - -> **Trust publisher `acme-redteam`?** -> Fingerprint: `ed25519:a1b2…c3d4`. Once trusted, you'll be prompted separately for each plugin's capabilities. You can revoke this publisher in Settings ▸ Plugins ▸ Publishers. -> **[Trust publisher] [Cancel]** - -then - -> **Install `acme-exfil-monitor` v1.2.0?** -> This plugin asks for: `read:events`, `net:outbound` (⚠ outbound network). -> Content hash: `sha256:9f8c…` -> **[Install] [Cancel]** - -The existing `trust.json` grant is written on approval, pinning both the content hash and the capability set — the enforcement path in `src/core/plugins/trust.ts` already handles this. - -**Update behaviour** — after a version bump: - -- Hash changed, capability set unchanged → silent upgrade, toast "acme-exfil-monitor updated 1.2.0 → 1.3.0". -- Hash changed, capability set expanded → status flips to `needs-consent`, plugin stops loading its code, operator is re-prompted with the delta highlighted. -- Signature invalid or publisher no longer trusted → status flips to `error`, no code runs. - -## 6. Update flow - -- Poll `index.json` on app launch (skipped if last poll < 6 h ago) and on every visit to Settings ▸ Plugins. -- Declarative plugins: auto-update by default, togglable per-plugin. -- Privileged plugins: never auto-update the *code* silently. New version → notification badge on the Plugins tab; operator must open it and click "Install update". Applies whether or not capabilities changed — the extra click is cheap and the operator is the audit trail. -- Rollback: keep the previous version's directory at `~/.redlog/plugins//versions//`, retain the last three versions, and expose a "Rollback" button in the plugin detail panel. The trust grant for a rolled-back hash is re-usable without re-consent because the hash was previously approved. - -## 7. In-app UX - -Three new views under Settings ▸ Plugins: - -- **Marketplace tab** — search bar, category chips (Loot patterns, Capture, MCP tools, Redaction, Event types), plugin cards showing name, publisher badge, one-line description, `[Install]` button. Filters: "Signed only", "Publisher trusted", "Tier: privileged". -- **Publisher page** — reached by clicking a publisher badge. Lists all plugins from that publisher, trust status (trusted / unverified / revoked), and a Revoke Publisher button. -- **Plugin detail** — rendered README, requested capabilities with tooltips, install size, current content hash, version history with per-version content hash and "Rollback to this version" affordance, "Report abuse" link that opens a `mailto:` or the registry repo's issue tracker. - -The existing Plugins tab (installed list, enable/disable, per-plugin capability view) stays as it is; the marketplace is a sibling tab. - -## 8. Revocation - -The registry serves a `revocations.json` blob alongside `index.json`, signed by the RedLog project key (baked into the app binary at build time — same key that signs the release itself). Each entry: `{ pluginId, version, contentHash, reason, revokedAt }`. - -On every poll RedLog fetches the blob, verifies the signature, and: - -- If an installed plugin's contentHash is on the list → disable it immediately, set status to `error` with the reason, toast the operator. -- Publisher-level revocation is also possible (whole `pubkey` marked revoked) — every plugin signed by that key flips to `needs-consent` or `error` on next load. - -Operator override exists (edit `revocations-override.json` by hand) because an air-gapped shop may need to keep running a plugin the upstream registry has yanked. Not exposed in the UI. - -## 9. Security threats - -1. **Typosquatting** (`acme-redteem` vs `acme-redteam`) — the marketplace UI shows publisher next to every plugin name and dims plugins whose publisher isn't in the trust store; PR review on the git registry is the primary defence for new registrations. -2. **Publisher key compromise** — the operator untrusts the key in Settings ▸ Plugins ▸ Marketplace ▸ Publishers; every plugin signed by it stops verifying, and an update cannot install. `~/.redlog/plugins/revocations.json` is a **local blocklist the operator maintains** — RedLog never fetches it from the network, because without a root of trust a fetched revocation list is only as honest as whoever served it (and a compromised publisher will not revoke itself). *(Earlier revisions described automatic signed revocations. That mechanism was never built; the file and the UI tab are the manual blocklist they always were.)* -3. **Capability escalation via update** — content-hash pinning + capability comparison in `trust.ts` already forces re-consent on any capability delta. The UX highlights added capabilities in red. -4. **Malicious index** — **the index is untrusted and RedLog does not verify it.** There is no root key to check it against, and TLS only proves the bytes came from whoever holds the domain, which is exactly who an attacker would need to be. The index says *where to look*, never *what is safe*. The trust boundary is one step later: `installFromRegistry` verifies each tarball's Ed25519 signature against a key the **operator** pinned, and refuses a privileged plugin that has none. A hostile index can therefore waste your time and serve you a tarball, but cannot get code executed — unless you pin its key, which is why trusting a publisher is a per-publisher action showing the fingerprint you are expected to compare against that publisher's own channel. *(Earlier revisions of this document claimed unsigned index mutations were rejected. They never were; the claim was removed in v0.11.0 along with the one-click "trust all suggested publishers" button, which let whoever controlled the index pin their own key.)* -5. **Network downgrade / MITM** — HTTPS-only, no `http://` fallback, and the tarball sha256 is pinned in the index so a modified tarball fails even if TLS is broken. -6. **Malicious tarball contents** — the unpack rules in §4 prevent symlink escapes and path traversal; the plugin runs in the existing utility-process sandbox with capability-gated RPC (see `src/core/plugins/host.ts`), so even a hostile privileged plugin can't reach the DB handle or signing keys. - -## 10. Implementation phases - -**v1 — minimum shippable** - -- Registry repo skeleton + `index.json` schema + `revocations.json` schema. -- In-app Marketplace tab: browse, search, install, uninstall, update. -- Publisher trust store + first-install consent flow. -- Ed25519 signing tooling (a small `redlog-sign` CLI in the repo). -- Rollback to previous content hash. -- Documentation update to `plugin-development.md` on how to publish. - -**v2 — nice-to-have** - -- Bulk "trust curated set" for new operators (one-click trust of `redlog-official`). -- Publisher key rotation flow (old key signs an attestation for the new key). -- Delta updates (bsdiff-style) for large plugins — probably unnecessary given the 5 MiB cap. -- Air-gap export bundle: `redlog plugins export --all` produces a zip an operator can `redlog plugins import` on a disconnected machine. - -**v3 — far future** - -- Private/internal registries (point RedLog at a company-internal git repo). -- Plugin dependency resolution (plugin A depends on plugin B's event types). -- In-app publisher onboarding (currently: file a PR). - -## 11. Open questions - -1. **Root of trust distribution** — the RedLog project key is baked into the binary. What is the rotation story if that key is compromised? Ship a new binary is the honest answer; is that acceptable? -2. **Publisher onboarding friction** — is "open a PR against the registry repo" enough gatekeeping, or do we need out-of-band identity verification for publishers who want to ship privileged plugins? -3. **Capability granularity** — `net:outbound` is a single bit today. Do we need domain-scoped variants (`net:outbound:*.slack.com`) before opening the marketplace, or after evidence that plugins want it? -4. **Registry scale** — at what plugin count does the single `index.json` become a problem? Rough cap before we split into category shards? -5. **Event provenance** — a privileged plugin with `write:events` currently attributes its events to itself. Should the marketplace enforce a stricter provenance label (`plugin:@@`) in the event data so audit reviewers can filter plugin-authored events without operator config? diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b012bc8e..45c0d7e9 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -96,7 +96,6 @@ privileged path that skips the gate: | Item | Ref | |---|---| | `tailers`: isolate like `mcpTools`, or make first-party-only explicit and move it out of `PRIVILEGED_KEYS` | P1-3 | -| Registry index signing — implement it or delete the claim; replace "Trust all suggested" with per-publisher confirmation; drop the unregistered default registry URL | P1-4 | | Fetch + verify signed revocations, or remove the tab and the doc section | P1-5 | | `vps-deploy.sh` refuses the primary token; prints the `operators add` command | P1-6 | | Content hash covers `capture[].hookFile` | P2-1 | diff --git a/docs/plugin-development.md b/docs/plugin-development.md index b7dfd70c..5846c9f1 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -68,7 +68,6 @@ Every path referenced in the manifest must stay **inside** the plugin directory "contributes": { … }, // required — see below "capabilities": ["…"], // 🔴 only — what the code needs (least authority) "signature": "…", // optional — reserved for signed publishing - "publisher": "…" // optional — reserved } ``` diff --git a/docs/screenshots/marketplace.jpg b/docs/screenshots/marketplace.jpg deleted file mode 100644 index 6dfab2dca036545415107772e667eeddaaf03d45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 261681 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

jZ%NV0%EN*pqpSgV1k!sT0xV+XMJMiV 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 diff --git a/e2e/cloud-share-flow.spec.ts b/e2e/cloud-share-flow.spec.ts index 5953810d..b3b23886 100644 --- a/e2e/cloud-share-flow.spec.ts +++ b/e2e/cloud-share-flow.spec.ts @@ -45,7 +45,6 @@ function readMagic(path: string, n: number): Buffer { // Shared electronApp across the four tests — one launch, one project, and // Test 2 depends on Test 1's UI state (Data tab already open, panel visible). -// Same pattern as marketplace-flow.spec.ts. test.describe.serial('cloud share', () => { let app: ElectronApplication let page: Page diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 29457acc..fa501c84 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -46,9 +46,8 @@ export async function launchWithTempHome(): Promise<{ NODE_ENV: 'test', HOME: tmpHome, USERPROFILE: tmpHome, - // Enables dev-only IPC endpoints used by E2E specs (e.g. - // `marketplace:testInstall` which injects a fetched tarball via bytes). - // The main process refuses the endpoint when this flag is unset. + // Enables dev-only IPC endpoints used by E2E specs. The main process + // refuses those endpoints when this flag is unset. REDLOG_E2E: '1' } }) diff --git a/e2e/marketplace-flow.spec.ts b/e2e/marketplace-flow.spec.ts deleted file mode 100644 index 6ef18a22..00000000 --- a/e2e/marketplace-flow.spec.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { test, expect, type ElectronApplication, type Page } from '@playwright/test' -import { createHash, generateKeyPairSync } from 'node:crypto' -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { gzipSync } from 'node:zlib' -import { launchWithTempHome, openTestProject, openSettingsPage } from './helpers' - -/** Installed plugins and the marketplace are two sections of one page now - * (§10 — they were a tab holding sub-tabs holding publisher and revocation - * lists, three levels under a second-level tab). One click, no sub-tab. */ -async function openMarketplaceTab(page: Page): Promise { - await openSettingsPage(page, 'plugins') -} - -const SCREENSHOT_DIR = join(__dirname, 'screenshots') - -// ----------------------------------------------------------------------------- -// Test-scoped tarball builder — mirrors the shape src/core/plugins/marketplace -// installs via the default (system `tar`) extractor. Entries are prefixed with -// `plugin/` so `--strip-components=1` lands them at the plugin dir root. Copied -// from test/redlog-sign.test.ts on purpose: keeping it local means the E2E -// spec has no coupling to Vitest fixtures. -// ----------------------------------------------------------------------------- - -interface TarEntry { - /** path INSIDE the tarball, e.g. 'plugin/plugin.json' */ - name: string - body: Buffer -} - -function tarHeader(name: string, size: number): Buffer { - const header = Buffer.alloc(512, 0) - Buffer.from(name).copy(header, 0) - // Null bytes as \x00 rather than \0 — Playwright's strict-mode transpile - // rejects '\000' as an octal escape, so we keep all null bytes hex-escaped - // to be safe. - Buffer.from('0000644\x00').copy(header, 100) - Buffer.from('0000000\x00').copy(header, 108) - Buffer.from('0000000\x00').copy(header, 116) - Buffer.from(size.toString(8).padStart(11, '0') + '\x00').copy(header, 124) - Buffer.from('00000000000\x00').copy(header, 136) - Buffer.from(' ').copy(header, 148) // checksum placeholder - header.write('0', 156) - Buffer.from('ustar\x00').copy(header, 257) - Buffer.from('00').copy(header, 263) - let checksum = 0 - for (let i = 0; i < 512; i++) checksum += header[i] - Buffer.from(checksum.toString(8).padStart(6, '0') + '\x00 ').copy(header, 148) - return header -} - -/** Build a real gzipped tar (POSIX ustar) from an array of entries. */ -function buildTarGz(entries: TarEntry[]): Buffer { - const blocks: Buffer[] = [] - for (const e of entries) { - blocks.push(tarHeader(e.name, e.body.length)) - const padded = Buffer.alloc(Math.ceil(e.body.length / 512) * 512, 0) - e.body.copy(padded, 0) - blocks.push(padded) - } - blocks.push(Buffer.alloc(1024, 0)) // two zero blocks = EOF - return gzipSync(Buffer.concat(blocks)) -} - -/** - * Build a minimal declarative plugin tarball. Returns the gz bytes, their hex - * sha256, and the manifest object (for anyone asserting on-disk shape later). - */ -function buildDeclarativeTarball(pluginId: string, version: string): { - bytes: Buffer - sha256: string - manifest: Record -} { - const manifest = { - id: pluginId, - name: `${pluginId} test plugin`, - version, - redlogApi: 1, - contributes: { - lootPatterns: [ - { - id: `${pluginId}-marker`, - name: 'marketplace-e2e marker', - // A regex we know won't fire on any real event — the pattern needs to - // exist for the plugin to load, not to match anything. - pattern: 'MARKETPLACE_E2E_FAKE_MATCH_XYZ', - category: 'test' - } - ] - } - } - const bytes = buildTarGz([ - { name: 'plugin/plugin.json', body: Buffer.from(JSON.stringify(manifest, null, 2)) } - ]) - const sha256 = createHash('sha256').update(bytes).digest('hex') - return { bytes, sha256, manifest } -} - -/** Preload bridge slice for the endpoints this spec drives. */ -interface MarketplaceBridge { - install: (entryJson: string) => Promise<{ ok: boolean; error?: string }> - testInstall: (entryJson: string, tarballBytesB64: string) => Promise<{ - ok: boolean - tier?: 'declarative' | 'privileged' - installedDir?: string - error?: string - }> - testSetIndex: (indexJson: string) => Promise<{ ok: boolean; error?: string }> - listPublishers: () => Promise }>> - listVersions: (pluginId: string) => Promise - rollback: (pluginId: string, versionKey: string) => Promise<{ ok: boolean; error?: string }> -} - -function bridge(page: Page): Promise { - // Just a nudge to prove the preload is wired — the actual calls happen inside - // page.evaluate blocks below, since the bridge lives in the renderer. - return page.evaluate(async () => { - const w = window as unknown as { redlog: { marketplace: unknown } } - return w.redlog.marketplace as unknown as MarketplaceBridge - }) -} - -// Shared electronApp across the three tests: same pattern as project-flow. -test.describe.serial('marketplace', () => { - let app: ElectronApplication - let page: Page - let tmpHome: string - - test.beforeAll(async () => { - const launched = await launchWithTempHome() - app = launched.app - page = launched.page - tmpHome = launched.tmpHome - // Sanity: preload wired the E2E-only endpoint. - await bridge(page) - // Force English locale so getByRole/getByPlaceholder can hit i18n strings - // deterministically (default detectLocale() picks zh-TW under a Chinese - // system locale, which breaks name-based selectors). - await page.evaluate(() => localStorage.setItem('redlog-locale', 'en')) - // Open a project so the main shell (with view-root + Settings) is mounted; - // otherwise ProjectPicker owns the window and none of the UI locators used - // by these tests can resolve. openTestProject reloads → the locale change - // takes effect before we start clicking. - await openTestProject(page, 'e2e-marketplace') - }) - - test.afterAll(async () => { - if (app) await app.close() - }) - - test('installs a declarative plugin via the test IPC', async () => { - const pluginId = 'mkt-e2e-plugin' - const { bytes, sha256 } = buildDeclarativeTarball(pluginId, '1.0.0') - - const entry = { - id: pluginId, - name: 'marketplace e2e plugin', - publisher: 'e2e-publisher', - version: '1.0.0', - tarball: 'https://example.invalid/mkt-e2e-plugin-1.0.0.tar.gz', - sha256 - } - - const result = await page.evaluate( - async ({ entryJson, tarballB64 }) => { - const api = (window as unknown as { redlog: { marketplace: MarketplaceBridge } }).redlog.marketplace - return api.testInstall(entryJson, tarballB64) - }, - { entryJson: JSON.stringify(entry), tarballB64: bytes.toString('base64') } - ) - - expect(result.ok).toBe(true) - expect(result.tier).toBe('declarative') - - // Assert the manifest actually landed on disk under the temp HOME. - const installedManifest = join(tmpHome, '.redlog', 'plugins', pluginId, 'plugin.json') - expect(existsSync(installedManifest)).toBe(true) - const parsed = JSON.parse(readFileSync(installedManifest, 'utf-8')) - expect(parsed.id).toBe(pluginId) - expect(parsed.version).toBe('1.0.0') - - // Screenshot the marketplace tab so the deliverable also has visual proof. - // Settings is ⌘9 (dashboard=1..settings=9 order from project-flow.spec.ts). - const mod = process.platform === 'darwin' ? 'Meta' : 'Control' - await page.keyboard.press(`${mod}+9`) - const viewRoot = page.locator('[data-testid="view-root"]') - await expect(viewRoot).toHaveAttribute('data-view', 'settings', { timeout: 2_000 }) - // v0.9.10: Marketplace is a sub-tab under Plugins, not a top-level tab — - // open the parent first. - await openMarketplaceTab(page) - // The Plugins sub-tab is the default landing view — good enough for a - // screenshot; we're not asserting on the DOM here. - mkdirSync(SCREENSHOT_DIR, { recursive: true }) - await page.screenshot({ path: join(SCREENSHOT_DIR, 'marketplace-after-install.png') }) - }) - - test('trusts a publisher via the Publishers tab UI', async () => { - // Publisher key: real SPKI Ed25519 key so the marketplace publisher-trust - // store accepts it (createPublicKey verifies the encoding on read). - const { publicKey } = generateKeyPairSync('ed25519') - const spkiB64 = publicKey.export({ format: 'der', type: 'spki' }).toString('base64') - const publisherId = 'e2e-trusted-pub' - - // Should already be on the marketplace tab after test 1, but re-select - // defensively — tests should be robust to running in isolation. - const mod = process.platform === 'darwin' ? 'Meta' : 'Control' - await page.keyboard.press(`${mod}+9`) - await expect(page.locator('[data-testid="view-root"]')).toHaveAttribute('data-view', 'settings', { timeout: 2_000 }) - // v0.9.10: Marketplace is a sub-tab under Plugins, not a top-level tab — - // open the parent first. - await openMarketplaceTab(page) - - // Sub-tab: Publishers. MarketplacePanel renders the three sub-tabs from - // i18n strings ("Plugins" / "Publishers" / "Revocations"). - await page.getByRole('button', { name: /^publishers$/i }).click() - - // Form: publisher id → key → Trust. Placeholders are the reliable hook — - // there are no data-testids on the inputs. - await page.getByPlaceholder(/publisher id|publisher slug|slug/i).first().fill(publisherId) - await page.getByPlaceholder(/spki|public key|base64/i).first().fill(spkiB64) - // i18n key 'marketplace.trustPublisher' → English label is just "Trust". - // Scope by placeholder-adjacent form so we don't collide with a plausible - // future "Trust" button elsewhere on the tab. - await page.getByRole('button', { name: /^trust$/i }).click() - - // Confirm via the IPC — quicker + less flaky than reading the rendered list. - const publishers = await page.evaluate(async () => { - const api = (window as unknown as { redlog: { marketplace: MarketplaceBridge } }).redlog.marketplace - return api.listPublishers() - }) - const found = publishers.find((p) => p.id === publisherId) - expect(found).toBeTruthy() - expect(found!.keys.some((k) => k.publicKey === spkiB64)).toBe(true) - - await page.screenshot({ path: join(SCREENSHOT_DIR, 'marketplace-publisher-trusted.png') }) - }) - - test('rollback restores the previous version snapshot', async () => { - const pluginId = 'mkt-e2e-rollback' - - // --- v1.0.0 ------------------------------------------------------------ - const v1 = buildDeclarativeTarball(pluginId, '1.0.0') - const entryV1 = { - id: pluginId, - name: 'rollback e2e plugin', - publisher: 'e2e-publisher', - version: '1.0.0', - tarball: 'https://example.invalid/mkt-e2e-rollback-1.0.0.tar.gz', - sha256: v1.sha256 - } - const r1 = await page.evaluate( - async ({ entryJson, tarballB64 }) => { - const api = (window as unknown as { redlog: { marketplace: MarketplaceBridge } }).redlog.marketplace - return api.testInstall(entryJson, tarballB64) - }, - { entryJson: JSON.stringify(entryV1), tarballB64: v1.bytes.toString('base64') } - ) - expect(r1.ok).toBe(true) - - // Drop a marker inside the installed dir so we can prove the swap moved - // the *whole* directory (not just the manifest) into snapshots on next - // install, and that rollback moves it back verbatim. - const pluginDir = join(tmpHome, '.redlog', 'plugins', pluginId) - const markerPath = join(pluginDir, 'VERSION_MARKER') - mkdirSync(dirname(markerPath), { recursive: true }) - writeFileSync(markerPath, 'v1.0.0-marker') - expect(existsSync(markerPath)).toBe(true) - - // --- v1.1.0 ------------------------------------------------------------ - const v2 = buildDeclarativeTarball(pluginId, '1.1.0') - const entryV2 = { - id: pluginId, - name: 'rollback e2e plugin', - publisher: 'e2e-publisher', - version: '1.1.0', - tarball: 'https://example.invalid/mkt-e2e-rollback-1.1.0.tar.gz', - sha256: v2.sha256 - } - const r2 = await page.evaluate( - async ({ entryJson, tarballB64 }) => { - const api = (window as unknown as { redlog: { marketplace: MarketplaceBridge } }).redlog.marketplace - return api.testInstall(entryJson, tarballB64) - }, - { entryJson: JSON.stringify(entryV2), tarballB64: v2.bytes.toString('base64') } - ) - expect(r2.ok).toBe(true) - - // The v1 install (with marker) should have been snapshotted; the fresh - // v1.1.0 directory has no marker. - expect(existsSync(markerPath)).toBe(false) - const newManifest = JSON.parse(readFileSync(join(pluginDir, 'plugin.json'), 'utf-8')) - expect(newManifest.version).toBe('1.1.0') - - // --- listVersions ------------------------------------------------------ - const versions = await page.evaluate(async (id) => { - const api = (window as unknown as { redlog: { marketplace: MarketplaceBridge } }).redlog.marketplace - return api.listVersions(id) - }, pluginId) - expect(versions.length).toBe(1) - const snapshotKey = versions[0] - - // --- rollback ---------------------------------------------------------- - const rr = await page.evaluate( - async ({ id, key }) => { - const api = (window as unknown as { redlog: { marketplace: MarketplaceBridge } }).redlog.marketplace - return api.rollback(id, key) - }, - { id: pluginId, key: snapshotKey } - ) - expect(rr.ok).toBe(true) - - // Marker file must reappear because rollback renames the snapshot back - // over the plugin dir — verifying the whole tree was preserved. - expect(existsSync(markerPath)).toBe(true) - expect(readFileSync(markerPath, 'utf-8')).toBe('v1.0.0-marker') - const restoredManifest = JSON.parse(readFileSync(join(pluginDir, 'plugin.json'), 'utf-8')) - expect(restoredManifest.version).toBe('1.0.0') - }) - - test('publisher auto-fill banner appears + Trust-all lands the key on disk', async () => { - // v0.6.79 flow: fetchIndex() may return a `publishers[]` block; if any of - // those publishers aren't yet in ~/.redlog/trusted-publishers.json the - // panel shows an amber banner with a "Trust all" button. Inject a fake - // index via the dev-only marketplace:testSetIndex IPC (gated on - // REDLOG_E2E=1) so we don't need a live HTTPS registry. - const { publicKey } = generateKeyPairSync('ed25519') - const spkiB64 = publicKey.export({ format: 'der', type: 'spki' }).toString('base64') - const publisherId = 'e2e-banner-pub' - // Not the same id as the "trust via UI" test above so that publisher stays - // in the store from test 2 and this one starts with a fresh untrusted id. - const fakeIndex = { - updatedAt: Date.now(), - entries: [] as Array>, - publishers: [ - { id: publisherId, homepage: 'https://example.invalid', keys: [{ publicKey: spkiB64, label: 'e2e' }] } - ] - } - - const setResult = await page.evaluate(async (indexJson) => { - const api = (window as unknown as { redlog: { marketplace: MarketplaceBridge } }).redlog.marketplace - return api.testSetIndex(indexJson) - }, JSON.stringify(fakeIndex)) - expect(setResult.ok).toBe(true) - - // Ensure we're on Settings ▸ Marketplace ▸ Plugins. - const mod = process.platform === 'darwin' ? 'Meta' : 'Control' - await page.keyboard.press(`${mod}+9`) - await expect(page.locator('[data-testid="view-root"]')).toHaveAttribute('data-view', 'settings', { timeout: 2_000 }) - // v0.9.10: Marketplace is a sub-tab under Plugins, not a top-level tab — - // open the parent first. - await openMarketplaceTab(page) - // Sub-tab "Plugins" — test 2 above left us on Publishers; jump back. - // Two buttons match /Plugins/ (Settings-level tab + Marketplace sub-tab); - // `.last()` picks the sub-tab which is rendered after the top-level tabs. - await page.getByRole('button', { name: /^plugins$/i }).last().click() - - // Trigger fetchIndex — the URL box is empty so it uses the default; main - // short-circuits to the injected index because REDLOG_E2E is set. - // English button label is "Fetch index" per i18n/en.json. - await page.getByRole('button', { name: /fetch index/i }).click() - - // Banner appears with the injected publisher count. English string is - // "This registry suggests trusting {n} publisher(s)". - const banner = page.locator('[data-testid="marketplace-suggested-banner"]') - await expect(banner).toBeVisible({ timeout: 5_000 }) - await expect(banner).toContainText(/trusting 1 publisher/i) - await expect(banner).toContainText(publisherId) - - // v0.11.0: no more one-click "trust everything the registry advertised" — - // that let whoever controlled the index pin their own key and thereafter - // sign privileged plugins that passed every other check. Trust is - // per-publisher now, shown next to the key fingerprint the operator is - // meant to verify out-of-band. - // - // The row must carry that fingerprint, and it must be the same string the - // trust store shows — computed in main from publisher-trust.ts rather than - // reimplemented in the renderer, where it could drift. - await expect(banner).toContainText(/[0-9a-f]{2}(:[0-9a-f]{2}){15}/) - await page.locator(`[data-testid="marketplace-trust-${publisherId}"]`).click() - - // Store on disk carries the injected id + key. reloadPublishers refreshes - // React state from the IPC, which reads the store — so once the banner - // is gone we know the file must have been written. - await expect(banner).toHaveCount(0, { timeout: 5_000 }) - - const storePath = join(tmpHome, '.redlog', 'trusted-publishers.json') - expect(existsSync(storePath)).toBe(true) - const store = JSON.parse(readFileSync(storePath, 'utf-8')) as Record< - string, - { id: string; keys: Array<{ publicKey: string }> } - > - expect(store[publisherId]).toBeTruthy() - expect(store[publisherId].keys.some((k) => k.publicKey === spkiB64)).toBe(true) - - mkdirSync(SCREENSHOT_DIR, { recursive: true }) - await page.screenshot({ path: join(SCREENSHOT_DIR, 'marketplace-publishers-trusted.png') }) - - // Clear the injected index so nothing leaks into later specs if the - // Electron instance somehow gets reused. - await page.evaluate(async () => { - const api = (window as unknown as { redlog: { marketplace: MarketplaceBridge } }).redlog.marketplace - await api.testSetIndex('') - }) - }) -}) diff --git a/examples/registry/.gitignore b/examples/registry/.gitignore deleted file mode 100644 index c914a5a4..00000000 --- a/examples/registry/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Private signing key — never commit -redlog-example-keypair.json - -# Intermediate signed entries (merged into index.json) -*.entry.json diff --git a/examples/registry/README.md b/examples/registry/README.md deleted file mode 100644 index a83a8380..00000000 --- a/examples/registry/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# RedLog example plugin registry - -An example marketplace index the operator can point RedLog at until (or -instead of) a "real" `plugins.redlog.dev` DNS-backed service exists. Serves as: - -- a smoke-testable index that Settings ▸ 外掛市集 can actually fetch, -- a reference layout for anyone building their own private registry. - -## Contents - -- `index.json` — the manifest RedLog fetches. Lists every published plugin with - its download URL, sha256, Ed25519 signature, and publisher metadata. -- `-.tar.gz` — the plugin bundles. Made with `tar -czf .tar.gz `. -- `redlog-example-keypair.json` (git-ignored) — private key used to sign the - bundles in this example. **Not committed.** If you clone this repo and want to - add a plugin, generate your own key: - ```bash - npx redlog-sign keygen --out my-keypair.json - ``` - -## Pointing RedLog at this registry - -In Settings ▸ 外掛市集, replace the default URL with: - -``` -https://raw.githubusercontent.com/guan4tou2/REDLOG/main/examples/registry/index.json -``` - -Fetch → three entries appear (`recon-pack`, `aider-hook`, `codex-hook`). All -three are 🟢 declarative — no privileged capabilities, no signature-required -gate. Install works even before you trust the `redlog-project` publisher; the -UI just shows a "publisher untrusted" warning. - -To silence the warning, go to the Publishers sub-tab, paste in the public key -from `index.json` (the `publishers[0].keys[0].publicKey` field), and click Trust. - -## Adding a plugin - -1. Drop the plugin dir at the repo root of your registry (parallel to the - existing ones). -2. `tar -czf my-plugin-1.0.0.tar.gz my-plugin` -3. Sign it: - ```bash - npx redlog-sign sign my-plugin-1.0.0.tar.gz \ - --key my-keypair.json \ - --id my-plugin --version 1.0.0 \ - --publisher my-org \ - --url https:///my-plugin-1.0.0.tar.gz - ``` -4. Merge the printed JSON entry into `index.json`'s `entries` array. - -## When you're ready for real DNS - -- Set up your own hosting (GitHub Pages, S3, a static Worker, whatever). -- CNAME `plugins.redlog.dev` (or a subdomain of your own) to that hosting. -- Point RedLog's default marketplace URL at it via - `src/renderer/src/components/Settings.tsx` `MarketplacePanel` placeholder or - ship a build with a config default. - -Until then, this example lives under GitHub raw and requires no infra beyond -this repo. diff --git a/examples/registry/codex-hook-0.1.0.tar.gz b/examples/registry/codex-hook-0.1.0.tar.gz deleted file mode 100644 index 29c815c8638ea447711b760ff858bcbb7e002cc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5267 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 diff --git a/examples/registry/index.json b/examples/registry/index.json deleted file mode 100644 index e6fcd149..00000000 --- a/examples/registry/index.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "updatedAt": 1754074800000, - "publishers": [ - { - "id": "redlog-project", - "homepage": "https://github.com/guan4tou2/REDLOG", - "keys": [ - { - "label": "redlog example registry key", - "publicKey": "MCowBQYDK2VwAyEAgFExXpbIae5uqR6OPtbNrSf9Lw1AMs5uMga9Hb+L0ZU=" - } - ] - } - ], - "entries": [ - { - "id": "recon-pack", - "name": "recon-pack", - "publisher": "redlog-project", - "version": "0.1.0", - "tarball": "https://raw.githubusercontent.com/guan4tou2/REDLOG/main/examples/registry/recon-pack-0.1.0.tar.gz", - "sha256": "623d5ddc5d1f61a191b42f41d62f61f673d1a1282aed81589de427abf8f6bac7", - "signature": "qgujT/HbsIFc+Ajt86A4czgh5mU3/e5CbHGPYWm43nglIBLpB5kz3iDzMXtffcH1wE504725B5kE/f1e0G0SAA==", - "sizeKb": 2 - }, - { - "id": "codex-hook", - "name": "codex-hook", - "publisher": "redlog-project", - "version": "0.1.0", - "tarball": "https://raw.githubusercontent.com/guan4tou2/REDLOG/main/examples/registry/codex-hook-0.1.0.tar.gz", - "sha256": "ec86cbc4be757fe0d40bfefd6cf6919a48e12fe61c3d1dfa79a2e6b48bf9649c", - "signature": "vpgDd6ZhIvgdpQ+MzzIAfiDuGoy1mK7FR4/eGzwDCG2cuu/7SAB9zb9DFQYxvkjNc/IVfelulp/0mN3iS4VaDw==", - "sizeKb": 5 - }, - { - "id": "opencode-hook", - "name": "opencode-hook", - "publisher": "redlog-project", - "version": "0.1.0", - "tarball": "https://raw.githubusercontent.com/guan4tou2/REDLOG/main/examples/registry/opencode-hook-0.1.0.tar.gz", - "sha256": "0428a27c18c0cb5cb2d0202c04e3fa49fa91b47057a05e29b0ad524e9284244c", - "signature": "ma8jqEASz/EJbpLl5vBUuPi5/MTagx56QCm9LA9V3I7OUrJ22nPLsBDWXWGQ671md2KjbIN/ZWMObWKArONHDg==", - "sizeKb": 5 - } - ] -} \ No newline at end of file diff --git a/examples/registry/opencode-hook-0.1.0.tar.gz b/examples/registry/opencode-hook-0.1.0.tar.gz deleted file mode 100644 index 0e6c3e200f644bb905ec73acd20dba237be500db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4777 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 DadaDA diff --git a/examples/registry/recon-pack-0.1.0.tar.gz b/examples/registry/recon-pack-0.1.0.tar.gz deleted file mode 100644 index 371ce352dc9c18a7afb85f7b9bdadd078a544c85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1753 zcmV;~1}6C*iwFRfy=`g$1MM2iZsf*sY#=bqoRf2fE37nF@i{L`*)^h7v{}VkOBo5V z*BWKeB3m=uC7X12b5^o;4CEhj$SsEexg}@+fPckjUwv~m$$3h$ys$KORxE%Zr@FeH zUCmCK2URG`iY};-%8j#(a`Q3Kw`8|KGfzxnnuG@6ndJ}A?ZnxYD*tez7 zG^9cjj;i!h!epFYNrz-7CqKX^HwDJ`{}=exZ{MrDTd6!F9_;SJr%-^xRo=(T#p`pt z)bD@)IF=g+2kqif&G;u?zn&v`XM+EY7e^*Zl8~ASj|Y?y=F!SK?^d3C{r9i_^UeRh zy8$ChxC-{j=~GI4%B|bm2d>S3$DYgoM$@$`aC*~roDifFGS34 zQU>~?S+D=d`md(q->lW@W&O7hDC@sF4O96)DFttMAGkLE?bdAmyGU5pe+vP3ZLa=v zUDI)#R_lZ2T3P4 zZH0%uF@8X~2x6v&9Md-K9P3lz@gR}K$kr)|FyFzaUPw5R!GJ=(z}DfIiX?tXA&g@Q z32qRSGqDN;FcLi~fu2fEJQ;I1!Ha;0F^VG)9$}1fMaZd-1Skr48dAhCAF+(S0=1Wg z0i!^1n~@;T5^6#_Wdc|XUhGo{mV8rs-=wq+b{7@yh3+R?ew9I9~?bAra8{H4lVsCBP~?d^L4Q`S{8H8@}g|{{!KZ{P>ZC~5lX#c z8L2VD$XVdXgOyHa|3P(L&uqCIpnK++W8`!EBpfI~u@A{H4M&lH{V|qhq{U**-4y0) z>t4&N5$Y>k;N@xZ?Z5u<-+5I$lE{?PX+5-2Sze@SW~vtk`s!q4l&+aNn>*XnV{^&5 znM=TYh$39-0<(yL?DIJ7_3=(R9d~GW;CcI`mBKkB8r$;&cJPmm6#fh3nQxyF; ze)%K_^A(EE!pg`BNv7yev%_SSBn+QX%+Jhn)HfwF5_8PHq{(#Xitvqt^a-+`HBH3D z9sq^I1yJJkaeo-Z9+5@H6)sXNI{NuY_hCl|!$8}UQSh<=7EeVuHT5+7g_kxn-~U-S zZeGTl_kS9$(vp9 zRw}r4959Qr0UQ(2*Y3fndsCc@f?66`Ge(`DPL@p3(J^s`I$sq)#=V`*oo#i(N(Ypm zO&zI1AhP+~r#{Hh3GA{M_iYrf;#f9j;aNc};8j0BeX;}GBvD-AgfdtOBvKF@|I#w^ zu8T#Iz(`^)UrW@3RZUH6Z~bv+_fOru-S$EE$@b3HBYo-V?z626GxQPV!R|*}A74qD z@zpi0o8!8IGJmC-9nv2jf^h=+(!4|+?%$sonMly#=m=iFMl;aU3~b~QqO(F!8bKxt zQR>H|rIm-5u&(|-rnc=XkO$sU zZRO<=Hy=H*lAIwwRs#cn{6U9@5103zFKa=R8Lr4zL7WLK;Q^B?>bL2qQ6g5(^AJ=;((M|cigTy=pqnq^fUTeilknc)&{;pK4u{pMw)b4C>hPiE(}Bg( vFa-C*`;H2cZh72_=+J!Zq+ckyA2I05$*sH71Zx diff --git a/package.json b/package.json index 9424cf0d..b8377a1c 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,7 @@ "license": "MIT", "main": "./out/main/index.js", "bin": { - "redlog-cli": "./cli/redlog-cli.js", - "redlog-sign": "./cli/redlog-sign.js" + "redlog-cli": "./cli/redlog-cli.js" }, "scripts": { "predev": "electron-rebuild", diff --git a/src/core/config.ts b/src/core/config.ts index 49a4ca22..8b4eee9b 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -148,13 +148,6 @@ export interface RedLogConfig { * client-side value only helps if the backend also allows it. */ maxBundleBytes?: number } - /** Plugin marketplace overrides. Empty defaults ship the built-in - * placeholder (GitHub raw of the example registry). Air-gapped shops - * point this at their internal registry mirror. */ - marketplace: { - /** Registry URL the Settings placeholder + one-click fetch use. */ - defaultRegistryUrl: string - } /** v0.6.92 W-project: file-watcher (chokidar). Opt-in — file activity is * noisy without a well-scoped watchPaths list. Emits `file_transfer` * events with subtype `file_created/modified/deleted`. */ @@ -290,11 +283,6 @@ const DEFAULT_CONFIG: RedLogConfig = { endpoint: '', authToken: '' }, - marketplace: { - // Default: the example registry hosted from this repo on GitHub raw. - // Deployers running a private registry override this in config.yaml. - defaultRegistryUrl: 'https://raw.githubusercontent.com/guan4tou2/REDLOG/main/examples/registry/index.json' - }, fileWatcher: { enabled: false, watchPaths: [], diff --git a/src/core/paths.ts b/src/core/paths.ts index 79af2c05..d80c83a9 100644 --- a/src/core/paths.ts +++ b/src/core/paths.ts @@ -8,8 +8,8 @@ import path from 'path' * are caught too, and case-folds both sides on Windows only. * * The `screenshot:read` / `screenshot:deleteFile` IPC guards, plugin - * marketplace extract, and any other "resolve then check inside root" call - * should route through this helper rather than reinventing it. Audit P1-2 + * extraction, and any other "resolve then check inside root" call should + * route through this helper rather than reinventing it. Audit P1-2 * (docs/WINDOWS_COMPAT_AUDIT.md). */ export function isInsideDir(root: string, target: string): boolean { diff --git a/src/core/plugins/marketplace.ts b/src/core/plugins/marketplace.ts deleted file mode 100644 index 4a507e04..00000000 --- a/src/core/plugins/marketplace.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { createHash } from 'crypto' -import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, renameSync, readdirSync, statSync } from 'fs' -import { join, resolve, sep } from 'path' -import { homedir } from 'os' -import { get as httpsGet } from 'https' -import { spawnSync } from 'child_process' -import { verifySignature, type Publisher } from './publisher-trust' -import { validateManifest, tierOf, computeContentHash } from './manifest' - -// Registry client for the plugin marketplace. Fetches an index.json listing -// all published plugins, downloads a signed tarball for a chosen plugin, -// verifies content-hash + publisher signature + revocation list, extracts to -// ~/.redlog/plugins//, keeping the previous version snapshotted under -// versions// for rollback. -// -// Everything network-facing here is HTTP GET only — no auth, no POST — so the -// blast radius of a compromised registry is bounded to serving a bad plugin, -// which the trust gate still catches on install. - -// v0.11.0: no default registry. `plugins.redlog.dev` was never registered, so -// shipping it as the default meant every install pointed at a domain anyone -// could claim — and the index it serves names the publisher keys the operator -// is invited to pin. A registry is a supply chain; the operator has to choose -// it deliberately, and there is no honest default to choose for them. -const DEFAULT_REGISTRY_URL = '' -const PLUGINS_ROOT = () => join(homedir(), '.redlog', 'plugins') -const REVOCATIONS_PATH = () => join(homedir(), '.redlog', 'plugins', 'revocations.json') - -/** Max bytes we'll download for a plugin tarball. Matches the spec cap. */ -const MAX_TARBALL_BYTES = 5 * 1024 * 1024 -/** Registry index is smaller — 1 MB is generous. */ -const MAX_INDEX_BYTES = 1024 * 1024 - -export interface RegistryEntry { - id: string - name: string - description?: string - homepage?: string - publisher: string - /** semver — latest version listed */ - version: string - /** absolute URL to a .tar.gz */ - tarball: string - /** hex sha256 of the tarball bytes */ - sha256: string - /** base64 Ed25519 signature over `sha256:` message */ - signature?: string - /** Kilobytes; UI hint before download */ - sizeKb?: number - /** categories/tags for filtering */ - tags?: string[] -} - -/** A publisher block the registry advertises so the UI can offer a one-click - * "trust this publisher" flow, saving operators from pasting a base64 SPKI - * key by hand. The operator still has to confirm the trust prompt — this - * just fills in the key. */ -export interface RegistryPublisherAd { - id: string - homepage?: string - keys: Array<{ label?: string; publicKey: string }> -} - -export interface RegistryIndex { - updatedAt: number - entries: RegistryEntry[] - /** Optional — v0.6.77+ registries include this so operators don't have to - * hand-paste publisher keys. Absent means the UI falls back to the manual - * publishers form. */ - publishers?: RegistryPublisherAd[] -} - -interface RevocationList { - updatedAt: number - /** plugin ids blocked from further installs; existing installs get warned. */ - plugins?: string[] - /** publisher ids fully distrusted */ - publishers?: string[] -} - -// ---- Network helpers ------------------------------------------------------ - -function httpsGetBuffer(url: string, maxBytes: number): Promise { - return new Promise((resolveP, reject) => { - const req = httpsGet(url, (res) => { - if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - // Single redirect hop — no infinite loops. - httpsGetBuffer(res.headers.location, maxBytes).then(resolveP, reject) - res.resume() - return - } - if (res.statusCode !== 200) { - reject(new Error(`GET ${url} → HTTP ${res.statusCode}`)) - res.resume() - return - } - const chunks: Buffer[] = [] - let bytes = 0 - res.on('data', (chunk: Buffer) => { - bytes += chunk.length - if (bytes > maxBytes) { - req.destroy() - reject(new Error(`response exceeds ${maxBytes} bytes`)) - return - } - chunks.push(chunk) - }) - res.on('end', () => resolveP(Buffer.concat(chunks))) - res.on('error', reject) - }) - req.on('error', reject) - req.setTimeout(20_000, () => { - req.destroy(new Error('registry request timeout')) - }) - }) -} - -// ---- Index fetching ------------------------------------------------------- - -export async function fetchIndex(url = DEFAULT_REGISTRY_URL): Promise { - if (!url) throw new Error('no registry URL configured — set one in Settings ▸ Plugins ▸ Marketplace') - const raw = await httpsGetBuffer(url, MAX_INDEX_BYTES) - // The index is UNTRUSTED input. RedLog cannot verify it: there is no root - // key to check it against, and TLS only proves the bytes came from whoever - // holds the domain — which is exactly who an attacker would need to be. - // Everything here is therefore treated as a hint about WHERE to look, never - // as evidence that anything is safe. The trust boundary is one step later: - // installFromRegistry verifies each tarball's Ed25519 signature against a - // key the OPERATOR has pinned, and refuses a privileged plugin without one. - // - // docs/PLUGIN_MARKETPLACE.md used to claim unsigned index mutations were - // rejected. They never were, and the claim is gone. - const parsed = JSON.parse(raw.toString('utf-8')) - if (typeof parsed !== 'object' || parsed === null) throw new Error('index is not an object') - if (!Array.isArray(parsed.entries)) throw new Error('index.entries missing') - for (const e of parsed.entries) { - if (typeof e.id !== 'string' || typeof e.tarball !== 'string' || typeof e.sha256 !== 'string') { - throw new Error('index entry missing required fields (id/tarball/sha256)') - } - // sha256 must be hex, 64 chars — reject anything else so we don't silently - // compare against a Base64 hash later and get a false match. - if (!/^[a-f0-9]{64}$/i.test(e.sha256)) throw new Error(`invalid sha256 for ${e.id}`) - } - // Publishers block is optional. Validate lightly if present — we don't want - // a hostile registry stuffing garbage keys into the trust prompt. - let publishers: RegistryPublisherAd[] | undefined - if (Array.isArray(parsed.publishers)) { - publishers = [] - for (const p of parsed.publishers) { - if (typeof p?.id !== 'string' || !Array.isArray(p.keys)) continue - const keys: Array<{ label?: string; publicKey: string }> = [] - for (const k of p.keys) { - if (typeof k?.publicKey === 'string' && k.publicKey.length >= 32) { - keys.push({ publicKey: k.publicKey, label: typeof k.label === 'string' ? k.label : undefined }) - } - } - if (keys.length) publishers.push({ id: p.id, homepage: typeof p.homepage === 'string' ? p.homepage : undefined, keys }) - } - } - return { updatedAt: Number(parsed.updatedAt) || Date.now(), entries: parsed.entries as RegistryEntry[], publishers } -} - -// ---- Revocations ---------------------------------------------------------- - -export function loadRevocations(): RevocationList { - const p = REVOCATIONS_PATH() - if (!existsSync(p)) return { updatedAt: 0 } - try { - const parsed = JSON.parse(readFileSync(p, 'utf-8')) as RevocationList - return parsed && typeof parsed === 'object' ? parsed : { updatedAt: 0 } - } catch { - return { updatedAt: 0 } - } -} - -export function isRevoked(entry: RegistryEntry): { revoked: boolean; reason?: string } { - const rev = loadRevocations() - if (rev.plugins?.includes(entry.id)) return { revoked: true, reason: 'plugin revoked' } - if (rev.publishers?.includes(entry.publisher)) return { revoked: true, reason: `publisher ${entry.publisher} revoked` } - return { revoked: false } -} - -// ---- Install flow --------------------------------------------------------- - -export interface InstallResult { - ok: boolean - pluginId: string - contentHash?: string - installedDir?: string - rolledBackFrom?: string - error?: string - tier?: 'declarative' | 'privileged' - signatureVerified?: boolean -} - -/** - * Download and install `entry`. Verifies: - * 1. revocation list — abort if plugin or publisher is revoked - * 2. tarball sha256 == entry.sha256 - * 3. entry.signature verifies against a pinned publisher key (REQUIRED for - * privileged plugins — tier is determined AFTER install by inspecting the - * manifest, so we check tier post-hoc and reject/rollback if the install - * produced a privileged plugin without a valid signature) - * 4. manifest passes validateManifest() - * - * Prior version (if any) is snapshotted under versions// before - * the swap. - */ -export async function installFromRegistry( - entry: RegistryEntry, - opts: { - extractTar?: (tarball: Buffer, destDir: string) => Promise - /** Overrides the default HTTPS fetcher — useful for tests and for future - * mirror/proxy support. Given the entry's tarball URL, must resolve to - * the raw bytes. */ - fetchTarball?: (url: string) => Promise - } = {} -): Promise { - const revocation = isRevoked(entry) - if (revocation.revoked) return { ok: false, pluginId: entry.id, error: revocation.reason } - - let tarball: Buffer - try { - const fetcher = opts.fetchTarball ?? ((url: string) => httpsGetBuffer(url, MAX_TARBALL_BYTES)) - tarball = await fetcher(entry.tarball) - } catch (e) { - return { ok: false, pluginId: entry.id, error: (e as Error).message } - } - - const actualHash = createHash('sha256').update(tarball).digest('hex') - if (actualHash.toLowerCase() !== entry.sha256.toLowerCase()) { - return { ok: false, pluginId: entry.id, error: `sha256 mismatch: expected ${entry.sha256}, got ${actualHash}` } - } - - // Signature is optional at fetch time — if the plugin turns out to be - // privileged, we require it below. Verify eagerly when supplied so we can - // record the auditing outcome regardless of tier. - let signatureVerified = false - if (entry.signature) { - const v = verifySignature(entry.publisher, `sha256:${actualHash}`, entry.signature) - if (!v.ok) { - return { ok: false, pluginId: entry.id, error: `signature verify failed: ${v.reason}` } - } - signatureVerified = true - } - - // Extract to a scratch dir first, validate the manifest, THEN swap into place - // so a bad tarball can never partially clobber an existing install. - const pluginsRoot = PLUGINS_ROOT() - mkdirSync(pluginsRoot, { recursive: true }) - const finalDir = join(pluginsRoot, entry.id) - const scratchDir = join(pluginsRoot, `.installing-${entry.id}-${Date.now()}`) - mkdirSync(scratchDir, { recursive: true }) - - try { - const extract = opts.extractTar ?? defaultExtractTar - // v0.6.93 P0-C: pre-flight the tar entry list BEFORE extraction — the - // post-extract walk in assertInsideDir only sees files that landed - // inside scratchDir; a `../../.ssh/authorized_keys` entry writes ELSEWHERE - // and would slip past. Only run for the default extractor (tests use - // in-memory fixtures that aren't real tarballs). - if (!opts.extractTar) assertNoTarEscape(tarball, scratchDir) - await extract(tarball, scratchDir) - - // Zip Slip / symlink defense — refuse anything that escaped the plugin dir - // during extraction. - assertInsideDir(scratchDir, scratchDir) - - const manifestPath = join(scratchDir, 'plugin.json') - if (!existsSync(manifestPath)) throw new Error('plugin.json missing after extract') - const rawManifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) - const parsed = validateManifest(rawManifest, scratchDir) - if (!parsed.ok || !parsed.manifest) throw new Error(`manifest invalid: ${parsed.error}`) - - if (parsed.manifest.id !== entry.id) { - throw new Error(`id mismatch: registry says ${entry.id}, manifest says ${parsed.manifest.id}`) - } - if (parsed.manifest.version !== entry.version) { - throw new Error(`version mismatch: registry says ${entry.version}, manifest says ${parsed.manifest.version}`) - } - if (parsed.manifest.publisher && parsed.manifest.publisher !== entry.publisher) { - throw new Error(`publisher mismatch: registry says ${entry.publisher}, manifest says ${parsed.manifest.publisher}`) - } - - const tier = tierOf(parsed.manifest) - if (tier === 'privileged' && !signatureVerified) { - throw new Error('privileged plugin without a verified publisher signature') - } - - const contentHash = computeContentHash(parsed.manifest, scratchDir) - - // Snapshot prior version for rollback. - let rolledBackFrom: string | undefined - if (existsSync(finalDir)) { - const prev = existsSync(join(finalDir, 'plugin.json')) - ? readFileSync(join(finalDir, 'plugin.json'), 'utf-8') - : '' - let prevHash = '' - try { prevHash = JSON.parse(prev).__contentHash || Date.now().toString(16) } catch { prevHash = Date.now().toString(16) } - const versionsDir = join(finalDir, '..', `.${entry.id}-versions`) - mkdirSync(versionsDir, { recursive: true }) - const snapshotDir = join(versionsDir, prevHash) - if (!existsSync(snapshotDir)) { - renameSync(finalDir, snapshotDir) - } else { - // Rare — same content hash already snapshotted. Just delete the current. - rmSync(finalDir, { recursive: true, force: true }) - } - rolledBackFrom = snapshotDir - } - renameSync(scratchDir, finalDir) - - return { ok: true, pluginId: entry.id, contentHash, installedDir: finalDir, rolledBackFrom, tier, signatureVerified } - } catch (e) { - rmSync(scratchDir, { recursive: true, force: true }) - return { ok: false, pluginId: entry.id, error: (e as Error).message, signatureVerified } - } -} - -// ---- Rollback ------------------------------------------------------------- - -export function listVersions(pluginId: string): string[] { - const versionsDir = join(PLUGINS_ROOT(), `.${pluginId}-versions`) - if (!existsSync(versionsDir)) return [] - return readdirSync(versionsDir).filter((n) => { - try { return statSync(join(versionsDir, n)).isDirectory() } catch { return false } - }) -} - -export function rollback(pluginId: string, versionKey: string): { ok: boolean; error?: string } { - const versionsDir = join(PLUGINS_ROOT(), `.${pluginId}-versions`) - const snapshot = join(versionsDir, versionKey) - if (!existsSync(snapshot)) return { ok: false, error: 'snapshot not found' } - const finalDir = join(PLUGINS_ROOT(), pluginId) - try { - if (existsSync(finalDir)) { - const scratchKey = `pre-rollback-${Date.now().toString(16)}` - renameSync(finalDir, join(versionsDir, scratchKey)) - } - renameSync(snapshot, finalDir) - return { ok: true } - } catch (e) { - return { ok: false, error: (e as Error).message } - } -} - -// ---- Utilities ------------------------------------------------------------ - -function assertInsideDir(root: string, current: string): void { - // v0.6.93 P0-C: use `resolve(root) + sep` for the prefix compare — bare - // `startsWith` treats `/foo` as a prefix of `/foobar`. Also this walks - // INSIDE root, so a tar entry that escaped to a sibling directory - // (`../hostile`) is never enumerated. `assertNoTarEscape` below runs - // BEFORE extraction to catch that class. - const rootReal = resolve(root) - const currentReal = resolve(current) - const prefix = rootReal.endsWith(sep) ? rootReal : rootReal + sep - if (currentReal !== rootReal && !currentReal.startsWith(prefix)) { - throw new Error(`path escape: ${currentReal} not inside ${rootReal}`) - } - for (const entry of readdirSync(currentReal, { withFileTypes: true })) { - if (entry.isSymbolicLink()) throw new Error(`symlink not allowed: ${entry.name}`) - if (entry.isDirectory()) assertInsideDir(rootReal, join(currentReal, entry.name)) - } -} - -// v0.6.93 P0-C: enumerate tar entries BEFORE extraction and reject any -// absolute path or `..` segment. The post-extract `assertInsideDir` only -// walks INSIDE the scratch dir, so a hostile tarball that clobbers -// `../../.ssh/authorized_keys` never gets checked otherwise. -function assertNoTarEscape(tarball: Buffer, destDir: string): void { - const tmpTar = join(destDir, '..', `.plugin-list-${Date.now()}.tar.gz`) - writeFileSync(tmpTar, tarball) - const tarBin = process.platform === 'win32' ? 'tar.exe' : 'tar' - try { - const res = spawnSync(tarBin, ['-tzf', tmpTar], { - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 30_000 - }) - if (res.status !== 0) { - throw new Error(`${tarBin} -tzf exited ${res.status}: ${res.stderr?.toString() ?? ''}`) - } - const listing = (res.stdout?.toString() ?? '').split(/\r?\n/).filter(Boolean) - for (const entry of listing) { - // Trim leading `./` so a well-formed relative tar entry passes. - const normalized = entry.replace(/^\.\/+/, '') - // Absolute paths (Unix `/etc/…`, Windows `C:\…`) never allowed. - if (normalized.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(normalized)) { - throw new Error(`tar entry has absolute path: ${entry}`) - } - // Any `..` component escapes even under `--strip-components`. - if (normalized.split(/[\\/]/).some((seg) => seg === '..')) { - throw new Error(`tar entry escapes via ..: ${entry}`) - } - } - } finally { - try { rmSync(tmpTar, { force: true }) } catch { /* ignore */ } - } -} - -/** - * Default tar extractor. Shells out to the system `tar` — every platform we - * ship on has it (macOS bsdtar, Linux GNU tar, Windows 10+ bsdtar). Callers - * can override via opts.extractTar for tests. - */ -async function defaultExtractTar(tarball: Buffer, destDir: string): Promise { - // Write to a temp file — piping large buffers to tar via stdin is fine on - // POSIX but flaky on Windows tar-via-cmd wrappers. - const tmpTar = join(destDir, '..', `.plugin-${Date.now()}.tar.gz`) - writeFileSync(tmpTar, tarball) - // Explicit `.exe` on Windows — bare `tar` relies on PATHEXT which is - // fragile inside a packaged Electron shim. Mirrors cloud-share.ts. - // Audit P1-4 (docs/WINDOWS_COMPAT_AUDIT.md). - const tarBin = process.platform === 'win32' ? 'tar.exe' : 'tar' - try { - const res = spawnSync(tarBin, ['-xzf', tmpTar, '-C', destDir, '--strip-components=1'], { - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 30_000 - }) - if (res.status !== 0) { - throw new Error(`${tarBin} exited ${res.status}: ${res.stderr?.toString() ?? ''}`) - } - } finally { - try { rmSync(tmpTar, { force: true }) } catch { /* ignore */ } - } -} - -// Re-export a subset of publisher-trust to give callers one import surface. -export type { Publisher } from './publisher-trust' -export { listPublishers, trustPublisher, untrustPublisher, fingerprint, getPublisher } from './publisher-trust' diff --git a/src/core/plugins/publisher-trust.ts b/src/core/plugins/publisher-trust.ts deleted file mode 100644 index e193b5d7..00000000 --- a/src/core/plugins/publisher-trust.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs' -import { join } from 'path' -import { homedir } from 'os' -import { createHash, createPublicKey, verify as cryptoVerify } from 'crypto' - -// Publisher trust store. Separate from ./trust.ts (which pins per-plugin -// content hashes for the run-time gate) — this one records which registry -// publishers the operator is willing to install plugins FROM. Each publisher -// has an Ed25519 public key; every plugin tarball is expected to carry a -// detached signature over its content hash, signed by one of the publisher's -// pinned keys. -// -// Marketplace flow: -// 1. Operator picks a plugin from the registry index. -// 2. If publisher isn't trusted yet, show "Trust ? Their plugins -// will be signed with key ." → operator confirms → grant(). -// 3. Download tarball. Verify sha256 against index, then verify signature -// against the pinned publisher key. Any mismatch aborts install. -// -// 🟢 declarative plugins can install from any publisher (unsigned OK for now, -// but we still record where they came from). 🔴 privileged plugins MUST come -// from a trusted publisher and MUST carry a valid signature. - -export interface PublisherKey { - /** short human label ("gutou main", "shop A signing"); UI-only */ - label?: string - /** SPKI-encoded Ed25519 public key, base64 */ - publicKey: string - /** when the operator first trusted this key, ms epoch */ - trustedAt: number - /** operator id that added it */ - trustedBy?: string -} - -export interface Publisher { - /** publisher slug — matches `manifest.publisher` in plugin.json */ - id: string - /** homepage / contact — surfaced in the UI, never used for auth */ - homepage?: string - /** every key the operator has pinned for this publisher */ - keys: PublisherKey[] -} - -type PublisherStore = Record - -function storePath(): string { - return join(homedir(), '.redlog', 'trusted-publishers.json') -} - -function read(): PublisherStore { - const p = storePath() - if (!existsSync(p)) return {} - try { - const parsed = JSON.parse(readFileSync(p, 'utf-8')) - return parsed && typeof parsed === 'object' ? parsed : {} - } catch { - return {} - } -} - -function write(store: PublisherStore): void { - const p = storePath() - mkdirSync(join(homedir(), '.redlog'), { recursive: true }) - writeFileSync(p, JSON.stringify(store, null, 2)) -} - -export function listPublishers(): Publisher[] { - return Object.values(read()) -} - -export function getPublisher(id: string): Publisher | null { - return read()[id] ?? null -} - -export function trustPublisher(id: string, key: PublisherKey, homepage?: string): void { - const store = read() - const existing = store[id] - if (existing) { - // Additional key for a publisher we already trust — key rotation flow. - if (!existing.keys.some((k) => k.publicKey === key.publicKey)) existing.keys.push(key) - if (homepage && !existing.homepage) existing.homepage = homepage - } else { - store[id] = { id, homepage, keys: [key] } - } - write(store) -} - -export function untrustPublisher(id: string): void { - const store = read() - if (store[id]) { - delete store[id] - write(store) - } -} - -/** SHA-256 fingerprint of an SPKI-encoded Ed25519 public key, as `xx:xx:...`. */ -export function fingerprint(publicKeyB64: string): string { - const raw = Buffer.from(publicKeyB64, 'base64') - const hash = createHash('sha256').update(raw).digest('hex') - return hash.match(/.{2}/g)!.slice(0, 16).join(':') -} - -/** - * Verify a detached Ed25519 signature over `message` using ANY of the pinned - * keys for `publisherId`. Returns { ok, keyLabel } — keyLabel identifies which - * key signed, useful for audit-logging the trust decision. - * - * Publisher unknown → always fails (no implicit trust). - */ -export function verifySignature( - publisherId: string, - message: Buffer | string, - signatureB64: string -): { ok: true; keyLabel?: string } | { ok: false; reason: string } { - const pub = read()[publisherId] - if (!pub) return { ok: false, reason: `publisher not trusted: ${publisherId}` } - const msg = typeof message === 'string' ? Buffer.from(message, 'utf-8') : message - const sig = Buffer.from(signatureB64, 'base64') - for (const k of pub.keys) { - try { - const pubKey = createPublicKey({ - key: Buffer.from(k.publicKey, 'base64'), - format: 'der', - type: 'spki' - }) - // Ed25519 uses `null` digest per Node's API. - if (cryptoVerify(null, msg, pubKey, sig)) { - return { ok: true, keyLabel: k.label } - } - } catch { - // Malformed key — skip and try the next one. - } - } - return { ok: false, reason: 'no pinned key matched the signature' } -} diff --git a/src/main/index.ts b/src/main/index.ts index 054183ae..80b098e3 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -82,11 +82,6 @@ let activeProject: ProjectMeta | null = null let currentEngagementId: string | null = null let currentOperatorId: string | null = null let forceQuit = false -// Dev/E2E-only stash for marketplace:fetchIndex. When REDLOG_E2E=1 and a -// test has called marketplace:testSetIndex, fetchIndex returns this object -// instead of hitting the network. Loose-typed so we don't pull the -// RegistryIndex import into main just for a mock hook. -let testFetchIndexOverride: unknown = null let overlayMouseInside = false let overlayTrackingInterval: ReturnType | null = null // v0.6.89 P1-A: read-path chain sampling. Runs periodically while a project @@ -1827,117 +1822,6 @@ app.whenReady().then(() => { } }) - // --- Plugin marketplace --- - // These wrap src/core/plugins/marketplace.ts + publisher-trust.ts. The whole - // fetch/verify/install pipeline lives in core so it stays unit-testable; - // main just exposes it over IPC. The registry client uses HTTPS only, hard - // caps size (5 MB tarball / 1 MB index), and every install goes through - // sha256 + Ed25519 signature verify + manifest re-validate. - // v0.11.0: stamp each advertised key with the same fingerprint the trust - // store shows. The operator is meant to compare it against the publisher's - // own channel before pinning, so it has to be the identical string in both - // places — computed here rather than in the renderer, where it would be a - // second implementation free to drift from publisher-trust.ts. - const annotateIndex = async (index: unknown): Promise => { - const idx = index as { publishers?: Array<{ keys?: Array<{ publicKey: string }> }> } | null - if (!idx?.publishers) return index - const { fingerprint } = await import('../core/plugins/publisher-trust') - for (const p of idx.publishers) { - for (const k of p.keys ?? []) { - try { (k as { fingerprint?: string }).fingerprint = fingerprint(k.publicKey) } catch { /* malformed key */ } - } - } - return idx - } - ipcMain.handle('marketplace:fetchIndex', async (_e, url?: string) => { - // Dev/E2E-only override: if a test has stashed a fake index via - // marketplace:testSetIndex, return it verbatim instead of hitting the - // network. Kept alongside the real path so the UI code driving fetchIndex - // stays identical between real and mocked runs. - if (process.env.REDLOG_E2E === '1' && testFetchIndexOverride) { - return { ok: true, index: await annotateIndex(testFetchIndexOverride) } - } - try { - const { fetchIndex } = await import('../core/plugins/marketplace') - return { ok: true, index: await annotateIndex(await fetchIndex(url)) } - } catch (e) { - return { ok: false, error: (e as Error).message } - } - }) - // Dev/E2E-only sibling of marketplace:fetchIndex. Lets a test inject a - // canned RegistryIndex (including publishers[]) so the "trust suggested - // publishers" banner can be exercised without a real HTTPS registry. - // Gated on REDLOG_E2E=1 — the endpoint is effectively absent in a normal - // launch. Pass an empty string to clear the override. - ipcMain.handle('marketplace:testSetIndex', async (_e, indexJson: string) => { - if (process.env.REDLOG_E2E !== '1') return { ok: false, error: 'testSetIndex disabled' } - try { - testFetchIndexOverride = indexJson ? JSON.parse(indexJson) : null - return { ok: true } - } catch (e) { - return { ok: false, error: (e as Error).message } - } - }) - ipcMain.handle('marketplace:listPublishers', async () => { - const { listPublishers } = await import('../core/plugins/publisher-trust') - return listPublishers() - }) - ipcMain.handle('marketplace:trustPublisher', async (_e, id: string, publicKey: string, homepage?: string, label?: string) => { - const { trustPublisher, fingerprint } = await import('../core/plugins/publisher-trust') - const opId = activeProject ? loadConfig(getProjectPath(activeProject)).operator.id : 'unknown' - trustPublisher(id, { publicKey, trustedAt: Date.now(), trustedBy: opId, label }, homepage) - return { ok: true, fingerprint: fingerprint(publicKey) } - }) - ipcMain.handle('marketplace:untrustPublisher', async (_e, id: string) => { - const { untrustPublisher } = await import('../core/plugins/publisher-trust') - untrustPublisher(id); return { ok: true } - }) - ipcMain.handle('marketplace:install', async (_e, entryJson: string) => { - try { - const entry = JSON.parse(entryJson) - const { installFromRegistry } = await import('../core/plugins/marketplace') - const result = await installFromRegistry(entry) - // Re-scan plugins on successful install so the newly landed plugin - // shows up in Settings ▸ Plugins immediately. - if (result.ok) { invalidateHooksCache(); reloadPlugins() } - return result - } catch (e) { - return { ok: false, error: (e as Error).message } - } - }) - // Dev/E2E-only sibling of marketplace:install. Skips the network by taking - // the tarball bytes directly from the caller (base64 for IPC-safety) and - // handing them to installFromRegistry via its `fetchTarball` hook. Gated on - // REDLOG_E2E=1 so the endpoint is effectively absent in a normal launch — - // never gate on NODE_ENV, electron-vite already claims that flag. - ipcMain.handle('marketplace:testInstall', async (_e, entryJson: string, tarballBytesB64: string) => { - if (process.env.REDLOG_E2E !== '1') return { ok: false, error: 'testInstall disabled' } - try { - const entry = JSON.parse(entryJson) - const bytes = Buffer.from(tarballBytesB64, 'base64') - const { installFromRegistry } = await import('../core/plugins/marketplace') - const result = await installFromRegistry(entry, { fetchTarball: async () => bytes }) - if (result.ok) { invalidateHooksCache(); reloadPlugins() } - return result - } catch (e) { - return { ok: false, error: (e as Error).message } - } - }) - ipcMain.handle('marketplace:listVersions', async (_e, pluginId: string) => { - const { listVersions } = await import('../core/plugins/marketplace') - return listVersions(pluginId) - }) - ipcMain.handle('marketplace:rollback', async (_e, pluginId: string, versionKey: string) => { - const { rollback } = await import('../core/plugins/marketplace') - const r = rollback(pluginId, versionKey) - if (r.ok) { invalidateHooksCache(); reloadPlugins() } - return r - }) - ipcMain.handle('marketplace:revocations', async () => { - const { loadRevocations } = await import('../core/plugins/marketplace') - return loadRevocations() - }) - // --- Recording --- ipcMain.handle('recording:get', () => !eventBus.paused) ipcMain.handle('recording:toggle', () => toggleRecording()) diff --git a/src/preload/index.ts b/src/preload/index.ts index 4e87dcf8..9723a05b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -168,28 +168,6 @@ contextBridge.exposeInMainWorld('redlog', { upload: (zipPath: string, manifestJson: string, expiresIn: string | undefined, endpoint: string, authToken: string) => ipcRenderer.invoke('cloudShare:upload', zipPath, manifestJson, expiresIn, endpoint, authToken) }, - marketplace: { - // Fetch the registry index. Optional URL override so operators can point - // at their own mirror / air-gapped registry via Settings. - fetchIndex: (url?: string) => ipcRenderer.invoke('marketplace:fetchIndex', url), - listPublishers: () => ipcRenderer.invoke('marketplace:listPublishers'), - trustPublisher: (id: string, publicKey: string, homepage?: string, label?: string) => - ipcRenderer.invoke('marketplace:trustPublisher', id, publicKey, homepage, label), - untrustPublisher: (id: string) => ipcRenderer.invoke('marketplace:untrustPublisher', id), - install: (entryJson: string) => ipcRenderer.invoke('marketplace:install', entryJson), - // Dev/E2E-only path — main gates this on REDLOG_E2E=1. Bytes are base64 - // so we can hop across the IPC boundary without a Buffer polyfill in the - // renderer. Never called by production code paths. - testInstall: (entryJson: string, tarballBytesB64: string) => - ipcRenderer.invoke('marketplace:testInstall', entryJson, tarballBytesB64), - // Dev/E2E-only override for fetchIndex — gated on REDLOG_E2E=1 in main. - // Pass a JSON-stringified RegistryIndex to install a mock; pass '' to clear. - testSetIndex: (indexJson: string) => - ipcRenderer.invoke('marketplace:testSetIndex', indexJson), - listVersions: (pluginId: string) => ipcRenderer.invoke('marketplace:listVersions', pluginId), - rollback: (pluginId: string, versionKey: string) => ipcRenderer.invoke('marketplace:rollback', pluginId, versionKey), - revocations: () => ipcRenderer.invoke('marketplace:revocations') - }, capture: { health: () => ipcRenderer.invoke('capture:health') }, diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index f3c7b273..0dbae087 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -130,8 +130,10 @@ export default function Settings(): JSX.Element { // monitor, all of which already live there; on its own it was a tab // holding three checkboxes, which made it look like a subsystem rather // than one source among several. - // · Marketplace folded into Plugins as a sub-tab. "What is installed" and - // "where to get more" are one task split across two top-level tabs. + // · The marketplace is gone entirely. "Where to get more capture code" + // serves extensibility; the core is "nothing missing, findable + // afterwards" (docs/DESIGN-core-and-capture.md §1). Managing what is + // installed stays — acquiring more is not this product's job. // §10 / §5.7: a left list of categories with the content on the right. // // Eight tabs in a single row at 13px was already hard to scan, and two of @@ -972,26 +974,18 @@ interface PluginView { error?: string } -/** v0.9.10: Plugins and Marketplace were separate top-level tabs, but they are - * one task — "what do I have" and "what can I get" — and the operator moves - * between them constantly while installing something. Sub-tabs keep both a - * click away without spending two of the eight top-level slots. */ +// §10: two levels, not four. This page used to hold sub-tabs for installed +// and marketplace, and the marketplace held its own for publishers and +// revocations — three levels below a second-level tab, when the standard +// allows two. +// +// The marketplace is now gone rather than flattened. Browsing a registry, +// trusting publishers by fingerprint, and reading revocation lists are the +// machinery of distributing capture code, and distribution is not what this +// product is for. What stays is the part an operator needs to answer "is +// anything capturing that I did not put there" — the installed list. function PluginsTab({ t }: { t: (key: string, vars?: Record) => string }): JSX.Element { - // §10: two levels, not four. This page used to hold sub-tabs for installed - // and marketplace, and the marketplace held its own for publishers and - // revocations — three levels below a second-level tab, when the standard - // allows two. - // - // "What is installed" and "where to get more" are one task, so they are two - // sections of one page rather than two destinations. Publisher trust and - // revocation stop being lists you navigate to and become state on the row - // they describe, which is where an operator was looking anyway. - return ( - <> - - - - ) + return } function PluginsPanel({ t }: { t: (key: string, vars?: Record) => string }): JSX.Element { @@ -2461,385 +2455,3 @@ function CloudSharePanel({ t }: { t: (key: string, vars?: Record ) } - -// -------- Marketplace panel --------------------------------------------------- -// -// Thin wrapper around window.redlog.marketplace.*. The heavy lifting (fetch, -// sha256 verify, signature verify, atomic swap, rollback) lives in -// src/core/plugins/marketplace.ts. This panel only decides what to show and -// which IPC to call. -// -// Three sub-tabs: -// Plugins — fetch a registry index, install entries. -// Publishers — list trusted publishers, add a new one manually (paste -// an SPKI base64 pubkey), untrust. -// Revocations — surface the local revocation cache so operators can see -// why an install is being blocked. - -interface RegistryEntryView { - id: string - name?: string - description?: string - homepage?: string - publisher: string - version: string - tarball: string - sha256: string - signature?: string - sizeKb?: number - tags?: string[] -} -interface RegistryPublisherAdView { - id: string - homepage?: string - keys: Array<{ label?: string; publicKey: string }> -} -interface RegistryIndexView { updatedAt: number; entries: RegistryEntryView[]; publishers?: RegistryPublisherAdView[] } -interface PublisherKeyView { publicKey: string; label?: string; trustedAt: number; trustedBy?: string } -interface PublisherView { id: string; homepage?: string; keys: PublisherKeyView[] } -interface InstallResultView { - ok: boolean; pluginId?: string; error?: string - contentHash?: string; installedDir?: string; rolledBackFrom?: string - tier?: 'declarative' | 'privileged'; signatureVerified?: boolean -} -interface RevocationsView { updatedAt: number; plugins?: string[]; publishers?: string[] } - -function MarketplacePanel({ t }: { t: (key: string, vars?: Record) => string }): JSX.Element { - const [sub, setSub] = useState<'plugins' | 'publishers' | 'revocations'>('plugins') - const [registryUrl, setRegistryUrl] = useState('') - const [defaultRegistryUrl, setDefaultRegistryUrl] = useState('https://raw.githubusercontent.com/guan4tou2/REDLOG/main/examples/registry/index.json') - useEffect(() => { - // Load config once so the placeholder + one-click fetch honour any - // config-defined default. Air-gapped shops override this to point at - // their internal mirror. - window.redlog.config.get().then((c) => { - const url = (c as { marketplace?: { defaultRegistryUrl?: string } }).marketplace?.defaultRegistryUrl - if (url) setDefaultRegistryUrl(url) - }).catch(() => {}) - }, []) - const [index, setIndex] = useState(null) - const [fetchState, setFetchState] = useState<'idle' | 'loading' | 'error'>('idle') - const [fetchError, setFetchError] = useState('') - const [installBusy, setInstallBusy] = useState(null) - // Last install failure per plugin id — sticks around like cloud-share's - // persistent error box so the operator can read it after the toast fades. - // Keyed so multiple failed installs don't overwrite each other. - const [installError, setInstallError] = useState>({}) - const [publishers, setPublishers] = useState([]) - const [revocations, setRevocations] = useState(null) - - const api = (window.redlog as unknown as { marketplace: { - fetchIndex: (url?: string) => Promise<{ ok: boolean; index?: RegistryIndexView; error?: string }> - listPublishers: () => Promise - trustPublisher: (id: string, publicKey: string, homepage?: string, label?: string) => Promise<{ ok: boolean; fingerprint?: string; error?: string }> - untrustPublisher: (id: string) => Promise<{ ok: boolean }> - install: (entryJson: string) => Promise - revocations: () => Promise - } }).marketplace - - const reloadPublishers = async (): Promise => { setPublishers(await api.listPublishers()) } - const reloadRevocations = async (): Promise => { setRevocations(await api.revocations()) } - useEffect(() => { reloadPublishers(); reloadRevocations() }, []) - - const doFetch = async (): Promise => { - setFetchState('loading'); setFetchError('') - // Empty box → use the config-declared default (see marketplace.defaultRegistryUrl). - const url = registryUrl.trim() || defaultRegistryUrl - const r = await api.fetchIndex(url) - if (r.ok && r.index) { setIndex(r.index); setFetchState('idle') } - else { setFetchState('error'); setFetchError(r.error ?? 'unknown error') } - } - - const doInstall = async (entry: RegistryEntryView): Promise => { - setInstallBusy(entry.id) - // Clear a previous failure for this plugin so the box collapses on retry. - setInstallError((prev) => { const next = { ...prev }; delete next[entry.id]; return next }) - const r = await api.install(JSON.stringify(entry)) - setInstallBusy(null) - if (r.ok) { - toast(t('marketplace.installed'), 'success') - reloadPublishers() - } else { - const msg = r.error ?? 'unknown' - setInstallError((prev) => ({ ...prev, [entry.id]: msg })) - toast(t('marketplace.installFailed'), { - type: 'error', - why: t('marketplace.installFailedWhy'), - detail: msg, - action: { label: t('common.retry'), onClick: () => { void install(entry) } } - }) - } - } - const dismissInstallError = (id: string): void => { - setInstallError((prev) => { const next = { ...prev }; delete next[id]; return next }) - } - - const publisherTrusted = (id: string): boolean => publishers.some((p) => p.id === id) - - // v0.11.0: the registry index is UNTRUSTED input, and this list is the only - // place that mattered. `index.json` carries no signature RedLog can check — - // it names publishers and their keys, and nothing more. The real trust - // boundary is one step later: each tarball's Ed25519 signature verified - // against a key the operator has pinned. - // - // The old "Trust all suggested" button collapsed that boundary. Whoever - // controlled the index (or the domain, or a MITM without cert pinning) could - // advertise their own key, get it pinned in one click, and thereafter sign - // privileged plugins that passed every check. One button undid the whole - // model. - // - // Trusting a key is now per-publisher and shows the fingerprint the operator - // is supposed to compare against the publisher's own channel — a keypress - // per key, which is the correct amount of friction for "let this stranger - // run code in my audit tool". - const suggestedUntrusted = (index?.publishers ?? []).filter((p) => !publisherTrusted(p.id)) - const [trustingId, setTrustingId] = useState(null) - const trustOne = async (pub: { id: string; homepage?: string; keys: Array<{ publicKey: string; label?: string }> }): Promise => { - setTrustingId(pub.id) - try { - for (const k of pub.keys) { - await api.trustPublisher(pub.id, k.publicKey, pub.homepage, k.label) - } - toast(t('marketplace.publisherTrusted', { id: pub.id }), 'success') - reloadPublishers() - } catch (e) { - toast(t('marketplace.trustFailed', { id: pub.id }), { - type: 'error', - why: t('marketplace.trustFailedWhy'), - detail: String((e as Error)?.message ?? e) - }) - } finally { setTrustingId(null) } - } - - return ( - -

{t('marketplace.hint')}

- -
- {(['plugins', 'publishers', 'revocations'] as const).map((k) => ( - - ))} -
- - {sub === 'plugins' && ( - <> -
- setRegistryUrl(e.target.value)} - placeholder={defaultRegistryUrl} - className="flex-1 bg-redlog-surface border border-redlog-border rounded px-2 py-1 text-xs text-redlog-text" /> - -
- - {fetchState === 'error' && ( -

{t('marketplace.fetchFailed')}: {fetchError}

- )} - {!index && fetchState !== 'error' && ( -

{t('marketplace.indexEmpty')}

- )} - {index && index.entries.length === 0 && ( -

{t('marketplace.noEntries')}

- )} - - {suggestedUntrusted.length > 0 && ( -
-

- {t('marketplace.suggestedPublishersTitle', { n: suggestedUntrusted.length })} -

-

{t('marketplace.suggestedPublishersHint')}

-
    - {suggestedUntrusted.map((p) => ( -
  • -
    -

    {p.id}

    - {/* The fingerprint is the point of this row: the operator - is meant to compare it against the publisher's own - site before pinning, not take the registry's word. */} - {p.keys.map((k) => ( -

    - {(k as { fingerprint?: string }).fingerprint ?? k.publicKey.slice(0, 16)}{k.label ? ` · ${k.label}` : ''} -

    - ))} -
    - -
  • - ))} -
-
- )} - -
- {index?.entries.map((e) => { - const trusted = publisherTrusted(e.publisher) - const signed = !!e.signature - return ( -
-
-
-
- {e.name || e.id} - v{e.version} - {e.publisher} - {signed && {t('marketplace.signatureVerified')}} - {e.sizeKb !== undefined && ( - {t('marketplace.sizeKb', { size: e.sizeKb })} - )} -
- {e.description &&

{e.description}

} - {!trusted && ( -

{t('marketplace.publisherUntrusted')}

- )} -
- -
- {installError[e.id] && ( -
- ⚠ -

- {t('marketplace.installFailed')}: {installError[e.id]} -

- -
- )} -
- ) - })} -
- - )} - - {sub === 'publishers' && ( - - )} - - {sub === 'revocations' && ( -
-

{t('marketplace.revocationsHint')}

- {(!revocations || ((revocations.plugins?.length ?? 0) === 0 && (revocations.publishers?.length ?? 0) === 0)) && ( -

{t('marketplace.revocationsEmpty')}

- )} - {revocations && (revocations.plugins?.length ?? 0) > 0 && ( -
-

{t('marketplace.revokedPlugins')}

-
- {revocations.plugins!.map((p) => {p})} -
-
- )} - {revocations && (revocations.publishers?.length ?? 0) > 0 && ( -
-

{t('marketplace.revokedPublishers')}

-
- {revocations.publishers!.map((p) => {p})} -
-
- )} -
- )} -
- ) -} - -function PublisherEditor({ t, publishers, api, onReload }: { - t: (key: string, vars?: Record) => string - publishers: PublisherView[] - api: { - trustPublisher: (id: string, publicKey: string, homepage?: string, label?: string) => Promise<{ ok: boolean; fingerprint?: string; error?: string }> - untrustPublisher: (id: string) => Promise<{ ok: boolean }> - } - onReload: () => Promise -}): JSX.Element { - const [draftId, setDraftId] = useState('') - const [draftKey, setDraftKey] = useState('') - const [draftLabel, setDraftLabel] = useState('') - const [busy, setBusy] = useState(false) - - const add = async (): Promise => { - if (!draftId.trim() || !draftKey.trim()) return - setBusy(true) - const r = await api.trustPublisher(draftId.trim(), draftKey.trim(), undefined, draftLabel.trim() || undefined) - setBusy(false) - if (r.ok) { - toast(`${t('marketplace.publisherAdded')} · fp ${r.fingerprint ?? ''}`, 'success') - setDraftId(''); setDraftKey(''); setDraftLabel('') - onReload() - } else { - toast(t('marketplace.addPublisherFailed'), { - type: 'error', - why: t('marketplace.addPublisherFailedWhy'), - detail: r.error - }) - } - } - - const untrust = async (id: string): Promise => { await api.untrustPublisher(id); onReload() } - - return ( -
-
-

{t('marketplace.addPublisher')}

-

{t('marketplace.addPublisherHint')}

-
- setDraftId(e.target.value)} placeholder={t('marketplace.publisherId')} - className="bg-redlog-surface border border-redlog-border rounded px-2 py-1 text-xs text-redlog-text font-mono" /> - setDraftLabel(e.target.value)} placeholder={t('marketplace.publisherLabel')} - className="bg-redlog-surface border border-redlog-border rounded px-2 py-1 text-xs text-redlog-text" /> -
-