Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b90066a
feat(web): scripted full-screen product demo mode
vibegui Jun 15, 2026
6e26101
feat(web): pace the demos and end with a CTA card
vibegui Jun 15, 2026
cd10054
feat(web): inline sprint plan + PR, agent sidebar, daily digest
vibegui Jun 16, 2026
d324df6
feat(sidebar): separate team threads, my threads, and agents
vibegui Jul 1, 2026
3551520
feat(sidebar): make Team/My/Agents collapsible peer sections
vibegui Jul 1, 2026
1590f60
feat(demo): replace the two demos with one 'turtles' scenario — bread…
vibegui Jul 5, 2026
664bdce
feat(web): Library is a toolbar toggle panel, not a route you navigat…
vibegui Jul 5, 2026
c54ca9d
fix(demo): use the real Studio UX — shell, hero, composer, tiles; Sha…
vibegui Jul 5, 2026
a493ede
fix(library-panel): drop the duplicate panel header so the card top-a…
vibegui Jul 5, 2026
1068c9f
feat(demo): org rail as mini-dashboards; org agents in home sidebar
vibegui Jul 5, 2026
084d1c5
feat(library-panel): preview takes over the narrow panel; close lives…
vibegui Jul 5, 2026
be5f807
feat(demo): give the org rail equal billing — 50/50 with the chat
vibegui Jul 5, 2026
bdd6de2
feat(demo): threads everywhere + the brief ends in a navigable action
vibegui Jul 5, 2026
c00ed2e
polish(demo): narrator pill purple/top/blurred; CTA card as app UI wi…
vibegui Jul 5, 2026
dd163a7
fix(demo): the pilot finds and delegates the fix itself; agent zoom b…
vibegui Jul 5, 2026
b263664
feat(demo): sender identities, Settings-as-agent with MCP-app preview…
vibegui Jul 5, 2026
e4aaf15
feat(demo): per-agent MCP apps (ops dashboard, sales, kanban), invite…
vibegui Jul 5, 2026
eec3770
chore(demo): regenerate narration clips with Zoe (Premium)
vibegui Jul 5, 2026
746047d
feat(demo): director's-cut narration — full script, Zoe @186wpm, on b…
vibegui Jul 5, 2026
dabba57
feat(demo): start card (audio unlock), Esc to pause, Zoe @176wpm
vibegui Jul 5, 2026
299a77b
fix(demo): rewrite line 4; every narration clip now finishes before i…
vibegui Jul 5, 2026
3c9c506
feat(demo): narration voice → Isha (Premium) @168wpm; re-time holds t…
vibegui Jul 5, 2026
0329718
feat(demo): no-crop narration guarantee + 'org' purged from all copy
vibegui Jul 5, 2026
baeb166
chore(demo): narration to Isha @170wpm; VO_MS from real measurements
vibegui Jul 5, 2026
e24ba0b
chore(demo): opening line names the product — "your deco Studio"
vibegui Jul 5, 2026
189677f
feat(demo): subtitle cadence, agent-loop SLO card, sharper script @18…
vibegui Jul 5, 2026
0526294
feat(demo): narration via Maya1 TTS (open source, Indian-English)
vibegui Jul 5, 2026
3bf40f9
chore(demo): script tightening — 16 lines, fresh takes
vibegui Jul 5, 2026
b02d7da
fix(demo): no blank chat on team landing; share popover holds longer
vibegui Jul 5, 2026
ebd7917
feat(demo): narration voice → Maya1 US female; script down to 15 lines
vibegui Jul 6, 2026
fdeb02b
feat(demo): MCP-app switcher in preview chrome; on-brand kanban; 40/60
vibegui Jul 6, 2026
155b154
fix(demo): minmax(0,·) grid tracks so thinking text can't widen chat
vibegui Jul 6, 2026
df2a2f1
feat(demo): breadcrumb-context spotlight + full share arc; Studio kanban
vibegui Jul 6, 2026
f00259e
chore(demo): line 17 flows from the logo line — "You can also connect…"
vibegui Jul 6, 2026
f750202
feat(demo): day-zero onboarding scenario (deferred setup)
Jul 6, 2026
cff1fc3
fix(demo): use cn() for app-switcher conditional classes (lint)
vibegui Jul 6, 2026
bef6774
feat(demo): day-zero narration — 9 Maya1 clips, measured holds
vibegui Jul 6, 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
Binary file added apps/mesh/public/demo/vo/dz-01.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/dz-02.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/dz-03.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/dz-04.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/dz-05.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/dz-06.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/dz-07.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/dz-08.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/dz-09.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-01.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-02.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-03.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-04.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-05.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-06.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-07.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-08.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-09.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-10.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-11.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-12.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-13.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-14.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-15.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-16.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-17.m4a
Binary file not shown.
Binary file added apps/mesh/public/demo/vo/vo-18.m4a
Binary file not shown.
7 changes: 7 additions & 0 deletions apps/mesh/src/web/components/chat/chat-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,13 @@ interface TaskProviderInternals {
// ============================================================================

const ChatStreamCtx = createContext<ChatStreamContextValue | null>(null);
/**
* Exposed so Demo Mode (`web/demo/`) can supply a scripted `ChatStreamContextValue`
* directly — feeding the real chat renderers a recorded stream without the live
* `ActiveTaskProvider` (MCP transport, SSE, etc.). Not for app use; prefer the
* hooks. See `web/demo/demo-chat-stream.tsx`.
*/
export { ChatStreamCtx as DemoChatStreamContext };
const ChatTaskCtx = createContext<ChatTaskContextValue | null>(null);
const ChatPrefsCtx = createContext<ChatPrefsContextValue | null>(null);

Expand Down
6 changes: 6 additions & 0 deletions apps/mesh/src/web/components/chat/message/assistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import type { ToolUIPart } from "ai";
import { type ReactNode, Suspense, useEffect, useState } from "react";
import { ToolCallShell } from "./parts/tool-call-part/common.tsx";
import { getPartRenderer as getExtraPartRenderer } from "./parts/extra-part-renderers.ts";
import type { ChatMessage } from "../types.ts";
import { MessageStatsBar } from "../usage-stats.tsx";
import { MessageTextPart } from "./parts/text-part.tsx";
Expand Down Expand Up @@ -518,6 +519,11 @@ function MessagePart({
const getSubtaskMeta = (toolCallId: string) =>
dataParts.toolSubtaskMetadata.get(toolCallId);

// Custom part renderers (e.g. Demo Mode) take precedence over the built-in
// switch. Empty registry in normal use — see extra-part-renderers.ts.
const extraRenderer = getExtraPartRenderer(part.type);
if (extraRenderer) return <>{extraRenderer(part as { type: string })}</>;

switch (part.type) {
case "dynamic-tool":
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Tiny extension point for rendering custom message parts inline in the chat.
*
* The core renderer (`message/assistant.tsx`) consults this registry before its
* built-in switch, so non-core surfaces (e.g. Demo Mode) can render their own
* part types in the real chat flow WITHOUT the core importing them. The registry
* is empty in normal use; a consumer registers a renderer at module load.
*/
import type { ReactNode } from "react";

type PartRenderer = (part: { type: string; output?: unknown }) => ReactNode;

const registry = new Map<string, PartRenderer>();

export function registerPartRenderer(type: string, render: PartRenderer): void {
registry.set(type, render);
}

export function getPartRenderer(type: string): PartRenderer | undefined {
return registry.get(type);
}
116 changes: 116 additions & 0 deletions apps/mesh/src/web/components/sidebar/task-groups/agent-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Plus } from "@untitledui/icons";
import type { DraggableAttributes } from "@dnd-kit/core";
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities";
import { useVirtualMCP } from "@decocms/mesh-sdk";
import { cn } from "@deco/ui/lib/utils.ts";
import {
ContextMenu,
ContextMenuContent,
ContextMenuTrigger,
} from "@deco/ui/components/context-menu.tsx";
import { AgentAvatar } from "@/web/components/agent-icon";
import { AgentGroupContextMenuItems } from "./agent-group-context-menu-items";

export interface AgentRowProps {
virtualMcpId: string;
/** Highlighted when the active thread belongs to this agent. */
isActive: boolean;
/** The current user's thread count for this agent (shown at rest). */
threadCount?: number;
/** Click the row → open the agent (its most recent thread, or a new one). */
onOpen: (virtualMcpId: string) => void;
/** The hover "+" and context-menu "New task" → start a fresh thread. */
onNewTask: (virtualMcpId: string) => void;
onShowSettings: (virtualMcpId: string) => void;
onHideGroup: (virtualMcpId: string) => void;
isOrgPinned?: boolean;
canManageOrgPin?: boolean;
onToggleOrgPin?: (virtualMcpId: string, pinned: boolean) => void;
/** When set, the row is draggable for reordering. */
sortable?: {
attributes: DraggableAttributes;
listeners: SyntheticListenerMap | undefined;
isDragging: boolean;
};
}

export function AgentRow({
virtualMcpId,
isActive,
threadCount,
onOpen,
onNewTask,
onShowSettings,
onHideGroup,
isOrgPinned = false,
canManageOrgPin = false,
onToggleOrgPin,
sortable,
}: AgentRowProps) {
const entity = useVirtualMCP(virtualMcpId);
const isDragging = sortable?.isDragging ?? false;
const title = entity?.title ?? "Agent";

const row = (
<div
{...(sortable ? { ...sortable.attributes, ...sortable.listeners } : {})}
role="button"
tabIndex={0}
onClick={() => onOpen(virtualMcpId)}
onKeyDown={(e) => {
if (e.target !== e.currentTarget) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onOpen(virtualMcpId);
}
}}
className={cn(
"group/group flex items-center gap-2 px-2 py-1.5 rounded-md text-sm font-medium transition-colors",
"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
sortable && "touch-none active:cursor-grabbing",
isActive
? "bg-accent text-accent-foreground"
: "text-foreground hover:bg-accent/60",
isDragging && "shadow-md bg-accent/40 cursor-grabbing",
)}
>
<AgentAvatar icon={entity?.icon ?? null} name={title} size="2xs" />
<span className="flex-1 truncate">{title}</span>
<div className="shrink-0 grid [grid-template-areas:'slot'] items-center justify-items-center">
{typeof threadCount === "number" && threadCount > 0 && (
<span className="[grid-area:slot] size-7 flex items-center justify-center text-xs text-muted-foreground tabular-nums transition-opacity group-hover/group:opacity-0">
{threadCount}
</span>
)}
<button
type="button"
aria-label="New thread with this agent"
onClick={(e) => {
e.stopPropagation();
onNewTask(virtualMcpId);
}}
className="[grid-area:slot] size-7 flex items-center justify-center rounded-md opacity-0 group-hover/group:opacity-100 focus-visible:opacity-100 transition-opacity text-muted-foreground hover:text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
>
<Plus size={14} />
</button>
</div>
</div>
);

return (
<ContextMenu>
<ContextMenuTrigger asChild>{row}</ContextMenuTrigger>
<ContextMenuContent className="w-56">
<AgentGroupContextMenuItems
virtualMcpId={virtualMcpId}
isOrgPinned={isOrgPinned}
canManageOrgPin={canManageOrgPin}
onNewTaskInGroup={onNewTask}
onShowSettings={onShowSettings}
onToggleOrgPin={onToggleOrgPin ?? (() => {})}
onHideGroup={onHideGroup}
/>
</ContextMenuContent>
</ContextMenu>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { TaskRow } from "@/web/layouts/tasks-panel/task-row";
import type { Task } from "@/web/components/chat/task/types";
import { groupThreadsByStatus } from "./group-threads";
import { StatusGroup } from "./task-group";
import { ShowMoreButton } from "./show-more-button";
import type { SidebarFilters } from "./next-page-offset";

/**
* The current user's threads, all agents mixed. Two renderings toggled by the
* sidebar's group-by control:
* - "flat": a single chronological list (each row shows its agent icon).
* - "status": collapsible groups by thread status.
*
* In flat mode, pagination piggybacks on the global thread store's
* `fetchNextPage` (the same feed the Team section reads) via the Show more
* button. Status mode keeps its existing per-status server pagination.
*/
export function MyThreadsSection({
threads,
groupBy,
activeTaskId,
onSelectTask,
onArchiveTask,
filters,
hasMore,
isFetchingMore,
onLoadMore,
}: {
threads: Task[];
groupBy: "flat" | "status";
activeTaskId: string | null;
onSelectTask: (task: Task) => void;
onArchiveTask: (task: Task) => void;
filters: SidebarFilters;
hasMore: boolean;
isFetchingMore: boolean;
onLoadMore: () => void;
}) {
if (groupBy === "status") {
return (
<>
{groupThreadsByStatus(threads).map((group) => (
<StatusGroup
key={group.status}
status={group.status}
threads={group.threads}
activeTaskId={activeTaskId}
onSelectTask={onSelectTask}
onArchiveTask={onArchiveTask}
filters={filters}
/>
))}
</>
);
}

return (
<>
{threads.map((task) => (
<TaskRow
key={task.id}
task={task}
isActive={activeTaskId === task.id}
onClick={() => onSelectTask(task)}
onArchive={() => onArchiveTask(task)}
showAutomationBadge={Boolean(task.trigger_id)}
showAgentIcon
/>
))}
{(hasMore || isFetchingMore) && (
<ShowMoreButton onClick={onLoadMore} isFetching={isFetchingMore} />
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ReactNode } from "react";
import { ChevronDown, ChevronRight } from "@untitledui/icons";

/**
* Small uppercase section label that doubles as an accordion toggle. Shared by
* the sidebar's Team threads / My threads / Agents sections so they read as
* peers. Optional trailing `action` (e.g. "See all") reveals on header hover.
*/
export function SidebarSectionHeader({
label,
open,
onToggle,
count,
action,
}: {
label: string;
open: boolean;
onToggle: () => void;
count?: number;
action?: { label: string; icon?: ReactNode; onClick: () => void };
}) {
return (
<div className="group/section flex items-center gap-1 px-2 pt-1 pb-0.5">
<button
type="button"
aria-expanded={open}
onClick={onToggle}
className="flex flex-1 min-w-0 items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/80 hover:text-foreground transition-colors focus-visible:outline-none"
>
{open ? (
<ChevronDown size={12} className="shrink-0" />
) : (
<ChevronRight size={12} className="shrink-0" />
)}
<span className="truncate">{label}</span>
{typeof count === "number" && count > 0 && (
<span className="tabular-nums text-muted-foreground/60">{count}</span>
)}
</button>
{action && (
<button
type="button"
onClick={action.onClick}
className="shrink-0 flex items-center gap-0.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60 hover:text-foreground opacity-0 group-hover/section:opacity-100 focus-visible:opacity-100 transition-opacity focus-visible:outline-none"
>
{action.label}
{action.icon}
</button>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import type { TaskGroupProps } from "./task-group";
import { TaskGroup } from "./task-group";
import type { TaskGroupData } from "./group-threads";
import type { SidebarOrderScope } from "./stable-order";
import { AgentGroupsSortableLayout } from "./sortable-agent-groups-layout";
import { AgentRow, type AgentRowProps } from "./agent-row";

export interface SortableTaskGroupsProps {
export interface SortableAgentRowsProps {
groups: TaskGroupData[];
orderScope: SidebarOrderScope;
decopilotId: string;
orgPinnedIds: string[];
onReorder: () => void;
renderGroup: (group: TaskGroupData) => TaskGroupProps;
renderGroup: (group: TaskGroupData) => AgentRowProps;
}

export function SortableTaskGroups({
/**
* Flat, draggable list of agents (one row per agent). Reuses the shared
* `AgentGroupsSortableLayout` so decopilot-pinning, org-pinned/user sections,
* and drag-to-reorder behave exactly as they did for the old grouped view —
* only the rendered item changed from a thread group to a single agent row.
*/
export function SortableAgentRows({
groups,
orderScope,
decopilotId,
orgPinnedIds,
onReorder,
renderGroup,
}: SortableTaskGroupsProps) {
}: SortableAgentRowsProps) {
return (
<AgentGroupsSortableLayout
groups={groups}
Expand All @@ -30,9 +35,9 @@ export function SortableTaskGroups({
onReorder={onReorder}
surface="expanded"
renderGroup={renderGroup}
renderFixed={(props) => <TaskGroup key={props.virtualMcpId} {...props} />}
renderFixed={(props) => <AgentRow key={props.virtualMcpId} {...props} />}
renderSortable={(props, sortable) => (
<TaskGroup key={props.virtualMcpId} {...props} sortable={sortable} />
<AgentRow key={props.virtualMcpId} {...props} sortable={sortable} />
)}
/>
);
Expand Down
Loading
Loading