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
5 changes: 4 additions & 1 deletion desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ const overrides = new Map([
// importIdentity, persistCurrentIdentity) moved to tauriIdentity.ts;
// limit ratcheted down 1380 → 1360 to bank the headroom (absorbs main-side
// growth landed between the split and the rebase).
["src/shared/api/tauri.ts", 1360],
// mention-alias fix: profile wrappers (RawProfile/RawUserProfileSummary types,
// getProfile/updateProfile/getUserProfile/getUsersBatch/searchUsers) moved to
// tauriProfiles.ts; limit ratcheted down 1360 → 1241 to bank the headroom.
["src/shared/api/tauri.ts", 1241],
// readiness-gate: PersonaDialog.tsx threads computeLocalModeGate +
// requiredCredentialEnvKeys + RequiredFieldLabel so the "New agent" dialog
// shows required markers and credential amber rows (parity with
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/commands/agent_settings.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use tauri::{AppHandle, Manager, State};
use tauri::{AppHandle, Manager};

use crate::{
app_state::AppState,
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/commands/channels_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn directory_cursor_keeps_same_second_tiebreaker() {
let event = ev_at(39000, "{}", vec![], timestamp);
let mut filter = serde_json::json!({"kinds": [39000], "limit": DIRECTORY_PAGE_SIZE});

advance_directory_cursor(&mut filter, &[event.clone()]);
advance_directory_cursor(&mut filter, std::slice::from_ref(&event));

assert_eq!(filter["until"], serde_json::json!(timestamp.as_secs()));
assert_eq!(filter["before_id"], serde_json::json!(event.id.to_hex()));
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/managed_agents/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ const DEV_MIGRATION_MARKER: &str = "_dev_migration_v1";
///
/// On subsequent boots (marker already present):
/// 1. One `dst.load_all_readonly()` — dev blob read (1 keychain prompt)
/// Returns immediately — prod keyring is NEVER accessed.
/// Returns immediately — prod keyring is NEVER accessed.
///
/// Idempotency: keys already present in `dst` are not overwritten (the agent
/// may have rotated their key in the dev service after initial migration).
Expand Down
6 changes: 3 additions & 3 deletions desktop/src-tauri/src/migration_databricks_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ fn reconcile_databricks_v1_to_v2_rewrites_v1_provider_on_block_build() {
// Stale V1 model must be cleared so the baked DATABRICKS_MODEL is not
// shadowed by BUZZ_AGENT_MODEL at spawn time (last-write-wins in Command::env).
assert!(
records[0].get("model").map_or(true, |v| v.is_null()),
records[0].get("model").is_none_or(|v| v.is_null()),
"stale V1 model field must be cleared when provider is rewritten to V2"
);
}
Expand Down Expand Up @@ -98,12 +98,12 @@ fn reconcile_databricks_v1_to_v2_clears_model_on_provider_rewrite() {
// V1 records: provider migrated, model cleared.
assert_eq!(records[0]["provider"], "databricks_v2");
assert!(
records[0].get("model").map_or(true, |v| v.is_null()),
records[0].get("model").is_none_or(|v| v.is_null()),
"model must be cleared for V1→V2 migrated record A"
);
assert_eq!(records[1]["provider"], "databricks_v2");
assert!(
records[1].get("model").map_or(true, |v| v.is_null()),
records[1].get("model").is_none_or(|v| v.is_null()),
"model must be cleared for V1→V2 migrated record B"
);
// V2 record: model untouched.
Expand Down
5 changes: 5 additions & 0 deletions desktop/src-tauri/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ pub struct ProfileInfo {
#[derive(Serialize, Deserialize)]
pub struct UserProfileSummaryInfo {
pub display_name: Option<String>,
/// Kind-0 `name` field, carried separately from `display_name` so clients
/// can match @mention text against either alias (agents and the CLI
/// resolve mentions server-side against `display_name` *or* `name`).
#[serde(default)]
pub name: Option<String>,
pub avatar_url: Option<String>,
pub nip05_handle: Option<String>,
pub owner_pubkey: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/nostr_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ pub fn users_batch_from_events(
.and_then(Value::as_str)
.or_else(|| v.get("name").and_then(Value::as_str))
.map(str::to_string),
name: v.get("name").and_then(Value::as_str).map(str::to_string),
avatar_url: v.get("picture").and_then(Value::as_str).map(str::to_string),
nip05_handle: v.get("nip05").and_then(Value::as_str).map(str::to_string),
is_agent: owner_pubkey.is_some(),
Expand Down
40 changes: 23 additions & 17 deletions desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ import {
} from "@/features/channels/readState/readStateFormat";
import { ChannelScreenEmptyState } from "@/features/channels/ui/ChannelScreenEmptyState";
import { ChannelScreenHeader } from "@/features/channels/ui/ChannelScreenHeader";
import {
ChannelPane,
ForumView,
} from "@/features/channels/ui/ChannelScreenLazyViews";
import { ChannelPane } from "@/features/channels/ui/ChannelScreenLazyViews";
import { ForumChannelContent } from "@/features/channels/ui/ForumChannelContent";
import { MembersSidebar } from "@/features/channels/ui/MembersSidebar";
import {
useManagedAgentsQuery,
Expand Down Expand Up @@ -839,19 +837,27 @@ export function ChannelScreen({
>
{activeChannel ? (
activeChannel.channelType === "forum" ? (
<>
{channelHeader}
<React.Suspense fallback={<ViewLoadingFallback kind="forum" />}>
<ForumView
channel={activeChannel}
currentPubkey={currentPubkey}
onClosePost={onCloseForumPost}
onSelectPost={onSelectForumPost}
selectedPostId={selectedForumPostId}
targetReplyId={targetForumReplyId}
/>
</React.Suspense>
</>
<ForumChannelContent
canResetPanelWidth={canResetThreadPanelWidth}
channel={activeChannel}
currentPubkey={currentPubkey}
header={channelHeader}
onClosePost={onCloseForumPost}
onCloseProfilePanel={handleCloseProfilePanel}
onOpenDm={handleOpenDm}
onOpenProfilePanel={handleOpenProfilePanel}
onPanelResizeStart={handleThreadPanelResizeStart}
onProfilePanelTabChange={setProfilePanelTab}
onProfilePanelViewChange={setProfilePanelView}
onResetPanelWidth={handleThreadPanelWidthReset}
onSelectPost={onSelectForumPost}
panelWidthPx={threadPanelWidthPx}
profilePanelPubkey={profilePanelPubkey}
profilePanelTab={profilePanelTab}
profilePanelView={profilePanelView}
selectedPostId={selectedForumPostId}
targetReplyId={targetForumReplyId}
/>
) : (
<React.Suspense
fallback={<ViewLoadingFallback includeHeader kind="channel" />}
Expand Down
5 changes: 5 additions & 0 deletions desktop/src/features/channels/ui/ChannelScreenLazyViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export const ForumView = React.lazy(async () => {
const module = await import("@/features/forum/ui/ForumView");
return { default: module.ForumView };
});

export const UserProfilePanel = React.lazy(async () => {
const module = await import("@/features/profile/ui/UserProfilePanel");
return { default: module.UserProfilePanel };
});
121 changes: 121 additions & 0 deletions desktop/src/features/channels/ui/ForumChannelContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as React from "react";

import {
ForumView,
UserProfilePanel,
} from "@/features/channels/ui/ChannelScreenLazyViews";
import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane";
import type {
ProfilePanelTab,
ProfilePanelView,
} from "@/features/profile/ui/UserProfilePanelUtils";
import type { Channel } from "@/shared/api/types";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

type ForumChannelContentProps = {
canResetPanelWidth: boolean;
channel: Channel;
currentPubkey?: string;
header: React.ReactNode;
onClosePost: () => void;
onCloseProfilePanel: () => void;
onOpenDm?: (pubkeys: string[]) => Promise<void> | void;
onOpenProfilePanel: (pubkey: string) => void;
onPanelResizeStart: (event: React.PointerEvent<HTMLButtonElement>) => void;
onProfilePanelTabChange: (
tab: ProfilePanelTab,
options?: { replace?: boolean },
) => void;
onProfilePanelViewChange: (
view: ProfilePanelView,
options?: { replace?: boolean },
) => void;
onResetPanelWidth: () => void;
onSelectPost: (postId: string) => void;
panelWidthPx: number;
profilePanelPubkey?: string | null;
profilePanelTab: ProfilePanelTab;
profilePanelView: ProfilePanelView;
selectedPostId: string | null;
targetReplyId: string | null;
};

/**
* Forum-channel body for ChannelScreen: the post list/thread plus the
* user-profile auxiliary pane. Forums replace ChannelPane (which hosts the
* profile panel for message channels), so without this host, opening a
* profile from a mention chip, avatar, or the members sidebar would set
* state that never renders.
*/
export function ForumChannelContent({
canResetPanelWidth,
channel,
currentPubkey,
header,
onClosePost,
onCloseProfilePanel,
onOpenDm,
onOpenProfilePanel,
onPanelResizeStart,
onProfilePanelTabChange,
onProfilePanelViewChange,
onResetPanelWidth,
onSelectPost,
panelWidthPx,
profilePanelPubkey,
profilePanelTab,
profilePanelView,
selectedPostId,
targetReplyId,
}: ForumChannelContentProps) {
return (
<>
{header}
<div className="flex min-h-0 min-w-0 flex-1 flex-row overflow-hidden">
<section
aria-label="Forum posts"
className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
>
<React.Suspense fallback={<ViewLoadingFallback kind="forum" />}>
<ForumView
channel={channel}
currentPubkey={currentPubkey}
onClosePost={onClosePost}
onSelectPost={onSelectPost}
selectedPostId={selectedPostId}
targetReplyId={targetReplyId}
/>
</React.Suspense>
</section>
{profilePanelPubkey ? (
<RightAuxiliaryPane
canResetWidth={canResetPanelWidth}
onResetWidth={onResetPanelWidth}
onResizeStart={onPanelResizeStart}
testId="user-profile-panel"
widthPx={panelWidthPx}
>
<React.Suspense fallback={null}>
<UserProfilePanel
callerChannelId={channel.id}
currentPubkey={currentPubkey}
isSinglePanelView={false}
layout="split"
onClose={onCloseProfilePanel}
onOpenDm={onOpenDm}
onOpenProfile={onOpenProfilePanel}
onTabChange={onProfilePanelTabChange}
onViewChange={onProfilePanelViewChange}
pubkey={profilePanelPubkey}
splitPaneClamp
tab={profilePanelTab}
view={profilePanelView}
widthPx={panelWidthPx}
/>
</React.Suspense>
</RightAuxiliaryPane>
) : null}
</div>
</>
);
}
7 changes: 2 additions & 5 deletions desktop/src/features/channels/ui/MembersSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,18 +511,15 @@ export function MembersSidebar({
useFeedbackToasts(actionNoticeMessage, actionErrorMessage);

const { openProfilePanel } = useProfilePanel();
// UserProfilePanel only renders inside ChannelPane, which forums replace
// with ForumView — opening there would close the sheet and show nothing.
const isForumChannel = channel?.channelType === "forum";
const handleOpenProfile = React.useMemo(
() =>
openProfilePanel && !isForumChannel
openProfilePanel
? (pubkey: string) => {
onOpenChange(false);
openProfilePanel(pubkey);
}
: undefined,
[isForumChannel, onOpenChange, openProfilePanel],
[onOpenChange, openProfilePanel],
);

const [editRespondToAgent, setEditRespondToAgent] =
Expand Down
8 changes: 6 additions & 2 deletions desktop/src/features/forum/ui/ForumPostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { UserAvatar } from "@/shared/ui/UserAvatar";
import type { ForumPost } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { parseImetaTags } from "@/features/messages/lib/parseImeta";
import { resolveMentionNames } from "@/shared/lib/resolveMentionNames";
import { resolveMentionProps } from "@/shared/lib/resolveMentionNames";
import { Markdown } from "@/shared/ui/markdown";

import { formatRelativeTime } from "../lib/time";
Expand Down Expand Up @@ -44,7 +44,10 @@ export function ForumPostCard({
preferResolvedSelfLabel: true,
});
const avatarUrl = profiles?.[post.pubkey.toLowerCase()]?.avatarUrl ?? null;
const mentionNames = resolveMentionNames(post.tags, profiles);
const { mentionNames, mentionPubkeysByName } = resolveMentionProps(
post.tags,
profiles,
);
// Memoize the imeta map: `parseImetaTags` builds a fresh object each render,
// and the `Markdown` memo compares `imetaByUrl` by reference. Without this,
// the post's Markdown (and the FileCard <button> it renders) is rebuilt on
Expand Down Expand Up @@ -120,6 +123,7 @@ export function ForumPostCard({
content={previewContent}
imetaByUrl={imetaByUrl}
mentionNames={mentionNames}
mentionPubkeysByName={mentionPubkeysByName}
/>
</div>

Expand Down
14 changes: 11 additions & 3 deletions desktop/src/features/forum/ui/ForumThreadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { channelChrome } from "@/shared/layout/chromeLayout";
import { cn } from "@/shared/lib/cn";
import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
import { parseImetaTags } from "@/features/messages/lib/parseImeta";
import { resolveMentionNames } from "@/shared/lib/resolveMentionNames";
import { resolveMentionProps } from "@/shared/lib/resolveMentionNames";
import { Button } from "@/shared/ui/button";
import { Markdown } from "@/shared/ui/markdown";
import { Skeleton } from "@/shared/ui/skeleton";
Expand Down Expand Up @@ -72,7 +72,10 @@ function ReplyRow({
const replyAvatarUrl =
profiles?.[reply.pubkey.toLowerCase()]?.avatarUrl ?? null;
const showDelete = onDelete && canDeleteReply(reply, currentPubkey);
const replyMentionNames = resolveMentionNames(reply.tags, profiles);
const {
mentionNames: replyMentionNames,
mentionPubkeysByName: replyMentionPubkeysByName,
} = resolveMentionProps(reply.tags, profiles);

return (
<div
Expand Down Expand Up @@ -114,6 +117,7 @@ function ReplyRow({
content={reply.content}
imetaByUrl={parseImetaTags(reply.tags)}
mentionNames={replyMentionNames}
mentionPubkeysByName={replyMentionPubkeysByName}
/>
</div>
</div>
Expand Down Expand Up @@ -184,7 +188,10 @@ export function ForumThreadPanel({
}

const { post, replies } = thread;
const postMentionNames = resolveMentionNames(post.tags, profiles);
const {
mentionNames: postMentionNames,
mentionPubkeysByName: postMentionPubkeysByName,
} = resolveMentionProps(post.tags, profiles);
const postAuthorLabel = resolveUserLabel({
pubkey: post.pubkey,
currentPubkey,
Expand Down Expand Up @@ -253,6 +260,7 @@ export function ForumThreadPanel({
content={post.content}
imetaByUrl={parseImetaTags(post.tags)}
mentionNames={postMentionNames}
mentionPubkeysByName={postMentionPubkeysByName}
/>
</div>
</div>
Expand Down
Loading
Loading