Skip to content

Commit febdf36

Browse files
GiniGini
authored andcommitted
feat: persist first-class navigation views
1 parent cc033c6 commit febdf36

4 files changed

Lines changed: 24 additions & 9 deletions

File tree

docs/IMPLEMENTATION-LOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
- Replaced the decorative Skills navigation with a usable capability library. Users can select up to four explicit working guides for new tasks; those IDs persist on the task, become server-side operating guidance, and create a `Skill packs attached` evidence event that explicitly states no permission change occurred.
4747
- Replaced the decorative Library navigation with a server-derived artifact index. It lists completed task outputs across projects while excluding raw `inputs/` and `evidence/` paths, and every card returns to the originating governed task rather than detaching artifacts from their evidence chain.
4848
- Added transparent queued steering for non-interruptible providers. Guidance submitted during an active turn is bounded, persisted, and recorded immediately; after a successful terminal state, ONEVibe resumes the same task with the next queued instruction. This is deliberately not represented as live prompt injection into a running Claude SDK or sandbox CLI process.
49+
- Made the first-class Skills, Library, and Scheduled surfaces URL-addressable (`?view=…`). Refresh and browser history now restore the intended surface and active navigation state instead of silently returning users to the home composer.
4950
- Added durable run identity to the event chain. `beginTurn` persists a run ID before the first event; every subsequent event—including Computer frames—binds it into the hash, and terminal events clear the active run. The Computer header exposes a concise run marker for replay across follow-ups and retries.
5051
- Replaced the hidden click-to-cycle composer mode control with an animated, keyboard-accessible nine-mode creation catalogue for Agent, Website, Slides, Document, Research, Data story, Design, App, and Game. Each option explains its output contract; the selected mode persists into normal task creation.
5152
- Added an expandable pre-delegation safety cue to the primary composer. It makes the secret-handling, untrusted-context, workspace-policy, and independent VTI Wallet boundaries legible at the point users supply a task.

docs/MANUS-PARITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This is the implementation gate, not a marketing checklist. **I** means behavior
1616
8. **I** Expandable task messages — long user and grouped assistant turns collapse and expand in place.
1717
9. **I** Structured waiting state — runtime parks durably, UI renders the focused request, and an answer resumes the same execution.
1818
10. **I** Terminal-input request state — native Claude can invoke the ONEVibe input MCP tool and receive the user's answer as its tool result.
19-
11. **I** Persistent task routes — `/tasks/:taskId` survives reload and browser navigation.
19+
11. **I** Persistent routes — `/tasks/:taskId` and the Skills, Library, and Scheduled surfaces survive reload and browser navigation through explicit view URLs.
2020
12. **I** Concurrent workspace plus conversation and Computer timeline — server-classified, run-bound, evidence-backed task events render as a scrub-able terminal, visual-frame, artifact, diff, preview, deck, and approval record beside the conversation. The rail supports explicit live follow/pause and keyboard step/scrub; authenticated sandbox execution now adds bounded five-second live X11 checkpoints. High-scale replay and deployed production visual capture remain P0 work.
2121
13. **I** Agent-mode entry point — primary ONEVibe surface.
2222
14. **I** Task history surface — durable turn-based chat history, timestamps/status, cursor pagination, full-text search, reload persistence, and evidence export.

src/App.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ const starterPrompts = [
2121
'Research a market and produce an evidence-backed report',
2222
'Create an internal tool with a governed approval flow',
2323
]
24+
type AppView = 'agent' | 'schedules' | 'skills' | 'library'
25+
const viewFromLocation = (): AppView => {
26+
const value = new URLSearchParams(window.location.search).get('view')
27+
return value === 'schedules' || value === 'skills' || value === 'library' ? value : 'agent'
28+
}
2429

2530
export default function App() {
2631
const shareId = window.location.pathname.match(/^\/share\/([^/]+)$/)?.[1]
@@ -29,7 +34,7 @@ export default function App() {
2934
const [schedules, setSchedules] = useState<TaskSchedule[]>([])
3035
const [library, setLibrary] = useState<LibraryItem[]>([])
3136
const [activeProjectId, setActiveProjectId] = useState('project_onevibe')
32-
const [view, setView] = useState<'agent' | 'schedules' | 'skills' | 'library'>('agent')
37+
const [view, setView] = useState<AppView>(viewFromLocation)
3338
const [selectedSkills, setSelectedSkills] = useState<TaskSkill[]>([])
3439
const [activeTaskId, setActiveTaskId] = useState<string | null>(() => window.location.pathname.match(/^\/tasks\/([^/]+)$/)?.[1] ?? null)
3540
const [creating, setCreating] = useState(false)
@@ -46,7 +51,10 @@ export default function App() {
4651
useEffect(() => { void listSchedules().then(({ schedules }) => setSchedules(schedules)) }, [])
4752
useEffect(() => { void listProjects().then(({ projects }) => { setProjects(projects); if (!projects.some((project) => project.id === activeProjectId)) setActiveProjectId(projects[0]?.id ?? 'project_onevibe') }) }, [activeProjectId])
4853
useEffect(() => {
49-
const onPopState = () => setActiveTaskId(window.location.pathname.match(/^\/tasks\/([^/]+)$/)?.[1] ?? null)
54+
const onPopState = () => {
55+
setActiveTaskId(window.location.pathname.match(/^\/tasks\/([^/]+)$/)?.[1] ?? null)
56+
setView(viewFromLocation())
57+
}
5058
window.addEventListener('popstate', onPopState)
5159
return () => window.removeEventListener('popstate', onPopState)
5260
}, [])
@@ -73,6 +81,11 @@ export default function App() {
7381
setActiveTaskId(taskId)
7482
window.history.pushState({}, '', taskId ? `/tasks/${taskId}` : '/')
7583
}
84+
const navigateToView = (nextView: Exclude<AppView, 'agent'>) => {
85+
setActiveTaskId(null)
86+
setView(nextView)
87+
window.history.pushState({}, '', `/?view=${nextView}`)
88+
}
7689
const toggleSkill = (skill: TaskSkill) => setSelectedSkills((current) => current.includes(skill) ? current.filter((item) => item !== skill) : current.length >= 4 ? current : [...current, skill])
7790

7891
const addProject = async (name: string, context: string) => {
@@ -108,7 +121,7 @@ export default function App() {
108121

109122
return (
110123
<div className={`app-shell ${sidebarOpen ? '' : 'sidebar-collapsed'}`}>
111-
<AnimatePresence>{sidebarOpen && <motion.div initial={{ x: -260 }} animate={{ x: 0 }} exit={{ x: -260 }}><Sidebar tasks={tasks} activeTaskId={activeTaskId} onNewTask={() => navigateToTask(null)} onSelectTask={(taskId) => navigateToTask(taskId)} projects={projects} activeProjectId={activeProjectId} onSelectProject={setActiveProjectId} onCreateProject={addProject} onAttachProjectFile={attachProjectFile} onOpenSkills={() => { setActiveTaskId(null); setView('skills'); window.history.pushState({}, '', '/') }} onOpenLibrary={() => { setActiveTaskId(null); setView('library'); window.history.pushState({}, '', '/') }} onOpenSchedules={() => { setActiveTaskId(null); setView('schedules'); window.history.pushState({}, '', '/') }} /></motion.div>}</AnimatePresence>
124+
<AnimatePresence>{sidebarOpen && <motion.div initial={{ x: -260 }} animate={{ x: 0 }} exit={{ x: -260 }}><Sidebar view={view} tasks={tasks} activeTaskId={activeTaskId} onNewTask={() => navigateToTask(null)} onSelectTask={(taskId) => navigateToTask(taskId)} projects={projects} activeProjectId={activeProjectId} onSelectProject={setActiveProjectId} onCreateProject={addProject} onAttachProjectFile={attachProjectFile} onOpenSkills={() => navigateToView('skills')} onOpenLibrary={() => navigateToView('library')} onOpenSchedules={() => navigateToView('schedules')} /></motion.div>}</AnimatePresence>
112125
<main className="main-shell">
113126
<header className="topbar">
114127
<div className="topbar-left"><button className="icon-button" type="button" aria-label={sidebarOpen ? 'Collapse sidebar' : 'Open sidebar'} onClick={() => setSidebarOpen((value) => !value)}>{sidebarOpen ? <PanelLeftClose size={17} /> : <Menu size={17} />}</button><span className="model-selector"><Sparkles size={14} /> ONEVibe 0.1 <ChevronDown size={13} /></span></div>

src/components/Sidebar.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { searchChat } from '../lib/api'
66
import { BrandMark } from './BrandMark'
77

88
type Props = {
9+
view: 'agent' | 'schedules' | 'skills' | 'library'
910
tasks: Task[]
1011
activeTaskId: string | null
1112
onNewTask: () => void
@@ -20,7 +21,7 @@ type Props = {
2021
onOpenSchedules: () => void
2122
}
2223

23-
export const Sidebar = ({ tasks, activeTaskId, onNewTask, onSelectTask, projects, activeProjectId, onSelectProject, onCreateProject, onAttachProjectFile, onOpenSkills, onOpenLibrary, onOpenSchedules }: Props) => {
24+
export const Sidebar = ({ view, tasks, activeTaskId, onNewTask, onSelectTask, projects, activeProjectId, onSelectProject, onCreateProject, onAttachProjectFile, onOpenSkills, onOpenLibrary, onOpenSchedules }: Props) => {
2425
const [query, setQuery] = useState('')
2526
const [creatingProject, setCreatingProject] = useState(false)
2627
const [projectName, setProjectName] = useState('')
@@ -44,10 +45,10 @@ export const Sidebar = ({ tasks, activeTaskId, onNewTask, onSelectTask, projects
4445
<button className="new-task" onClick={onNewTask}><Plus size={16} /> New task <kbd>⌘ K</kbd></button>
4546
<label className="history-search"><Search size={13} /><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search conversations" /></label>
4647
<nav className="primary-nav" aria-label="Primary">
47-
<button className="nav-item active"><Sparkles size={16} /> Agent</button>
48-
<button className="nav-item" onClick={onOpenSkills}><Blocks size={16} /> Skills <span className="nav-pill">8</span></button>
49-
<button className="nav-item" onClick={onOpenSchedules}><Clock3 size={16} /> Scheduled</button>
50-
<button className="nav-item" onClick={onOpenLibrary}><Library size={16} /> Library</button>
48+
<button className={`nav-item ${view === 'agent' ? 'active' : ''}`} onClick={onNewTask}><Sparkles size={16} /> Agent</button>
49+
<button className={`nav-item ${view === 'skills' ? 'active' : ''}`} onClick={onOpenSkills}><Blocks size={16} /> Skills <span className="nav-pill">8</span></button>
50+
<button className={`nav-item ${view === 'schedules' ? 'active' : ''}`} onClick={onOpenSchedules}><Clock3 size={16} /> Scheduled</button>
51+
<button className={`nav-item ${view === 'library' ? 'active' : ''}`} onClick={onOpenLibrary}><Library size={16} /> Library</button>
5152
</nav>
5253
<div className="nav-section-label"><span>Projects</span><button aria-label="Create project" onClick={() => setCreatingProject((value) => !value)}><Plus size={13} /></button></div>
5354
{creatingProject && <form className="project-create" onSubmit={(event) => { event.preventDefault(); const name = projectName.trim(); if (!name) return; void onCreateProject(name, projectContext.trim()).then(() => { setProjectName(''); setProjectContext(''); setCreatingProject(false) }) }}><input autoFocus value={projectName} onChange={(event) => setProjectName(event.target.value)} placeholder="Project name" maxLength={100} /><textarea value={projectContext} onChange={(event) => setProjectContext(event.target.value)} placeholder="Governed brief (optional)" maxLength={8000} rows={2} /><button type="submit">Create project</button></form>}

0 commit comments

Comments
 (0)