Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a0c29ef
Add task link cards (#1325)
klopez4212 Jun 29, 2026
b72824e
Remove automatic agent reply routing
klopez4212 Jun 26, 2026
0c22bf4
Preserve task link context routing
klopez4212 Jun 27, 2026
38dd806
Restore task link markdown handler
klopez4212 Jun 28, 2026
7b5c0e4
Gate channel tasks experiment
klopez4212 Jun 27, 2026
8188769
Allow tasks from any message
klopez4212 Jun 27, 2026
2b4afb8
Fix task review regressions
klopez4212 Jun 27, 2026
bfdc5c6
Keep source task thread replies visible
klopez4212 Jun 27, 2026
295c7a0
Infer agents from task source mentions
klopez4212 Jun 27, 2026
7594045
Fix task link feature gate regressions
klopez4212 Jun 27, 2026
0d94a35
Defer task opens until agents resolve
klopez4212 Jun 27, 2026
14f5a40
Infer DM agents for task starts
klopez4212 Jun 27, 2026
e4c069a
Infer DM task agent from participants
klopez4212 Jun 27, 2026
eec16a3
Wait for task link agent inference
klopez4212 Jun 27, 2026
38777fb
Stabilize settings e2e helper
klopez4212 Jun 28, 2026
f915433
Fix task route readiness and backfill
klopez4212 Jun 29, 2026
a82c36d
Preserve agent p-tags on focused sends
klopez4212 Jun 29, 2026
b89fd20
Wait for task marker backfill before opening links
klopez4212 Jun 29, 2026
ab74fcf
Restore task auto-route exports after rebase
klopez4212 Jun 29, 2026
db57069
Limit DM task inference to one agent participant
klopez4212 Jun 29, 2026
7353a2e
Fix task markdown rebase cleanup
klopez4212 Jun 29, 2026
e05076f
Fix task link DM agent inference
klopez4212 Jun 29, 2026
5e28bde
Hide new task action in read-only channels
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
7 changes: 4 additions & 3 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const overrides = new Map([
["src/features/messages/ui/MessageComposer.tsx", 1010],
// continued-agent-conversations: channel sidebar children and active
// conversation unread suppression. Queued to split with sidebar sections.
["src/features/sidebar/ui/AppSidebar.tsx", 1081],
["src/features/sidebar/ui/AppSidebar.tsx", 1087],
// PersistBackend enum + marker-on-keyring-success plumbing and its three
// fail-closed regression tests (silent identity rotation on keyring outage).
// A small overage from load-bearing security plumbing on a file already at
Expand All @@ -170,8 +170,9 @@ const overrides = new Map([
// Shared UI was added to this guard after splitting globals/markdown so
// large shared renderers cannot grow further while follow-up splits land.
// 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],
// plus experiment-gate wiring are temporarily housed here until markdown
// renderers are split further.
["src/shared/ui/markdown.tsx", 2279],
["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
17 changes: 14 additions & 3 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { useApplyTemplate } from "@/features/channel-templates/useApplyTemplate"
import { relayClient } from "@/shared/api/relayClient";
import { useIdentityQuery } from "@/shared/api/hooks";
import { useRelayAutoHeal } from "@/shared/api/useRelayAutoHeal";
import { CHANNEL_TASKS_FEATURE_ID, useFeatureEnabled } from "@/shared/features";
import { useDeferredStartup } from "@/shared/hooks/useDeferredStartup";
import { joinChannel } from "@/shared/api/tauri";
import type { SearchHit } from "@/shared/api/types";
Expand All @@ -89,6 +90,7 @@ const LazySettingsScreen = React.lazy(async () => {
export function AppShell() {
useWebviewZoomShortcuts();
useTauriWindowDrag();
const isChannelTasksEnabled = useFeatureEnabled(CHANNEL_TASKS_FEATURE_ID);

const workspacesHook = useWorkspaces();
const [isAddWorkspaceOpen, setIsAddWorkspaceOpen] = React.useState(false);
Expand Down Expand Up @@ -212,6 +214,7 @@ export function AppShell() {
} = useAgentConversationShellState({
channels,
currentPubkey,
enabled: isChannelTasksEnabled,
goAgents,
goChannel,
selectedView,
Expand Down Expand Up @@ -715,7 +718,11 @@ export function AppShell() {
}}
onAddWorkspaceOpenChange={setIsAddWorkspaceOpen}
onNewDmOpenChange={setIsNewDmOpen}
onHideAgentConversation={handleHideAgentConversation}
onHideAgentConversation={
isChannelTasksEnabled
? handleHideAgentConversation
: undefined
}
onCreateChannelOpenChange={setIsCreateChannelOpen}
onOpenAddWorkspace={() => setIsAddWorkspaceOpen(true)}
onUpdateWorkspace={workspacesHook.updateWorkspace}
Expand Down Expand Up @@ -790,7 +797,9 @@ export function AppShell() {
await goChannel(directMessage.id);
}}
onSelectAgentConversation={
handleSelectAgentConversation
isChannelTasksEnabled
? handleSelectAgentConversation
: undefined
}
onSelectAgents={() => {
clearSelectedAgentConversation();
Expand Down Expand Up @@ -839,7 +848,9 @@ export function AppShell() {
}
selectedChannelId={selectedChannelId}
selectedAgentConversationId={
selectedAgentConversationId
isChannelTasksEnabled
? selectedAgentConversationId
: null
}
selectedView={selectedView}
unreadChannelIds={unreadChannelIds}
Expand Down
78 changes: 57 additions & 21 deletions desktop/src/app/routes/ChannelRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
CHANNEL_TIMELINE_CONTENT_KINDS,
CHANNEL_TIMELINE_STATE_KINDS,
} from "@/shared/constants/kinds";
import { CHANNEL_TASKS_FEATURE_ID, useFeatureEnabled } from "@/shared/features";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

type ChannelRouteScreenProps = {
Expand Down Expand Up @@ -150,6 +151,7 @@ export function ChannelRouteScreen({
targetThreadRootId,
}: ChannelRouteScreenProps) {
const queryClient = useQueryClient();
const isChannelTasksEnabled = useFeatureEnabled(CHANNEL_TASKS_FEATURE_ID);
const { closeForumPost, goForumPost } = useAppNavigation();
const channelsQuery = useChannelsQuery();
const identityQuery = useIdentityQuery();
Expand All @@ -163,6 +165,27 @@ export function ChannelRouteScreen({
const cachedTarget = getCachedSearchHitEvent(targetMessageId);
return cachedTarget ? [cachedTarget] : [];
});
const effectiveAgentConversationReplyId = isChannelTasksEnabled
? targetAgentConversationReplyId
: null;
const targetAgentConversationBackfillKey =
effectiveAgentConversationReplyId && !selectedPostId
? [
channelId,
effectiveAgentConversationReplyId,
targetMessageId ?? "",
targetThreadRootId ?? "",
].join(":")
: null;
const [
completedTargetAgentConversationBackfillKey,
setCompletedTargetAgentConversationBackfillKey,
] = React.useState<string | null>(null);
const targetAgentConversationBackfillPending = Boolean(
targetAgentConversationBackfillKey &&
completedTargetAgentConversationBackfillKey !==
targetAgentConversationBackfillKey,
);

// Reset spliced target events when the channel context changes (channel
// switch or entering/leaving a forum post). Tied to channel identity rather
Expand Down Expand Up @@ -191,11 +214,12 @@ export function ChannelRouteScreen({
// 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 (
(!targetAgentConversationReplyId &&
(!effectiveAgentConversationReplyId &&
!targetMessageId &&
!targetThreadRootId) ||
selectedPostId
) {
setCompletedTargetAgentConversationBackfillKey(null);
return () => {
isCancelled = true;
};
Expand All @@ -215,7 +239,7 @@ export function ChannelRouteScreen({
}

const eventIds = [
targetAgentConversationReplyId,
effectiveAgentConversationReplyId,
targetMessageId,
targetThreadRootId && targetThreadRootId !== targetMessageId
? targetThreadRootId
Expand All @@ -225,24 +249,32 @@ export function ChannelRouteScreen({
void fetchRouteTargetEvents(
channelId,
eventIds,
targetAgentConversationReplyId ?? targetMessageId,
targetAgentConversationReplyId,
effectiveAgentConversationReplyId ?? targetMessageId,
effectiveAgentConversationReplyId,
targetThreadRootId,
Comment thread
klopez4212 marked this conversation as resolved.
).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]) {
eventsById.set(event.id, event);
}
return Array.from(eventsById.values());
});
}
});
)
.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]) {
eventsById.set(event.id, event);
}
return Array.from(eventsById.values());
});
}
})
.finally(() => {
if (!isCancelled && targetAgentConversationBackfillKey) {
setCompletedTargetAgentConversationBackfillKey(
targetAgentConversationBackfillKey,
);
}
});

return () => {
isCancelled = true;
Expand All @@ -251,7 +283,8 @@ export function ChannelRouteScreen({
selectedPostId,
channelId,
queryClient,
targetAgentConversationReplyId,
effectiveAgentConversationReplyId,
targetAgentConversationBackfillKey,
targetMessageId,
targetThreadRootId,
]);
Expand All @@ -277,7 +310,10 @@ export function ChannelRouteScreen({
void goForumPost(channelId, postId);
}}
selectedForumPostId={selectedPostId}
targetAgentConversationReplyId={targetAgentConversationReplyId}
targetAgentConversationReplyId={effectiveAgentConversationReplyId}
targetAgentConversationBackfillPending={
targetAgentConversationBackfillPending
}
targetForumReplyId={targetReplyId}
targetMessageEvents={targetMessageEvents}
targetMessageId={targetMessageId}
Expand Down
95 changes: 91 additions & 4 deletions desktop/src/features/agents/agentConversations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,12 @@ test("continued conversation mention routing preserves explicit multi-agent ment
);
});

function markerEvent({ content = {}, createdAt = 1, id = "marker" } = {}) {
function markerEvent({
content = {},
createdAt = 1,
id = "marker",
includeAgent = true,
} = {}) {
return {
id,
pubkey: "starter",
Expand All @@ -166,15 +171,15 @@ function markerEvent({ content = {}, createdAt = 1, id = "marker" } = {}) {
["h", "channel"],
["e", "root", "", "root"],
["e", "agent-reply", "", "agent-reply"],
["p", "agent"],
...(includeAgent ? [["p", "agent"]] : []),
["title", "Data in Buzz app"],
],
content: JSON.stringify({
version: 1,
title: "Data in Buzz app",
titleStatus: "resolved",
agentName: "Fizz",
agentPubkey: "agent",
agentName: includeAgent ? "Fizz" : "",
agentPubkey: includeAgent ? "agent" : "",
threadRootId: "root",
threadRootMessageId: "root",
parentMessageId: "root",
Expand Down Expand Up @@ -227,6 +232,16 @@ test("continued conversation marker parses summary metadata", () => {
assert.equal(marker?.summaryCreatedAt, 12);
});

test("continued conversation marker can anchor a task without a primary agent", () => {
const marker = parseAgentConversationMarker(
markerEvent({ includeAgent: false }),
);

assert.equal(marker?.agentName, "Task");
assert.equal(marker?.agentPubkey, "");
assert.equal(marker?.agentReplyId, "agent-reply");
});

test("continued conversations persist across app restarts", () => {
withMockLocalStorage(() => {
const workspaceScope = "wss://relay.example.com";
Expand Down Expand Up @@ -266,6 +281,34 @@ test("continued conversations persist across app restarts", () => {
});
});

test("message-anchored tasks persist without a primary agent", () => {
withMockLocalStorage(() => {
const workspaceScope = "wss://relay.example.com";
const root = message({
body: "Can someone turn this into a task?",
createdAt: 1,
id: "root",
});
const conversation = buildAgentConversation({
agentName: "",
agentPubkey: "",
agentReply: root,
channel: { id: "channel", name: "general" },
contextMessages: [root],
parentMessage: null,
threadRootMessage: root,
});

writePersistedAgentConversations("human", workspaceScope, [conversation]);
const persisted = readPersistedAgentConversations("human", workspaceScope);

assert.equal(persisted.length, 1);
assert.equal(persisted[0].id, conversation.id);
assert.equal(persisted[0].agentPubkey, "");
assert.equal(persisted[0].agentReply.id, "root");
});
});

test("continued conversation marker summary update replaces earlier marker", () => {
const markers = buildAgentConversationMarkers([
markerEvent({
Expand Down Expand Up @@ -517,6 +560,50 @@ test("continued conversation marker hides loaded task replies when anchor is out
assert.deepEqual([...hiddenIds], ["task-reply"]);
});

test("source-message task marker does not hide later thread replies", () => {
const root = message({
body: "Can you look into the data model?",
createdAt: 1,
id: "root",
});
const humanAnchor = message({
body: "Let's make this a task.",
createdAt: 2,
id: "human-anchor",
});
const laterReply = message({
body: "This normal thread reply should stay visible.",
createdAt: 3,
id: "later",
});
const marker = parseAgentConversationMarker({
...markerEvent({
content: {
agentName: "",
agentPubkey: "",
agentReplyId: "human-anchor",
startedAt: 2,
},
createdAt: 2,
id: "source-marker",
includeAgent: false,
}),
tags: [
["h", "channel"],
["e", "root", "", "root"],
["e", "human-anchor", "", "agent-reply"],
["title", "Source task"],
],
});

const hiddenIds = getHiddenAgentConversationMessageIds(
[root, humanAnchor, laterReply],
marker ? [marker] : [],
);

assert.deepEqual([...hiddenIds], []);
});

test("continued conversation markers keep later task anchors visible", () => {
const root = message({
body: "Can you look into the data model?",
Expand Down
Loading
Loading