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 design-system/packages/ui/src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import {
import { classNames } from "../../internal/classNames";
import { Galaxy } from './galaxy';
import { LayersPlus } from './layersPlus';
import { PlayingCardsFan } from './playingCardsFan';
import gitUrl from "./assets/git.svg";
import thinkingUrl from "./assets/thinking.svg";
import creativeUrl from "./assets/creative.svg";
Expand Down Expand Up @@ -176,6 +177,7 @@ export const iconNames = [
"palette",
"panels-top-left",
"pin",
"playing-cards-fan",
"plus",
"plug",
"progress-25",
Expand Down Expand Up @@ -307,6 +309,7 @@ const lineGlyphs = {
"palette": Palette,
"panels-top-left": PanelsTopLeft,
"pin": Pin,
"playing-cards-fan": PlayingCardsFan,
"plus": Plus,
"plug": Plug,
"progress-25": LoaderCircle,
Expand Down
25 changes: 25 additions & 0 deletions design-system/packages/ui/src/components/Icon/playingCardsFan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Lucide playing-cards-fan, absent from the currently bundled lucide-react 0.560.
* Source: https://github.com/lucide-icons/lucide/blob/1.36.0/icons/playing-cards-fan.svg
* Replace with the package export when the shared Lucide dependency is upgraded.
*
* ISC License
* Copyright (c) 2026 Lucide Icons and Contributors
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
import { createLucideIcon } from 'lucide-react';

export const PlayingCardsFan = createLucideIcon('PlayingCardsFan', [
['path', { d: 'M12.65 7.65a2 2 0 012.629-1.046l5.51 2.374a2 2 0 011.046 2.628l-3.957 9.184a2 2 0 01-2.628 1.046l-5.51-2.374a2 2 0 01-1.046-2.628z', key: 'front-card' }],
['path', { d: 'M18 7.777V4a2 2 0 00-2-2h-6a2 2 0 00-2 2v10a2 2 0 001.137 1.805', key: 'middle-card' }],
['path', { d: 'm8 4.389-4.364.809a2 2 0 00-1.602 2.33l1.822 9.833a2 2 0 002.331 1.602l2.542-.47', key: 'back-card' }],
]);
14 changes: 10 additions & 4 deletions design-system/packages/ui/src/components/Menu/MenuPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface MenuPopoverProps extends Omit<MenuProps, "children"> {
ownerRef?: RefObject<HTMLElement | null>;
position?: { x: number; y: number };
placement?: LayerPlacement;
/** Replace the trigger visually while keeping the layer attached to it. */
overlapAnchor?: boolean;
/** Retain custom closing motion before releasing the layer. */
exitDurationMs?: number;
/** Stable wrappers must forward all props (and refs for root/item/separator). */
parts?: MenuPopoverParts;
}
Expand All @@ -57,11 +61,11 @@ function ownItems(menu: HTMLElement) {
}

/** Anchored/coordinate menu with nested navigation, safe pointer corridors and focus return. */
export function MenuPopover({ items, open, onClose, anchorRef, ownerRef, position, placement = "bottom", autoFocusFirstItem = true, ...props }: MenuPopoverProps) {
export function MenuPopover({ items, open, onClose, anchorRef, ownerRef, position, placement = "bottom", overlapAnchor = false, exitDurationMs = 100, autoFocusFirstItem = true, ...props }: MenuPopoverProps) {
const markerRef = useRef<HTMLSpanElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const treeId = useId();
const { present, state: phase } = usePresence(open, 100);
const { present, state: phase } = usePresence(open, exitDurationMs);
const resolvedAnchor = anchorRef ?? markerRef;
const branchRefs = anchorRef ? [anchorRef] : [];
const closeAndRestoreFocus = () => {
Expand Down Expand Up @@ -96,6 +100,7 @@ export function MenuPopover({ items, open, onClose, anchorRef, ownerRef, positio
open={open}
phase={phase}
placement={placement}
overlapAnchor={overlapAnchor}
position={position}
treeId={treeId}
/>
Expand All @@ -118,11 +123,12 @@ interface MenuLevelProps extends Omit<MenuProps, "children"> {
anchorRef: RefObject<HTMLElement | null>;
position?: { x: number; y: number };
placement: LayerPlacement;
overlapAnchor?: boolean;
menuRef?: RefObject<HTMLDivElement | null>;
parts?: MenuPopoverParts;
}

function MenuLevel({ items, open, phase, treeId, onClose, onBack, anchorRef, position, placement, menuRef: externalRef, autoFocusFirstItem, className, inlineSize, style, parts, ...props }: MenuLevelProps) {
function MenuLevel({ items, open, phase, treeId, onClose, onBack, anchorRef, position, placement, overlapAnchor, menuRef: externalRef, autoFocusFirstItem, className, inlineSize, style, parts, ...props }: MenuLevelProps) {
const MenuSurface = parts?.root ?? Menu;
const Item = parts?.item ?? MenuItem;
const Separator = parts?.separator ?? MenuSeparator;
Expand All @@ -140,7 +146,7 @@ function MenuLevel({ items, open, phase, treeId, onClose, onBack, anchorRef, pos
const submenuId = useId();
const intent = useSubmenuIntent({ activeId, onActiveIdChange: setActiveId, parentRef: menuRef, submenuRef, enabled: open, openDelayMs: 150, closeDelayMs: 300, switchDelayMs: 300, tolerance: 50 });
// Keep geometry until presence unmounts the menu, including its exit transition.
const layout = useAnchoredLayer({ open: true, anchorRef, layerRef: menuRef, placement, point: position });
const layout = useAnchoredLayer({ open: true, anchorRef, layerRef: menuRef, placement, overlapAnchor, point: position });
const activeEntry = items.find(item => item.id === activeId && !item.disabled && item.submenu?.length);
const openSubmenu = (item: MenuEntry, trigger: HTMLElement, keyboard: boolean) => {
if (item.disabled || !item.submenu?.length) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface TabGroupItem {
labelTransitionKey?: string | number;
panelId?: string;
/** Presentation and product data attributes on the native tab; selection stays owned by TabGroup. */
tabProps?: Pick<HTMLAttributes<HTMLButtonElement>, "className" | "style" | "title" | "aria-label" | "aria-describedby">
tabProps?: Pick<HTMLAttributes<HTMLButtonElement>, "className" | "style" | "title" | "aria-label" | "aria-describedby" | "aria-haspopup" | "aria-expanded">
& Record<`data-${string}`, string | number | boolean | undefined>;
value: string;
}
Expand Down
3 changes: 2 additions & 1 deletion design-system/packages/ui/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,8 @@ export function Tooltip({

return (
<>
<TooltipTriggerContext.Provider value={!disabled}>
{/* Disabling an authored hint must not enable a nested overflow hint. */}
<TooltipTriggerContext.Provider value={true}>
{triggerElement}
</TooltipTriggerContext.Provider>
{visible && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext } from "react";

/** An explicit tooltip owns this subtree; text slots should not open another one. */
/** An explicit tooltip owns this subtree even while disabled; text slots must not open another one. */
export const TooltipTriggerContext = createContext(false);

/** A legacy title fallback must yield to authored tooltips, not claim their trigger. */
Expand Down
6 changes: 3 additions & 3 deletions design-system/packages/ui/tests/icon.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { Icon, iconNames, canonicalIconNames, iconAliases } from "../dist/index.
import { Network } from "lucide-react";

test("Icon exposes the complete named catalog without duplicate names", () => {
assert.equal(iconNames.length, 100);
assert.equal(canonicalIconNames.length, 97);
assert.equal(iconNames.length, 101);
assert.equal(canonicalIconNames.length, 98);
assert.deepEqual(Object.keys(iconAliases).sort(), ["circle", "download"]);
for (const name of ["thinking", "git", "duplicate", "chevron-left", "selected", "delete", "waitlist-message", "creative", "ultimate", "standard", "minimal", "arrow-down", "unselected"]) {
assert.ok(canonicalIconNames.includes(name), name);
Expand All @@ -34,7 +34,7 @@ test("Icon exposes the complete named catalog without duplicate names", () => {
assert.ok(iconNames.includes("chevron-up"));
assert.ok(iconNames.includes("refresh"));
assert.ok(iconNames.includes("layers-plus"));
for (const name of ["target", "users", "history", "git-pull-request", "battery-full", "galaxy"]) {
for (const name of ["target", "users", "history", "git-pull-request", "battery-full", "galaxy", "playing-cards-fan"]) {
assert.ok(canonicalIconNames.includes(name));
assert.match(renderToStaticMarkup(createElement(Icon, { name })), new RegExp(`lucide-${name}`));
}
Expand Down
3 changes: 3 additions & 0 deletions design-system/packages/ui/tests/tab-group.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ test("tab presentation props reach the button without replacing its owned semant
className: "product-tab", "data-openbitfun-product-component": "product",
"data-openbitfun-product-part": "tab", title: "Full title",
"aria-describedby": "help", style: { opacity: 0.8 },
"aria-haspopup": "menu", "aria-expanded": true,
// Untyped callers must not override component-owned interaction/identity.
role: "button", type: "submit", tabIndex: 8, id: "wrong-id",
"aria-selected": false, "aria-controls": "wrong-panel", "data-openbitfun-part": "wrong-part",
Expand All @@ -58,6 +59,8 @@ test("tab presentation props reach the button without replacing its owned semant
assert.match(button, /data-openbitfun-product-component="product"/);
assert.match(button, /data-openbitfun-product-part="tab"/);
assert.match(button, /aria-describedby="help"/);
assert.match(button, /aria-haspopup="menu"/);
assert.match(button, /aria-expanded="true"/);
assert.match(button, /style="opacity:0.8"/);
assert.match(button, /title="Full title"/);
assert.match(button, /id="product-tab"/);
Expand Down
9 changes: 9 additions & 0 deletions src/crates/assembly/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,15 @@ Narrower local guides already exist for some subtrees:

## Verification

For Agent name references, enabled-catalog admission, and prompt catalog names:

```bash
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib agentic::tools::implementations::task::tests
```

These fixtures cover prompt name mappings and simulated workspace policies;
they do not establish live SSH, relay, Peer, or Detached Dispatch behavior.

For smart routing, unchanged subagent model policy and resumed subagent choices:

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ impl AgentExecutionTool {
let mut out = String::from("<available_agents>\n");
for agent in agents {
out.push_str(&format!(
"<agent type=\"{}\">\n<description>\n{}\n</description>\n<tools>{}</tools>\n</agent>\n",
"<agent type=\"{}\">\n<name>{}</name>\n<description>\n{}\n</description>\n<tools>{}</tools>\n</agent>\n",
agent.id,
agent.name.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;"),
agent.description,
agent.default_tools.join(", ")
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Two modes for action='spawn':
The two modes are mutually exclusive: do not provide `subagent_type` when `fork_context=true`.
1. With an explicit `subagent_type` (default)
- Provide `agent_id`, `subagent_type`, and `prompt`.
- Available types are listed in the <available_agents> section. Each type has specific capabilities and tools.
- Available types and names are listed in the <available_agents> section. Users reference agents by name; match their requested name to this catalog and pass the corresponding internal type. If a name is ambiguous, ask which agent they mean instead of choosing one. Refer to agents by name when talking to the user; do not ask users to provide internal types. Each type has specific capabilities and tools.
- In this mode, the subagent does not share your context. Include all necessary background information in the prompt.
2. By forking the current context
- Set `fork_context=true`, and provide `agent_id` and `prompt`. Do not provide `subagent_type`.
Expand Down Expand Up @@ -162,7 +162,7 @@ impl AgentSpawnTool {
pub(super) fn render_agent_spawn_description(&self) -> String {
r#"Launch a new agent to work independently in the background.

Choose an `agent_type` permitted by the current system prompt. Each type has its own role and tool set.
Choose an `agent_type` permitted by the current system prompt. The <available_agents> catalog includes each agent's name and internal type. Users reference agents by name; match their requested name to this catalog and pass the corresponding internal type as `agent_type`. If a name matches multiple agents, ask for clarification instead of choosing one. Refer to agents by name when talking to the user; do not ask users to provide internal types. Each type has its own role and tool set.

Choose a meaningful `agent_id` for this agent. It must be unique among agents launched by you and match `[a-z][a-z0-9_-]{0,31}`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ struct PromptOrderTestAgent {
id: String,
}

fn named_agent(id: &str, name: &str) -> crate::agentic::agents::AgentInfo {
serde_json::from_value(json!({
"key": id, "id": id, "name": name, "description": "A specialist agent",
"isReadonly": true, "isReview": false, "toolCount": 1,
"defaultTools": ["Read"], "promptCacheScopeKey": "test", "source": "user"
}))
.expect("legacy catalog entry should deserialize")
}

#[test]
fn agent_name_reference_catalog_exposes_the_display_name_and_escapes_markup() {
let available = vec![named_agent("agent-internal", "Research & <review>")];
let listing = AgentExecutionTool::format_agent_descriptions(&available);
assert!(listing.contains("<agent type=\"agent-internal\">"));
assert!(listing.contains("<name>Research &amp; &lt;review&gt;</name>"));
assert!(listing.contains("<tools>Read</tools>"));
assert!(AgentSpawnTool
.render_agent_spawn_description()
.contains("Users reference agents by name"));
}

#[test]
fn external_subagent_validation_accepts_model_emitted_casing() {
let available = vec!["FileFinder".to_string(), "explore".to_string()];
Expand Down
27 changes: 27 additions & 0 deletions src/web-ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ deliberate layout transitions or animate virtualized content. Rely on CI for
full lint, build, and broad test coverage unless the local change specifically
needs it.

For Agent creation, hidden internal IDs, editing, and surface-switch draft fences:

```bash
pnpm --dir src/web-ui run test:run src/app/scenes/agents/components/CreateAgentDialog.test.tsx src/app/scenes/agents/components/suggestAgentId.test.ts src/app/scenes/agents/components/CreateAgentPage.presentation.test.ts src/app/scenes/agents/AgentsScene.test.tsx src/app/scenes/personal-overview/PersonalOverviewScene.actions.test.tsx
```

These DOM fixtures cover local forms and simulated Peer surface switches;
they do not establish native rendering or live remote behavior.

For navigation-divider dragging, elastic feedback, cancellation, and keyboard resizing:

```bash
Expand Down Expand Up @@ -172,6 +181,15 @@ also run the focused state contracts:
pnpm --dir src/web-ui run test:run src/app/services/sessionSceneLifecycle.test.ts src/flow_chat/services/sessionActivation.test.ts src/flow_chat/services/storeSync.test.ts src/app/stores/sceneStore.test.ts
```

For top workspace tab recent-session disclosure, selection and reconciliation:

```bash
pnpm --dir src/web-ui run test:run src/app/components/SceneBar/SceneBar.test.tsx src/app/stores/sceneStore.test.ts src/app/services/sessionSceneLifecycle.test.ts src/app/hooks/useSessionTabLabels.test.tsx src/shared/ui/MenuPopover.test.tsx src/shared/ui/MenuComposition.contract.test.ts
```

These local DOM/store fixtures cover bounded history, legacy tabs and simulated
SSH/Peer identities; they do not establish native rendering or live remote behavior.

For composer mode restoration, passive synchronization, or explicit mode selection:

```bash
Expand Down Expand Up @@ -218,6 +236,15 @@ pnpm --dir src/web-ui run test:run src/app/components/NavPanel/components/Worksp

These DOM and store fixtures do not establish native rendering or live remote behavior.

For assistant history disclosure and default-session selection:

```bash
pnpm --dir src/web-ui run test:run src/app/components/NavPanel/sections/workspaces/WorkspaceListSectionLayout.test.ts src/app/components/NavPanel/sections/workspaces/AssistantSessionHistory.test.tsx src/app/components/NavPanel/sections/workspaces/assistantSessionPreferences.test.ts
```

These local fixtures cover inline disclosure and legacy-host preference gating;
they do not establish native rendering or live remote behavior.

For application update discovery, skip persistence, notification timing, and
download/install transitions, run the focused behavior tests (these do not
establish visual fidelity):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
margin: 0 var(--openbitfun-space-1) 0 calc(var(--openbitfun-space-1) + 4px);

&.is-flat-workspace-view {
margin: 0 var(--openbitfun-space-3);
// The avatar artwork is inset inside its box. Use the navigation's 8px
// optical inset for titles and the toggle, including the 1px list margin.
--openbitfun-nav-session-rail: calc(var(--openbitfun-space-2) - 1px);
margin: 0 1px;
padding: 2px 0;
}
}
Expand Down Expand Up @@ -246,8 +249,8 @@
}
}

// Status and the menu share one trailing cell, including when status is idle.
// Visibility changes never alter the title's available width.
// Nested rows keep one trailing cell, including when status is idle.
// Flat lists release unused menu space below.
&__inline-item-trailing {
display: grid;
flex: 0 0 var(--openbitfun-space-5);
Expand All @@ -267,6 +270,27 @@
}
}

&__inline-list.is-flat-workspace-view {
.openbitfun-nav-panel__inline-item.has-workspace-name .openbitfun-nav-panel__inline-item-trailing {
// Only an actual status occupies row space; hidden actions do not.
display: contents;
}

.openbitfun-nav-panel__inline-item.has-workspace-name .openbitfun-nav-panel__inline-item-actions {
position: absolute;
top: 50%;
right: var(--openbitfun-space-1);
transform: translateY(-50%);
}

// Preserve title geometry while the project name yields to row actions.
.openbitfun-nav-panel__inline-item:hover .openbitfun-nav-panel__inline-item-workspace-name,
.openbitfun-nav-panel__inline-item:focus-within .openbitfun-nav-panel__inline-item-workspace-name,
.openbitfun-nav-panel__inline-item.is-menu-open .openbitfun-nav-panel__inline-item-workspace-name {
visibility: hidden;
}
}

&__inline-item-label {
flex: 1 1 0;
min-width: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,7 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
<div
className={[
'openbitfun-nav-panel__inline-item',
sessionWorkspaceScope && !isChildSession && 'has-workspace-name',
showAssistantIdentity && 'is-assistant-session',
level === 1 && 'is-child',
isChildSession && 'is-btw-child',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
.assistant-session-history {
&__body {
padding-inline: var(--openbitfun-overlay-dialog-header-padding-inline);
}

.openbitfun-nav-panel__inline-list.is-flat-workspace-view {
--openbitfun-nav-session-rail: var(--openbitfun-space-2);

// Text follows the dialog heading; row feedback extends into the gutter.
max-width: none;
margin-inline: calc(-1 * var(--openbitfun-space-2));
padding-block: 0;
}

.openbitfun-nav-panel__inline-item,
.openbitfun-nav-panel__inline-toggle {
padding-right: var(--openbitfun-space-2);
Expand Down
Loading
Loading