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
374 changes: 374 additions & 0 deletions docs/superpowers/plans/2026-08-10-unified-agent-chat.md

Large diffs are not rendered by default.

347 changes: 347 additions & 0 deletions docs/superpowers/specs/2026-08-10-unified-agent-chat-design.md

Large diffs are not rendered by default.

1,209 changes: 1,178 additions & 31 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "herdr-desktop",
"productName": "Herdr Desktop",
"version": "0.1.11",
"version": "0.1.12",
"description": "A native workspace for Herdr-powered agents, terminals, worktrees, and live sessions.",
"main": ".vite/build/main.js",
"private": true,
Expand Down Expand Up @@ -87,6 +87,7 @@
"next-themes": "0.4.6",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-markdown": "10.1.0",
"react-resizable-panels": "4.12.2",
"sonner": "2.0.7",
"tailwind-merge": "3.6.0",
Expand Down
8 changes: 4 additions & 4 deletions packaging/aur/.SRCINFO
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pkgbase = herdr-desktop-bin
pkgdesc = A native workspace for Herdr-powered agents, terminals, worktrees, and live sessions
pkgver = 0.1.11
pkgver = 0.1.12
pkgrel = 1
url = https://github.com/marcelormendes/herdr-desktop
arch = x86_64
Expand All @@ -11,9 +11,9 @@ pkgbase = herdr-desktop-bin
conflicts = herdr-desktop
options = !strip
options = !debug
source = herdr-desktop-linux-x86_64.AppImage::https://github.com/marcelormendes/herdr-desktop/releases/download/v0.1.11/herdr-desktop-linux-x86_64.AppImage
source = icon-1024.png::https://raw.githubusercontent.com/marcelormendes/herdr-desktop/v0.1.11/resources/icon-1024.png
source = LICENSE::https://raw.githubusercontent.com/marcelormendes/herdr-desktop/v0.1.11/LICENSE
source = herdr-desktop-linux-x86_64.AppImage::https://github.com/marcelormendes/herdr-desktop/releases/download/v0.1.12/herdr-desktop-linux-x86_64.AppImage
source = icon-1024.png::https://raw.githubusercontent.com/marcelormendes/herdr-desktop/v0.1.12/resources/icon-1024.png
source = LICENSE::https://raw.githubusercontent.com/marcelormendes/herdr-desktop/v0.1.12/LICENSE
sha256sums = 8305307a608bb81c979debbf1478d98ffa03a6705f1337b425cfda7f847a862c
sha256sums = 26bc1e96021e651612ed207632d0992415a79deabf77634ca590666b0b400f93
sha256sums = c340e32ed2706b814e99e58e7cc87419a635fac31bfcf22567f8a6a0454729dd
Expand Down
2 changes: 1 addition & 1 deletion packaging/aur/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Contributor: Marcelo Ribeiro Mendes

pkgname=herdr-desktop-bin
pkgver=0.1.11
pkgver=0.1.12
pkgrel=1
pkgdesc="A native workspace for Herdr-powered agents, terminals, worktrees, and live sessions"
arch=('x86_64')
Expand Down
10 changes: 9 additions & 1 deletion src/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ import { cn } from '@/lib/utils';

type ScrollAreaProps = React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {
viewportRef?: React.Ref<HTMLDivElement>;
onViewportScroll?: React.UIEventHandler<HTMLDivElement>;
};

function ScrollArea({ className, children, viewportRef, ...props }: ScrollAreaProps) {
function ScrollArea({
className,
children,
viewportRef,
onViewportScroll,
...props
}: ScrollAreaProps) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
Expand All @@ -18,6 +25,7 @@ function ScrollArea({ className, children, viewportRef, ...props }: ScrollAreaPr
<ScrollAreaPrimitive.Viewport
className="h-full w-full min-w-0 max-w-full font-base [&>div]:!block [&>div]:min-w-0 [&>div]:max-w-full"
data-slot="scroll-area-viewport"
onScroll={onViewportScroll}
ref={viewportRef}
>
{children}
Expand Down
88 changes: 79 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@ import { app, BrowserWindow, dialog, ipcMain, Menu, session, shell } from 'elect
import started from 'electron-squirrel-startup';
import { APP_NAME, configureApplicationBranding } from '@/main/app-branding';
import { applicationMenuTemplate } from '@/main/application-menu';
import { stageChatImages } from '@/main/chat-images';
import { DesktopPreferencesStore } from '@/main/desktop-preferences';
import { checkDesktopUpdate } from '@/main/desktop-update';
import { chatImageStagingDir, hostPathFromSandboxPath, isFlatpakHost } from '@/main/flatpak';
import { hostPathFromSandboxPath, isFlatpakHost } from '@/main/flatpak';
import { resolveHerdrBinary } from '@/main/herdr/binary-locator';
import { HerdrBinaryPreference } from '@/main/herdr/binary-preference';
import { HerdrEngine, NodeHerdrCommandRunner, NodeHerdrServerLauncher } from '@/main/herdr/engine';
import { HerdrEventSubscription } from '@/main/herdr/event-subscription';
import { TerminalController } from '@/main/herdr/terminal-controller';
import { TerminalControllerPool } from '@/main/herdr/terminal-controller-pool';
import {
parseChatImageDrafts,
parseConversationAttachmentAbortRequest,
parseConversationAttachmentBeginRequest,
parseConversationAttachmentChunkRequest,
parseConversationAttachmentFinishRequest,
parseConversationPromptRequest,
parseConversationReadRequest,
parseConversationRespondRequest,
parseHerdrCommand,
parseHerdrQuery,
parsePaneId,
Expand Down Expand Up @@ -68,6 +73,7 @@ let engine = createEngine(herdrBinary);
let binaryPreference: HerdrBinaryPreference | null = null;
let desktopPreferences: DesktopPreferencesStore | null = null;
let preferencesWriteQueue = Promise.resolve();
const conversationSubscriptions = new Set<string>();

function enqueuePreferencesWrite<T>(operation: () => Promise<T>): Promise<T> {
const run = preferencesWriteQueue.then(operation);
Expand Down Expand Up @@ -140,6 +146,7 @@ const eventSubscription = new HerdrEventSubscription(
return {
socketPath: result.status.server.socket,
paneIds: result.snapshot.panes.map((pane) => pane.pane_id),
conversationPaneIds: eventSubscription.getConversationPaneIds(),
};
},
onStateChange: (state) =>
Expand Down Expand Up @@ -293,13 +300,76 @@ function registerIpcHandlers(): void {
return engine.query(parseHerdrQuery(candidate));
});

ipcMain.handle(IPC_CHANNELS.stageChatImages, async (event, candidate: unknown) => {
ipcMain.handle(IPC_CHANNELS.conversationRead, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
// Files are staged at the sandbox-visible path (where the grant mounts
// the host directory); the renderer and the host agent receive the
// host-equivalent paths.
const staged = stageChatImages(chatImageStagingDir(), parseChatImageDrafts(candidate));
return staged.map((filePath) => hostPathFromSandboxPath(filePath));
if (demoMode) {
throw new Error('Structured Chat is unavailable in demo mode.');
}
return engine.conversationRead(parseConversationReadRequest(candidate));
});

ipcMain.handle(IPC_CHANNELS.conversationPrompt, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
if (demoMode) {
return DEMO_BOOTSTRAP;
}
return trackConnectedSession(
await engine.conversationPrompt(parseConversationPromptRequest(candidate)),
);
});

ipcMain.handle(IPC_CHANNELS.conversationRespond, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
if (demoMode) {
throw new Error('Structured Chat is unavailable in demo mode.');
}
return engine.conversationRespond(parseConversationRespondRequest(candidate));
});

ipcMain.handle(IPC_CHANNELS.attachmentBegin, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
if (demoMode) {
throw new Error('Structured Chat is unavailable in demo mode.');
}
return engine.conversationAttachmentBegin(parseConversationAttachmentBeginRequest(candidate));
});

ipcMain.handle(IPC_CHANNELS.attachmentChunk, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
if (demoMode) {
throw new Error('Structured Chat is unavailable in demo mode.');
}
return engine.conversationAttachmentChunk(parseConversationAttachmentChunkRequest(candidate));
});

ipcMain.handle(IPC_CHANNELS.attachmentFinish, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
if (demoMode) {
throw new Error('Structured Chat is unavailable in demo mode.');
}
return engine.conversationAttachmentFinish(parseConversationAttachmentFinishRequest(candidate));
});

ipcMain.handle(IPC_CHANNELS.attachmentAbort, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
if (demoMode) {
throw new Error('Structured Chat is unavailable in demo mode.');
}
return engine.conversationAttachmentAbort(parseConversationAttachmentAbortRequest(candidate));
});

ipcMain.handle(IPC_CHANNELS.conversationSubscribe, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
const paneId = parsePaneId(candidate);
conversationSubscriptions.add(paneId);
eventSubscription.setConversationPanes([...conversationSubscriptions]);
});

ipcMain.handle(IPC_CHANNELS.conversationUnsubscribe, async (event, candidate: unknown) => {
assertTrustedSender(event.senderFrame?.url);
const paneId = parsePaneId(candidate);
conversationSubscriptions.delete(paneId);
eventSubscription.setConversationPanes([...conversationSubscriptions]);
});

ipcMain.handle(IPC_CHANNELS.readPreferences, async (event) => {
Expand Down
193 changes: 0 additions & 193 deletions src/main/chat-images.test.ts

This file was deleted.

Loading