Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ac7b6dd
Add design spec for guided tips feature discovery system
selfcontained Jun 12, 2026
7f5380f
Add implementation plan for guided tips feature
selfcontained Jun 12, 2026
20b213e
feat(tips): add semver comparison utility
selfcontained Jun 12, 2026
020038e
feat(tips): add tip registry with starter tips
selfcontained Jun 12, 2026
eff7f7e
chore: move tips test to __tests__ directory
selfcontained Jun 12, 2026
de24430
feat(tips): add Jotai atoms for tips state
selfcontained Jun 12, 2026
479351f
feat(tips): add useTip hook with version gating
selfcontained Jun 12, 2026
794a7c2
feat(tips): add TipPopoverContent component
selfcontained Jun 12, 2026
cf79e2f
feat(tips): add TipQueueProvider for single-popover coordination
selfcontained Jun 12, 2026
8947e06
feat(tips): add TipSpot wrapper component
selfcontained Jun 12, 2026
e1c327a
feat(tips): add AmbientTipBar with idle detection and auto-hide
selfcontained Jun 12, 2026
39f9b4b
feat(tips): wire AmbientTipBar and TipSpot into agents view
selfcontained Jun 12, 2026
005041b
feat(tips): add version init and settings integration
selfcontained Jun 12, 2026
74cce1d
fix(tips): fix TipSpot popover not appearing
selfcontained Jun 12, 2026
25d6804
fix(tips): improve popover positioning and add arrow
selfcontained Jun 12, 2026
3f9e87e
fix(tips): reset lastSeenVersion when resetting tips
selfcontained Jun 12, 2026
743ac60
fix(tips): style arrow border to match popover
selfcontained Jun 12, 2026
3c000cf
fix(tips): solid border-colored arrow on tip popover
selfcontained Jun 12, 2026
9e305ac
fix(tips): match arrow colors to popover border and background
selfcontained Jun 12, 2026
8f21087
fix(tips): use rotated-square clip-path arrow for seamless base
selfcontained Jun 12, 2026
2586d55
fix(tips): use inline SVG arrow for seamless popover integration
selfcontained Jun 12, 2026
3b87547
fix(tips): Learn More navigation, arrow alignment, and mobile visibility
selfcontained Jun 12, 2026
8b4d9ec
refactor(tips): replace polling with MutationObserver-driven reachabi…
selfcontained Jun 12, 2026
8447fee
refactor(tips): scope visibility to trigger element via IntersectionO…
selfcontained Jun 12, 2026
d602195
refactor(tips): replace observers with render-time reachability check
selfcontained Jun 12, 2026
2acc216
fix(tips): use useLayoutEffect for reachability to read committed DOM
selfcontained Jun 12, 2026
0581f5f
feat(tips): ambient tip bar with on-demand tips, layout polish, and n…
selfcontained Jun 13, 2026
7c14eb7
Address review feedback: accessibility, pointer-events, extract TipsV…
selfcontained Jun 13, 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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ dispatch/
├── scripts/ # e2e-isolated.sh, generate-icon-colors.ts
├── .dispatch/ # repo-level Dispatch config
│ ├── config.json # repo-level settings (e.g. Linear integration)
│ ├── job-prompts/ # job prompt definitions
│ ├── job-state/ # persistent state files for recurring jobs
│ ├── personas/ # persona definitions (*.md)
│ └── tools.json # repo-specific MCP tools + lifecycle hooks
└── docs/
└── jobs/ # job prompt definitions (backup copies)
```

- Use `pnpm` (not npm) for all package management.
Expand Down
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"devDependencies": {
"@eslint/js": "^9.39.4",
"@tailwindcss/typography": "^0.5.19",
"@testing-library/react": "^16.3.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.7.0",
Expand All @@ -61,6 +62,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^15.15.0",
"jsdom": "^29.1.1",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"typescript-eslint": "^8.56.1",
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
} from "@/lib/agent-types";
import { type IdeType, sanitizeEnabledIdes } from "@/lib/ide-types";
import { sortAgentsByCreatedAtDesc } from "@/lib/agent-sort";
import { TipQueueProvider } from "@/components/tips/tip-queue-provider";
import { TipsVersionInit } from "@/components/tips/tips-version-init";
import { agentRoute } from "@/lib/agent-routes";
import { ReleaseAvailableToast } from "@/components/app/release-available-toast";
import { UpdateAvailableToast } from "@/components/app/update-available-toast";
Expand Down Expand Up @@ -68,7 +70,7 @@
clearIconColorError: () => void;
};

export function useDashboardContext(): DashboardContextValue {

Check warning on line 73 in apps/web/src/App.tsx

View workflow job for this annotation

GitHub Actions / ci

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
return useOutletContext<DashboardContextValue>();
}

Expand Down Expand Up @@ -240,7 +242,8 @@
};

return (
<>
<TipQueueProvider>
<TipsVersionInit />
<Outlet context={context} />
<ReleaseAvailableToast />
<UpdateAvailableToast />
Expand All @@ -254,11 +257,11 @@
richColors
toastOptions={{ duration: 3000 }}
/>
</>
</TipQueueProvider>
);
}

export async function openAgentFromJobs(

Check warning on line 264 in apps/web/src/App.tsx

View workflow job for this annotation

GitHub Actions / ci

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
navigate: ReturnType<typeof useNavigate>,
agent: Agent
): Promise<void> {
Expand Down
25 changes: 17 additions & 8 deletions apps/web/src/components/app/agents-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { SidebarShell, type NavSection } from "@/components/app/sidebar-shell";
import { StopAgentDialog } from "@/components/app/stop-agent-dialog";
import { QuickPhrasesButton } from "@/components/app/quick-phrases";
import { TerminalPane } from "@/components/app/terminal-pane";
import { AmbientTipBar } from "@/components/tips/ambient-tip-bar";
import { TipSpot } from "@/components/tips/tip-spot";
import {
type Agent,
type AgentVisualState,
Expand Down Expand Up @@ -567,14 +569,16 @@ export function AgentsView({
<PanelRightOpen className="h-4 w-4" />
</Button>
) : null}
<QuickPhrasesButton
agentId={
hasActiveAgent && connState === "connected"
? focusedAgentId!
: null
}
focusTerminal={focusTerminal}
/>
<TipSpot tipId="quick-phrases" side="bottom" align="center">
<QuickPhrasesButton
agentId={
hasActiveAgent && connState === "connected"
? focusedAgentId!
: null
}
focusTerminal={focusTerminal}
/>
</TipSpot>
</div>
<div className="relative h-full min-h-0 min-w-0 pb-14 pt-14">
{focusedAgent?.name ? (
Expand Down Expand Up @@ -625,6 +629,11 @@ export function AgentsView({
: null
}
/>
{!isMobile ? (
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-14">
<AmbientTipBar />
</div>
) : null}
</div>

{!isMobile ? (
Expand Down
20 changes: 18 additions & 2 deletions apps/web/src/components/app/docs-pane.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import {
ArrowDownToLine,
Expand Down Expand Up @@ -160,27 +161,42 @@ export function DocsContent({
onSectionChange: _onSectionChange,
title = "Docs",
}: DocsContentProps): JSX.Element {
const location = useLocation();
const resolvedInitial = isValidDocsSection(initialSection)
? initialSection
: null;
const [activeSection, setActiveSectionState] = useState<DocsSection | null>(
resolvedInitial
);
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (isValidDocsSection(initialSection)) {
setActiveSectionState(initialSection);
}
}, [initialSection]);

useEffect(() => {
const hash = location.hash.replace("#", "");
if (!hash || !contentRef.current) return;
const frame = requestAnimationFrame(() => {
const el = contentRef.current?.querySelector(`#${CSS.escape(hash)}`);
el?.scrollIntoView({ behavior: "smooth", block: "start" });
});
return () => cancelAnimationFrame(frame);
}, [location.hash, activeSection]);

const active =
SECTIONS.find((section) => section.id === activeSection) ?? SECTIONS[0];

return (
<div className="flex min-h-0 flex-1 items-stretch">
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6 px-5 py-6 md:px-8 md:py-8">
<div
ref={contentRef}
className="mx-auto flex w-full max-w-3xl flex-col gap-6 px-5 py-6 md:px-8 md:py-8"
>
<div className="border-b border-border pb-5">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/app/docs-sections/agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export function AgentsContent() {
</Section>

<Section>
<H3>Quick Phrases</H3>
<H3 id="quick-phrases">Quick Phrases</H3>
<P>
The <strong>Quick Phrases</strong> button (speech-bubble icon) in the
terminal top rail lets you save reusable text snippets and inject them
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/components/app/docs-sections/primitives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ export function P({ children }: { children: React.ReactNode }) {
);
}

export function H3({ children }: { children: React.ReactNode }) {
export function H3({
id,
children,
}: {
id?: string;
children: React.ReactNode;
}) {
return (
<h3 className="text-base font-semibold text-foreground">{children}</h3>
<h3 id={id} className="text-base font-semibold text-foreground">
{children}
</h3>
);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/app/docs-sections/tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export function ToolsContent() {
</Section>

<Section>
<H3>Brain (shared memory)</H3>
<H3 id="brain">Brain (shared memory)</H3>
<P>
The Brain is a repo-scoped key-value store and event log that lets
agents share structured state. Objects are organized into collections,
Expand Down
58 changes: 56 additions & 2 deletions apps/web/src/components/app/notification-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useAtom } from "jotai";
import { Play } from "lucide-react";
import { useAtom, useSetAtom } from "jotai";
import { Play, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { api } from "@/lib/api";
import { soundCuesEnabledAtom } from "@/lib/store";
import {
dismissedTipsAtom,
lastSeenVersionAtom,
tipsEnabledAtom,
} from "@/lib/tips/tips-state";
import { CUE_INTENTS, playCueForIntent, playTapCue } from "@/lib/sound-cues";
import {
getNotificationPermission,
Expand Down Expand Up @@ -91,6 +97,50 @@ function SoundCuesSection(): JSX.Element {
);
}

function TipsSection(): JSX.Element {
const [enabled, setEnabled] = useAtom(tipsEnabledAtom);
const setDismissed = useSetAtom(dismissedTipsAtom);
const setLastSeenVersion = useSetAtom(lastSeenVersionAtom);

return (
<div>
<h3 className="mb-1.5 text-[10px] uppercase tracking-widest text-muted-foreground">
Tips & Guidance
</h3>
<p className="mb-3 text-sm text-muted-foreground">
Contextual tips that highlight features and link to docs. This device
only.
</p>
<div className="max-w-lg space-y-3">
<label className="flex cursor-pointer items-center gap-3 rounded border border-border px-3 py-2.5 transition-colors hover:bg-muted/50">
<Checkbox
checked={enabled}
onCheckedChange={(checked) => setEnabled(checked === true)}
data-testid="tips-enabled"
/>
<div className="text-sm font-medium text-foreground">Show tips</div>
</label>
<Button
variant="default"
size="sm"
onClick={() => {
setDismissed([]);
setLastSeenVersion("0.0.0");
toast.success(
"Tips reset — you'll see them again as you use the app."
);
}}
data-testid="reset-dismissed-tips"
className="gap-1.5"
>
<RotateCcw className="h-3 w-3" />
Reset dismissed tips
</Button>
</div>
</div>
);
}

export function NotificationSettings(): JSX.Element {
// Slack settings
const [webhookUrl, setWebhookUrl] = useState("");
Expand Down Expand Up @@ -366,6 +416,10 @@ export function NotificationSettings(): JSX.Element {
<div className="flex flex-col gap-8 overflow-y-auto p-6">
<SoundCuesSection />

<div className="border-t border-border pt-8">
<TipsSection />
</div>

{/* Browser Notifications */}
<div className="border-t border-border pt-8">
<h3 className="mb-1.5 text-[10px] uppercase tracking-widest text-muted-foreground">
Expand Down
Loading
Loading