Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Invariants:
- Peer provenance + standing role (Wave 4 P1) — `sendToAgent` / `postToRoom` stamp `originUser` + `hopDepth` on peer-hop wakes; `standingRoleBlock` (name/title/rules) on every mouth system preamble; unattended = routine | peer-hop without interactive person; no CopilotKit; sisters stay separate; package 0.12.0.
- Session Activity strip (Wave 4 P2) — per-sister ephemeral commands/files next to feed from mouth-stream / action ledger (no second audit DB); writes show path·size only; `docs/activity.md`; no CopilotKit; package 0.12.0.
- Peer chase / head keep-alive (Wave 5 P0) — `offerSisterHop` / `sendToAgent` park `pendingHops` envelopes; head working while hops are out; assess injects `expecting` and may hop again if unmet; empty/fail sister auto-wakes a notice; `docs/peer-chase.md`; no CopilotKit / `@ag-ui/*`; sisters stay separate; package 0.12.0.
- GPUI polish P0 (Wave 6) — toast stack + level policy (`src/chrome/toast*.tsx` / toast-store); EmptyState icon/CTA; focus chords Cmd+L / Cmd+B / Cmd+, / Cmd+J (HID + gpuix); `docs/wave6-gpui-polish.md`; no CopilotKit / gpui-kit; idle parks intact; package 0.12.0.
- Bezel light appearance (Wave 2 P2) — designed light (Settings → Window → Appearance; not invert of graphite); Brand-complete Settings (remaining CARD/FIELD via useChrome; update modal scrim); ToggleGroup, EmptyState, Sheet extracted. Still vendored `@gpuix/react` (Wave 3.2 is 0.6.2 + matched native). Package 0.8.0.

## Safety
Expand Down
46 changes: 46 additions & 0 deletions docs/wave6-gpui-polish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Wave 6 — GPUI polish (toast / empty CTA / focus chords)

**Band:** P0 (feedback & findability)
**Package:** stays on current main (no version cut in this PR)
**Patterns only** — recipes from rgitui / ChatGPUI / Waku / Bezel. No CopilotKit, Electron, or `gpui-kit` / `gpui-component` crates.

## What shipped (P0)

### Toast stack + level policy
- `src/chrome/toast-store.ts` + `src/chrome/toast.tsx`
- Levels: `info` / `success` / `warn` / `error`
- TTL: info/success **~3s**, warn **~6s**, error **sticky** (dismiss only)
- Optional action chip; severity word is text (not color-only)
- Paint **above** the composer with **opaque** `menu` fill (`menuFill` / `groupBoxStyle(..., 'menu')`) so frost Metal never punches through
- Wired paths: mouth `onFail` (error + “Show sister”), secret save fail (feed card + Settings OpenRouter / MCP Connect), update-ready info toast when git tip offers

Idle: no timers when the stack is empty; auto-dismiss timers clear on unmount/dismiss.

### EmptyState + CTA
- `EmptyState` accepts optional `icon`, `actionLabel`, `onAction`
- Feed empty → **Write something** (focuses composer via `focusNonce`)
- Jobs empty → **Expand board**
- Settings empties (routines / skills / rooms / MCP search miss) get one verb where sensible

### Focus chords
GPUIx `onKeyDown` plus the same HID watch discipline as paste/copy/quit (textarea swallows React keys):

| Chord | Action |
| --- | --- |
| **Cmd+L** | Focus composer |
| **Cmd+B** | Toggle rail compact ↔ default width |
| **Cmd+,** | Toggle Settings sheet |
| **Cmd+J** | Toggle Jobs sheet |
| Cmd+Shift+I | Inspector (existing) |

No full keymap file yet — P2 shortcuts-help can generate from one table later.

## Parks (unchanged)
- Idle parks / resting-motion
- Steer queue still hint-string (P1 queue card)
- Command palette (P1)
- OS Notification Center (P2)
- No `bezel` / `gpui-kit` deps

## Audit
See `/workspace/automaton-wave6-gpui-polish-audit.md` (box) and `artifacts/automaton-wave6-gpui-polish-audit.md` when copied. P0 row status: **implemented on branch `feat/wave-6-p0-toast-empty-chords`**.
79 changes: 75 additions & 4 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
visibleAgents,
} from './domain'
import { ingestPath, insertClipboardText, pickLocalFiles, readClipboardPaths, readClipboardText } from './runtime/attachments'
import { watchCopyHotkey, watchCutHotkey, watchPasteHotkey, watchQuitHotkey, watchSelectAllHotkey } from './runtime/paste-hotkey'
import { watchCopyHotkey, watchCutHotkey, watchFocusComposerHotkey, watchJobsHotkey, watchPasteHotkey, watchQuitHotkey, watchSelectAllHotkey, watchSettingsHotkey, watchToggleRailHotkey } from './runtime/paste-hotkey'
import { runningTests } from './runtime/test-env'
import { applyUpdate, checkForUpdate, dismissUpdate, readDismissedSha, shouldOfferUpdate, relaunchAutomaton, type UpdateOffer } from './runtime/updates'
import {
Expand Down Expand Up @@ -84,6 +84,10 @@ import {
pasteChord,
quitChord,
selectAllChord,
focusComposerChord,
toggleRailChord,
settingsChord,
jobsChord,
} from './inspector'
import { DeskStage } from './desk'
import { JobsPane } from './jobs-pane'
Expand Down Expand Up @@ -147,9 +151,9 @@ import {
type Session,
} from './session'
import { SisterBlob, framePath, markFor } from './blob'
import { railDragOrigin, railIsCompact, railWidthFromDrag, readSkin, writeSkin } from './runtime/skin'
import { railDragOrigin, railIsCompact, railWidthFromDrag, readSkin, toggleRailWidth, writeSkin } from './runtime/skin'
import { ConfirmCard, QuestionCard, SecretRequestCard } from './cards'
import { ActivityZone, Composer, EmptyState, MouthWaitBubble, Sheet, Titlebar, activityVisible, groupBoxStyle, tracesFromJob } from './chrome'
import { ActivityZone, Composer, EmptyState, MouthWaitBubble, Sheet, Titlebar, ToastStack, activityVisible, groupBoxStyle, pushToast, tracesFromJob } from './chrome'
import { connectorDisplayName } from './runtime/connectors'
import { Settings } from './settings'
import { motionTransition } from './motion'
Expand Down Expand Up @@ -260,6 +264,7 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
return runningTests() ? seeded : playIntro(seeded, seeded.activeAgentId)
})
const [pane, setPane] = useState<Pane>('none')
const [composerFocusNonce, setComposerFocusNonce] = useState(0)
const [planeTick, setPlaneTick] = useState(0)
const [railWidth, setRailWidth] = useState(() => readSkin().railWidth)
const [railDragging, setRailDragging] = useState(false)
Expand Down Expand Up @@ -328,6 +333,22 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
setPane((current) => (current === 'jobs' ? 'none' : 'jobs'))
}

const toggleSettings = () => {
setPane((current) => (current === 'settings' ? 'none' : 'settings'))
}

const focusComposer = () => {
setComposerFocusNonce((n) => n + 1)
}

const toggleRail = () => {
setRailWidth((current) => {
const next = toggleRailWidth(current)
writeSkin({ ...readSkin(), railWidth: next })
return next
})
}

const popOutDashboard = (jobId?: string | null) => {
const located = ensureLocalDashboard(jobId)
if (!located.ok) {
Expand Down Expand Up @@ -385,6 +406,11 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
const offer = checkForUpdate()
if (gone || !shouldOfferUpdate(offer, readDismissedSha())) return
setUpdate(offer)
pushToast({
id: `update-${offer?.latest ?? 'ready'}`,
level: 'info',
message: 'Updates available — review the update dialog.',
})
}
const start = setTimeout(look, 800)
const pulse = setInterval(look, 4 * 60 * 60 * 1000)
Expand Down Expand Up @@ -504,6 +530,14 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
persistIntroIfUserSpoke(next)
return next
})
pushToast({
level: 'error',
message: spoken || "Couldn't speak.",
action: {
label: 'Show sister',
onAction: () => setSession((current) => ({ ...current, activeAgentId: agentId })),
},
})
},
onCompact: (agentId, summary) => {
setSession((current) => setThreadCompactSummary(current, agentId, summary))
Expand Down Expand Up @@ -817,12 +851,20 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
const stopSelectAll = watchSelectAllHotkey(() => feedApi.current?.selectAll())
const stopCut = watchCutHotkey(() => cutComposerIfFocused())
const stopQuit = watchQuitHotkey(() => quitAutomaton())
const stopFocus = watchFocusComposerHotkey(() => focusComposer())
const stopRail = watchToggleRailHotkey(() => toggleRail())
const stopSettings = watchSettingsHotkey(() => toggleSettings())
const stopJobs = watchJobsHotkey(() => toggleJobs())
return () => {
stopPaste()
stopCopy()
stopSelectAll()
stopCut()
stopQuit()
stopFocus()
stopRail()
stopSettings()
stopJobs()
}
}, [])

Expand Down Expand Up @@ -876,6 +918,10 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
}}
onKeyDown={(event) => {
if (inspectorChord(event)) toggleInspector()
if (focusComposerChord(event)) focusComposer()
if (toggleRailChord(event)) toggleRail()
if (settingsChord(event)) toggleSettings()
if (jobsChord(event)) toggleJobs()
if (pasteChord(event)) enqueueClipboard()
if (selectAllChord(event)) feedApi.current?.selectAll()
if (copyChord(event)) copyFeedIfIdle()
Expand Down Expand Up @@ -981,8 +1027,26 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
}
onAnswerWidget={(id, answer) => setSession((current) => answerWidget(current, id, answer))}
onDismissWidget={(id) => setSession((current) => dismissWidget(current, id))}
onSaveSecret={(id, value) => setSession((current) => fulfillSecretRequest(current, id, value))}
onSaveSecret={(id, value) => {
let failed = false
setSession((current) => {
const next = fulfillSecretRequest(current, id, value)
const before = current.threads[current.activeAgentId]?.items.find((row) => row.id === id)
const after = next.threads[next.activeAgentId]?.items.find((row) => row.id === id)
const saved =
before?.kind === 'secret-request' &&
after?.kind === 'secret-request' &&
before.status === 'open' &&
after.status === 'saved'
if (value.trim() && before?.kind === 'secret-request' && before.status === 'open' && !saved) {
failed = true
}
return next
})
if (failed) pushToast({ level: 'error', message: 'Need: could not save secret.' })
}}
onDismissSecret={(id) => setSession((current) => dismissSecretRequest(current, id))}
onEmptyWrite={focusComposer}
/>
<div
testId="dock"
Expand Down Expand Up @@ -1048,6 +1112,7 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
}}
/>
) : null}
<ToastStack style={{ paddingLeft: T.space.lg, paddingRight: T.space.lg, paddingBottom: T.space.sm }} />
<Composer
value={overlayBusy ? '' : (thread?.draft ?? '')}
agents={session.agents}
Expand All @@ -1062,6 +1127,7 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
thread.computerBusy === true ||
jobs.length > 0)),
)}
focusNonce={composerFocusNonce}
onChange={(value) => setSession((current) => setDraft(current, value))}
onAttach={onAttach}
onPaste={enqueueClipboard}
Expand Down Expand Up @@ -1129,6 +1195,7 @@ function StaffApp({ store: providedStore }: { store?: StaffStore } = {}) {
metrics={store.metrics()}
agents={visibleAgents(session.agents)}
onClose={() => setPane('none')}
onToast={(level, message) => pushToast({ level, message })}
onPlaneChange={() => setPlaneTick((n) => n + 1)}
onSkinChange={() => {
refreshTokens()
Expand Down Expand Up @@ -2133,6 +2200,7 @@ export const Feed = forwardRef<FeedApi, {
onDismissWidget?: (id: string) => void
onSaveSecret?: (id: string, value: string) => void
onDismissSecret?: (id: string) => void
onEmptyWrite?: () => void
}>(function Feed(
{
items,
Expand All @@ -2146,6 +2214,7 @@ export const Feed = forwardRef<FeedApi, {
onDismissWidget,
onSaveSecret,
onDismissSecret,
onEmptyWrite,
},
api,
) {
Expand Down Expand Up @@ -2284,6 +2353,8 @@ export const Feed = forwardRef<FeedApi, {
testId="feed-empty"
title="Start shipping. No strings attached."
detail="Pick a mouth on the rail. Words go here."
actionLabel={onEmptyWrite ? 'Write something' : undefined}
onAction={onEmptyWrite}
style={{
...feedLane,
paddingTop: T.space.hero,
Expand Down
4 changes: 4 additions & 0 deletions src/chrome/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function Composer({
onDropPending,
onSend,
onStop,
focusNonce = 0,
}: {
value: string
pendingPaths: string[]
Expand All @@ -52,6 +53,8 @@ export function Composer({
onDropPending: (path: string) => void
onSend: () => void
onStop?: () => void
/** Bump to remount the textarea with autoFocus (Cmd+L / empty CTA). */
focusNonce?: number
}) {
const T = useTokens()
const [highlight, setHighlight] = useState(0)
Expand Down Expand Up @@ -197,6 +200,7 @@ export function Composer({
</div>
) : null}
<textarea
key={`composer-focus-${focusNonce}`}
testId="composer"
value={value}
placeholder=""
Expand Down
20 changes: 19 additions & 1 deletion src/chrome/empty-state.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import React from 'react'
import { useTokens } from '../theme'
import { cardStyle } from '../ui'
import { cardStyle, Chip } from '../ui'

/** Feed, Jobs, and Settings list blanks — not a generic empty-zoo. */
export function EmptyState({
testId,
title,
detail,
icon,
actionLabel,
onAction,
variant = 'plain',
style,
}: {
testId?: string
title: string
detail?: string
/** Optional leading glyph or mark (string or node). */
icon?: React.ReactNode
actionLabel?: string
onAction?: () => void
variant?: 'plain' | 'card'
style?: Record<string, unknown>
}) {
const T = useTokens()
const card = variant === 'card'
const showAction = Boolean(actionLabel && onAction)
return (
<div
testId={testId}
Expand All @@ -32,10 +40,20 @@ export function EmptyState({
...style,
}}
>
{icon ? (
<div testId={testId ? `${testId}-icon` : undefined} style={{ fontSize: T.type.xl, color: T.tertiary }}>
{icon}
</div>
) : null}
<div style={{ fontSize: card ? T.type.sm : T.type.lg, lineHeight: card ? T.line.md : T.line.lg, color: T.secondary }}>
{title}
</div>
{detail ? <div style={{ fontSize: T.type.sm, color: T.ghost }}>{detail}</div> : null}
{showAction ? (
<Chip testId={testId ? `${testId}-action` : 'empty-state-action'} tone="action" onClick={onAction}>
{actionLabel}
</Chip>
) : null}
</div>
)
}
15 changes: 15 additions & 0 deletions src/chrome/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ export {
} from './activity'
export { Composer, COMPOSER_MAX_ROWS, COMPOSER_MIN_ROWS } from './composer'
export { EmptyState } from './empty-state'
export { ToastStack } from './toast'
export {
clearToasts,
dismissToast,
listToasts,
pushToast,
resetToastStore,
subscribeToasts,
toastLevelLabel,
toastTtlMs,
type PushToastInput,
type ToastAction,
type ToastEntry,
type ToastLevel,
} from './toast-store'
export { Sheet } from './sheet'
export { ToggleGroup, type ToggleOption } from './toggle-group'
export {
Expand Down
Loading
Loading