Skip to content

Commit d721efe

Browse files
authored
feat(web): reach projects from a folded sidebar (#13943)
1 parent 62c269a commit d721efe

18 files changed

Lines changed: 881 additions & 295 deletions

File tree

web/lib/opal/src/components/buttons/line-item-button/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A composite component that wraps `Interactive.Stateful > Interactive.Container >
99
```
1010
Interactive.Stateful <- selectVariant, state, interaction, onClick, href, ref
1111
└─ Interactive.Container <- width, rounding
12-
└─ ContentAction <- withInteractive, padding={2}
12+
└─ ContentAction <- withInteractive, padding
1313
├─ Content <- icon, title, description, sizePreset, variant, ...
1414
└─ rightChildren
1515
```
@@ -18,7 +18,12 @@ The row renders as a focusable `<div role="button">` (with Enter/Space activatio
1818
native `<button>`, so interactive `rightChildren` such as action buttons don't produce invalid
1919
button-in-button nesting. With `href` it renders an anchor instead.
2020

21-
`padding` is hardcoded to `2` and `withInteractive` is always `true`. These are not exposed as props.
21+
`withInteractive` is always `true` and is not exposed. `padding` is forwarded to the inner
22+
`ContentAction`, on top of the row's own `p-1.5` inset.
23+
24+
It is not an open `Spacing`: the prop is inherited from `ContentActionProps`, which narrows it to
25+
`0 | 0.5 | 1 | 2` — the four paddings `Interactive.Container` applies at its size presets, so that a
26+
row's label lines up with an adjacent button. A step outside that set is a type error.
2227

2328
## Props
2429

@@ -41,6 +46,7 @@ button-in-button nesting. With `href` it renders an anchor instead.
4146
|------|------|---------|-------------|
4247
| `rounding` | `InteractiveContainerRoundingVariant` | `"md"` | Corner rounding preset (height is content-driven) |
4348
| `width` | `WidthVariant` | `"full"` | Container width |
49+
| `padding` | `0 \| 0.5 \| 1 \| 2` | `0.5` | Padding around the inner `ContentAction`, as a spacing step (`N / 4` rem) |
4450
| `tooltip` | `string` || Tooltip text shown on hover |
4551
| `tooltipSide` | `TooltipSide` | `"top"` | Tooltip side |
4652

web/lib/opal/src/components/buttons/line-item-button/components.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { type ContentActionProps, ContentAction } from "@opal/layouts";
1414

1515
type ContentPassthroughProps = DistributiveOmit<
1616
ContentActionProps,
17-
"padding" | "width" | "ref"
17+
"width" | "ref"
1818
>;
1919

2020
type LineItemButtonOwnProps = Pick<
@@ -93,6 +93,7 @@ function LineItemButton({
9393
// Sizing
9494
rounding = "md",
9595
width = "full",
96+
padding = 0.5,
9697
tooltip,
9798
tooltipSide = "top",
9899

@@ -128,11 +129,11 @@ function LineItemButton({
128129
rounding={rounding}
129130
{...rowButtonProps}
130131
>
131-
<div className="w-full p-2">
132+
<div className="w-full p-1.5">
132133
<ContentAction
133134
color="interactive"
134135
{...(contentActionProps as ContentActionProps)}
135-
padding={0}
136+
padding={padding}
136137
/>
137138
</div>
138139
</Interactive.Container>

web/lib/opal/src/components/buttons/sidebar-tab/components.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function SidebarTab({
136136
icon ??
137137
(nested
138138
? ((() => (
139-
<div className="w-6" aria-hidden="true" />
139+
<div className="w-4" aria-hidden="true" />
140140
)) as IconFunctionComponent)
141141
: null);
142142

web/lib/opal/src/components/inputs/shared.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
.opal-input {
1919
/* `--interactive-duration` (150ms) felt too laggy, so we're using 100ms instead. */
20-
@apply flex flex-row items-center justify-between h-fit p-[5px] rounded-08 relative w-full transition-all duration-100 ease-(--interactive-easing);
20+
@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);
2121
}
2222

2323
.opal-input[data-variant="primary"] {
Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,17 @@
1-
import React from "react";
21
import type { IconProps } from "@opal/types";
3-
4-
const SvgFolderOpen = React.forwardRef<SVGSVGElement, IconProps>(
5-
({ size = 32, color = "currentColor", title, className, ...props }, ref) => (
6-
<svg
7-
ref={ref}
8-
xmlns="http://www.w3.org/2000/svg"
9-
viewBox="0 0 32 26"
10-
width={size}
11-
height={size}
12-
fill="none"
13-
role={title ? "img" : "presentation"}
14-
aria-label={title}
15-
className={className}
16-
stroke="currentColor"
17-
{...props}
18-
>
19-
{title ? <title>{title}</title> : null}
20-
<path
21-
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"
22-
fill={color}
23-
fillOpacity={0.8}
24-
stroke={color}
25-
strokeOpacity={0.8}
26-
strokeWidth={0.2}
27-
/>
28-
</svg>
29-
)
2+
const SvgFolderOpen = ({ size, ...props }: IconProps) => (
3+
<svg
4+
width={size}
5+
height={size}
6+
viewBox="0 0 16 16"
7+
fill="none"
8+
xmlns="http://www.w3.org/2000/svg"
9+
{...props}
10+
>
11+
<path
12+
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"
13+
fill="currentColor"
14+
/>
15+
</svg>
3016
);
31-
32-
SvgFolderOpen.displayName = "SvgFolderOpen";
3317
export default SvgFolderOpen;
Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,25 @@
1-
import React from "react";
21
import type { IconProps } from "@opal/types";
3-
4-
const SvgFolderPartialOpen = React.forwardRef<SVGSVGElement, IconProps>(
5-
({ size = 32, color = "currentColor", title, className, ...props }, ref) => (
6-
<svg
7-
ref={ref}
8-
xmlns="http://www.w3.org/2000/svg"
9-
viewBox="0 0 16 13"
10-
width={size}
11-
height={size}
12-
fill="none"
13-
role={title ? "img" : "presentation"}
14-
aria-label={title}
15-
className={className}
16-
stroke="currentColor"
17-
{...props}
18-
>
19-
{title ? <title>{title}</title> : null}
20-
<path
21-
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"
22-
stroke={color}
23-
strokeWidth={1.5}
24-
strokeLinecap="round"
25-
strokeLinejoin="round"
26-
/>
27-
<path
28-
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"
29-
stroke={color}
30-
strokeWidth={1.5}
31-
strokeLinecap="round"
32-
/>
33-
</svg>
34-
)
2+
const SvgFolderPartialOpen = ({ size, ...props }: IconProps) => (
3+
<svg
4+
width={size}
5+
height={size}
6+
viewBox="0 0 16 16"
7+
fill="none"
8+
xmlns="http://www.w3.org/2000/svg"
9+
stroke="currentColor"
10+
{...props}
11+
>
12+
<path
13+
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"
14+
strokeWidth={1.5}
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
<path
19+
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"
20+
strokeWidth={1.5}
21+
strokeLinecap="round"
22+
/>
23+
</svg>
3524
);
36-
37-
SvgFolderPartialOpen.displayName = "SvgFolderPartialOpen";
3825
export default SvgFolderPartialOpen;

web/src/app/app/services/lib.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,10 +566,10 @@ export function buildChatUrl(
566566
const finalSearchParamsString = finalSearchParams.join("&");
567567

568568
if (finalSearchParamsString) {
569-
return `/${search ? "search" : "chat"}?${finalSearchParamsString}`;
569+
return `/${search ? "search" : "app"}?${finalSearchParamsString}`;
570570
}
571571

572-
return `/${search ? "search" : "chat"}`;
572+
return `/${search ? "search" : "app"}`;
573573
}
574574

575575
export async function uploadFilesForChat(

web/src/hooks/useChatSessions.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { SWR_KEYS } from "@/lib/swr-keys";
1414
import { MinimalAgent } from "@/lib/agents/types";
1515
import useAppFocus from "./useAppFocus";
1616
import { useAgents } from "@/lib/agents/hooks";
17+
import { useActiveProject } from "@/lib/projects/hooks";
1718
import { DEFAULT_AGENT_ID } from "@/lib/constants";
1819

1920
const PAGE_SIZE = 50;
@@ -175,6 +176,7 @@ export default function useChatSessions(): UseChatSessionsOutput {
175176
);
176177

177178
const appFocus = useAppFocus();
179+
const activeProject = useActiveProject();
178180
const pendingSessions = usePendingSessions();
179181

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

243245
const currentChatSessionId = appFocus.isChat() ? appFocus.getId() : null;
244-
const currentChatSession =
245-
chatSessions.find(
246-
(chatSession) => chatSession.id === currentChatSessionId
247-
) ?? null;
246+
247+
// `chatSessions` is the Recents feed, which excludes project chats on purpose
248+
// (`only_non_project_chats` defaults to true on the server). Fall back to the
249+
// owning project's session list so a project chat still resolves — otherwise
250+
// the header actions, document title, and agent lookup all see `null`.
251+
const currentChatSession = useMemo(() => {
252+
if (!currentChatSessionId) return null;
253+
return (
254+
chatSessions.find(
255+
(chatSession) => chatSession.id === currentChatSessionId
256+
) ??
257+
activeProject?.chat_sessions.find(
258+
(chatSession) => chatSession.id === currentChatSessionId
259+
) ??
260+
null
261+
);
262+
}, [chatSessions, activeProject, currentChatSessionId]);
248263

249264
const agentForCurrentChatSession =
250265
useFindAgentForCurrentChatSession(currentChatSession);

web/src/layouts/chromes/AppChrome.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { useTierAtLeast } from "@/hooks/useTierAtLeast";
7070
import { Tier } from "@/lib/settings/types";
7171
import { useAppDocumentTitle, useCustomFooterContent } from "@/lib/app/hooks";
7272
import { useFullWidthChat } from "@/providers/FullWidthChatProvider";
73+
import { ActiveProjectBreadcrumb } from "@/lib/projects/components";
7374
import { useIncognito } from "@/providers/IncognitoProvider";
7475

7576
// ---------------------------------------------------------------------------
@@ -412,9 +413,10 @@ function Header() {
412413
{/*
413414
Left:
414415
- (mobile) sidebar toggle
416+
- project breadcrumb
415417
- app-mode (for Unified S+C [EE gated])
416418
*/}
417-
<div className="flex-1 flex flex-row items-center gap-2">
419+
<div className="flex-1 min-w-0 flex flex-row items-center gap-2">
418420
{isMobile && (
419421
<Button
420422
prominence="internal"
@@ -423,6 +425,7 @@ function Header() {
423425
onClick={() => setFolded(false)}
424426
/>
425427
)}
428+
<ActiveProjectBreadcrumb />
426429
{incognitoEnabled &&
427430
(appFocus.isChat() || appFocus.isNewSession()) && (
428431
<OpenButton

0 commit comments

Comments
 (0)