Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fde4d0b
feat(web): show the active project in the app header
raunakab Aug 11, 2026
19352b2
refactor(web): simplify the active-project header label
raunakab Aug 11, 2026
0588d8d
fix(web): hide the active-project label on the project page
raunakab Aug 11, 2026
0d1aba8
refactor(web): move ProjectFolderButton into lib/projects/components
raunakab Aug 11, 2026
f1a8ff1
fix(opal): re-export the open and partial-open folder icons
raunakab Aug 11, 2026
5782cc5
feat(web): show the partial-open folder whenever the icon is hovered
raunakab Aug 11, 2026
b105e99
fix(web): hold the hover preview after a click on the folder icon
raunakab Aug 11, 2026
d430f4e
fix(web): align project chat rows under the project label
raunakab Aug 11, 2026
f4ff133
feat(web): reach projects from the folded sidebar
raunakab Aug 12, 2026
b0ed6d2
wip(web): render the folded projects popover with line items
raunakab Aug 12, 2026
c85ca47
Merge branch 'main' into feat/projects-viz
raunakab Aug 13, 2026
1b39e69
fix(web): tighten the project folder icon and its nested indent
raunakab Aug 13, 2026
4dc86f7
fix(web): let the folder icon receive its own clicks again
raunakab Aug 13, 2026
81405d8
fix(opal): let LineItemButton size to its content again
raunakab Aug 13, 2026
4a71408
feat(web): make the folded popover's chat rows work
raunakab Aug 13, 2026
0d45118
feat(opal): let LineItemButton callers set the content padding
raunakab Aug 13, 2026
47c598d
refactor(web): render the popover rows as SidebarTab
raunakab Aug 13, 2026
48db616
Bump up default padding on line-item-buttons
raunakab Aug 13, 2026
73c1ea0
docs(opal): document LineItemButton's real padding type
raunakab Aug 13, 2026
7bb3b4f
fix(web): restore search expansion and let a name match keep its chats
raunakab Aug 13, 2026
bec9f6c
Merge branch 'main' into feat/projects-viz
raunakab Aug 21, 2026
37e7e28
fix(web): make the project folder icon a real control
raunakab Aug 22, 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
Expand Up @@ -9,7 +9,7 @@ A composite component that wraps `Interactive.Stateful > Interactive.Container >
```
Interactive.Stateful <- selectVariant, state, interaction, onClick, href, ref
└─ Interactive.Container <- width, rounding
└─ ContentAction <- withInteractive, padding={2}
└─ ContentAction <- withInteractive, padding
├─ Content <- icon, title, description, sizePreset, variant, ...
└─ rightChildren
```
Expand All @@ -18,7 +18,9 @@ The row renders as a focusable `<div role="button">` (with Enter/Space activatio
native `<button>`, so interactive `rightChildren` such as action buttons don't produce invalid
button-in-button nesting. With `href` it renders an anchor instead.

`padding` is hardcoded to `2` and `withInteractive` is always `true`. These are not exposed as props.
`withInteractive` is always `true` and is not exposed. `padding` is forwarded to the inner
`ContentAction`, defaulting to `0` so a row hugs its content — the surrounding `p-1.5` already
supplies the row's own inset.

## Props

Expand All @@ -41,6 +43,7 @@ button-in-button nesting. With `href` it renders an anchor instead.
|------|------|---------|-------------|
| `rounding` | `InteractiveContainerRoundingVariant` | `"md"` | Corner rounding preset (height is content-driven) |
| `width` | `WidthVariant` | `"full"` | Container width |
| `padding` | `Spacing` | `0` | Padding around the inner `ContentAction`, as a spacing step (`N / 4` rem) |
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
| `tooltip` | `string` | — | Tooltip text shown on hover |
| `tooltipSide` | `TooltipSide` | `"top"` | Tooltip side |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { type ContentActionProps, ContentAction } from "@opal/layouts";

type ContentPassthroughProps = DistributiveOmit<
ContentActionProps,
"padding" | "width" | "ref"
"width" | "ref"
>;

type LineItemButtonOwnProps = Pick<
Expand Down Expand Up @@ -93,6 +93,7 @@ function LineItemButton({
// Sizing
rounding = "md",
width = "full",
padding = 0,
tooltip,
tooltipSide = "top",

Expand Down Expand Up @@ -128,11 +129,11 @@ function LineItemButton({
rounding={rounding}
{...rowButtonProps}
>
<div className="w-full p-2">
<div className="w-full p-1.5">
<ContentAction
color="interactive"
{...(contentActionProps as ContentActionProps)}
padding={0}
padding={padding}
/>
</div>
</Interactive.Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function SidebarTab({
icon ??
(nested
? ((() => (
<div className="w-6" aria-hidden="true" />
<div className="w-4" aria-hidden="true" />
)) as IconFunctionComponent)
: null);

Expand Down
2 changes: 1 addition & 1 deletion web/lib/opal/src/components/inputs/shared.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

.opal-input {
/* `--interactive-duration` (150ms) felt too laggy, so we're using 100ms instead. */
@apply flex flex-row items-center justify-between h-fit p-[5px] rounded-08 relative w-full transition-all duration-100 ease-(--interactive-easing);
@apply flex flex-row items-center justify-between h-fit p-1.5 rounded-08 relative w-full transition-all duration-100 ease-(--interactive-easing);
}

.opal-input[data-variant="primary"] {
Expand Down
44 changes: 14 additions & 30 deletions web/lib/opal/src/icons/folder-open.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
import React from "react";
import type { IconProps } from "@opal/types";

const SvgFolderOpen = React.forwardRef<SVGSVGElement, IconProps>(
({ size = 32, color = "currentColor", title, className, ...props }, ref) => (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 26"
width={size}
height={size}
fill="none"
role={title ? "img" : "presentation"}
aria-label={title}
className={className}
stroke="currentColor"
{...props}
>
{title ? <title>{title}</title> : null}
<path
d="M30.4177 15.4931L29.1847 15.2876L30.4177 15.4931ZM29.4177 21.4932L30.6507 21.6987V21.6987L29.4177 21.4932ZM2.58209 21.4932L1.3491 21.6987L2.58209 21.4932ZM1.58209 15.4931L0.349095 15.6986L1.58209 15.4931ZM13.8786 2.87868L12.9947 3.76256V3.76256L13.8786 2.87868ZM16.1212 5.12132L17.0051 4.23744V4.23744L16.1212 5.12132ZM4.54127 11.9999V13.2499H27.4585V11.9999V10.7499H4.54127V11.9999ZM30.4177 15.4931L29.1847 15.2876L28.1847 21.2877L29.4177 21.4932L30.6507 21.6987L31.6507 15.6986L30.4177 15.4931ZM26.4585 24V22.75H5.54128V24V25.25H26.4585V24ZM2.58209 21.4932L3.81509 21.2877L2.81508 15.2876L1.58209 15.4931L0.349095 15.6986L1.3491 21.6987L2.58209 21.4932ZM5.54128 24V22.75C4.68581 22.75 3.95572 22.1315 3.81509 21.2877L2.58209 21.4932L1.3491 21.6987C1.69065 23.748 3.46371 25.25 5.54128 25.25V24ZM29.4177 21.4932L28.1847 21.2877C28.0441 22.1315 27.314 22.75 26.4585 22.75V24V25.25C28.5361 25.25 30.3091 23.748 30.6507 21.6987L29.4177 21.4932ZM18.2425 6V7.25H25.9999V6V4.75H18.2425V6ZM5.9999 2V3.25H11.7573V2V0.75H5.9999V2ZM13.8786 2.87868L12.9947 3.76256L15.2373 6.0052L16.1212 5.12132L17.0051 4.23744L14.7625 1.9948L13.8786 2.87868ZM11.7573 2V3.25C12.2214 3.25 12.6665 3.43437 12.9947 3.76256L13.8786 2.87868L14.7625 1.9948C13.9654 1.19777 12.8844 0.75 11.7573 0.75V2ZM18.2425 6V4.75C17.7784 4.75 17.3333 4.56563 17.0051 4.23744L16.1212 5.12132L15.2373 6.0052C16.0344 6.80223 17.1154 7.25 18.2425 7.25V6ZM28.9999 9H30.2499C30.2499 6.65279 28.3471 4.75 25.9999 4.75V6V7.25C26.9664 7.25 27.7499 8.0335 27.7499 9H28.9999ZM2.99989 5H4.24989C4.24989 4.0335 5.0334 3.25 5.9999 3.25V2V0.75C3.65269 0.75 1.74989 2.65279 1.74989 5H2.99989ZM28.9999 9H27.7499V12.4249H28.9999H30.2499V9H28.9999ZM27.4585 11.9999V13.2499C27.7932 13.2499 28.0975 13.3411 28.3564 13.4965L28.9999 12.4249L29.6434 11.3533C29.0065 10.9708 28.2589 10.7499 27.4585 10.7499V11.9999ZM28.9999 12.4249L28.3564 13.4965C28.9538 13.8553 29.3076 14.5505 29.1847 15.2876L30.4177 15.4931L31.6507 15.6986C31.9508 13.8982 31.0763 12.2138 29.6434 11.3533L28.9999 12.4249ZM2.99989 12.4249H4.24989V5H2.99989H1.74989V12.4249H2.99989ZM4.54127 11.9999V10.7499C3.74089 10.7499 2.99329 10.9708 2.35636 11.3533L2.99989 12.4249L3.64343 13.4965C3.90228 13.3411 4.20658 13.2499 4.54127 13.2499V11.9999ZM2.99989 12.4249L2.35636 11.3533C0.923529 12.2138 0.0490297 13.8982 0.349095 15.6986L1.58209 15.4931L2.81508 15.2876C2.69222 14.5505 3.04602 13.8553 3.64343 13.4965L2.99989 12.4249Z"
fill={color}
fillOpacity={0.8}
stroke={color}
strokeOpacity={0.8}
strokeWidth={0.2}
/>
</svg>
)
const SvgFolderOpen = ({ size, ...props }: IconProps) => (
Comment thread
raunakab marked this conversation as resolved.
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M15.2089 9.24657L14.4691 9.12327V9.12327L15.2089 9.24657ZM14.7089 12.2466L15.4487 12.3699V12.3699L14.7089 12.2466ZM1.29109 12.2466L0.551297 12.3699L1.29109 12.2466ZM0.79109 9.24657L0.051294 9.36987L0.79109 9.24657ZM6.93933 2.93934L6.409 3.46967V3.46967L6.93933 2.93934ZM8.06065 4.06066L8.59098 3.53033V3.53033L8.06065 4.06066ZM2.27068 7.49997V8.24997H13.7293V7.49997V6.74997H2.27068V7.49997ZM15.2089 9.24657L14.4691 9.12327L13.9691 12.1233L14.7089 12.2466L15.4487 12.3699L15.9487 9.36987L15.2089 9.24657ZM13.2293 13.5V12.75H2.77068V13.5V14.25H13.2293V13.5ZM1.29109 12.2466L2.03089 12.1233L1.53089 9.12327L0.79109 9.24657L0.051294 9.36987L0.551297 12.3699L1.29109 12.2466ZM2.77068 13.5V12.75C2.40405 12.75 2.09116 12.4849 2.03089 12.1233L1.29109 12.2466L0.551297 12.3699C0.732116 13.4548 1.67079 14.25 2.77068 14.25V13.5ZM14.7089 12.2466L13.9691 12.1233C13.9088 12.4849 13.5959 12.75 13.2293 12.75V13.5V14.25C14.3292 14.25 15.2679 13.4548 15.4487 12.3699L14.7089 12.2466ZM9.12131 4.5V5.25H13V4.5V3.75H9.12131V4.5ZM2.99999 2.5V3.25H5.87867V2.5V1.75H2.99999V2.5ZM6.93933 2.93934L6.409 3.46967L7.53032 4.59099L8.06065 4.06066L8.59098 3.53033L7.46966 2.40901L6.93933 2.93934ZM5.87867 2.5V3.25C6.07759 3.25 6.26835 3.32902 6.409 3.46967L6.93933 2.93934L7.46966 2.40901C7.04771 1.98705 6.47541 1.75 5.87867 1.75V2.5ZM9.12131 4.5V3.75C8.9224 3.75 8.73164 3.67098 8.59098 3.53033L8.06065 4.06066L7.53032 4.59099C7.95228 5.01295 8.52458 5.25 9.12131 5.25V4.5ZM14.5 6H15.25C15.25 4.75736 14.2426 3.75 13 3.75V4.5V5.25C13.4142 5.25 13.75 5.58579 13.75 6H14.5ZM1.49999 4H2.24999C2.24999 3.58579 2.58578 3.25 2.99999 3.25V2.5V1.75C1.75735 1.75 0.749993 2.75736 0.749993 4H1.49999ZM14.5 6H13.75V7.71247H14.5H15.25V6H14.5ZM13.7293 7.49997V8.24997C13.8734 8.24997 14.0034 8.28906 14.1139 8.35544L14.5 7.71247L14.8861 7.0695C14.5487 6.8669 14.1528 6.74997 13.7293 6.74997V7.49997ZM14.5 7.71247L14.1139 8.35544C14.3708 8.50973 14.5217 8.80785 14.4691 9.12327L15.2089 9.24657L15.9487 9.36987C16.1076 8.41651 15.6443 7.52481 14.8861 7.0695L14.5 7.71247ZM1.49999 7.71246H2.24999V4H1.49999H0.749993V7.71246H1.49999ZM2.27068 7.49997V6.74997C1.8472 6.74997 1.45124 6.86689 1.11387 7.06949L1.49999 7.71246L1.88611 8.35543C1.99663 8.28906 2.12662 8.24997 2.27068 8.24997V7.49997ZM1.49999 7.71246L1.11387 7.06949C0.355686 7.5248 -0.107599 8.4165 0.051294 9.36987L0.79109 9.24657L1.53089 9.12327C1.47832 8.80785 1.62918 8.50973 1.88611 8.35543L1.49999 7.71246Z"
fill="currentColor"
/>
</svg>
);

SvgFolderOpen.displayName = "SvgFolderOpen";
export default SvgFolderOpen;
57 changes: 22 additions & 35 deletions web/lib/opal/src/icons/folder-partial-open.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
import React from "react";
import type { IconProps } from "@opal/types";

const SvgFolderPartialOpen = React.forwardRef<SVGSVGElement, IconProps>(
({ size = 32, color = "currentColor", title, className, ...props }, ref) => (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 13"
width={size}
height={size}
fill="none"
role={title ? "img" : "presentation"}
aria-label={title}
className={className}
stroke="currentColor"
{...props}
>
{title ? <title>{title}</title> : null}
<path
d="M14.1431 4.98782V4.25C14.1431 3.42157 13.4715 2.75 12.6431 2.75H8.76442C8.36659 2.75 7.98506 2.59196 7.70376 2.31066L6.58244 1.18934C6.30113 0.908035 5.9196 0.75 5.52178 0.75H2.6431C1.81467 0.75 1.1431 1.42157 1.1431 2.25V4.9878"
stroke={color}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.2394 10.3532C14.1852 11.1397 13.5313 11.75 12.743 11.75H2.54321C1.75483 11.75 1.101 11.1397 1.04676 10.3532L0.753657 6.1032C0.693864 5.23621 1.38105 4.5 2.2501 4.5H13.0361C13.9051 4.5 14.5923 5.2362 14.5325 6.1032L14.2394 10.3532Z"
stroke={color}
strokeWidth={1.5}
strokeLinecap="round"
/>
</svg>
)
const SvgFolderPartialOpen = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M14.5 6.73782V6C14.5 5.17157 13.8284 4.5 13 4.5H9.1213C8.7235 4.5 8.342 4.34196 8.0607 4.06066L6.93934 2.93934C6.65804 2.65804 6.2765 2.5 5.87868 2.5H3C2.17157 2.5 1.5 3.17157 1.5 4V6.7378"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.5963 12.1032C14.5421 12.8897 13.8883 13.5 13.0999 13.5H2.90011C2.11174 13.5 1.45791 12.8897 1.40366 12.1032L1.11056 7.8532C1.05077 6.98621 1.73795 6.25 2.607 6.25H13.393C14.262 6.25 14.9492 6.9862 14.8894 7.8532L14.5963 12.1032Z"
strokeWidth={1.5}
strokeLinecap="round"
/>
</svg>
);

SvgFolderPartialOpen.displayName = "SvgFolderPartialOpen";
export default SvgFolderPartialOpen;
4 changes: 2 additions & 2 deletions web/src/app/app/services/lib.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,10 +577,10 @@ export function buildChatUrl(
const finalSearchParamsString = finalSearchParams.join("&");

if (finalSearchParamsString) {
return `/${search ? "search" : "chat"}?${finalSearchParamsString}`;
return `/${search ? "search" : "app"}?${finalSearchParamsString}`;
}

return `/${search ? "search" : "chat"}`;
return `/${search ? "search" : "app"}`;
}

export async function uploadFilesForChat(
Expand Down
23 changes: 19 additions & 4 deletions web/src/hooks/useChatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SWR_KEYS } from "@/lib/swr-keys";
import { MinimalAgent } from "@/lib/agents/types";
import useAppFocus from "./useAppFocus";
import { useAgents } from "@/lib/agents/hooks";
import { useActiveProject } from "@/lib/projects/hooks";
import { DEFAULT_AGENT_ID } from "@/lib/constants";

const PAGE_SIZE = 50;
Expand Down Expand Up @@ -175,6 +176,7 @@ export default function useChatSessions(): UseChatSessionsOutput {
);

const appFocus = useAppFocus();
const activeProject = useActiveProject();
const pendingSessions = usePendingSessions();

// Flatten all pages into a single session list
Expand Down Expand Up @@ -241,10 +243,23 @@ export default function useChatSessions(): UseChatSessionsOutput {
}, [allFetchedSessions, pendingSessions]);

const currentChatSessionId = appFocus.isChat() ? appFocus.getId() : null;
const currentChatSession =
chatSessions.find(
(chatSession) => chatSession.id === currentChatSessionId
) ?? null;

// `chatSessions` is the Recents feed, which excludes project chats on purpose
// (`only_non_project_chats` defaults to true on the server). Fall back to the
// owning project's session list so a project chat still resolves — otherwise
// the header actions, document title, and agent lookup all see `null`.
const currentChatSession = useMemo(() => {
if (!currentChatSessionId) return null;
return (
chatSessions.find(
(chatSession) => chatSession.id === currentChatSessionId
) ??
activeProject?.chat_sessions.find(
(chatSession) => chatSession.id === currentChatSessionId
) ??
null
);
}, [chatSessions, activeProject, currentChatSessionId]);

const agentForCurrentChatSession =
useFindAgentForCurrentChatSession(currentChatSession);
Expand Down
5 changes: 4 additions & 1 deletion web/src/layouts/chromes/AppChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { useTierAtLeast } from "@/hooks/useTierAtLeast";
import { Tier } from "@/lib/settings/types";
import { useAppDocumentTitle, useCustomFooterContent } from "@/lib/app/hooks";
import { useFullWidthChat } from "@/providers/FullWidthChatProvider";
import { ActiveProjectBreadcrumb } from "@/lib/projects/components";
import { useIncognito } from "@/providers/IncognitoProvider";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -412,9 +413,10 @@ function Header() {
{/*
Left:
- (mobile) sidebar toggle
- project breadcrumb
- app-mode (for Unified S+C [EE gated])
*/}
<div className="flex-1 flex flex-row items-center gap-2">
<div className="flex-1 min-w-0 flex flex-row items-center gap-2">
{isMobile && (
<Button
prominence="internal"
Expand All @@ -423,6 +425,7 @@ function Header() {
onClick={() => setFolded(false)}
/>
)}
<ActiveProjectBreadcrumb />
{incognitoEnabled &&
(appFocus.isChat() || appFocus.isNewSession()) && (
<OpenButton
Expand Down
Loading
Loading