Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3ed7b94
refactor(db): rename channel_badge_counts to badge_counts (general pu…
iscekic Apr 29, 2026
e8d062c
feat(db): migration to rename badge_counts and reset rows
iscekic Apr 29, 2026
20b9b3b
feat(notifications): add badge-bucket key builders
iscekic Apr 29, 2026
1bb97c6
chore(notifications): add EVENT_SERVICE binding, drop STREAM_CHAT_API…
iscekic Apr 29, 2026
d87c0fb
chore(notifications): add vitest scaffold
iscekic Apr 29, 2026
2a621db
feat(notifications): rewrite NotificationChannelDO around dispatchPush
iscekic Apr 29, 2026
26fccf5
chore(notifications): drop orphan badgeBucketForInstance helper
iscekic Apr 29, 2026
7fad879
feat(notifications): add sendPushForConversation WorkerEntrypoint RPC
iscekic Apr 29, 2026
f6e1848
chore(notifications): delete Stream webhook route
iscekic Apr 29, 2026
3c7c82e
chore(notifications): type EVENT_SERVICE RPC and enable cloudflare:te…
iscekic Apr 29, 2026
227b90e
feat(event-service): add kiloclaw event-context helpers; migrate kilo…
iscekic Apr 29, 2026
87f0fab
feat(kilo-chat): add fetchSandboxLabel helper
iscekic Apr 29, 2026
822d327
chore(kilo-chat): add NOTIFICATIONS service binding
iscekic Apr 29, 2026
372f0a0
feat(kilo-chat): publish push on message.created via NOTIFICATIONS RPC
iscekic Apr 29, 2026
52fe8a6
chore(notifications): drop orphan stream-chat dep, refresh worker typ…
iscekic Apr 29, 2026
4e95291
fix(notifications): named entrypoint export, retry-safe badge, alarm-…
iscekic Apr 29, 2026
4faf0dd
fix(notifications): close two cleanup-alarm leaks
iscekic Apr 29, 2026
8d7b9d7
refactor(event-service): compose presence contexts from kiloclaw helpers
iscekic Apr 29, 2026
893b7f1
feat(web): add kiloChat.getToken tRPC procedure
iscekic Apr 29, 2026
a35c98c
refactor(web): use kiloclaw-context helpers for event subscriptions
iscekic Apr 29, 2026
a43585d
feat(web): lift EventServiceClient to global provider
iscekic Apr 29, 2026
e98f370
feat(web): add usePresenceSubscription primitive
iscekic Apr 29, 2026
6bfbf95
refactor(web): collapse kilo-chat event subscriptions into usePresenc…
iscekic Apr 29, 2026
832e2b7
feat(web): subscribe to /presence/web while tab is visible
iscekic Apr 29, 2026
99b52d5
feat(web): subscribe to /presence/kiloclaw/{sandboxId} on instance views
iscekic Apr 29, 2026
bdb99c6
refactor(web): extract useDocumentVisible primitive
iscekic Apr 29, 2026
405b185
feat(web): subscribe to conversation presence while tab visible
iscekic Apr 29, 2026
4429bdf
style(web): reflow useDocumentVisible useState init to one line
iscekic Apr 29, 2026
eca983e
refactor(web): tighten presence hook + kilo-chat router contract
iscekic Apr 29, 2026
0adb587
fix(event-service): re-check destroyed after token fetch
iscekic Apr 29, 2026
5092ae8
Merge remote-tracking branch 'origin/feat/kilo-chat-migration-pr1' in…
iscekic Apr 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use client';

import { useInstancePresence } from '@/hooks/useInstancePresence';
import { useKiloClawStatus } from '@/hooks/useKiloClaw';

export function PersonalInstancePresenceMount() {
const { data: status } = useKiloClawStatus();
useInstancePresence(status?.sandboxId ?? undefined);
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { useQueryClient } from '@tanstack/react-query';
import { formatKiloChatError } from '@kilocode/kilo-chat';
import { ConversationList } from './ConversationList';
import { KiloChatContext, type KiloChatContextValue } from './kiloChatContext';
import { useEventService, useInstanceContext } from '../hooks/useEventService';
import { kiloclawInstanceContext } from '@kilocode/event-service';
import { usePresenceSubscription } from '@/hooks/usePresenceSubscription';
import { useEventServiceClient } from '@/contexts/EventServiceContext';
import {
useConversations,
useCreateConversation,
Expand All @@ -20,7 +22,6 @@ import {

// ── Layout component ────────────────────────────────────────────────
type KiloChatLayoutProps = {
getToken: () => Promise<string>;
currentUserId: string;
sandboxId: string | null;
basePath: string;
Expand All @@ -32,7 +33,6 @@ type KiloChatLayoutProps = {
};

export function KiloChatLayout({
getToken,
currentUserId,
sandboxId,
basePath,
Expand All @@ -44,8 +44,11 @@ export function KiloChatLayout({
}: KiloChatLayoutProps) {
const router = useRouter();

const { eventService, kiloChatClient } = useEventService(getToken);
useInstanceContext(eventService, sandboxId);
const { eventService, kiloChatClient } = useEventServiceClient();
usePresenceSubscription(
sandboxId ? kiloclawInstanceContext(sandboxId) : null,
Boolean(sandboxId)
);

const queryClient = useQueryClient();
const params = useParams<{ conversationId?: string }>();
Expand Down Expand Up @@ -189,7 +192,6 @@ export function KiloChatLayout({

const contextValue = useMemo<KiloChatContextValue>(
() => ({
getToken,
currentUserId,
instanceStatus,
leavingConversationId,
Expand All @@ -202,7 +204,6 @@ export function KiloChatLayout({
kiloChatClient,
}),
[
getToken,
currentUserId,
instanceStatus,
leavingConversationId,
Expand Down
27 changes: 23 additions & 4 deletions apps/web/src/app/(app)/claw/kilo-chat/components/MessageArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import {
useRemoveReaction,
useExecuteAction,
} from '../hooks/useMessages';
import { useConversationContext } from '../hooks/useEventService';
import {
kiloclawConversationContext,
presenceContextForConversation,
} from '@kilocode/event-service';
import { usePresenceSubscription } from '@/hooks/usePresenceSubscription';
import { useDocumentVisible } from '@/hooks/useDocumentVisible';
import { useTypingSender, useTypingState } from '../hooks/useTyping';
import {
useConversationDetail,
Expand Down Expand Up @@ -75,13 +80,27 @@ export function MessageArea({ conversationId }: MessageAreaProps) {
const [isRenamingTitle, setIsRenamingTitle] = useState(false);
const [renameText, setRenameText] = useState('');

// Subscribe to this conversation's events via the event-service WebSocket
useConversationContext(eventService, sandboxId, conversationId);
const visible = useDocumentVisible();

// Subscribe to this conversation's chat-event stream while the conversation
// is open. Not gated on visibility — we want incoming messages to land in
// the cache even when the tab is hidden.
usePresenceSubscription(
sandboxId && conversationId ? kiloclawConversationContext(sandboxId, conversationId) : null,
Boolean(sandboxId && conversationId)
);

// Signal our own presence on this conversation. Gated on visibility so we
// only appear "viewing" while the tab is actually in the foreground.
usePresenceSubscription(
sandboxId && conversationId ? presenceContextForConversation(sandboxId, conversationId) : null,
Boolean(sandboxId && conversationId) && visible
);

// Event Service delivers subscribed contexts to every handler, so each
// handler must validate the incoming `ctx` against this string before
// applying changes to the active conversation's state.
const expectedContext = sandboxId ? `/kiloclaw/${sandboxId}/${conversationId}` : null;
const expectedContext = sandboxId ? kiloclawConversationContext(sandboxId, conversationId) : null;

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useMessages(
kiloChatClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { EventServiceClient } from '@kilocode/event-service';
import type { KiloChatClient } from '@kilocode/kilo-chat';

export type KiloChatContextValue = {
getToken: () => Promise<string>;
currentUserId: string;
instanceStatus: string | null;
leavingConversationId: string | null;
Expand Down
70 changes: 0 additions & 70 deletions apps/web/src/app/(app)/claw/kilo-chat/hooks/useEventService.ts

This file was deleted.

3 changes: 2 additions & 1 deletion apps/web/src/app/(app)/claw/kilo-chat/hooks/useMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
ExecApprovalDecision,
} from '@kilocode/kilo-chat';
import { useEffect } from 'react';
import { kiloclawConversationContext } from '@kilocode/event-service';
import { toast } from 'sonner';

const PAGE_SIZE = 50;
Expand Down Expand Up @@ -401,7 +402,7 @@ export function useMessageCacheUpdater(
useEffect(() => {
if (!conversationId || !sandboxId) return;
const queryKey = ['kilo-chat', 'messages', conversationId];
const expectedContext = `/kiloclaw/${sandboxId}/${conversationId}`;
const expectedContext = kiloclawConversationContext(sandboxId, conversationId);

const onCreated = (ctx: string, e: MessageCreatedEvent) => {
if (ctx !== expectedContext) return;
Expand Down
5 changes: 0 additions & 5 deletions apps/web/src/app/(app)/claw/kilo-chat/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
'use client';

import { useCallback } from 'react';
import { useUser } from '@/hooks/useUser';
import { useKiloClawStatus } from '@/hooks/useKiloClaw';
import { getKiloChatToken } from './token';
import { KiloChatLayout } from './components/KiloChatLayout';

export default function KiloChatRootLayout({ children }: { children: React.ReactNode }) {
const { data: user } = useUser();
const { data: status, isLoading } = useKiloClawStatus();

const getToken = useCallback(() => getKiloChatToken(), []);

return (
<KiloChatLayout
getToken={getToken}
currentUserId={user?.id ?? ''}
sandboxId={status?.sandboxId ?? null}
basePath="/claw/kilo-chat"
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/app/(app)/claw/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { getUserFromAuthOrRedirect } from '@/lib/user.server';
import { PylonWidget } from '@/components/pylon-widget';
import { PylonSupportButton } from '@/components/pylon-support-button';
import { PersonalInstancePresenceMount } from './components/PersonalInstancePresenceMount';
import './claw-chat.css';

export default async function ClawLayout({ children }: { children: React.ReactNode }) {
await getUserFromAuthOrRedirect();
return (
<>
<PersonalInstancePresenceMount />
{children}
<PylonWidget>
<PylonSupportButton />
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/app/(app)/components/PlatformPresenceMount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client';

import { usePlatformPresence } from '@/hooks/usePlatformPresence';

export function PlatformPresenceMount() {
usePlatformPresence();
return null;
}
29 changes: 17 additions & 12 deletions apps/web/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,31 @@ import { AppTopbar } from './components/AppTopbar';
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
import { RoleTestingProvider } from '@/contexts/RoleTestingContext';
import { PageTitleProvider } from '@/contexts/PageTitleContext';
import { EventServiceProvider } from '@/contexts/EventServiceContext';
import { AdminOmnibox } from '@/components/admin-omnibox';
import { PrefetchedOrganizations } from './components/PrefetchedOrganizations';
import { PlatformPresenceMount } from './components/PlatformPresenceMount';
import { ImpactIdentify } from '@/components/ImpactIdentify';

export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<RoleTestingProvider>
<PageTitleProvider>
<SidebarProvider>
<ImpactIdentify />
<PrefetchedOrganizations>
<div className="flex min-h-screen w-full">
<AppSidebar />
<SidebarInset>
<AppTopbar />
<main className="bg-background w-full flex-1">{children}</main>
</SidebarInset>
</div>
</PrefetchedOrganizations>
</SidebarProvider>
<EventServiceProvider>
<PlatformPresenceMount />
<SidebarProvider>
<ImpactIdentify />
<PrefetchedOrganizations>
<div className="flex min-h-screen w-full">
<AppSidebar />
<SidebarInset>
<AppTopbar />
<main className="bg-background w-full flex-1">{children}</main>
</SidebarInset>
</div>
</PrefetchedOrganizations>
</SidebarProvider>
</EventServiceProvider>
</PageTitleProvider>
<AdminOmnibox />
</RoleTestingProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';

import { useParams } from 'next/navigation';
import { useInstancePresence } from '@/hooks/useInstancePresence';
import { useOrgKiloClawStatus } from '@/hooks/useOrgKiloClaw';

export function OrgInstancePresenceMount() {
const params = useParams<{ id: string }>();
const organizationId = params?.id;
const { data: status } = useOrgKiloClawStatus(organizationId);
useInstancePresence(status?.sandboxId ?? undefined);
return null;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
'use client';

import { useCallback } from 'react';
import { useParams } from 'next/navigation';
import { useUser } from '@/hooks/useUser';
import { useOrgKiloClawStatus } from '@/hooks/useOrgKiloClaw';
import { getKiloChatToken } from '@/app/(app)/claw/kilo-chat/token';
import { KiloChatLayout } from '@/app/(app)/claw/kilo-chat/components/KiloChatLayout';

export default function OrgKiloChatRootLayout({ children }: { children: React.ReactNode }) {
Expand All @@ -13,14 +11,11 @@ export default function OrgKiloChatRootLayout({ children }: { children: React.Re
const { data: user } = useUser();
const { data: status, isLoading } = useOrgKiloClawStatus(organizationId);

const getToken = useCallback(() => getKiloChatToken(), []);

const basePath = `/organizations/${organizationId}/claw/kilo-chat`;
const noInstanceRedirect = `/organizations/${organizationId}/claw/new`;

return (
<KiloChatLayout
getToken={getToken}
currentUserId={user?.id ?? ''}
sandboxId={status?.sandboxId ?? null}
basePath={basePath}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { PylonSupportButton } from '@/components/pylon-support-button';
import { PylonWidget } from '@/components/pylon-widget';
import { OrgInstancePresenceMount } from './components/OrgInstancePresenceMount';
import '@/app/(app)/claw/claw-chat.css';

export default function OrgClawLayout({ children }: { children: React.ReactNode }) {
return (
<>
<OrgInstancePresenceMount />
{children}
<PylonWidget>
<PylonSupportButton />
Expand Down
Loading