diff --git a/README.md b/README.md index c222d4c..9604d98 100644 --- a/README.md +++ b/README.md @@ -111,10 +111,11 @@ while you use the phone. silently creating duplicate Copilot processes. - **Shared terminal** — open and resume one real shell on supported Windows laptops. See [terminal controls and access boundaries](docs/terminal.md). -- **Explore between turns** — open the permanent compass destination for local agent - activity streamed into a compact bottom dock, a balanced swipe-through deck of 50 - offline Discover cards, two lightweight games, guided rest activities, and an optional - explicitly loaded third-party short-video widget. +- **Explore between turns** — use compact header tiles for Discover, Watch, Play, and + Unwind while real assistant and tool activity rolls through the bottom Copilot dock. + Discover provides a balanced animated swipe deck of 50 offline cards, alongside two + lightweight games, guided rest activities, and an optional explicitly loaded + third-party short-video widget. ## Trust model diff --git a/mobile/src/ui/explore/ExploreScreen.tsx b/mobile/src/ui/explore/ExploreScreen.tsx index 13f93ea..2592a54 100644 --- a/mobile/src/ui/explore/ExploreScreen.tsx +++ b/mobile/src/ui/explore/ExploreScreen.tsx @@ -6,12 +6,13 @@ import { useState, } from 'react'; import type { + CSSProperties, JSX, KeyboardEvent as ReactKeyboardEvent, PointerEvent as ReactPointerEvent, } from 'react'; import type { SessionView } from '@/session/view'; -import type { TimelineItem, ToolItem } from '@/lib/timeline'; +import type { AssistantItem, TimelineItem, ToolItem } from '@/lib/timeline'; import { isWorking } from '@/ui/sessions/sessionStatus'; import { DISCOVER_CARDS, @@ -55,6 +56,13 @@ export interface LiveDockState { startedAt: number | null; } +export interface LiveDockActivity { + id: string; + text: string; + kind: 'assistant' | 'tool' | 'context'; + state: 'live' | 'complete' | 'error' | 'context'; +} + const CATEGORY_META: Record = { discover: { title: 'Discover', @@ -187,6 +195,86 @@ function dockExcerpt(value: string, max = 220): string { return `…${clean.slice(clean.length - max + 1)}`; } +function plainToolName(name: string): string { + return name.replace(/[_-]+/g, ' ').trim(); +} + +export function deriveLiveDockActivity( + active: SessionView, + dock: LiveDockState, +): LiveDockActivity[] { + if (dock.tone !== 'working') { + return [ + { + id: `${dock.tone}-primary`, + text: dock.text, + kind: 'context', + state: dock.tone === 'error' ? 'error' : dock.tone === 'ready' ? 'complete' : 'context', + }, + ...(dock.detail + ? [{ + id: `${dock.tone}-detail`, + text: dock.detail, + kind: 'context' as const, + state: 'context' as const, + }] + : []), + ]; + } + + const activity = active.timeline.items + .filter((item): item is AssistantItem | ToolItem => + (item.kind === 'assistant' && Boolean(item.text.trim())) || item.kind === 'tool', + ) + .slice(-6) + .map((item): LiveDockActivity => { + if (item.kind === 'assistant') { + return { + id: `assistant-${item.id}`, + text: dockExcerpt(item.text, 150), + kind: 'assistant', + state: item.final ? 'complete' : 'live', + }; + } + const activityLabel = toolLabel(item).replace(/ in progress$/, ''); + return { + id: `tool-${item.id}`, + text: + item.status === 'running' + ? toolLabel(item) + : item.status === 'success' + ? `${activityLabel} completed` + : `${activityLabel} failed`, + kind: 'tool', + state: + item.status === 'running' + ? 'live' + : item.status === 'success' + ? 'complete' + : 'error', + }; + }); + + const intent = active.intent?.trim(); + if (intent && activity.length < 3 && !activity.some((item) => item.text === intent)) { + activity.unshift({ + id: 'current-intent', + text: dockExcerpt(intent, 150), + kind: 'context', + state: 'context', + }); + } + if (activity.length === 0) { + activity.push({ + id: 'working-context', + text: dock.text, + kind: 'context', + state: 'live', + }); + } + return activity.slice(-3); +} + export function deriveLiveDock(active: SessionView, replyCompletedInExplore = false): LiveDockState { if (active.timeline.approvals.length > 0) { const count = active.timeline.approvals.length; @@ -235,7 +323,7 @@ export function deriveLiveDock(active: SessionView, replyCompletedInExplore = fa text: streamed ? dockExcerpt(streamed) : active.intent?.trim() || (running ? toolLabel(running) : 'Working in the active session'), - detail: running ? `Using ${running.name.replace(/[_-]+/g, ' ')}` : active.intent?.trim() || null, + detail: running ? `Using ${plainToolName(running.name)}` : active.intent?.trim() || null, startedAt: active.thinkingSince ?? running?.startedAt ?? active.timeline.busyFrom, }; } @@ -281,15 +369,6 @@ function formatElapsed(startedAt: number | null, now: number): string | null { return minutes > 0 ? `${minutes}:${remainder.toString().padStart(2, '0')}` : `${seconds}s`; } -function CompassGlyph(): JSX.Element { - return ( - - ); -} - function CompassSpark(): JSX.Element { return (
- Explore
+ {view === null ? ( ) : ( - <> - -
- {view === 'discover' ? ( - - ) : view === 'watch' ? ( - - ) : view === 'play' ? ( - - ) : ( - - )} -
- +
+ {view === 'discover' ? ( + + ) : view === 'watch' ? ( + + ) : view === 'play' ? ( + + ) : ( + + )} +
)} - + ); @@ -486,24 +563,26 @@ function ExploreHome({ ); } -function CategoryTabs({ +function CategoryHeaderNav({ active, onSelect, }: { - active: ExploreCategory; + active: ExploreView; onSelect(category: ExploreCategory): void; }): JSX.Element { return ( -