feat(app): Settings/Devices section + getDevicesScan bridge

Settings rail (Devices active) with This device (getIdentity), Discovered nearby
(new getDevicesScan bridge → `codeburn devices scan --format json`), and Paired
(getDevices perDevice). Pairing/approve/pull/visibility/combine are M1 visual
affordances (mutations = M2); share status is process-local so not shown as
authoritative. Settings uses a title-only bar (no period/provider).

Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
This commit is contained in:
iamtoruk 2026-07-10 19:17:14 -07:00
parent 73fb86dec6
commit dc52b3226b
7 changed files with 287 additions and 9 deletions

View file

@ -29,6 +29,7 @@ const CHANNELS = [
'codeburn:getYield',
'codeburn:getSpendFlow',
'codeburn:getDevices',
'codeburn:getDevicesScan',
'codeburn:getShareStatus',
'codeburn:getIdentity',
'codeburn:cliStatus',
@ -43,12 +44,13 @@ const ARGV_CASES: Array<{ channel: string; args: unknown[]; argv: string[] }> =
{ channel: 'codeburn:getYield', args: ['today'], argv: ['yield', '--format', 'json', '--period', 'today'] },
{ channel: 'codeburn:getSpendFlow', args: ['month', 'openai'], argv: ['spend', '--format', 'flow-json', '--period', 'month', '--provider', 'openai'] },
{ channel: 'codeburn:getDevices', args: ['week'], argv: ['devices', '--format', 'json', '--period', 'week'] },
{ channel: 'codeburn:getDevicesScan', args: [], argv: ['devices', 'scan', '--format', 'json'] },
{ channel: 'codeburn:getShareStatus', args: [], argv: ['share', 'status', '--format', 'json'] },
{ channel: 'codeburn:getIdentity', args: [], argv: ['identity', '--format', 'json'] },
]
describe('createBridgeHandlers (channel → argv for all channels)', () => {
it('exposes exactly the nine codeburn:* channels', () => {
it('exposes exactly the ten codeburn:* channels', () => {
const handlers = createBridgeHandlers({ spawnCli: vi.fn(), resolveCodeburnPath: () => null })
expect(Object.keys(handlers).sort()).toEqual([...CHANNELS].sort())
})

View file

@ -53,6 +53,7 @@ export function createBridgeHandlers(deps: Deps = { spawnCli, resolveCodeburnPat
'spend', '--format', 'flow-json', '--period', period, ...providerArgs(provider),
]),
'codeburn:getDevices': run((period: string) => ['devices', '--format', 'json', '--period', period]),
'codeburn:getDevicesScan': run(() => ['devices', 'scan', '--format', 'json']),
'codeburn:getShareStatus': run(() => ['share', 'status', '--format', 'json']),
'codeburn:getIdentity': run(() => ['identity', '--format', 'json']),
'codeburn:cliStatus': async () => {

View file

@ -22,6 +22,7 @@ const bridge = {
getYield: (period: string) => invoke('codeburn:getYield', period),
getSpendFlow: (period: string, provider: string) => invoke('codeburn:getSpendFlow', period, provider),
getDevices: (period: string) => invoke('codeburn:getDevices', period),
getDevicesScan: () => invoke('codeburn:getDevicesScan'),
getShareStatus: () => invoke('codeburn:getShareStatus'),
getIdentity: () => invoke('codeburn:getIdentity'),
cliStatus: () => invoke('codeburn:cliStatus'),

View file

@ -11,6 +11,7 @@ import { Overview } from './sections/Overview'
import { Optimize } from './sections/Optimize'
import { Models } from './sections/Models'
import { Plans } from './sections/Plans'
import { Settings } from './sections/Settings'
import { Spend } from './sections/Spend'
import type { MenubarPayload, Period } from './lib/types'
@ -64,11 +65,13 @@ export function App() {
<div className="ct">
{section === 'plans' ? (
<Plans period={period} />
) : section === 'settings' ? (
<Settings period={period} />
) : (
<>
<TopBar
title={SECTION_TITLES[section]}
scope={section === 'settings' ? undefined : scope}
scope={scope}
period={period}
onPeriodChange={onPeriodChange}
providerLabel="All providers"
@ -88,13 +91,15 @@ export function App() {
</div>
</>
)}
<Hint
items={[
{ k: '⌘K', label: 'Command' },
{ k: '⌘E', label: 'Export view' },
]}
right={overview.loading ? 'refreshing…' : 'auto-refresh 30s'}
/>
{section !== 'settings' && (
<Hint
items={[
{ k: '⌘K', label: 'Command' },
{ k: '⌘E', label: 'Export view' },
]}
right={overview.loading ? 'refreshing…' : 'auto-refresh 30s'}
/>
)}
</div>
</Window>
)

View file

@ -357,6 +357,7 @@ export interface CodeburnBridge {
getYield(period: Period): Promise<YieldJsonReport>
getSpendFlow(period: Period, provider: string): Promise<SpendFlow>
getDevices(period: Period): Promise<CombinedUsage>
getDevicesScan(): Promise<DeviceScanResult>
getShareStatus(): Promise<ShareStatus>
getIdentity(): Promise<Identity>
cliStatus(): Promise<{ found: boolean; path: string | null; error?: string }>

View file

@ -0,0 +1,110 @@
// @vitest-environment jsdom
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { CombinedUsage, DeviceScanResult, Identity } from '../lib/types'
import { Settings } from './Settings'
const { getIdentity, getDevices, getDevicesScan } = vi.hoisted(() => ({
getIdentity: vi.fn<() => Promise<Identity>>(),
getDevices: vi.fn<(period: string) => Promise<CombinedUsage>>(),
getDevicesScan: vi.fn<() => Promise<DeviceScanResult>>(),
}))
vi.mock('../lib/ipc', async orig => {
const actual = await orig<typeof import('../lib/ipc')>()
return { ...actual, codeburn: { getIdentity, getDevices, getDevicesScan } }
})
const identity: Identity = {
name: 'Toruk MacBook Pro',
fingerprint: 'AA:11:22:33:44:55:66:77',
}
const devices: CombinedUsage = {
perDevice: [
{
id: 'local',
name: 'Toruk MacBook Pro',
local: true,
cost: 120.1,
calls: 100,
sessions: 10,
inputTokens: 1,
outputTokens: 2,
cacheCreateTokens: 3,
cacheReadTokens: 4,
totalTokens: 10,
},
{
id: 'mini',
name: 'toruk-mini',
local: false,
cost: 41.2,
calls: 680,
sessions: 34,
inputTokens: 11,
outputTokens: 12,
cacheCreateTokens: 13,
cacheReadTokens: 14,
totalTokens: 50,
},
],
combined: {
cost: 161.3,
calls: 780,
sessions: 44,
inputTokens: 12,
outputTokens: 14,
cacheCreateTokens: 16,
cacheReadTokens: 18,
totalTokens: 60,
deviceCount: 2,
reachableCount: 2,
},
}
const scan: DeviceScanResult = {
found: [
{
name: 'Mac Studio',
host: 'mac-studio.local',
port: 9732,
fingerprint: '7F:2A:19:88:55:44:33:C4',
code: 'pair-1',
paired: false,
},
],
}
describe('Settings', () => {
beforeEach(() => {
getIdentity.mockReset()
getDevices.mockReset()
getDevicesScan.mockReset()
})
it('renders device identity, scan results, paired devices, and M2 affordances', async () => {
getIdentity.mockResolvedValue(identity)
getDevices.mockResolvedValue(devices)
getDevicesScan.mockResolvedValue(scan)
const { container } = render(<Settings period="month" />)
expect(await screen.findByText('Toruk MacBook Pro')).toBeInTheDocument()
expect(screen.getByText('Visible on the local network as Toruk MacBook Pro.local')).toBeInTheDocument()
expect(screen.getByText('AA:11:22:33:44:55:66:77')).toBeInTheDocument()
expect(await screen.findByText('Mac Studio')).toBeInTheDocument()
expect(screen.getByText('wants to pair · fingerprint 7F:2A:…:C4')).toBeInTheDocument()
expect(await screen.findByText('toruk-mini')).toBeInTheDocument()
expect(screen.getByText('34 sessions · $41.20 this month')).toBeInTheDocument()
expect(screen.getByText('Visibility: on')).toBeInTheDocument()
expect(screen.getByText('Approve')).toBeInTheDocument()
expect(screen.getByText('Pull now')).toBeInTheDocument()
expect(screen.getByText('Combine usage from paired devices')).toBeInTheDocument()
expect(container.querySelector('.tglon')).toBeInTheDocument()
expect(getDevices).toHaveBeenCalledWith('month')
})
})

View file

@ -0,0 +1,158 @@
import { Hint } from '../components/Hint'
import { Panel } from '../components/Panel'
import { usePolled } from '../hooks/usePolled'
import { codeburn } from '../lib/ipc'
import type { CombinedUsage, DeviceScanResult, Identity, Period } from '../lib/types'
const RAIL_ITEMS = ['General', 'Providers', 'Model aliases', 'Plans', 'Devices', 'Export', 'Privacy & data'] as const
function fmtUsd(n: number): string {
return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
function periodLabel(period: Period): string {
if (period === 'today') return 'today'
if (period === 'week') return 'last 7 days'
if (period === 'month') return 'this month'
if (period === '30days') return 'last 30 days'
return 'all time'
}
function shortFingerprint(fingerprint: string): string {
const parts = fingerprint.split(':').filter(Boolean)
if (parts.length < 3) return fingerprint
return `${parts[0]}:${parts[1]}:…:${parts[parts.length - 1]}`
}
function isPermissionError(message: string): boolean {
return /permission|full disk access|eacces/i.test(message)
}
function errorText(error: { kind: string; message: string } | null): string | null {
if (!error) return null
if (error.kind === 'not-found') return 'codeburn CLI not found'
if (error.kind === 'nonzero' && isPermissionError(error.message)) return 'permission denied — grant Full Disk Access'
return error.message
}
export function Settings({ period }: { period: Period }) {
const identity = usePolled<Identity>(() => codeburn.getIdentity(), [])
const scan = usePolled<DeviceScanResult>(() => codeburn.getDevicesScan(), [])
const devices = usePolled<CombinedUsage>(() => codeburn.getDevices(period), [period])
return (
<>
<div className="bar">
<div className="t">Settings</div>
</div>
<div className="body" style={{ flexDirection: 'row', gap: 14 }}>
<nav className="rail">
{RAIL_ITEMS.map(item => (
<div key={item} className={item === 'Devices' ? 'ni on' : 'ni'}>
{item}
</div>
))}
</nav>
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
<ThisDevicePanel identity={identity} />
<DiscoveredPanel scan={scan} />
<PairedPanel devices={devices} period={period} />
</div>
</div>
<Hint items={[{ k: 'esc', label: 'Back' }]} right="pairing uses mutual TLS · approve-style, no PIN" />
</>
)
}
function ThisDevicePanel({ identity }: { identity: ReturnType<typeof usePolled<Identity>> }) {
const error = errorText(identity.error)
return (
<Panel title="This device">
{identity.data ? (
<div className="li">
<div className="lx">
<b>{identity.data.name}</b>
<span>Visible on the local network as {identity.data.name}.local</span>
<span>{identity.data.fingerprint}</span>
</div>
<span className="btn btn-s" aria-disabled="true">
Visibility: on
</span>
</div>
) : (
<p style={{ color: error ? 'var(--amber)' : 'var(--t3)', margin: 0, fontSize: 12 }}>
{error ?? 'Reading this device identity…'}
</p>
)}
</Panel>
)
}
function DiscoveredPanel({ scan }: { scan: ReturnType<typeof usePolled<DeviceScanResult>> }) {
const error = errorText(scan.error)
const found = scan.data?.found.filter(device => !device.paired) ?? []
return (
<Panel title="Discovered nearby" right={scan.loading ? 'listening…' : undefined}>
{!scan.data ? (
<p style={{ color: error ? 'var(--amber)' : 'var(--t3)', margin: 0, fontSize: 12 }}>
{error ?? 'listening…'}
</p>
) : found.length === 0 ? (
<p style={{ color: 'var(--t3)', margin: 0, fontSize: 12 }}>No nearby devices found.</p>
) : (
found.map(device => (
<div className="li" key={`${device.host}:${device.port}:${device.fingerprint}`}>
<div className="lx">
<b>{device.name}</b>
<span className="hot">wants to pair · fingerprint {shortFingerprint(device.fingerprint)}</span>
</div>
<span className="btn btn-p" aria-disabled="true">
Approve
</span>
</div>
))
)}
</Panel>
)
}
function PairedPanel({ devices, period }: { devices: ReturnType<typeof usePolled<CombinedUsage>>; period: Period }) {
const error = errorText(devices.error)
const paired = devices.data?.perDevice.filter(device => !device.local) ?? []
const deviceScope = devices.data ? `· ${devices.data.combined.deviceCount} devices` : '· paired devices'
return (
<Panel title="Paired">
{!devices.data ? (
<p style={{ color: error ? 'var(--amber)' : 'var(--t3)', margin: 0, fontSize: 12 }}>
{error ?? 'Loading paired devices…'}
</p>
) : paired.length === 0 ? (
<p style={{ color: 'var(--t3)', margin: 0, fontSize: 12 }}>No paired devices yet.</p>
) : (
paired.map(device => (
<div className="li" key={device.id}>
<div className="lx">
<b>{device.name}</b>
<span>
{device.sessions.toLocaleString('en-US')} sessions · {fmtUsd(device.cost)} {periodLabel(period)}
</span>
</div>
<span className="btn btn-s" aria-disabled="true">
Pull now
</span>
</div>
))
)}
<div className="li">
<div className="lx">
<b>Combine usage from paired devices</b>
<span>scope captions gain {deviceScope} when on</span>
</div>
<span className="tglon" aria-disabled="true" />
</div>
</Panel>
)
}