Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e315b0d
Open agent activity in companion window
tellaho Aug 22, 2026
3240554
Grant activity windows Tauri capabilities
tellaho Aug 22, 2026
96bb026
Polish agent activity companion window
tellaho Aug 22, 2026
2ab62fd
Make external agent activity window optional
tellaho Aug 23, 2026
fe121d7
Rename agent activity pop-out action
tellaho Aug 23, 2026
4790729
Keep default activity ingress in app
tellaho Aug 23, 2026
42002ef
refactor(desktop): centralize companion window ownership
tellaho Aug 24, 2026
22caeb7
refactor(desktop): extract agent activity opening actions
tellaho Aug 24, 2026
fc4aec4
fix(desktop): keep archive sync in main window
tellaho Aug 24, 2026
4a50a5e
fix(desktop): reserve deep links for main window
tellaho Aug 24, 2026
ad9da72
fix(desktop): scope activity windows to communities
tellaho Aug 24, 2026
bd49723
fix(desktop): preserve read state in activity windows
tellaho Aug 24, 2026
aa781c6
fix(desktop): show unavailable activity state
tellaho Aug 24, 2026
f156835
fix(desktop): keep companion effects passive
tellaho Aug 24, 2026
5f7f503
fix(desktop): make activity windows passive
tellaho Aug 24, 2026
9d3c54a
fix(desktop): preserve huddle community bootstrap
tellaho Aug 24, 2026
dde8a9e
fix(desktop): preserve activity window navigation context
tellaho Aug 24, 2026
bd4980a
fix(desktop): keep popped activity channel-scoped
tellaho Aug 24, 2026
3739bd0
fix(desktop): keep popped activity controls scoped
tellaho Aug 24, 2026
d9392d2
fix(desktop): pin activity window navigation
tellaho Aug 24, 2026
1502d62
fix(desktop): keep activity companions pinned
tellaho Aug 24, 2026
675bed8
fix(desktop): keep activity pop-outs display-only
tellaho Aug 24, 2026
dac834c
fix(desktop): make pop-out timestamps inert
tellaho Aug 24, 2026
3222729
fix(desktop): preserve pop-out code blocks
tellaho Aug 24, 2026
2e697be
fix(desktop): show full pop-out prompts
tellaho Aug 24, 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 @@ -51,6 +51,7 @@ export default defineConfig({
"**/observer-feed-screenshots.spec.ts",
"**/core-memory-screenshots.spec.ts",
"**/activity-scope-label-screenshots.spec.ts",
"**/agent-activity-window.spec.ts",
"**/welcome-agent-modal-screenshots.spec.ts",
"**/local-archive-screenshots.spec.ts",
"**/voice-settings.spec.ts",
Expand Down
5 changes: 3 additions & 2 deletions desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window and trusted huddle companions",
"windows": ["main", "huddle-*"],
"description": "Capability for the main window and trusted companion windows",
"windows": ["main", "huddle-*", "agent-activity-*"],
"permissions": [
"core:default",
"core:webview:allow-set-webview-zoom",
"core:window:allow-set-badge-count",
"core:window:allow-set-badge-label",
"core:window:allow-request-user-attention",
"core:window:allow-set-focus",
"core:window:allow-set-title",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize",
"core:window:allow-unminimize",
Expand Down
130 changes: 130 additions & 0 deletions desktop/src-tauri/src/agent_activity_window.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Native companion-window lifecycle for an agent activity feed.

use sha2::{Digest, Sha256};
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
use url::form_urlencoded;
use uuid::Uuid;

const PUBKEY_HEX_LENGTH: usize = 64;

fn normalized_pubkey(pubkey: &str) -> Result<String, String> {
let normalized = pubkey.trim().to_ascii_lowercase();
if normalized.len() != PUBKEY_HEX_LENGTH
|| !normalized.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return Err("agent pubkey must be 64 hexadecimal characters".to_string());
}
Ok(normalized)
}

fn normalized_community_id(community_id: &str) -> Result<String, String> {
let normalized = community_id.trim();
if normalized.is_empty() {
return Err("community id must not be empty".to_string());
}
Ok(normalized.to_string())
}

fn window_label(community_id: &str, channel_id: &Uuid, pubkey: &str) -> String {
let community_scope = hex::encode(Sha256::digest(community_id.as_bytes()));
format!("agent-activity-{community_scope}-{pubkey}-{channel_id}")
}

fn activity_route(community_id: &str, channel_id: &Uuid, pubkey: &str) -> String {
let query = form_urlencoded::Serializer::new(String::new())
.append_pair("community", community_id)
.append_pair("agentSession", pubkey)
.append_pair("agentSessionChannel", &channel_id.to_string())
.finish();
format!("index.html#/channels/{channel_id}?{query}")
}

/// Open an agent's channel-scoped activity feed without replacing the main
/// window's thread panel. Each agent/channel pair owns one reusable window.
#[tauri::command]
pub fn open_agent_activity_window(
app: tauri::AppHandle,
community_id: String,
channel_id: String,
pubkey: String,
) -> Result<bool, String> {
let community_id = normalized_community_id(&community_id)?;
let channel_id =
Uuid::parse_str(channel_id.trim()).map_err(|_| "channel id must be a UUID".to_string())?;
let pubkey = normalized_pubkey(&pubkey)?;
let label = window_label(&community_id, &channel_id, &pubkey);

if let Some(window) = app.get_webview_window(&label) {
window.show().map_err(|error| error.to_string())?;
window.set_focus().map_err(|error| error.to_string())?;
return Ok(true);
}

let route = activity_route(&community_id, &channel_id, &pubkey);
WebviewWindowBuilder::new(&app, label, WebviewUrl::App(route.into()))
.title("Agent activity")
.inner_size(560.0, 760.0)
.min_inner_size(420.0, 520.0)
.build()
.map_err(|error| error.to_string())?;
Ok(true)
}

#[cfg(test)]
mod tests {
use super::{activity_route, normalized_community_id, normalized_pubkey, window_label};
use uuid::Uuid;

#[test]
fn normalizes_valid_pubkeys() {
let uppercase = "AB".repeat(32);
assert_eq!(normalized_pubkey(&uppercase), Ok("ab".repeat(32)));
}

#[test]
fn labels_distinguish_pubkeys_with_the_same_prefix() {
let channel_id = Uuid::nil();
let first = format!("{}{}", "ab".repeat(6), "cd".repeat(26));
let second = format!("{}{}", "ab".repeat(6), "ef".repeat(26));

assert_ne!(
window_label("community-a", &channel_id, &first),
window_label("community-a", &channel_id, &second)
);
}

#[test]
fn labels_distinguish_communities() {
let channel_id = Uuid::nil();
let pubkey = "ab".repeat(32);

assert_ne!(
window_label("community-a", &channel_id, &pubkey),
window_label("community-b", &channel_id, &pubkey)
);
}

#[test]
fn route_carries_immutable_community_scope() {
let channel_id = Uuid::nil();
let pubkey = "ab".repeat(32);

assert_eq!(
activity_route("community & one", &channel_id, &pubkey),
format!(
"index.html#/channels/{channel_id}?community=community+%26+one&agentSession={pubkey}&agentSessionChannel={channel_id}"
)
);
}

#[test]
fn rejects_empty_community_ids() {
assert!(normalized_community_id(" ").is_err());
}

#[test]
fn rejects_invalid_pubkeys() {
assert!(normalized_pubkey("abc").is_err());
assert!(normalized_pubkey(&"zz".repeat(32)).is_err());
}
}
3 changes: 3 additions & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![recursion_limit = "256"] // Deep Tauri command futures exceed the default layout query depth.
mod agent_activity_window;
mod app_menu;
mod app_state;
mod archive;
Expand Down Expand Up @@ -50,6 +51,7 @@ mod unread_catch_up;
mod util;
#[cfg(target_os = "linux")]
pub mod webkit_rendering;
use agent_activity_window::open_agent_activity_window;
use app_state::{build_app_state, resolve_persisted_identity, AppState};
use builderlab::*;
#[doc(hidden)]
Expand Down Expand Up @@ -781,6 +783,7 @@ pub fn run() {
get_huddle_state,
close_huddle_companion,
open_huddle_window,
open_agent_activity_window,
push_audio_pcm,
reconnect_huddle_audio,
start_stt_pipeline,
Expand Down
22 changes: 21 additions & 1 deletion desktop/src-tauri/tests/csp.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Guards on the packaged-app Content-Security-Policy in `tauri.conf.json`.
//! Guards on packaged-app security configuration in `tauri.conf.json` and
//! `capabilities/default.json`.
//!
//! The CSP is only enforced on assets Tauri itself serves, so neither
//! `just dev` (loads the Vite `devUrl`) nor the Playwright suite (runs under
Expand All @@ -12,6 +13,25 @@
use std::collections::HashMap;

const TAURI_CONF: &str = include_str!("../tauri.conf.json");
const DEFAULT_CAPABILITY: &str = include_str!("../capabilities/default.json");

#[test]
fn companion_windows_receive_the_default_capability() {
let capability: serde_json::Value =
serde_json::from_str(DEFAULT_CAPABILITY).expect("default capability is valid JSON");
let windows = capability["windows"]
.as_array()
.expect("default capability declares trusted windows");

for pattern in ["huddle-*", "agent-activity-*"] {
assert!(
windows
.iter()
.any(|window| window.as_str() == Some(pattern)),
"default capability must include trusted companion pattern {pattern}"
);
}
}

fn csp_directives() -> HashMap<String, Vec<String>> {
let conf: serde_json::Value =
Expand Down
14 changes: 10 additions & 4 deletions desktop/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import { CommunityThemeController } from "@/shared/theme/CommunityThemeControlle
import { useReloadShortcut } from "@/app/useReloadShortcut";
import { useCloseWindowShortcut } from "@/app/useCloseWindowShortcut";
import { KnownAgentPubkeysProvider } from "@/features/agents/useKnownAgentPubkeys";
import { huddleWindowChannelId } from "@/features/huddle/lib/huddleWindow";
import {
acceptsNativeDeepLinks,
currentCompanionWindowKind,
} from "@/app/companionWindow";
import { useAppOnboardingState } from "@/features/onboarding/hooks";
import { useMachineOnboardingState } from "@/features/onboarding/machineOnboarding";
import {
Expand Down Expand Up @@ -396,6 +399,7 @@ function CommunityApp({
communityKey,
sharedIdentity,
isFindingCommunityAfterLeave,
currentCompanionWindowKind() === null,
);

const transitionCommunity = useCallback(
Expand Down Expand Up @@ -712,10 +716,12 @@ function MachineBootstrap({ sharedIdentity }: { sharedIdentity: boolean }) {
[activeCommunity, communityOnboarding.start],
);

// Community links are app-global work. A Huddle companion loads the same
// Community links are app-global work. Companion windows load the same
// React tree, but must never race the main window for the native pending-link
// queue or replace its dedicated transcript surface with onboarding.
const acceptsCommunityDeepLinks = huddleWindowChannelId() === null;
// queue or replace their dedicated surface with onboarding.
const acceptsCommunityDeepLinks = acceptsNativeDeepLinks(
currentCompanionWindowKind(),
);
useEffect(() => {
if (!acceptsCommunityDeepLinks) return;

Expand Down
6 changes: 4 additions & 2 deletions desktop/src/app/AppHuddleShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type AppHuddleShellProps = {
isCompanionOpen: boolean;
isDrawerOpen: boolean;
isRoom: boolean;
isPassiveWindow?: boolean;
onCompanionOpen: () => void;
onHuddleStartPendingChange: (pending: boolean) => void;
onHuddleStarted: (ephemeralChannelId: string) => void | Promise<void>;
Expand Down Expand Up @@ -48,6 +49,7 @@ export function AppHuddleShell({
isCompanionOpen,
isDrawerOpen,
isRoom,
isPassiveWindow = false,
onCompanionOpen,
onHuddleStartPendingChange,
onHuddleStarted,
Expand All @@ -57,7 +59,7 @@ export function AppHuddleShell({
}: AppHuddleShellProps) {
return (
<HuddleProvider
ownsAudioSession={!isRoom}
ownsAudioSession={!isRoom && !isPassiveWindow}
onHuddleStartPendingChange={
isRoom ? undefined : onHuddleStartPendingChange
}
Expand Down Expand Up @@ -91,7 +93,7 @@ export function AppHuddleShell({
<BuzzTheme.GradientLayer />
{children}
</div>
{isRoom || !isCompanionOpen ? (
{!isPassiveWindow && (isRoom || !isCompanionOpen) ? (
<div className="buzz-huddle-drawer-slot absolute inset-x-0 bottom-0 z-[2] h-(--buzz-huddle-drawer-height)">
<AppHuddleBar
mode={isRoom ? "room" : "main"}
Expand Down
23 changes: 23 additions & 0 deletions desktop/src/app/AppShell.helpers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,34 @@ import test from "node:test";

import {
markAllReadSources,
mainOwnedEffects,
activateDesktopNotificationTarget,
createDesktopNotificationActivationQueue,
shouldBounceForChannelNotification,
} from "./AppShell.helpers.ts";

const MAIN_OWNED_EFFECTS_ENABLED = {
agentRuntimeReconciliation: true,
autoRestart: true,
membershipNotifications: true,
presenceSession: true,
reminderNotifications: true,
markAsReadShortcuts: true,
};

test("primary window owns singleton and mutating app effects", () => {
assert.deepEqual(mainOwnedEffects(false), MAIN_OWNED_EFFECTS_ENABLED);
});

test("companion windows disable singleton and mutating app effects", () => {
assert.deepEqual(
mainOwnedEffects(true),
Object.fromEntries(
Object.keys(MAIN_OWNED_EFFECTS_ENABLED).map((effect) => [effect, false]),
),
);
});

test("shouldBounceForChannelNotification_allowsTopLevelChannelMessages", () => {
assert.equal(shouldBounceForChannelNotification([["h", "channel"]]), true);
});
Expand Down
21 changes: 21 additions & 0 deletions desktop/src/app/AppShell.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ import { isThreadReply } from "@/features/messages/lib/threading";
import type { DesktopNotificationTarget } from "@/features/notifications/lib/desktop";
import type { SearchHit } from "@/shared/api/types";

export type MainOwnedEffects = {
agentRuntimeReconciliation: boolean;
autoRestart: boolean;
membershipNotifications: boolean;
presenceSession: boolean;
reminderNotifications: boolean;
markAsReadShortcuts: boolean;
};

export function mainOwnedEffects(isCompanionWindow: boolean): MainOwnedEffects {
const enabled = !isCompanionWindow;
return {
agentRuntimeReconciliation: enabled,
autoRestart: enabled,
membershipNotifications: enabled,
presenceSession: enabled,
reminderNotifications: enabled,
markAsReadShortcuts: enabled,
};
}

export type AppView =
| "home"
| "channel"
Expand Down
Loading
Loading