|
1 | 1 | import { useEffect, useState } from 'react' |
| 2 | +import { Events } from '@wailsio/runtime' |
2 | 3 | import { Button, Heading, Label, type LabelProps, SkeletonBox, Stack, Text } from '@primer/react' |
3 | | -import { BeakerIcon, KeyIcon, MarkdownIcon } from '@primer/octicons-react' |
| 4 | +import { BeakerIcon, CheckCircleIcon, KeyIcon, MarkdownIcon, XCircleIcon } from '@primer/octicons-react' |
4 | 5 | import { RunbookService, HotkeyService } from '../bindings/github.com/alicoding/mill' |
| 6 | +import type { HotkeyActivity } from '../bindings/github.com/alicoding/mill/models' |
5 | 7 | import type { Action } from '../bindings/github.com/alicoding/mill/internal/domain/runbook/models' |
6 | 8 | import { keyFromEventCode, modsFromEvent } from './keybinding' |
7 | 9 |
|
| 10 | +// A fired hotkey has no other UI surface — it runs headlessly and writes |
| 11 | +// straight to the clipboard (§2.2). Without this feed, a correctly firing |
| 12 | +// hotkey and a silently swallowed one look identical from the UI: nothing |
| 13 | +// visibly happens either way. Capped and in-memory only, same as the |
| 14 | +// bindings themselves — see SPEC.md §2.2's "Hotkey fire path is logged |
| 15 | +// end-to-end" entry. |
| 16 | +const MAX_ACTIVITY_ENTRIES = 5 |
| 17 | + |
8 | 18 | // Per-action leading icon. Falls back to KeyIcon for any future action not |
9 | 19 | // listed here rather than rendering nothing. |
10 | 20 | const ACTION_ICONS: Record<string, typeof BeakerIcon> = { |
@@ -35,12 +45,20 @@ function RunbookView() { |
35 | 45 | const [bindings, setBindings] = useState<Record<string, string>>({}) |
36 | 46 | const [bindingErrors, setBindingErrors] = useState<Record<string, string>>({}) |
37 | 47 | const [recordingId, setRecordingId] = useState<string | null>(null) |
| 48 | + const [activity, setActivity] = useState<(HotkeyActivity & { id: string; time: string })[]>([]) |
38 | 49 |
|
39 | 50 | useEffect(() => { |
40 | 51 | RunbookService.List().then((list) => setActions(list ?? [])).catch(console.error) |
41 | 52 | HotkeyService.List().then((list) => setBindings((list ?? {}) as Record<string, string>)).catch(console.error) |
42 | 53 | }, []) |
43 | 54 |
|
| 55 | + useEffect(() => { |
| 56 | + return Events.On('hotkey-activity', (evt) => { |
| 57 | + const entry = { ...evt.data, id: crypto.randomUUID(), time: new Date().toLocaleTimeString() } |
| 58 | + setActivity((prev) => [entry, ...prev].slice(0, MAX_ACTIVITY_ENTRIES)) |
| 59 | + }) |
| 60 | + }, []) |
| 61 | + |
44 | 62 | useEffect(() => { |
45 | 63 | if (!recordingId) return |
46 | 64 |
|
@@ -165,8 +183,36 @@ function RunbookView() { |
165 | 183 | })} |
166 | 184 | </Stack> |
167 | 185 | )} |
| 186 | + |
| 187 | + {activity.length > 0 && ( |
| 188 | + <> |
| 189 | + <Heading as="h2" variant="small" className="runbook-activity-heading">Recent activity</Heading> |
| 190 | + <Text as="p" size="small" className="runbook-muted runbook-subtitle"> |
| 191 | + What fired hotkeys actually did — hotkey triggers run headlessly with no other feedback. |
| 192 | + </Text> |
| 193 | + <Stack direction="vertical" gap="condensed"> |
| 194 | + {activity.map((entry) => ( |
| 195 | + <Stack key={entry.id} direction="horizontal" align="center" gap="condensed" className="runbook-activity-row"> |
| 196 | + {entry.success ? ( |
| 197 | + <CheckCircleIcon size={16} fill="var(--fgColor-success)" /> |
| 198 | + ) : ( |
| 199 | + <XCircleIcon size={16} fill="var(--fgColor-danger)" /> |
| 200 | + )} |
| 201 | + <Text size="small" className="runbook-muted">{entry.time}</Text> |
| 202 | + <Label variant="secondary" size="small">{entry.binding}</Label> |
| 203 | + <Text size="small">{actionName(actions, entry.actionID)}</Text> |
| 204 | + <Text size="small" className="runbook-muted">— {entry.detail}</Text> |
| 205 | + </Stack> |
| 206 | + ))} |
| 207 | + </Stack> |
| 208 | + </> |
| 209 | + )} |
168 | 210 | </div> |
169 | 211 | ) |
170 | 212 | } |
171 | 213 |
|
| 214 | +function actionName(actions: Action[] | null, actionID: string): string { |
| 215 | + return actions?.find((a) => a.ID === actionID)?.Name ?? actionID |
| 216 | +} |
| 217 | + |
172 | 218 | export default RunbookView |
0 commit comments