Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
85655c0
Refine task conversations
klopez4212 Jun 26, 2026
c1cd1f1
Fix channel task surface review issues
klopez4212 Jun 27, 2026
82caa72
Fix task link navigation
klopez4212 Jun 27, 2026
711fd65
Fix task source navigation fallback
klopez4212 Jun 27, 2026
7e7f20c
Route threaded tasks from loaded message metadata
klopez4212 Jun 27, 2026
891e97a
Autolink bare task URLs
klopez4212 Jun 27, 2026
354b1b8
Reset task surface for message targets
klopez4212 Jun 27, 2026
9c2f9a9
Stabilize virtualized timeline bottom pinning
klopez4212 Jun 27, 2026
5edb57e
Stabilize channel surface smoke tests
klopez4212 Jun 27, 2026
69af99a
Harden scroll E2E timing
klopez4212 Jun 28, 2026
9deef79
Stabilize scroll history header check
klopez4212 Jun 28, 2026
86e6db1
Guard task surfaces and route targets
klopez4212 Jun 28, 2026
cd636b3
Address task list review feedback
klopez4212 Jun 28, 2026
2b8ced7
Keep task timeline scroll node stable
klopez4212 Jun 28, 2026
7576016
Fix task paging and scroll settling
klopez4212 Jun 28, 2026
0c5ca1f
Fix task route targets and scroll checks
klopez4212 Jun 28, 2026
1495948
Fix task conversation scroll reset
klopez4212 Jun 28, 2026
7ef2a4c
Add task link cards (#1325)
klopez4212 Jun 29, 2026
c3d6efa
Fix task agent routing and scroll restore
klopez4212 Jun 29, 2026
80cfc27
Fix task message routing fallbacks
klopez4212 Jun 29, 2026
846891b
Fix rebased task channel checks
klopez4212 Jun 29, 2026
445da25
Allow task list paging from marker rows
klopez4212 Jun 29, 2026
5dba68b
Fix task channel smoke regressions
klopez4212 Jun 29, 2026
97a3962
Seed task link context into channel cache
klopez4212 Jun 29, 2026
07b8f35
Fix wave agent target classification
klopez4212 Jun 29, 2026
0558c09
Require real task thread roots
klopez4212 Jun 29, 2026
7d174a3
Wait for profile agent lookup before waving
klopez4212 Jun 29, 2026
c2132dd
Stop auto-paging empty task lists
klopez4212 Jun 29, 2026
51cc6be
Fix profile agent huddle routing
klopez4212 Jun 29, 2026
3488c7f
Preserve user scroll anchors during prepends
klopez4212 Jun 30, 2026
d7c5b0d
Wait for wave huddle agent lookup
klopez4212 Jun 30, 2026
b31c6e9
Update channel task file size guard
klopez4212 Jun 30, 2026
c1842df
Include flagged wave agent targets
klopez4212 Jun 30, 2026
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
25 changes: 18 additions & 7 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ const overrides = new Map([
["src-tauri/src/nostr_convert.rs", 1126],
["src/shared/api/relayClientSession.ts", 1022],
["src-tauri/src/migration.rs", 1449],
// persona-events rebase: boot-time event-sync wiring (run_boot_migrations
// syncs team-dir edits before all personas.json readers; run_event_sync
// signs the persona/team retention events post-identity) layered on top of
// main's growth. continued-agent-conversations: task deep-link parsing and
// regression tests. Load-bearing feature growth, queued to split with the list.
["src-tauri/src/lib.rs", 1092],
// onMarkRead + isUnread prop threading (mirrors the onMarkUnread prop
// already here) for the single-toggle mark-read/unread menu item — a small
// overage from load-bearing per-message plumbing, not generic debt growth.
Expand All @@ -134,12 +140,15 @@ const overrides = new Map([
// continued-agent-conversations: persisted channel-scoped conversation state
// and route wiring. Queued to split with the rest of AppShell state.
["src/app/AppShell.tsx", 1060],
// continued-agent-conversations: marker filtering, thread handoff, and
// activity handoff props live at the channel surface for now.
["src/features/channels/ui/ChannelPane.tsx", 1107],
// continued-agent-conversations: channel task/message surface routing is
// threaded through the screen while the pane split follow-up is pending.
["src/features/channels/ui/ChannelScreen.tsx", 1027],
// continued-agent-conversations: marker filtering, tasks tab list/focus
// behavior, thread handoff, and activity handoff props live at the channel
// surface for now.
["src/features/channels/ui/ChannelPane.tsx", 1415],
// continued-agent-conversations: channel task-tab state, deep-link task
// routing, side-panel suppression, and task-link route target plumbing sit at
// the channel orchestration seam.
// latest main rebase threads additional header routing through this seam.
["src/features/channels/ui/ChannelScreen.tsx", 1122],
// continued-agent-conversations: composer notice banner for read-only agent
// conversations.
["src/features/messages/ui/MessageComposer.tsx", 1010],
Expand All @@ -160,7 +169,9 @@ const overrides = new Map([
["src/features/channels/readState/readStateManager.ts", 1030],
// Shared UI was added to this guard after splitting globals/markdown so
// large shared renderers cannot grow further while follow-up splits land.
["src/shared/ui/markdown.tsx", 2082],
// continued-agent-conversations: task-link card renderer and marker lookup
// are temporarily housed here until markdown renderers are split further.
["src/shared/ui/markdown.tsx", 2258],
["src/shared/ui/VideoPlayer.tsx", 2199],
["src/shared/ui/sidebar.tsx", 1042],
// Option C databricks-model-discovery: parse/HTTP logic moved to buzz-agent
Expand Down
60 changes: 59 additions & 1 deletion desktop/src-tauri/src/deep_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,35 @@ fn parse_message_deep_link(url: &Url) -> Option<serde_json::Value> {
}))
}

/// Parse the query string of a `buzz://task?…` URL into the JSON payload
/// emitted on `deep-link-agent-conversation`.
fn parse_task_deep_link(url: &Url) -> Option<serde_json::Value> {
let mut channel: Option<String> = None;
let mut reply: Option<String> = None;
for (k, v) in url.query_pairs() {
let v = v.into_owned();
if v.is_empty() {
continue;
}
match k.as_ref() {
"channel" => channel = Some(v),
"reply" => reply = Some(v),
_ => {}
}
}
let (channel_id, agent_reply_id) = (channel?, reply?);
Some(serde_json::json!({
"channelId": channel_id,
"agentReplyId": agent_reply_id,
}))
}

/// Handle an incoming `buzz://` deep link URL.
///
/// Currently supports:
/// - `buzz://connect?relay=<ws(s)://...>` — emits `deep-link-connect` to the frontend
/// - `buzz://message?channel=<uuid>&id=<eventId>[&thread=<rootId>]` — emits `deep-link-message`
/// - `buzz://task?channel=<uuid>&reply=<eventId>` — emits `deep-link-agent-conversation`
pub(crate) fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
let url = match Url::parse(url_str) {
Ok(u) => u,
Expand Down Expand Up @@ -93,6 +118,13 @@ pub(crate) fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
};
let _ = app.emit("deep-link-message", payload);
}
Some("task") => {
let Some(payload) = parse_task_deep_link(&url) else {
eprintln!("buzz-desktop: task deep link missing channel or reply: {url_str}");
return;
};
let _ = app.emit("deep-link-agent-conversation", payload);
}
Some(action) => {
eprintln!("buzz-desktop: unknown deep link action: {action}");
}
Expand All @@ -106,7 +138,7 @@ pub(crate) fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
mod tests {
use url::Url;

use super::parse_message_deep_link;
use super::{parse_message_deep_link, parse_task_deep_link};

#[test]
fn parse_message_deep_link_extracts_required_params() {
Expand Down Expand Up @@ -157,4 +189,30 @@ mod tests {
let payload = parse_message_deep_link(&url).expect("required params present");
assert!(payload["threadRootId"].is_null());
}

#[test]
fn parse_task_deep_link_extracts_required_params() {
let url = Url::parse("buzz://task?channel=abc&reply=xyz").unwrap();
let payload = parse_task_deep_link(&url).expect("required params present");
assert_eq!(payload["channelId"], "abc");
assert_eq!(payload["agentReplyId"], "xyz");
}

#[test]
fn parse_task_deep_link_rejects_missing_reply() {
let url = Url::parse("buzz://task?channel=abc").unwrap();
assert!(parse_task_deep_link(&url).is_none());
}

#[test]
fn parse_task_deep_link_rejects_empty_channel() {
let url = Url::parse("buzz://task?channel=&reply=xyz").unwrap();
assert!(parse_task_deep_link(&url).is_none());
}

#[test]
fn parse_task_deep_link_rejects_empty_reply() {
let url = Url::parse("buzz://task?channel=abc&reply=").unwrap();
assert!(parse_task_deep_link(&url).is_none());
}
}
18 changes: 11 additions & 7 deletions desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export function useAppNavigation() {
options?: {
messageId?: string;
replace?: boolean;
taskReplyId?: string;
threadRootId?: string | null;
},
) =>
Expand All @@ -144,16 +145,19 @@ export function useAppNavigation() {
params: {
channelId,
},
search: options?.messageId
? {
messageId: options.messageId,
threadRootId: options.threadRootId ?? undefined,
}
: {},
search:
options?.messageId || options?.taskReplyId
? {
messageId: options.messageId,
taskReplyId: options.taskReplyId,
threadRootId: options.threadRootId ?? undefined,
}
: {},
},
{
replace: options?.replace,
resetScroll: options?.messageId ? true : undefined,
resetScroll:
options?.messageId || options?.taskReplyId ? true : undefined,
},
),
[commitNavigation],
Expand Down
82 changes: 78 additions & 4 deletions desktop/src/app/routes/ChannelRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { useQueryClient } from "@tanstack/react-query";

import { getCachedSearchHitEvent } from "@/app/navigation/searchHitEventCache";
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
Expand All @@ -8,21 +9,30 @@ import {
getThreadReference,
isBroadcastReply,
} from "@/features/messages/lib/threading";
import { mergeTimelineCacheMessages } from "@/features/messages/hooks";
import { channelMessagesKey } from "@/features/messages/lib/messageQueryKeys";
import { useProfileQuery } from "@/features/profile/hooks";
import { relayClient } from "@/shared/api/relayClient";
import { useIdentityQuery } from "@/shared/api/hooks";
import { getEventById } from "@/shared/api/tauri";
import type { RelayEvent } from "@/shared/api/types";
import {
CHANNEL_TIMELINE_CONTENT_KINDS,
CHANNEL_TIMELINE_STATE_KINDS,
} from "@/shared/constants/kinds";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

type ChannelRouteScreenProps = {
channelId: string;
selectedPostId: string | null;
targetAgentConversationReplyId: string | null;
targetMessageId: string | null;
targetReplyId: string | null;
targetThreadRootId: string | null;
};

const MAX_ROUTE_ANCESTOR_HOPS = 50;
const MAX_ROUTE_TASK_EVENTS = 1000;

async function fetchRouteEvent(eventId: string): Promise<RelayEvent | null> {
try {
Expand All @@ -42,8 +52,10 @@ function getReplyParentId(event: RelayEvent): string | null {
}

async function fetchRouteTargetEvents(
channelId: string,
eventIds: string[],
targetMessageId: string | null,
targetAgentConversationReplyId: string | null,
targetThreadRootId: string | null,
): Promise<RelayEvent[]> {
const eventsById = new Map<string, RelayEvent>();
Expand All @@ -67,11 +79,37 @@ async function fetchRouteTargetEvents(
}

const targetThreadRef = getThreadReference(targetEvent.tags);
const threadRootId = targetThreadRootId ?? targetThreadRef.rootId ?? null;
const threadRootId =
targetThreadRootId ??
targetThreadRef.rootId ??
(targetAgentConversationReplyId ? targetEvent.id : null);
if (threadRootId && !eventsById.has(threadRootId)) {
addEvent(await fetchRouteEvent(threadRootId));
}

if (targetAgentConversationReplyId && threadRootId) {
Comment thread
klopez4212 marked this conversation as resolved.
try {
const taskEvents = await relayClient.fetchEvents({
"#e": [threadRootId],
"#h": [channelId],
kinds: [
...CHANNEL_TIMELINE_CONTENT_KINDS,
...CHANNEL_TIMELINE_STATE_KINDS,
],
limit: MAX_ROUTE_TASK_EVENTS,
});
for (const event of taskEvents) {
addEvent(event);
}
Comment thread
klopez4212 marked this conversation as resolved.
} catch (error) {
console.error(
"Failed to load route task conversation",
targetAgentConversationReplyId,
error,
);
}
}

let parentId = getReplyParentId(targetEvent);
let guard = 0;
while (
Expand All @@ -93,13 +131,25 @@ async function fetchRouteTargetEvents(
return [...eventsById.values()];
}

function mergeRouteEvents(
currentEvents: RelayEvent[] | undefined,
routeEvents: RelayEvent[],
): RelayEvent[] {
return routeEvents.reduce(
(messages, event) => mergeTimelineCacheMessages(messages, event),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve old task events when seeding route cache

Fresh evidence after the task-backfill fix is that the route-fetched events are seeded through mergeTimelineCacheMessages, which normalizes the channel cache with the newest-content cap. In a long-lived channel whose cache already has the 2,000 newest content events, an older task link can have its fetched source/root events discarded while recent descendants remain; AgentConversationScreen then sees a non-empty but partial scoped cache and opens the task without the source/early replies. Use an uncapped merge for route-pinned task context or merge the fetched context directly into the conversation source before leaving the route.

Useful? React with 👍 / 👎.

currentEvents ?? [],
);
}

export function ChannelRouteScreen({
channelId,
selectedPostId,
targetAgentConversationReplyId,
targetMessageId,
targetReplyId,
targetThreadRootId,
}: ChannelRouteScreenProps) {
const queryClient = useQueryClient();
const { closeForumPost, goForumPost } = useAppNavigation();
const channelsQuery = useChannelsQuery();
const identityQuery = useIdentityQuery();
Expand Down Expand Up @@ -140,14 +190,23 @@ export function ChannelRouteScreen({
// deep-linked message, the spliced event is the only copy — dropping it on
// param-clear blanks the timeline. Resetting on channel / forum-post change
// is handled by the effect below; here we only fetch when there's a target.
if ((!targetMessageId && !targetThreadRootId) || selectedPostId) {
if (
(!targetAgentConversationReplyId &&
!targetMessageId &&
!targetThreadRootId) ||
selectedPostId
) {
return () => {
isCancelled = true;
};
}

const cachedTarget = getCachedSearchHitEvent(targetMessageId);
if (cachedTarget) {
queryClient.setQueryData<RelayEvent[]>(
channelMessagesKey(channelId),
(currentEvents) => mergeRouteEvents(currentEvents, [cachedTarget]),
);
setTargetMessageEvents((currentEvents) =>
currentEvents.some((event) => event.id === cachedTarget.id)
? currentEvents
Expand All @@ -156,18 +215,25 @@ export function ChannelRouteScreen({
}

const eventIds = [
targetAgentConversationReplyId,
Comment thread
klopez4212 marked this conversation as resolved.
targetMessageId,
targetThreadRootId && targetThreadRootId !== targetMessageId
? targetThreadRootId
: null,
].filter((eventId): eventId is string => eventId !== null);

void fetchRouteTargetEvents(
channelId,
eventIds,
targetMessageId,
targetAgentConversationReplyId ?? targetMessageId,
targetAgentConversationReplyId,
targetThreadRootId,
).then((events) => {
if (!isCancelled) {
queryClient.setQueryData<RelayEvent[]>(
channelMessagesKey(channelId),
(currentEvents) => mergeRouteEvents(currentEvents, events),
);
setTargetMessageEvents((currentEvents) => {
const eventsById = new Map<string, RelayEvent>();
for (const event of [...currentEvents, ...events]) {
Expand All @@ -181,7 +247,14 @@ export function ChannelRouteScreen({
return () => {
isCancelled = true;
};
}, [selectedPostId, targetMessageId, targetThreadRootId]);
}, [
selectedPostId,
channelId,
queryClient,
targetAgentConversationReplyId,
targetMessageId,
targetThreadRootId,
]);

if (channelsQuery.isPending && !activeChannel) {
return (
Expand All @@ -204,6 +277,7 @@ export function ChannelRouteScreen({
void goForumPost(channelId, postId);
}}
selectedForumPostId={selectedPostId}
targetAgentConversationReplyId={targetAgentConversationReplyId}
targetForumReplyId={targetReplyId}
targetMessageEvents={targetMessageEvents}
targetMessageId={targetMessageId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function ForumPostRouteComponent() {
<ChannelRouteScreen
channelId={channelId}
selectedPostId={postId}
targetAgentConversationReplyId={null}
targetMessageId={null}
targetReplyId={search.replyId ?? null}
targetThreadRootId={null}
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/app/routes/channels.$channelId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ChannelRouteSearch = {
profile?: string;
profileTab?: ProfilePanelTab;
profileView?: ProfilePanelView;
taskReplyId?: string;
thread?: string;
threadRootId?: string;
};
Expand All @@ -32,6 +33,7 @@ function validateChannelSearch(
profile: nonEmptyString(search.profile),
profileTab: parseProfilePanelTab(search.profileTab) ?? undefined,
profileView: parseProfilePanelView(search.profileView) ?? undefined,
taskReplyId: nonEmptyString(search.taskReplyId),
thread: nonEmptyString(search.thread),
threadRootId: nonEmptyString(search.threadRootId),
};
Expand All @@ -58,6 +60,7 @@ function ChannelRouteComponent() {
<ChannelRouteScreen
channelId={channelId}
selectedPostId={null}
targetAgentConversationReplyId={search.taskReplyId ?? null}
targetMessageId={search.messageId ?? null}
targetReplyId={null}
targetThreadRootId={search.threadRootId ?? search.thread ?? null}
Expand Down
Loading