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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ new version heading in the same commit.

## [Unreleased]

## [0.264.2] — 2026-07-24
### Changed
- **Task room tabs are deep-linked in the URL.** The hash detail becomes `<taskId>/<tab>`
(`discussion` | `description` | `session`), so refreshing or sharing a task URL lands on the right tab.
The tabs are real links and the active tab derives from the URL (a `/session` link on a task with no
session falls back to Discussion).

## [0.264.1] — 2026-07-24
### Fixed
- **The embedded Session terminal now fills the Session tab.** `TerminalFrame` is `flex-1`/`min-h-0` and
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.264.1",
"version": "0.264.2",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand Down
19 changes: 12 additions & 7 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7495,11 +7495,15 @@ function TasksPage({ me, agents, taskId, onOpen, nav }: { me: Member; agents: Ag
const [q, setQ] = useState('')
// Selection is URL-driven (#/tasks/<id>) so a task detail is a shareable permalink — pasting it opens
// the modal automatically. Opening a card just navigates; closing clears the detail segment.
const selId = taskId || null
// The hash detail is `<taskId>` or `<taskId>/<tab>` (tab ∈ discussion|description|session) — so the open
// task AND its room tab are deep-linked (refresh/share lands on the right tab).
const [routeTaskId, routeTab] = (taskId || '').split('/')
const selId = routeTaskId || null
const roomTab: 'discussion' | 'description' | 'session' = routeTab === 'description' || routeTab === 'session' ? routeTab : 'discussion'
const openTask = (id: string) => nav('tasks', id)
const openTaskTab = (id: string, tab: 'discussion' | 'description' | 'session') => nav('tasks', tab === 'discussion' ? id : `${id}/${tab}`)
const closeTask = () => { setEditing(false); nav('tasks') }
const [detail, setDetail] = useState<{ task: Task; events: TaskEvent[]; attachments: TaskAttachment[]; dependents: string[]; discussion: TaskTimelineEntry[]; unread: number } | null>(null)
const [roomTab, setRoomTab] = useState<'discussion' | 'description' | 'session'>('discussion')
const [busy, setBusy] = useState(false)
const [hint, setHint] = useState('')
// view + filters
Expand Down Expand Up @@ -7611,7 +7615,7 @@ function TasksPage({ me, agents, taskId, onOpen, nav }: { me: Member; agents: Ag
if (editing) return // don't overwrite an in-progress edit on a background refresh
api.task(selId).then((r) => { if (r.task) setDetail({ task: r.task, events: r.events ?? [], attachments: r.attachments ?? [], dependents: r.dependents ?? [], discussion: r.discussion ?? [], unread: r.unread ?? 0 }) })
}, [selId, tasks, editing])
useEffect(() => { setEditing(false); setConfirmDel(false); setRoomTab('discussion') }, [selId]) // fresh drawer per selection
useEffect(() => { setEditing(false); setConfirmDel(false) }, [selId]) // fresh drawer per selection

// Client-side filtering over the (≤500) board — cheap, and keeps the lens shareable via UI state.
const labelsPresent = [...new Set((tasks ?? []).flatMap((t) => t.labels))].sort()
Expand Down Expand Up @@ -7921,8 +7925,9 @@ function TasksPage({ me, agents, taskId, onOpen, nav }: { me: Member; agents: Ag
const parts = discussions[t.id]?.participants ?? []
const live = liveOf(t)
const sessTmux = live?.tmux || (t.lastSessionId ? 'aos-' + t.lastSessionId : '')
const activeTab: 'discussion' | 'description' | 'session' = roomTab === 'session' && !sessTmux ? 'discussion' : roomTab
const roomTab_btn = (id: 'discussion' | 'description' | 'session', label: ReactNode) => (
<button onClick={() => setRoomTab(id)} className={`-mb-px flex items-center gap-1.5 border-b-2 px-3 py-2 text-[13px] font-medium transition-colors ${roomTab === id ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'}`}>{label}</button>
<a href={navHref('tasks', id === 'discussion' ? t.id : `${t.id}/${id}`)} onClick={onNavClick(() => openTaskTab(t.id, id))} className={`-mb-px flex items-center gap-1.5 border-b-2 px-3 py-2 text-[13px] font-medium no-underline transition-colors ${activeTab === id ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'}`}>{label}</a>
)
return (
<div className="flex h-[calc(100vh-5.5rem)] flex-col overflow-hidden rounded-lg border bg-background">
Expand All @@ -7944,15 +7949,15 @@ function TasksPage({ me, agents, taskId, onOpen, nav }: { me: Member; agents: Ag
{sessTmux && roomTab_btn('session', <><TerminalSquare className="h-3.5 w-3.5" />Session{live && <span className="ml-0.5 h-1.5 w-1.5 rounded-full bg-sky-500 motion-safe:animate-pulse" />}</>)}
</div>
<div className="min-h-0 flex-1 overflow-hidden">
{roomTab === 'discussion' && <div className="h-full px-4 pt-3"><TaskDiscussion pinned taskId={t.id} entries={detail.discussion} unread={detail.unread} me={me} members={members} agents={agents} onChange={() => refreshDetail(t.id)} /></div>}
{roomTab === 'description' && (
{activeTab === 'discussion' && <div className="h-full px-4 pt-3"><TaskDiscussion pinned taskId={t.id} entries={detail.discussion} unread={detail.unread} me={me} members={members} agents={agents} onChange={() => refreshDetail(t.id)} /></div>}
{activeTab === 'description' && (
<div className="h-full overflow-y-auto p-4">
{t.body
? <div className="break-words text-sm [&_pre]:whitespace-pre-wrap [&_pre]:break-words"><ReactMarkdown remarkPlugins={[remarkGfm, remarkWikiLinks]} components={mdComponents}>{t.body}</ReactMarkdown></div>
: <div className="flex h-full flex-col items-center justify-center gap-2 text-center text-sm text-muted-foreground"><FileText className="h-6 w-6 opacity-40" />No description. <button className="text-primary underline" onClick={() => setEditing(true)}>Add one</button></div>}
</div>
)}
{roomTab === 'session' && sessTmux && <div className="flex h-full min-h-0 flex-col"><TerminalFrame session={live ?? undefined} tmux={sessTmux} standalone /></div>}
{activeTab === 'session' && sessTmux && <div className="flex h-full min-h-0 flex-col"><TerminalFrame session={live ?? undefined} tmux={sessTmux} standalone /></div>}
</div>
</div>
<div className="overflow-y-auto bg-muted/20 p-4">
Expand Down
Loading