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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,6 @@ Use `bash scripts/start.sh` for the whole stack. Use `bun run dev` only when you
## License

[MIT](./LICENSE) — upstream copyright remains with CopilotKit; this product fork is maintained by Kayco.

Some adapted UI components retain their original Apache-2.0 terms. See
[THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for source and license details.
23 changes: 23 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Third-party notices

## OpenMausBot UI components

Portions of Kayco OpenBot's user interface are adapted from
[OpenMausBot](https://github.com/milind-soni/OpenMausBot) commit
`ca3131444bbb2b125e8d90593be930efb2f19854`.

Copyright 2026 Milind Soni and OpenMausBot contributors.

OpenMausBot is licensed under the Apache License, Version 2.0. A copy is
included at [licenses/OpenMausBot-APACHE-2.0.txt](licenses/OpenMausBot-APACHE-2.0.txt).

Adapted surfaces:

- `app/src/components/app-sidebar/channel.tsx`: contact-style conversation row and revealed actions.
- `app/src/components/channels/codex-status.tsx`: compact model trigger and grouped model menu.
- `app/src/components/gallery/decisions.tsx`: lettered option card and free-text answer.
- `app/src/routes/_authed/admin/plugins.tsx`: searchable two-column connected-app catalogue.

These files carry source notices and were modified to use Kayco's routing,
design tokens, server-owned permissions, credential vault, approval flow, and
Codex preference APIs.
49 changes: 38 additions & 11 deletions app/src/components/app-sidebar/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {
IconBolt,
IconBox,
IconBriefcase,
IconLogout,
IconPlugConnected,
IconPlus,
IconSearch,
IconSettings,
IconShieldLock,
IconBox,
} from "@tabler/icons-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, type LinkOptions, useNavigate } from "@tanstack/react-router";
Expand All @@ -29,6 +30,7 @@ import {
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
Expand All @@ -43,8 +45,8 @@ import {
} from "@/lib/channels/queries";
import { useChannelEvents } from "@/lib/channels/use-channel-events";
import { appConfig } from "@/lib/generated/application-config";
import { Button } from "../ui/button";
import { CommandPalette } from "../command-palette";
import { Button } from "../ui/button";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../ui/empty";
import { Channel } from "./channel";

Expand Down Expand Up @@ -176,7 +178,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {

return (
<Sidebar {...props}>
<SidebarHeader className="h-12 p-2">
<SidebarHeader className="h-14 p-2">
<SidebarMenu>
<SidebarMenuItem className="flex flex-row gap-1.5">
<SidebarMenuButton
Expand All @@ -188,6 +190,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
)}
/>
<Button
aria-label="Start a new conversation"
nativeButton={false}
size="icon"
variant="ghost"
render={(props) => (
Expand All @@ -208,21 +212,23 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarHeader>
<SidebarContent className="scroll-fade-b">
<SidebarMenu>
<SidebarGroup className="gap-px">
<SidebarGroup className="gap-px pt-1">
<SidebarMenuItem>
<InputGroup className="bg-background text-sm rounded-lg h-9">
<InputGroup className="h-10 rounded-xl bg-background text-sm shadow-xs">
<InputGroupInput
aria-label="Search channels"
onChange={(event) => setSearch(event.target.value)}
placeholder="Search..."
placeholder="Search conversations"
value={search}
/>
<InputGroupAddon>
<IconSearch />
</InputGroupAddon>
</InputGroup>
</SidebarMenuItem>
<div className="w-full h-2" />
<SidebarGroupLabel className="mt-2 px-1.5 text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
Conversations
</SidebarGroupLabel>
{/*
* TWO DIFFERENT NOTHINGS, AND SAYING THE WRONG ONE IS ALARMING. A roster nobody has
* used yet needs telling how to start. A roster that simply does not match what is in
Expand Down Expand Up @@ -285,9 +291,30 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<div className="size-[28px] flex items-center justify-center">
<IconBriefcase />
</div>
<span className="text-sm trackint-tight">Work</span>
<span className="text-sm tracking-tight">Work</span>
</SidebarMenuButton>
</SidebarMenuItem>
{currentUser?.role === "admin" ? (
<SidebarMenuItem>
<SidebarMenuButton
className="h-10 hover:bg-foreground/5"
render={(props) => (
<Link
{...props}
activeProps={{
className: "bg-foreground/5",
}}
to="/admin/plugins"
/>
)}
>
<div className="flex size-[28px] items-center justify-center">
<IconPlugConnected />
</div>
<span className="text-sm tracking-tight">Connected apps</span>
</SidebarMenuButton>
</SidebarMenuItem>
) : null}
<SidebarMenuItem>
{/* Beside Agents rather than inside Admin: writing a skill is something anybody does. */}
<SidebarMenuButton
Expand All @@ -305,7 +332,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<div className="size-[28px] flex items-center justify-center">
<IconBox />
</div>
<span className="text-sm trackint-tight">Skills</span>
<span className="text-sm tracking-tight">Skills</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
Expand All @@ -324,7 +351,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<div className="size-[28px] flex items-center justify-center">
<IconBolt />
</div>
<span className="text-sm trackint-tight">Coworkers</span>
<span className="text-sm tracking-tight">Coworkers</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
Expand All @@ -335,7 +362,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
}
>
<UserAvatar />
<span className="text-sm trackint-tight">
<span className="text-sm tracking-tight">
{currentUser?.name || currentUser?.email}
</span>
</DropdownMenuTrigger>
Expand Down
123 changes: 99 additions & 24 deletions app/src/components/app-sidebar/channel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import {
IconCopy,
IconDots,
IconMessagePlus,
IconUserCircle,
} from "@tabler/icons-react";
import { Link } from "@tanstack/react-router";
import { memo } from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChannelAvatar } from "../channels/avatar";

/*
* The contact-style row density and hover-revealed action pattern are adapted from OpenMausBot's
* src/components/Sidebar.tsx at ca3131444bbb2b125e8d90593be930efb2f19854.
* Copyright 2026 Milind Soni and OpenMausBot contributors, Apache-2.0.
* Modified for Kayco channels, TanStack Router, and server-owned coworker permissions.
*/

/**
* Memoized roster row. `use-channel-events` preserves unchanged row identity, and
* `content-visibility` keeps off-screen rows cheap without virtualization.
Expand All @@ -20,31 +41,85 @@ export const Channel = memo(function Channel({
lastMessageAt?: string;
}) {
return (
<Link
to="/channel/$channelId"
params={{ channelId }}
type="button"
className="flex flex-row py-2 px-2 gap-2 items-center w-full hover:bg-foreground/5 rounded-lg [contain-intrinsic-size:auto_3.25rem] [content-visibility:auto]"
activeProps={{
className: "bg-foreground/5",
}}
>
<div className="">
<ChannelAvatar participantIds={participantIds} size={32} />
</div>
<div className="flex-col min-w-0 flex-1">
<div className="flex flex-row items-center justify-between gap-2">
<span className="text-[14px] tracking-[-1%] truncate">{name}</span>
<div className="text-[12px] text-muted-foreground/70">
{lastMessageAt}
</div>
<div className="group/channel relative [contain-intrinsic-size:auto_3.75rem] [content-visibility:auto]">
<Link
to="/channel/$channelId"
params={{ channelId }}
type="button"
className="flex min-h-14 w-full flex-row items-center gap-2.5 rounded-xl px-2.5 py-2 pr-8 transition-colors hover:bg-foreground/5"
activeProps={{
className: "bg-foreground/[0.07] ring-1 ring-foreground/5",
}}
>
<div className="shrink-0">
<ChannelAvatar participantIds={participantIds} size={36} />
</div>
<div className="mt-px flex h-4 items-center gap-1.5">
<span className="min-w-0 flex-1 truncate text-[12px] leading-4 text-muted-foreground">
{lastMessage}
</span>
<div className="min-w-0 flex-1 flex-col">
<div className="flex flex-row items-center justify-between gap-2">
<span className="truncate text-[14px] font-medium tracking-tight">
{name}
</span>
<time className="shrink-0 text-[11px] text-muted-foreground">
{lastMessageAt}
</time>
</div>
<div className="mt-0.5 flex h-4 items-center gap-1.5">
<span className="min-w-0 flex-1 truncate text-[12px] leading-4 text-muted-foreground">
{lastMessage || "Start a conversation"}
</span>
</div>
</div>
</div>
</Link>
</Link>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
aria-label={`More options for ${name}`}
className="absolute right-1.5 top-1.5 opacity-50 shadow-none transition-opacity hover:opacity-100 group-focus-within/channel:opacity-100 group-hover/channel:opacity-100"
size="icon-sm"
variant="ghost"
/>
}
>
<IconDots />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-52" side="right">
<DropdownMenuItem
render={<Link params={{ channelId }} to="/channel/$channelId" />}
>
<IconMessagePlus />
Open conversation
</DropdownMenuItem>
<DropdownMenuItem
render={
<Link
search={{ agents: participantIds.join(",") }}
to="/channel/new"
/>
}
>
<IconMessagePlus />
Start another chat
</DropdownMenuItem>
{participantIds.length === 1 ? (
<DropdownMenuItem
render={
<Link search={{ agent: participantIds[0] }} to="/agents" />
}
>
<IconUserCircle />
View coworker
</DropdownMenuItem>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => void navigator.clipboard?.writeText(channelId)}
>
<IconCopy />
Copy conversation ID
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
});
4 changes: 3 additions & 1 deletion app/src/components/channels/channel-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import {
takeFirstMessage,
transcriptMessages,
} from "@/components/channels/transcript-messages";
import { TaskRunStatus } from "@/components/tasks/task-run-status";
import { agentListQueryOptions } from "@/lib/agents/queries";
import { useMessageTimes } from "@/lib/channels/message-times";
import {
indexContinuityMessageMutationOptions,
recordChannelActivityMutationOptions,
} from "@/lib/channels/mutations";
import type { AgentChannel } from "@/lib/channels/queries";
import { resolveContextReferences } from "@/lib/continuity/context-references";
import { useActiveBot } from "@/lib/copilot/active-bot";
import {
beginComputerTask,
Expand All @@ -34,7 +36,6 @@ import {
import { ConversationProvider } from "@/lib/copilot/conversation";
import { repairUnansweredToolCalls } from "@/lib/copilot/repair-history";
import { stoppedReason } from "@/lib/copilot/stopped-turn";
import { resolveContextReferences } from "@/lib/continuity/context-references";
import { useSkillCommands } from "@/lib/plugins/skill-commands";

/**
Expand Down Expand Up @@ -602,6 +603,7 @@ export function ChannelChat({
return (
<ConversationProvider ask={askFromComponent}>
<ConversationView
activity={<TaskRunStatus channelId={channel.id} />}
agentId={runtimeAgentId}
agents={toAgentOptions(agentProfiles, channel.agentIds)}
assistantName={assistantName}
Expand Down
Loading
Loading