diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index 1f9edd6d..a56923a9 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -13,9 +13,12 @@ import { useWorkspace } from './tabs/store'
/**
* Activity-bar pages — only items that appear as icons in the ActivityBar.
* Each maps to one or more tab kinds via tabs/registry.ts (defaultSpecForActivity).
+ *
+ * Diary is intentionally absent — it lives under the Chat sidebar as a
+ * peer of Notifications (both are "Alice surfaces, not user actions").
*/
export type Page =
- | 'chat' | 'diary' | 'portfolio' | 'news' | 'automation' | 'market'
+ | 'chat' | 'portfolio' | 'news' | 'automation' | 'market'
| 'trading-as-git'
| 'settings' | 'dev'
diff --git a/ui/src/components/ActivityBar.tsx b/ui/src/components/ActivityBar.tsx
index d97832ed..fb3bb7b9 100644
--- a/ui/src/components/ActivityBar.tsx
+++ b/ui/src/components/ActivityBar.tsx
@@ -1,4 +1,4 @@
-import { type LucideIcon, MessageSquare, LineChart, GitBranch, BarChart3, Newspaper, Notebook, Zap, Settings, Code2 } from 'lucide-react'
+import { type LucideIcon, MessageSquare, LineChart, GitBranch, BarChart3, Newspaper, Zap, Settings, Code2 } from 'lucide-react'
import { type Page } from '../App'
import { useWorkspace } from '../tabs/store'
import type { ActivitySection, ViewSpec } from '../tabs/types'
@@ -17,7 +17,6 @@ function activitySectionFor(page: Page): ActivitySection {
case 'portfolio': return 'portfolio'
case 'automation': return 'automation'
case 'news': return 'news'
- case 'diary': return 'diary'
}
}
@@ -62,7 +61,6 @@ const NAV_SECTIONS: NavSection[] = [
{ page: 'trading-as-git', label: 'Trading as Git', icon: GitBranch },
{ page: 'market', label: 'Market', icon: BarChart3 },
{ page: 'news', label: 'News', icon: Newspaper, defaultTab: { kind: 'news', params: {} } },
- { page: 'diary', label: 'Diary', icon: Notebook, defaultTab: { kind: 'diary', params: {} } },
],
},
{
diff --git a/ui/src/components/ChatChannelListContainer.tsx b/ui/src/components/ChatChannelListContainer.tsx
index 7d6348c2..5836c241 100644
--- a/ui/src/components/ChatChannelListContainer.tsx
+++ b/ui/src/components/ChatChannelListContainer.tsx
@@ -1,3 +1,4 @@
+import { Bell, Notebook } from 'lucide-react'
import { useChannels } from '../contexts/ChannelsContext'
import { useWorkspace } from '../tabs/store'
import { getFocusedTab } from '../tabs/types'
@@ -8,29 +9,38 @@ import { SidebarRow } from './SidebarRow'
/**
* Connects ChatChannelList to ChannelsContext + the workspace store.
*
- * Layout: a Notifications inbox row (with an unread badge) sits at the
- * top of the chat sidebar — clicking it opens the notifications inbox
- * as a tab. The user lives in this sidebar most of the time, so the
- * red dot here is the primary "you have new system pushes" signal.
+ * Layout reflects the framing of "Chat" as the catch-all activity for
+ * interactions with Alice — not strictly chat:
*
- * Below: the channel list. Active channel is derived from the focused
- * chat tab; when the focused tab isn't a chat tab, no row is highlighted.
+ * - Notifications (inbound system pushes; unread badge)
+ * - Diary (Alice's first-person output stream — read-only)
+ * ─────
+ * - Channels (the chat conversations the user opens)
+ *
+ * The two upper rows are "Alice surfaces"; the channel list is "user
+ * actions". They share this sidebar because the unifying mental model
+ * is "everything Alice-shaped" rather than "places to type messages".
+ *
+ * Active row tracking is derived from the focused tab — switching tabs
+ * naturally shifts the highlight without bespoke wiring.
*/
export function ChatChannelListContainer() {
const { channels, openEditDialog, deleteChannel } = useChannels()
const focused = useWorkspace((state) => getFocusedTab(state)?.spec)
- const focusedChannelId = focused?.kind === 'chat' ? focused.params.channelId : ''
- const inboxActive = focused?.kind === 'notifications-inbox'
+ const focusedKind = focused?.kind
+ const focusedChannelId = focusedKind === 'chat' ? focused.params.channelId : ''
+ const inboxActive = focusedKind === 'notifications-inbox'
+ const diaryActive = focusedKind === 'diary'
const openOrFocus = useWorkspace((state) => state.openOrFocus)
const unreadCount = useUnreadNotificationsCount()
return (
-
+
-
+
Notifications
}
@@ -47,9 +57,22 @@ export function ChatChannelListContainer() {
) : undefined
}
/>
+
+
+ Diary
+
+ }
+ active={diaryActive}
+ onClick={() => openOrFocus({ kind: 'diary', params: {} })}
+ />
-
+
+ Channels
+
+
)
}
-
-function BellIcon() {
- return (
-
- )
-}
diff --git a/ui/src/components/DiarySidebar.tsx b/ui/src/components/DiarySidebar.tsx
deleted file mode 100644
index 68d3c487..00000000
--- a/ui/src/components/DiarySidebar.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useWorkspace } from '../tabs/store'
-import { getFocusedTab } from '../tabs/types'
-import { SidebarRow } from './SidebarRow'
-
-/**
- * Diary sidebar — phase-2 placeholder. Single "All Entries" item that
- * opens the existing DiaryPage as a tab. Phase 3+ replaces this with a
- * date-organised navigator that opens per-day tabs.
- */
-export function DiarySidebar() {
- const focusedKind = useWorkspace((state) => getFocusedTab(state)?.spec.kind ?? null)
- const openOrFocus = useWorkspace((state) => state.openOrFocus)
-
- return (
-
- openOrFocus({ kind: 'diary', params: {} })}
- />
-
- )
-}
diff --git a/ui/src/pages/DiaryPage.tsx b/ui/src/pages/DiaryPage.tsx
index aac7fdc6..e8d5d109 100644
--- a/ui/src/pages/DiaryPage.tsx
+++ b/ui/src/pages/DiaryPage.tsx
@@ -107,7 +107,11 @@ interface CycleGroup {
/**
* Walk cycles ascending and attach each session item to the cycle whose timestamp
- * window it falls into: (prev.ts, cycle.ts + slack].
+ * window it falls into: (prev.ts, cycle.ts + slack]. The result is reversed
+ * before returning so the rendered feed reads newest-first — Diary is an
+ * observation surface (feed-shaped), not a chat (bottom-anchored conversation).
+ * Items inside a single cycle stay ascending: within one heartbeat tick the
+ * thought-then-tool-call order is the natural read.
*/
function groupItemsByCycle(items: ChatHistoryItem[], cycles: DiaryCycle[]): CycleGroup[] {
const sorted = [...cycles].sort((a, b) => a.ts - b.ts)
@@ -129,7 +133,7 @@ function groupItemsByCycle(items: ChatHistoryItem[], cycles: DiaryCycle[]): Cycl
groups[cursor].items.push(item)
}
- return groups
+ return groups.reverse()
}
// ==================== Page ====================
@@ -144,9 +148,11 @@ export function DiaryPage() {
const latestSeqRef = useRef(0)
latestSeqRef.current = latestSeq
- const messagesEndRef = useRef(null)
const containerRef = useRef(null)
- const userScrolledUp = useRef(false)
+ /** True once the user has scrolled away from the top — drives the
+ * "back to top" floating button. Top of list is newest, so the
+ * button is conceptually a "jump to newest", same UX as Twitter's
+ * "new tweets ↑". */
const [showScrollBtn, setShowScrollBtn] = useState(false)
const fetchFull = useCallback(async () => {
@@ -206,23 +212,13 @@ export function DiaryPage() {
return () => clearInterval(id)
}, [fetchDelta])
- // Auto-scroll to bottom on new content, unless user scrolled up
- const scrollToBottom = useCallback((behavior: ScrollBehavior = 'auto') => {
- if (!userScrolledUp.current) {
- messagesEndRef.current?.scrollIntoView({ behavior })
- }
- }, [])
- useEffect(() => { scrollToBottom() }, [items, cycles, scrollToBottom])
-
- // Track scroll position
+ // Track scroll position so we can offer a "back to top (newest)"
+ // floating button once the user has scrolled into history.
useEffect(() => {
const el = containerRef.current
if (!el) return
const onScroll = () => {
- const { scrollTop, scrollHeight, clientHeight } = el
- const isUp = scrollHeight - scrollTop - clientHeight > 80
- userScrolledUp.current = isUp
- setShowScrollBtn(isUp)
+ setShowScrollBtn(el.scrollTop > 80)
}
el.addEventListener('scroll', onScroll)
return () => el.removeEventListener('scroll', onScroll)
@@ -235,10 +231,9 @@ export function DiaryPage() {
fetchFull()
}, [fetchFull])
- const handleScrollToBottom = useCallback(() => {
- userScrolledUp.current = false
+ const handleScrollToTop = useCallback(() => {
setShowScrollBtn(false)
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
+ containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
}, [])
// Render: walk groups and emit a date divider whenever the calendar day changes.
@@ -320,19 +315,18 @@ export function DiaryPage() {
{node.render()}
))}
-
-
{showScrollBtn && (
)}
diff --git a/ui/src/sections.tsx b/ui/src/sections.tsx
index 1eec6179..45ea9740 100644
--- a/ui/src/sections.tsx
+++ b/ui/src/sections.tsx
@@ -21,7 +21,6 @@ import { MarketSidebar } from './components/MarketSidebar'
import { PortfolioSidebar } from './components/PortfolioSidebar'
import { AutomationSidebar } from './components/AutomationSidebar'
import { NewsSidebar } from './components/NewsSidebar'
-import { DiarySidebar } from './components/DiarySidebar'
import type { ActivitySection } from './tabs/types'
export interface SidebarSection {
@@ -67,10 +66,6 @@ const SECTION_BY_KEY: Record
= {
title: 'News',
Secondary: NewsSidebar,
},
- diary: {
- title: 'Diary',
- Secondary: DiarySidebar,
- },
}
/** Resolve the sidebar config for the currently selected ActivitySection. */
diff --git a/ui/src/tabs/types.ts b/ui/src/tabs/types.ts
index 299838f0..1d66f4ab 100644
--- a/ui/src/tabs/types.ts
+++ b/ui/src/tabs/types.ts
@@ -37,6 +37,11 @@ export type ViewKind = ViewSpec['kind']
* Note: trading-as-git has no associated tab kind — it's sidebar-only
* (the approval queue lives in the sidebar; future commit-detail tabs will
* be opened from there).
+ *
+ * Diary deliberately does NOT have its own activity section — it's a
+ * read-only Alice-output surface, conceptually grouped with Chat under
+ * "interactions with Alice" (chat sidebar carries an entry that opens
+ * the Diary tab).
*/
export type ActivitySection =
| 'chat'
@@ -47,7 +52,6 @@ export type ActivitySection =
| 'portfolio'
| 'automation'
| 'news'
- | 'diary'
export interface Tab {
id: string