diff --git a/lovable.md b/lovable.md new file mode 100644 index 000000000000..5a2821c9c503 --- /dev/null +++ b/lovable.md @@ -0,0 +1,298 @@ +# Design System Inspired by Lovable + +## 1. Visual Theme & Atmosphere + +Lovable's website radiates warmth through restraint. The entire page sits on a creamy, parchment-toned background (`#f7f4ed`) that immediately separates it from the cold-white conventions of most developer tool sites. This isn't minimalism for minimalism's sake — it's a deliberate choice to feel approachable, almost analog, like a well-crafted notebook. The near-black text (`#1c1c1c`) against this warm cream creates a contrast ratio that's easy on the eyes while maintaining sharp readability. + +The custom Camera Plain Variable typeface is the system's secret weapon. Unlike geometric sans-serifs that signal "tech company," Camera Plain has a humanist warmth — slightly rounded terminals, organic curves, and a comfortable reading rhythm. At display sizes (48px–60px), weight 600 with aggressive negative letter-spacing (-0.9px to -1.5px) compresses headlines into confident, editorial statements. The font uses `ui-sans-serif, system-ui` as fallbacks, acknowledging that the custom typeface carries the brand personality. + +What makes Lovable's visual system distinctive is its opacity-driven depth model. Rather than using a traditional gray scale, the system modulates `#1c1c1c` at varying opacities (0.03, 0.04, 0.4, 0.82–0.83) to create a unified tonal range. Every shade of gray on the page is technically the same hue — just more or less transparent. This creates a visual coherence that's nearly impossible to achieve with arbitrary hex values. The border system follows suit: `1px solid #eceae4` for light divisions and `1px solid rgba(28, 28, 28, 0.4)` for stronger interactive boundaries. + +**Key Characteristics:** +- Warm parchment background (`#f7f4ed`) — not white, not beige, a deliberate cream that feels hand-selected +- Camera Plain Variable typeface with humanist warmth and editorial letter-spacing at display sizes +- Opacity-driven color system: all grays derived from `#1c1c1c` at varying transparency levels +- Inset shadow technique on buttons: `rgba(255,255,255,0.2) 0px 0.5px 0px 0px inset, rgba(0,0,0,0.2) 0px 0px 0px 0.5px inset` +- Warm neutral border palette: `#eceae4` for subtle, `rgba(28,28,28,0.4)` for interactive elements +- Full-pill radius (`9999px`) used extensively for action buttons and icon containers +- Focus state uses `rgba(0,0,0,0.1) 0px 4px 12px` shadow for soft, warm emphasis +- shadcn/ui + Radix UI component primitives with Tailwind CSS utility styling + +## 2. Color Palette & Roles + +### Primary +- **Cream** (`#f7f4ed`): Page background, card surfaces, button surfaces. The foundation — warm, paper-like, human. +- **Charcoal** (`#1c1c1c`): Primary text, headings, dark button backgrounds. Not pure black — organic warmth. +- **Off-White** (`#fcfbf8`): Button text on dark backgrounds, subtle highlight. Barely distinguishable from pure white. + +### Neutral Scale (Opacity-Based) +- **Charcoal 100%** (`#1c1c1c`): Primary text, headings, dark surfaces. +- **Charcoal 83%** (`rgba(28,28,28,0.83)`): Strong secondary text. +- **Charcoal 82%** (`rgba(28,28,28,0.82)`): Body copy. +- **Muted Gray** (`#5f5f5d`): Secondary text, descriptions, captions. +- **Charcoal 40%** (`rgba(28,28,28,0.4)`): Interactive borders, button outlines. +- **Charcoal 4%** (`rgba(28,28,28,0.04)`): Subtle hover backgrounds, micro-tints. +- **Charcoal 3%** (`rgba(28,28,28,0.03)`): Barely-visible overlays, background depth. + +### Surface & Border +- **Light Cream** (`#eceae4`): Card borders, dividers, image outlines. The warm divider line. +- **Cream Surface** (`#f7f4ed`): Card backgrounds, section fills — same as page background for seamless integration. + +### Interactive +- **Ring Blue** (`#3b82f6` at 50% opacity): `--tw-ring-color`, Tailwind focus ring. +- **Focus Shadow** (`rgba(0,0,0,0.1) 0px 4px 12px`): Focus and active state shadow — soft, warm, diffused. + +### Inset Shadows +- **Button Inset** (`rgba(255,255,255,0.2) 0px 0.5px 0px 0px inset, rgba(0,0,0,0.2) 0px 0px 0px 0.5px inset, rgba(0,0,0,0.05) 0px 1px 2px 0px`): The signature multi-layer inset shadow on dark buttons. + +## 3. Typography Rules + +### Font Family +- **Primary**: `Camera Plain Variable`, with fallbacks: `ui-sans-serif, system-ui` +- **Weight range**: 400 (body/reading), 480 (special display), 600 (headings/emphasis) +- **Feature**: Variable font with continuous weight axis — allows fine-tuned intermediary weights like 480. + +### Hierarchy + +| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes | +|------|------|------|--------|-------------|----------------|-------| +| Display Hero | Camera Plain Variable | 60px (3.75rem) | 600 | 1.00–1.10 (tight) | -1.5px | Maximum impact, editorial | +| Display Alt | Camera Plain Variable | 60px (3.75rem) | 480 | 1.00 (tight) | normal | Lighter hero variant | +| Section Heading | Camera Plain Variable | 48px (3.00rem) | 600 | 1.00 (tight) | -1.2px | Feature section titles | +| Sub-heading | Camera Plain Variable | 36px (2.25rem) | 600 | 1.10 (tight) | -0.9px | Sub-sections | +| Card Title | Camera Plain Variable | 20px (1.25rem) | 400 | 1.25 (tight) | normal | Card headings | +| Body Large | Camera Plain Variable | 18px (1.13rem) | 400 | 1.38 | normal | Introductions | +| Body | Camera Plain Variable | 16px (1.00rem) | 400 | 1.50 | normal | Standard reading text | +| Button | Camera Plain Variable | 16px (1.00rem) | 400 | 1.50 | normal | Button labels | +| Button Small | Camera Plain Variable | 14px (0.88rem) | 400 | 1.50 | normal | Compact buttons | +| Link | Camera Plain Variable | 16px (1.00rem) | 400 | 1.50 | normal | Underline decoration | +| Link Small | Camera Plain Variable | 14px (0.88rem) | 400 | 1.50 | normal | Footer links | +| Caption | Camera Plain Variable | 14px (0.88rem) | 400 | 1.50 | normal | Metadata, small text | + +### Principles +- **Warm humanist voice**: Camera Plain Variable gives Lovable its approachable personality. The slightly rounded terminals and organic curves contrast with the sharp geometric sans-serifs used by most developer tools. +- **Variable weight as design tool**: The font supports continuous weight values (e.g., 480), enabling nuanced hierarchy beyond standard weight stops. Weight 480 at 60px creates a display style that feels lighter than semibold but stronger than regular. +- **Compression at scale**: Headlines use negative letter-spacing (-0.9px to -1.5px) for editorial impact. Body text stays at normal tracking for comfortable reading. +- **Two weights, clear roles**: 400 (body/UI/links/buttons) and 600 (headings/emphasis). The narrow weight range creates hierarchy through size and spacing, not weight variation. + +## 4. Component Stylings + +### Buttons + +**Primary Dark (Inset Shadow)** +- Background: `#1c1c1c` +- Text: `#fcfbf8` +- Padding: 8px 16px +- Radius: 6px +- Shadow: `rgba(0,0,0,0) 0px 0px 0px 0px, rgba(0,0,0,0) 0px 0px 0px 0px, rgba(255,255,255,0.2) 0px 0.5px 0px 0px inset, rgba(0,0,0,0.2) 0px 0px 0px 0.5px inset, rgba(0,0,0,0.05) 0px 1px 2px 0px` +- Active: opacity 0.8 +- Focus: `rgba(0,0,0,0.1) 0px 4px 12px` shadow +- Use: Primary CTA ("Start Building", "Get Started") + +**Ghost / Outline** +- Background: transparent +- Text: `#1c1c1c` +- Padding: 8px 16px +- Radius: 6px +- Border: `1px solid rgba(28,28,28,0.4)` +- Active: opacity 0.8 +- Focus: `rgba(0,0,0,0.1) 0px 4px 12px` shadow +- Use: Secondary actions ("Log In", "Documentation") + +**Cream Surface** +- Background: `#f7f4ed` +- Text: `#1c1c1c` +- Padding: 8px 16px +- Radius: 6px +- No border +- Active: opacity 0.8 +- Use: Tertiary actions, toolbar buttons + +**Pill / Icon Button** +- Background: `#f7f4ed` +- Text: `#1c1c1c` +- Radius: 9999px (full pill) +- Shadow: same inset pattern as primary dark +- Opacity: 0.5 (default), 0.8 (active) +- Use: Additional actions, plan mode toggle, voice recording + +### Cards & Containers +- Background: `#f7f4ed` (matches page) +- Border: `1px solid #eceae4` +- Radius: 12px (standard), 16px (featured), 8px (compact) +- No box-shadow by default — borders define boundaries +- Image cards: `1px solid #eceae4` with 12px radius + +### Inputs & Forms +- Background: `#f7f4ed` +- Text: `#1c1c1c` +- Border: `1px solid #eceae4` +- Radius: 6px +- Focus: ring blue (`rgba(59,130,246,0.5)`) outline +- Placeholder: `#5f5f5d` + +### Navigation +- Clean horizontal nav on cream background, fixed +- Logo/wordmark left-aligned (128.75 x 22px) +- Links: Camera Plain 14–16px weight 400, `#1c1c1c` text +- CTA: dark button with inset shadow, 6px radius +- Mobile: hamburger menu with 6px radius button +- Subtle border or no border on scroll + +### Links +- Color: `#1c1c1c` +- Decoration: underline (default) +- Hover: primary accent (via CSS variable `hsl(var(--primary))`) +- No color change on hover — decoration carries the interactive signal + +### Image Treatment +- Showcase/portfolio images with `1px solid #eceae4` border +- Consistent 12px border radius on all image containers +- Soft gradient backgrounds behind hero content (warm multi-color wash) +- Gallery-style presentation for template/project showcases + +### Distinctive Components + +**AI Chat Input** +- Large prompt input area with soft borders +- Suggestion pills with `#eceae4` borders +- Voice recording / plan mode toggle buttons as pill shapes (9999px) +- Warm, inviting input area — not clinical + +**Template Gallery** +- Card grid showing project templates +- Each card: image + title, `1px solid #eceae4` border, 12px radius +- Hover: subtle shadow or border darkening +- Category labels as text links + +**Stats Bar** +- Large metrics: "0M+" pattern in 48px+ weight 600 +- Descriptive text below in muted gray +- Horizontal layout with generous spacing + +## 5. Layout Principles + +### Spacing System +- Base unit: 8px +- Scale: 8px, 10px, 12px, 16px, 24px, 32px, 40px, 56px, 80px, 96px, 128px, 176px, 192px, 208px +- The scale expands generously at the top end — sections use 80px–208px vertical spacing for editorial breathing room + +### Grid & Container +- Max content width: approximately 1200px (centered) +- Hero: centered single-column with massive vertical padding (96px+) +- Feature sections: 2–3 column grids +- Full-width footer with multi-column link layout +- Showcase sections with centered card grids + +### Whitespace Philosophy +- **Editorial generosity**: Lovable's spacing is lavish at section boundaries (80px–208px). The warm cream background makes these expanses feel cozy rather than empty. +- **Content-driven rhythm**: Tight internal spacing within cards (12–24px) contrasts with wide section gaps, creating a reading rhythm that alternates between focused content and visual rest. +- **Section separation**: Footer uses `1px solid #eceae4` border and 16px radius container. Sections defined by generous spacing rather than border lines. + +### Border Radius Scale +- Micro (4px): Small buttons, interactive elements +- Standard (6px): Buttons, inputs, navigation menu +- Comfortable (8px): Compact cards, divs +- Card (12px): Standard cards, image containers, templates +- Container (16px): Large containers, footer sections +- Full Pill (9999px): Action pills, icon buttons, toggles + +## 6. Depth & Elevation + +| Level | Treatment | Use | +|-------|-----------|-----| +| Flat (Level 0) | No shadow, cream background | Page surface, most content | +| Bordered (Level 1) | `1px solid #eceae4` | Cards, images, dividers | +| Inset (Level 2) | `rgba(255,255,255,0.2) 0px 0.5px 0px inset, rgba(0,0,0,0.2) 0px 0px 0px 0.5px inset, rgba(0,0,0,0.05) 0px 1px 2px` | Dark buttons, primary actions | +| Focus (Level 3) | `rgba(0,0,0,0.1) 0px 4px 12px` | Active/focus states | +| Ring (Accessibility) | `rgba(59,130,246,0.5)` 2px ring | Keyboard focus on inputs | + +**Shadow Philosophy**: Lovable's depth system is intentionally shallow. Instead of floating cards with dramatic drop-shadows, the system relies on warm borders (`#eceae4`) against the cream surface to create gentle containment. The only notable shadow pattern is the inset shadow on dark buttons — a subtle multi-layer technique where a white highlight line sits at the top edge while a dark ring and soft drop handle the bottom. This creates a tactile, pressed-into-surface feeling rather than a hovering-above-surface feeling. The warm focus shadow (`rgba(0,0,0,0.1) 0px 4px 12px`) is deliberately diffused and large, creating a soft glow rather than a sharp outline. + +### Decorative Depth +- Hero: soft, warm multi-color gradient wash (pinks, oranges, blues) behind hero — atmospheric, barely visible +- Footer: gradient background with warm tones transitioning to the bottom +- No harsh section dividers — spacing and background warmth handle transitions + +## 7. Do's and Don'ts + +### Do +- Use the warm cream background (`#f7f4ed`) as the page foundation — it's the brand's signature warmth +- Use Camera Plain Variable at display sizes with negative letter-spacing (-0.9px to -1.5px) +- Derive all grays from `#1c1c1c` at varying opacity levels for tonal unity +- Use the inset shadow technique on dark buttons for tactile depth +- Use `#eceae4` borders instead of shadows for card containment +- Keep the weight system narrow: 400 for body/UI, 600 for headings +- Use full-pill radius (9999px) only for action pills and icon buttons +- Apply opacity 0.8 on active states for responsive tactile feedback + +### Don't +- Don't use pure white (`#ffffff`) as a page background — the cream is intentional +- Don't use heavy box-shadows for cards — borders are the containment mechanism +- Don't introduce saturated accent colors — the palette is intentionally warm-neutral +- Don't use weight 700 (bold) — 600 is the maximum weight in the system +- Don't apply 9999px radius on rectangular buttons — pills are for icon/action toggles +- Don't use sharp focus outlines — the system uses soft shadow-based focus indicators +- Don't mix border styles — `#eceae4` for passive, `rgba(28,28,28,0.4)` for interactive +- Don't increase letter-spacing on headings — Camera Plain is designed to run tight at scale + +## 8. Responsive Behavior + +### Breakpoints +| Name | Width | Key Changes | +|------|-------|-------------| +| Mobile Small | <600px | Tight single column, reduced padding | +| Mobile | 600–640px | Standard mobile layout | +| Tablet Small | 640–700px | 2-column grids begin | +| Tablet | 700–768px | Card grids expand | +| Desktop Small | 768–1024px | Multi-column layouts | +| Desktop | 1024–1280px | Full feature layout | +| Large Desktop | 1280–1536px | Maximum content width, generous margins | + +### Touch Targets +- Buttons: 8px 16px padding (comfortable touch) +- Navigation: adequate spacing between items +- Pill buttons: 9999px radius creates large tap-friendly targets +- Menu toggle: 6px radius button with adequate sizing + +### Collapsing Strategy +- Hero: 60px → 48px → 36px headline scaling with proportional letter-spacing +- Navigation: horizontal links → hamburger menu at 768px +- Feature cards: 3-column → 2-column → single column stacked +- Template gallery: grid → stacked vertical cards +- Stats bar: horizontal → stacked vertical +- Footer: multi-column → stacked single column +- Section spacing: 128px+ → 64px on mobile + +### Image Behavior +- Template screenshots maintain `1px solid #eceae4` border at all sizes +- 12px border radius preserved across breakpoints +- Gallery images responsive with consistent aspect ratios +- Hero gradient softens/simplifies on mobile + +## 9. Agent Prompt Guide + +### Quick Color Reference +- Primary CTA: Charcoal (`#1c1c1c`) +- Background: Cream (`#f7f4ed`) +- Heading text: Charcoal (`#1c1c1c`) +- Body text: Muted Gray (`#5f5f5d`) +- Border: `#eceae4` (passive), `rgba(28,28,28,0.4)` (interactive) +- Focus: `rgba(0,0,0,0.1) 0px 4px 12px` +- Button text on dark: `#fcfbf8` + +### Example Component Prompts +- "Create a hero section on cream background (#f7f4ed). Headline at 60px Camera Plain Variable weight 600, line-height 1.10, letter-spacing -1.5px, color #1c1c1c. Subtitle at 18px weight 400, line-height 1.38, color #5f5f5d. Dark CTA button (#1c1c1c bg, #fcfbf8 text, 6px radius, 8px 16px padding, inset shadow) and ghost button (transparent bg, 1px solid rgba(28,28,28,0.4) border, 6px radius)." +- "Design a card on cream (#f7f4ed) background. Border: 1px solid #eceae4. Radius 12px. No box-shadow. Title at 20px Camera Plain Variable weight 400, line-height 1.25, color #1c1c1c. Body at 14px weight 400, color #5f5f5d." +- "Build a template gallery: grid of cards with 12px radius, 1px solid #eceae4 border, cream backgrounds. Each card: image with 12px top radius, title below. Hover: subtle border darkening." +- "Create navigation: sticky on cream (#f7f4ed). Camera Plain 16px weight 400 for links, #1c1c1c text. Dark CTA button right-aligned with inset shadow. Mobile: hamburger menu with 6px radius." +- "Design a stats section: large numbers at 48px Camera Plain weight 600, letter-spacing -1.2px, #1c1c1c. Labels below at 16px weight 400, #5f5f5d. Horizontal layout with 32px gap." + +### Iteration Guide +1. Always use cream (`#f7f4ed`) as the base — never pure white +2. Derive grays from `#1c1c1c` at opacity levels rather than using distinct hex values +3. Use `#eceae4` borders for containment, not shadows +4. Letter-spacing scales with size: -1.5px at 60px, -1.2px at 48px, -0.9px at 36px, normal at 16px +5. Two weights: 400 (everything except headings) and 600 (headings) +6. The inset shadow on dark buttons is the signature detail — don't skip it +7. Camera Plain Variable at weight 480 is for special display moments only diff --git a/mascort.gif b/mascort.gif new file mode 100644 index 000000000000..f0df6270a23f Binary files /dev/null and b/mascort.gif differ diff --git a/packages/app/public/mascort.gif b/packages/app/public/mascort.gif new file mode 100644 index 000000000000..f0df6270a23f Binary files /dev/null and b/packages/app/public/mascort.gif differ diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 3842b087914e..b1045dde794c 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -664,8 +664,8 @@ export const PromptInput: Component = (props) => { if (!query.trim()) return [...references, ...agents, ...mcpResources, ...pinned] const paths = await files.searchFilesAndDirectories(query) const fileOptions: AtOption[] = paths - .filter((path) => !seen.has(path)) - .map((path) => ({ type: "file", path, display: path })) + .filter((path: string) => !seen.has(path)) + .map((path: string) => ({ type: "file", path, display: path })) return [...references, ...agents, ...mcpResources, ...pinned, ...fileOptions] }, key: atKey, @@ -1462,6 +1462,7 @@ export const PromptInput: Component = (props) => { void }) { const language = useLanguage() + const sync = useSync() let pending: string | undefined const selected = () => (props.value === props.projectRoot ? "main" : props.value) const icon = () => { @@ -93,17 +95,14 @@ export function PromptWorkspaceSelector(props: { - + ) } export function PromptGitStatus(props: { branch?: string; noGit?: boolean }) { - const language = useLanguage() - const label = () => { - if (props.noGit) return language.t("session.new.git.none") - return props.branch - } + const isNoGit = () => Boolean(props.noGit || !props.branch) + const label = () => (isNoGit() ? "non git" : props.branch) return ( @@ -112,7 +111,7 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean }) { diff --git a/packages/app/src/components/session/session-new-design-view.tsx b/packages/app/src/components/session/session-new-design-view.tsx index a324c64fa6df..8531098db8ce 100644 --- a/packages/app/src/components/session/session-new-design-view.tsx +++ b/packages/app/src/components/session/session-new-design-view.tsx @@ -1,14 +1,336 @@ -import type { JSX } from "solid-js" +import { createMemo, For, type JSX } from "solid-js" +import { useNavigate } from "@solidjs/router" import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2" -import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { usePrompt } from "@/context/prompt" +import { useSync } from "@/context/sync" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { getFilename } from "@opencode-ai/core/util/path" +import { sessionTitle } from "@/utils/session-title" +import { legacySessionHref } from "@/utils/session-route" + +const ALL_PROMPT_CARDS = [ + { + title: "Build Feature", + desc: "Generate new components or logic", + icon: "sparkles", + prompt: "Help me design and implement a new feature for this project.", + }, + { + title: "Explain Codebase", + desc: "Understand architecture & flow", + icon: "magnifying-glass", + prompt: "Can you give me an overview of how this codebase is structured?", + }, + { + title: "Debug & Fix", + desc: "Identify errors and stack traces", + icon: "help", + prompt: "Review the workspace for potential bugs or code smells.", + }, + { + title: "Refactor Code", + desc: "Optimize speed and readability", + icon: "edit", + prompt: "Suggest performance optimizations and code quality refactoring.", + }, + { + title: "Write Tests", + desc: "Create comprehensive test suites", + icon: "check", + prompt: "Write unit and integration tests for the primary modules.", + }, + { + title: "Add Features", + desc: "Expand functionality & UI", + icon: "plus", + prompt: "Add new interactive controls and expand application capabilities.", + }, + { + title: "Review Security", + desc: "Check for vulnerabilities & leaks", + icon: "settings-gear", + prompt: "Audit the codebase for security flaws and credential exposure.", + }, + { + title: "Optimize Performance", + desc: "Reduce latency & bundle size", + icon: "grid-plus", + prompt: "Analyze performance bottlenecks and suggest optimizations.", + }, + { + title: "Clean Up Code", + desc: "Tidy up structure & types", + icon: "folder", + prompt: "Clean up unused code, dead imports, and refine file organization.", + }, + { + title: "Branch Workflow", + desc: "Manage VCS & git changes", + icon: "branch", + prompt: "Help me manage git branches and prepare changes for review.", + }, +] as const + +type HybridCard = { + id: string + title: string + desc: string + icon: string + kind: "prompt" | "project" | "session" + payload: string +} + +function timeAgo(timestamp?: number) { + if (!timestamp) return "Resume session" + const diff = Date.now() - timestamp + if (diff < 60_000) return "Active just now" + const mins = Math.floor(diff / 60_000) + if (mins < 60) return `Updated ${mins}m ago` + const hours = Math.floor(mins / 60) + if (hours < 24) return `Updated ${hours}h ago` + const days = Math.floor(hours / 24) + if (days < 30) return `Updated ${days}d ago` + return "Resume session" +} + +function isGenericTitle(title?: string) { + if (!title) return true + const clean = sessionTitle(title) + return !clean || clean === "Greeting" || clean === "New session" || clean === "Child session" || clean === "Untitled Session" +} + +function getDynamicGreeting(projectName?: string) { + const hour = new Date().getHours() + let timeGreeting = "Good day" + if (hour >= 5 && hour < 12) timeGreeting = "Good morning" + else if (hour >= 12 && hour < 17) timeGreeting = "Good afternoon" + else if (hour >= 17 && hour < 22) timeGreeting = "Good evening" + else timeGreeting = "Good night" + + const target = projectName ? `for ${projectName}` : "your project" + + // Diverse Claude & ChatGPT inspired greetings (mixing occasional time greetings with action & thought prompts) + const options = [ + `${timeGreeting}! What are we building today?`, + `${timeGreeting}! How can I help with ${target}?`, + `${timeGreeting}! Ready to build?`, + `What would you like to build today?`, + `Where shall we start with ${target}?`, + `What's on your mind today?`, + `How can I help with ${target}?`, + `Let's turn your ideas into code.`, + `What are we working on next?`, + `Ready to craft something great?`, + ] + + const index = Math.floor(Math.random() * options.length) + return options[index]! +} export function NewSessionDesignView(props: { children: JSX.Element }) { + const prompt = usePrompt() + const sync = useSync() + const navigate = useNavigate() + + const activeProjectName = createMemo(() => { + const proj = sync().project + if (proj?.name) return proj.name + if (proj?.worktree) return getFilename(proj.worktree) + return undefined + }) + + const greeting = createMemo(() => getDynamicGreeting(activeProjectName())) + + // Hybrid Action Cards: Combines Recent Projects/Sessions (if available) with Dynamic Prompt Cards + const hybridCards = createMemo(() => { + const cards: HybridCard[] = [] + const seen = new Set() + + // 1. Collect Recent Projects (max 1) + const rawProjects = Array.isArray(sync().data.project) ? (sync().data.project as unknown as any[]) : [] + const projects = rawProjects + .slice() + .sort((a: any, b: any) => (b.time?.updated ?? b.time?.created ?? 0) - (a.time?.updated ?? a.time?.created ?? 0)) + for (const project of projects) { + if (cards.length >= 1) break + const name = getFilename(project.worktree) || project.name || "Project" + if (!seen.has(`project:${name}`)) { + seen.add(`project:${name}`) + cards.push({ + id: `proj-${project.id}`, + title: name, + desc: "Recent Project", + icon: "folder", + kind: "project", + payload: `/${base64Encode(project.worktree)}`, + }) + } + } + + + // 2. Collect Recent Sessions (max 1-2, keeping total recent items <= 2 to guarantee prompt suggestions) + const maxSessions = Math.max(1, 2 - cards.length) + const rawSessions = Array.isArray(sync().data.session) ? (sync().data.session as unknown as any[]) : [] + const sessions = rawSessions + .slice() + .sort((a: any, b: any) => (b.time?.updated ?? b.time?.created ?? 0) - (a.time?.updated ?? a.time?.created ?? 0)) + let addedSessions = 0 + for (const sess of sessions) { + if (addedSessions >= maxSessions) break + if (isGenericTitle(sess.title)) continue + const title = sessionTitle(sess.title) || "Recent Session" + const dir = sess.directory || sess.location?.directory || sync().project?.worktree || "" + if (!seen.has(`session:${sess.id}`)) { + seen.add(`session:${sess.id}`) + addedSessions++ + cards.push({ + id: `sess-${sess.id}`, + title, + desc: "Resume session", + icon: "status", + kind: "session", + payload: legacySessionHref(dir, sess.id), + }) + } + } + + // 3. Fill remaining slots (at least 2) with Dynamic Prompt Cards + const shuffledPrompts = [...ALL_PROMPT_CARDS].sort(() => 0.5 - Math.random()) + for (const p of shuffledPrompts) { + if (cards.length >= 4) break + if (!seen.has(`prompt:${p.title}`)) { + seen.add(`prompt:${p.title}`) + cards.push({ + id: `prompt-${p.title}`, + title: p.title, + desc: p.desc, + icon: p.icon, + kind: "prompt", + payload: p.prompt, + }) + } + } + + return cards + }) + + function handleCardClick(card: HybridCard) { + if (card.kind === "prompt") { + if (prompt) { + prompt.set([{ type: "text", content: card.payload, start: 0, end: card.payload.length }], card.payload.length) + } + } else { + navigate(card.payload) + } + } + return ( -
-
-
- -
{props.children}
+
+ {/* Dynamic Ambient Background: Soft Black Shadow on Light Mode, Soft White Glow on Dark Mode */} +
+ {/* Soft Radial Ambient Spotlight & Vignette */} +
+
+
+ + {/* Embedded Theme Styles for Ambient Center Background */} + + + {/* Main Hero Container */} +
+ {/* Brand Header */} +
+
+ +
+

+ {greeting()} +

+
+ + {/* Prompt Input Box */} +
+ {/* Mascot sitting/peeking at the selected location top-right of prompt input box */} +
+ OpenCode Mascot +
+ {props.children} +
+ + {/* Dynamic Hybrid Action Cards (Recent Projects / Sessions + Prompt Suggestions) */} +
+ + {(card) => ( + + )} +
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 4f7b949e61a7..27519ae35894 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -235,12 +235,15 @@ export const loadProvidersQuery = ( return normalizeProviderList(result.data!) } const location = directory ? { location: { directory } } : undefined + const providerList = (sdk as any).provider?.list ?? (sdk as any).providers?.list + const modelList = (sdk as any).model?.list ?? (sdk as any).models?.list + const modelDefault = (sdk as any).model?.default ?? (sdk as any).models?.default const [providers, models, defaultModel] = await Promise.all([ - sdk.provider.list(location), - sdk.model.list(location), - sdk.model.default(location), + providerList ? providerList(location) : Promise.resolve({ data: [] }), + modelList ? modelList(location) : Promise.resolve({ data: [] }), + modelDefault ? modelDefault(location) : Promise.resolve({ data: undefined }), ]) - return normalizeProviderList(providers.data, models.data, defaultModel.data) + return normalizeProviderList(providers?.data ?? [], models?.data ?? [], defaultModel?.data) }), }) @@ -293,6 +296,7 @@ export const loadCommands = ( } }) } + if (!api?.list) return [] return api.list({ location: { directory } }).then((result) => result.data) }) @@ -434,9 +438,14 @@ export async function bootstrapDirectory(input: { retry(() => (async () => { if ((await input.protocol) === "v1") return (await input.sdk.permission.list()).data ?? [] - return input.api.permission.request - .list({ location: { directory: input.directory } }) - .then((result) => result.data.map(normalizePermissionRequest)) + const listFn = + (input.api.permission as any)?.request?.list ?? + (input.api.permissions as any)?.listRequests ?? + (input.api.permission as any)?.listRequests ?? + (() => Promise.resolve({ data: [] })) + return listFn({ location: { directory: input.directory } }).then((result: any) => + (result?.data ?? []).map(normalizePermissionRequest), + ) })().then((permissions) => { const ids = permissions.map((permission) => permission.sessionID) const grouped = groupBySession( @@ -470,9 +479,12 @@ export async function bootstrapDirectory(input: { retry(() => (async () => { if ((await input.protocol) === "v1") return (await input.sdk.question.list()).data ?? [] - return input.api.question.request - .list({ location: { directory: input.directory } }) - .then((result) => result.data) + const listFn = + (input.api.question as any)?.request?.list ?? + (input.api.questions as any)?.listRequests ?? + (input.api.question as any)?.listRequests ?? + (() => Promise.resolve({ data: [] })) + return listFn({ location: { directory: input.directory } }).then((result: any) => result?.data ?? []) })().then((questions) => { const ids = questions.map((question) => question.sessionID) const grouped = groupBySession( diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index d6d8019262a4..e4b05d42d770 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -261,9 +261,14 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync } if ((await input.sdk.protocol) === "v1") { return (await input.sdk.client.permission.list({ directory })).data ?? [] } - return input.sdk.api.permission.request - .list({ location: { directory } }) - .then((result) => result.data.map(normalizePermissionRequest)) + const listFn = + (input.sdk.api.permission as any)?.request?.list ?? + (input.sdk.api.permissions as any)?.listRequests ?? + (input.sdk.api.permission as any)?.listRequests ?? + (() => Promise.resolve({ data: [] })) + return listFn({ location: { directory } }).then((result: any) => + (result?.data ?? []).map(normalizePermissionRequest), + ) } function respondOnce(permission: PermissionRequest, directory?: string) { diff --git a/packages/app/src/context/server-sync.tsx b/packages/app/src/context/server-sync.tsx index 109a7bf7d65c..0b84a11ddab8 100644 --- a/packages/app/src/context/server-sync.tsx +++ b/packages/app/src/context/server-sync.tsx @@ -358,7 +358,12 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { void bootstrapInstance(directory) }, onMcp: (directory, setStore) => { - void loadCommands(directory, serverSDK.api.command, sdkFor(directory), serverSDK.protocol) + void loadCommands( + directory, + serverSDK.api.command ?? (serverSDK.api as any).commands, + sdkFor(directory), + serverSDK.protocol, + ) .then((commands) => setStore("command", commands)) .catch((err) => { showToast({ diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index fe8b4e3c03f7..fd9e6ae939df 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -184,7 +184,7 @@ const defaultSettings: Settings = { showSearch: false, showStatus: false, showTerminal: false, - showReasoningSummaries: false, + showReasoningSummaries: true, shellToolPartsExpanded: false, editToolPartsExpanded: false, showCustomAgents: false, diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 78a9e103a7f5..a5355cc4fef0 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -327,4 +327,22 @@ animation-range: 0 0.1px; } } + + /* Lovable Theme Utilities & Selection Styling */ + .lovable-gradient-text { + background: linear-gradient(135deg, #a855f7 0%, #ec4899 50%, #6366f1 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + } + + .lovable-glow-border { + border-color: rgba(168, 85, 247, 0.25); + box-shadow: 0 0 16px -2px rgba(124, 58, 237, 0.15); + } + + ::selection { + background: rgba(168, 85, 247, 0.3); + color: inherit; + } } diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx index d97f5779ec19..fcb52465d522 100644 --- a/packages/app/src/pages/new-session.tsx +++ b/packages/app/src/pages/new-session.tsx @@ -92,7 +92,7 @@ export default function NewSessionPage() { const selectedBranch = createMemo(() => { const worktree = newSessionWorktree() if (worktree === "main" || worktree === "create") return localBranch() - return serverSync().child(worktree)[0].vcs?.branch ?? localBranch() + return serverSync().child(worktree)[0].vcs?.branch }) const promptInputV2Controller = usePromptInputV2Controller({ get controls() { @@ -178,7 +178,12 @@ export default function NewSessionPage() { } + fallback={ + + } > view().review.mode() ?? "git" + const reviewMode = () => view().review.mode() ?? (sync().project?.vcs === "git" ? "git" : "turn") const reviewFile = () => view().review.file() const sessionOwnership = createSessionOwnership(sessionKey) const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns()) @@ -714,6 +714,7 @@ export default function Page() { const reviewCount = () => reviewDiffs().length const hasReview = () => reviewCount() > 0 const reviewReady = () => { + if (sync().project?.vcs !== "git") return true if (reviewMode() === "git" || reviewMode() === "branch") return !vcsQuery.isPending return true } @@ -1237,7 +1238,6 @@ export default function Page() { } if (reviewMode() === "turn") { - if (nogit()) return createGit(input) return empty(reviewEmptyText()) } @@ -1252,9 +1252,6 @@ export default function Page() { if ((reviewMode() === "git" || reviewMode() === "branch") && !reviewReady()) { return
{language.t("session.review.loadingChanges")}
} - if (reviewMode() === "turn" && nogit()) { - return - } return } diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index 600ff41e3def..2b1eb5d7a685 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -33,7 +33,7 @@ export function SessionComposerRegion(props: {
diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 497ebceb87ff..0f1ed95b471a 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -829,7 +829,7 @@ export function MessageTimeline(props: { navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] }) }) - .catch((err) => { + .catch((err: unknown) => { showToast({ title: language.t("common.requestFailed"), description: errorMessage(err), @@ -848,7 +848,7 @@ export function MessageTimeline(props: { const result = await sdk() .api.session.remove({ sessionID }) .then(() => true) - .catch((err) => { + .catch((err: unknown) => { showToast({ title: language.t("session.delete.failed.title"), description: errorMessage(err), @@ -1080,7 +1080,7 @@ export function MessageTimeline(props: { data-timeline-row={input.row()._tag} classList={{ "min-w-0 w-full max-w-full": true, - "md:max-w-200 2xl:max-w-[1000px]": props.centered, + "md:max-w-[48rem]": props.centered, "md:mx-auto": props.centered, "pt-3": previousAssistantPart(), }} @@ -1095,7 +1095,7 @@ export function MessageTimeline(props: { const renderTimelineRow = (row: Accessor, onSizeChange?: () => void) => { switch (row()._tag) { case "TurnGap": - return