Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dc1ee8f
feat(desktop): add conversation-style DM composer
Jul 12, 2026
a00c147
fix(desktop): show DM recipient search skeletons
Jul 12, 2026
4aa187e
test(desktop): cover new DM compose states
Jul 12, 2026
5af5df6
fix(desktop): keep DM recipient picker open
Jul 12, 2026
0a72163
fix(desktop): prepare DM before agent mentions
Jul 12, 2026
70b1c9c
fix(desktop): lock recipients during mention send
Jul 12, 2026
7072d28
fix(desktop): guard delayed DM navigation
Jul 12, 2026
5508d9e
fix(desktop): reveal DM recipient pubkeys on hover
Jul 13, 2026
3b00ed3
fix(desktop): restore full DM recipient verification
Jul 13, 2026
dd2d42f
fix(desktop): remove duplicate recipient input prop
Jul 13, 2026
510eb2a
fix(desktop): bind first DM send to opened channel
Jul 13, 2026
63fa592
fix(desktop): reseed opened DM before routing
Jul 13, 2026
310659b
fix(desktop): preserve attached DM participants
Jul 13, 2026
542bd4e
fix(desktop): cancel stale first DM sends
Jul 13, 2026
503b5c0
fix(desktop): retain first DM agent membership
Jul 14, 2026
1ac937c
Merge origin/main into kennylopez-dm-new-message-page
klopez4212 Jul 14, 2026
01db095
fix(desktop): preserve refreshed DM recency
klopez4212 Jul 14, 2026
ce1983f
fix(desktop): route agent mentions to expanded DM
klopez4212 Jul 14, 2026
1d45a6a
fix(desktop): reset failed DM send target
klopez4212 Jul 14, 2026
ad616d1
fix(desktop): route existing DM agent mentions
klopez4212 Jul 14, 2026
00af758
fix(desktop): guard DM route after cache refresh
klopez4212 Jul 14, 2026
7f53dab
fix(desktop): block cross-DM thread agent mentions
klopez4212 Jul 14, 2026
9f8df7f
fix(desktop): order rapid PR status changes
klopez4212 Jul 14, 2026
c191714
fix(desktop): preserve DM mention routing
klopez4212 Jul 14, 2026
b5e922a
fix(desktop): defer DM thread agent mentions
klopez4212 Jul 14, 2026
ee71434
fix(desktop): discard stale prepared DMs
klopez4212 Jul 14, 2026
384a059
fix(desktop): route relay agents in DMs
klopez4212 Jul 14, 2026
8cc68da
fix(desktop): prevent stale DM reroutes
klopez4212 Jul 14, 2026
1a2b1c0
fix(desktop): cancel stale DM sends
klopez4212 Jul 14, 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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default defineConfig({
"**/edit-agent.spec.ts",
"**/doctor-cta-screenshots.spec.ts",
"**/pubkey-display-screenshots.spec.ts",
"**/dm-new-message-screenshots.spec.ts",
"**/file-attachment.spec.ts",
"**/image-attachment-gallery.spec.ts",
"**/composer-image-draw.spec.ts",
Expand Down
8 changes: 8 additions & 0 deletions desktop/src/app/AppShell.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SearchHit } from "@/shared/api/types";
export type AppView =
| "home"
| "channel"
| "messages"
| "agents"
| "workflows"
| "pulse"
Expand Down Expand Up @@ -117,6 +118,13 @@ export function deriveShellRoute(pathname: string): {
};
}

if (pathname === "/messages/new") {
return {
selectedChannelId: null,
selectedView: "messages",
};
}

if (pathname === "/agents") {
return {
selectedChannelId: null,
Expand Down
11 changes: 6 additions & 5 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export function AppShell() {
const [searchFocusRequest, setSearchFocusRequest] = React.useState(0);
const [browseDialogType, setBrowseDialogType] =
React.useState<BrowseDialogType>(null);
const [isNewDmOpen, setIsNewDmOpen] = React.useState(false);
const [isCreateChannelOpen, setIsCreateChannelOpen] = React.useState(false);
const [isHuddleDrawerOpen, setIsHuddleDrawerOpen] = React.useState(false);
const mainInsetRef = React.useRef<HTMLElement>(null);
Expand All @@ -119,6 +118,7 @@ export function AppShell() {
goAgents,
goChannel,
goHome,
goNewMessage,
goProjects,
goPulse,
goSettings,
Expand Down Expand Up @@ -547,7 +547,10 @@ export function AppShell() {
// Dispatch `buzz://message` deep links into the router.
useMessageDeepLinks();

const handleOpenNewDm = React.useCallback(() => setIsNewDmOpen(true), []);
const handleOpenNewDm = React.useCallback(
() => void goNewMessage(),
[goNewMessage],
);
const handleOpenCreateChannel = React.useCallback(
() => setIsCreateChannelOpen(true),
[],
Expand Down Expand Up @@ -754,16 +757,14 @@ export function AppShell() {
isCreatingChannel={createChannelMutation.isPending}
isCreatingForum={createForumMutation.isPending}
isLoading={channelsQuery.isLoading}
isOpeningDm={openDmMutation.isPending}
isNewDmOpen={isNewDmOpen}
isCreateChannelOpen={isCreateChannelOpen}
isPresencePending={presenceSession.isPending}
onAddWorkspace={(workspace) => {
const id = workspacesHook.addWorkspace(workspace);
handleSwitchWorkspace(id);
}}
onAddWorkspaceOpenChange={setIsAddWorkspaceOpen}
onNewDmOpenChange={setIsNewDmOpen}
onNewMessage={handleOpenNewDm}
onCreateChannelOpenChange={setIsCreateChannelOpen}
onOpenAddWorkspace={() => setIsAddWorkspaceOpen(true)}
onUpdateWorkspace={workspacesHook.updateWorkspace}
Expand Down
12 changes: 12 additions & 0 deletions desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ export function useAppNavigation() {
[commitNavigation],
);

const goNewMessage = React.useCallback(
(behavior?: NavigationBehavior) =>
commitNavigation(
{
to: "/messages/new",
},
behavior,
),
[commitNavigation],
);

const goForumPost = React.useCallback(
(
channelId: string,
Expand Down Expand Up @@ -284,6 +295,7 @@ export function useAppNavigation() {
goChannel,
goForumPost,
goHome,
goNewMessage,
goProject,
goProjects,
goPulse,
Expand Down
21 changes: 21 additions & 0 deletions desktop/src/app/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Route as agentsRouteImport } from "./routes/agents";
import { Route as indexRouteImport } from "./routes/index";
import { Route as workflowsDotworkflowIdRouteImport } from "./routes/workflows.$workflowId";
import { Route as projectsDotprojectIdRouteImport } from "./routes/projects.$projectId";
import { Route as messagesDotnewRouteImport } from "./routes/messages.new";
import { Route as channelsDotchannelIdRouteImport } from "./routes/channels.$channelId";
import { Route as channelsDotchannelIdDotpostsDotpostIdRouteImport } from "./routes/channels.$channelId.posts.$postId";

Expand Down Expand Up @@ -62,6 +63,11 @@ const projectsDotprojectIdRoute = projectsDotprojectIdRouteImport.update({
path: "/projects/$projectId",
getParentRoute: () => rootRouteImport,
} as any);
const messagesDotnewRoute = messagesDotnewRouteImport.update({
id: "/messages/new",
path: "/messages/new",
getParentRoute: () => rootRouteImport,
} as any);
const channelsDotchannelIdRoute = channelsDotchannelIdRouteImport.update({
id: "/channels/$channelId",
path: "/channels/$channelId",
Expand All @@ -83,6 +89,7 @@ export interface FileRoutesByFullPath {
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
"/channels/$channelId": typeof channelsDotchannelIdRoute;
"/messages/new": typeof messagesDotnewRoute;
"/projects/$projectId": typeof projectsDotprojectIdRoute;
"/workflows/$workflowId": typeof workflowsDotworkflowIdRoute;
"/channels/$channelId/posts/$postId": typeof channelsDotchannelIdDotpostsDotpostIdRoute;
Expand All @@ -96,6 +103,7 @@ export interface FileRoutesByTo {
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
"/channels/$channelId": typeof channelsDotchannelIdRoute;
"/messages/new": typeof messagesDotnewRoute;
"/projects/$projectId": typeof projectsDotprojectIdRoute;
"/workflows/$workflowId": typeof workflowsDotworkflowIdRoute;
"/channels/$channelId/posts/$postId": typeof channelsDotchannelIdDotpostsDotpostIdRoute;
Expand All @@ -110,6 +118,7 @@ export interface FileRoutesById {
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
"/channels/$channelId": typeof channelsDotchannelIdRoute;
"/messages/new": typeof messagesDotnewRoute;
"/projects/$projectId": typeof projectsDotprojectIdRoute;
"/workflows/$workflowId": typeof workflowsDotworkflowIdRoute;
"/channels/$channelId/posts/$postId": typeof channelsDotchannelIdDotpostsDotpostIdRoute;
Expand All @@ -125,6 +134,7 @@ export interface FileRouteTypes {
| "/settings"
| "/workflows"
| "/channels/$channelId"
| "/messages/new"
| "/projects/$projectId"
| "/workflows/$workflowId"
| "/channels/$channelId/posts/$postId";
Expand All @@ -138,6 +148,7 @@ export interface FileRouteTypes {
| "/settings"
| "/workflows"
| "/channels/$channelId"
| "/messages/new"
| "/projects/$projectId"
| "/workflows/$workflowId"
| "/channels/$channelId/posts/$postId";
Expand All @@ -151,6 +162,7 @@ export interface FileRouteTypes {
| "/settings"
| "/workflows"
| "/channels/$channelId"
| "/messages/new"
| "/projects/$projectId"
| "/workflows/$workflowId"
| "/channels/$channelId/posts/$postId";
Expand All @@ -165,6 +177,7 @@ export interface RootRouteChildren {
settingsRoute: typeof settingsRoute;
workflowsRoute: typeof workflowsRoute;
channelsDotchannelIdRoute: typeof channelsDotchannelIdRoute;
messagesDotnewRoute: typeof messagesDotnewRoute;
projectsDotprojectIdRoute: typeof projectsDotprojectIdRoute;
workflowsDotworkflowIdRoute: typeof workflowsDotworkflowIdRoute;
channelsDotchannelIdDotpostsDotpostIdRoute: typeof channelsDotchannelIdDotpostsDotpostIdRoute;
Expand Down Expand Up @@ -235,6 +248,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof projectsDotprojectIdRouteImport;
parentRoute: typeof rootRouteImport;
};
"/messages/new": {
id: "/messages/new";
path: "/messages/new";
fullPath: "/messages/new";
preLoaderRoute: typeof messagesDotnewRouteImport;
parentRoute: typeof rootRouteImport;
};
"/channels/$channelId": {
id: "/channels/$channelId";
path: "/channels/$channelId";
Expand All @@ -261,6 +281,7 @@ const rootRouteChildren: RootRouteChildren = {
settingsRoute: settingsRoute,
workflowsRoute: workflowsRoute,
channelsDotchannelIdRoute: channelsDotchannelIdRoute,
messagesDotnewRoute: messagesDotnewRoute,
projectsDotprojectIdRoute: projectsDotprojectIdRoute,
workflowsDotworkflowIdRoute: workflowsDotworkflowIdRoute,
channelsDotchannelIdDotpostsDotpostIdRoute:
Expand Down
1 change: 1 addition & 0 deletions desktop/src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const routes = rootRoute("root.tsx", [
route("/workflows/$workflowId", "workflows.$workflowId.tsx"),
route("/projects", "projects.tsx"),
route("/projects/$projectId", "projects.$projectId.tsx"),
route("/messages/new", "messages.new.tsx"),
route("/channels/$channelId", "channels.$channelId.tsx"),
route(
"/channels/$channelId/posts/$postId",
Expand Down
7 changes: 7 additions & 0 deletions desktop/src/app/routes/messages.new.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";

import { NewMessageScreen } from "@/features/messages/ui/NewMessageScreen";

export const Route = createFileRoute("/messages/new")({
component: NewMessageScreen,
});
53 changes: 31 additions & 22 deletions desktop/src/features/agents/channelAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ export type CreateChannelManagedAgentResult =
runtimeId: string;
};

export type ProvisionChannelManagedAgentResult = {
agent: ManagedAgent;
created: boolean;
runtimeId: string;
};

export type CreateChannelManagedAgentBatchFailure = {
kind: "generic" | "persona";
name: string;
Expand Down Expand Up @@ -238,16 +244,13 @@ export async function ensureChannelAgentPresetInChannel(
};
}

export async function createChannelManagedAgent(
channelId: string,
export async function provisionChannelManagedAgent(
input: CreateChannelManagedAgentInput,
context?: {
managedAgents?: ManagedAgent[];
channelMemberPubkeys?: ReadonlySet<string>;
},
): Promise<CreateChannelManagedAgentResult> {
const role = input.role ?? "bot";
const ensureRunning = input.ensureRunning ?? true;
): Promise<ProvisionChannelManagedAgentResult> {
const trimmedName = input.name.trim();

if (trimmedName.length === 0) {
Expand Down Expand Up @@ -285,13 +288,8 @@ export async function createChannelManagedAgent(
).agent
: reusable;

const attached = await attachManagedAgentToChannel(channelId, {
agent: updatedAgent,
role,
ensureRunning,
});
return {
...attached,
agent: updatedAgent,
created: false,
runtimeId: input.runtime.id,
};
Expand Down Expand Up @@ -328,13 +326,8 @@ export async function createChannelManagedAgent(
).agent
: reusable;

const attached = await attachManagedAgentToChannel(channelId, {
agent: updatedAgent,
role,
ensureRunning,
});
return {
...attached,
agent: updatedAgent,
created: false,
runtimeId: input.runtime.id,
};
Expand Down Expand Up @@ -373,16 +366,32 @@ export async function createChannelManagedAgent(
throw new Error(created.spawnError);
}

const attached = await attachManagedAgentToChannel(channelId, {
return {
agent: created.agent,
role,
ensureRunning,
created: true,
runtimeId: input.runtime.id,
};
}

export async function createChannelManagedAgent(
channelId: string,
input: CreateChannelManagedAgentInput,
context?: {
managedAgents?: ManagedAgent[];
channelMemberPubkeys?: ReadonlySet<string>;
},
): Promise<CreateChannelManagedAgentResult> {
const provisioned = await provisionChannelManagedAgent(input, context);
const attached = await attachManagedAgentToChannel(channelId, {
agent: provisioned.agent,
role: input.role ?? "bot",
ensureRunning: input.ensureRunning ?? true,
});

return {
...attached,
created: true,
runtimeId: input.runtime.id,
created: provisioned.created,
runtimeId: provisioned.runtimeId,
};
}

Expand Down
Loading