Skip to content

Conversation

@kaochannel154
Copy link
Contributor

@kaochannel154 kaochannel154 commented Dec 22, 2025

Summary

This PR adds subtle, non-blocking idle hints to help users discover next actions.

Changes

  • /playground: Add an idle hint (text + arrow) that appears in sync with the “to send” nudge.
    • スクリーンショット 2025-12-22 15 25 11
    • Uses a 1-frame rAF flip to avoid a “pop” on fade-in.
    • Persists state so it only shows on first visit and never reappears after a run.
  • /tasks/[taskId]: Add an idle hint near Edit in Studio.
    • スクリーンショット 2025-12-22 16 24 33
    • Copy: "Explore how this app works."
    • Persisted so it only ever shows once.
    • Does not dismiss on pointerdown (so taking screenshots doesn’t hide it).

Hint conditions

/playground

Show when

  • First visit only (localStorage: giselle:playground:hasVisited is missing)
  • User has NOT run any app yet (localStorage: giselle:playground:hasRunAnyApp is missing)
  • Hint has NOT been shown before (localStorage: giselle:playground:hasShownIdleHint is missing)
  • No user interaction for 8 seconds (not running, no input, no pending uploads)

Hide when

  • Any interaction (typing/keys, selecting app, run) immediately hides it
  • Once shown, it won’t re-show in the same session
  • After any run, it will never show again (persisted)

/tasks/[taskId]

Show when

  • No user interaction for 8 seconds
  • Hint has NOT been shown before (localStorage: giselle:tasks:editInStudioHintShown is missing)

Hide when

  • keydown / focusin / clicking "Edit in Studio" hides it

Files

  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx

PR Type

Enhancement


Description

  • Add idle hint system to Playground showing "Run a sample app" after 8 seconds of inactivity

  • Add idle hint to Task detail page showing "Explore how this app works" near Edit in Studio button

  • Implement localStorage persistence to show hints only once per user

  • Add visual feedback with animated arrow SVG and subtle pulse animation on idle state


Diagram Walkthrough

flowchart LR
  User["User visits page"]
  Idle["8 seconds idle<br/>no interaction"]
  Check{"localStorage<br/>hint shown?"}
  Show["Show hint with<br/>arrow animation"]
  Interact["User interaction<br/>keydown/click"]
  Hide["Hide hint"]
  Persist["Mark hint as<br/>shown in storage"]
  
  User --> Idle
  Idle --> Check
  Check -->|No| Show
  Check -->|Yes| Hide
  Show --> Persist
  Show --> Interact
  Interact --> Hide
Loading

File Walkthrough

Relevant files
Enhancement
playground-stage-input.tsx
Implement idle hint system with interaction tracking         

apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx

  • Add idle nudge state management with localStorage keys for tracking
    first visit, hint display, and app runs
  • Implement interaction tracking via callbacks that reset idle timer on
    user actions (typing, file upload, app selection, submit)
  • Add conditional rendering of idle hint with rAF-based fade-in to
    prevent visual "pop"
  • Apply visual pulse animation to submit button and file info section
    when idle nudge is active
  • Wrap all event handlers to trigger interaction tracking
+182/-13
page.client.tsx
Add idle hint UI and interaction propagation to Playground

apps/studio.giselles.ai/app/(main)/playground/page.client.tsx

  • Add IdleHintArrow SVG component for visual idle hint indicator
  • Add interaction tick state and hasRunAnyApp tracking to parent
    component
  • Pass interaction tick and idle hint renderer to PlaygroundStageInput
    component
  • Add global event listeners for pointer, keyboard, and focus
    interactions
  • Render idle hint with custom text "Run a sample app." and arrow
    positioned near input area
+76/-4   
edit-in-studio-link-with-hint.tsx
Create Edit in Studio link with idle hint wrapper               

apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx

  • Create new component wrapping "Edit in Studio" link with idle hint
    functionality
  • Implement 8-second idle timer with localStorage persistence for hint
    display
  • Add IdleHintArrow SVG component with rotation and mirroring for
    right-to-left direction
  • Use rAF-based fade-in animation to prevent visual pop on hint
    appearance
  • Attach global keydown and focusin listeners to dismiss hint on any
    interaction
+143/-0 
task-header.tsx
Integrate idle hint component into Task header                     

apps/studio.giselles.ai/components/task/task-header.tsx

  • Replace inline "Edit in Studio" link with new EditInStudioLinkWithHint
    component
  • Pass workspace ID, icon, and hint text "Explore how this app works."
    to new component
  • Remove previous Link and StatusBadge markup in favor of extracted
    component
+8/-19   

Summary by CodeRabbit

  • New Features
    • Added idle-hint nudges that appear after inactivity to guide user interactions in the playground and workspace editing areas.
    • Idle hints are shown once per user and remembered via local storage.
    • Added visual feedback animations when idle to prompt engagement.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 22, 2025 07:38
@kaochannel154 kaochannel154 requested a review from shige as a code owner December 22, 2025 07:38
@changeset-bot
Copy link

changeset-bot bot commented Dec 22, 2025

⚠️ No Changeset found

Latest commit: 34cb6cb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

💥 An error occurred when fetching the changed packages and changesets in this PR
Some errors occurred when validating the changesets config:
The package or glob expression "giselles-ai" is specified in the `ignore` option but it is not found in the project. You may have misspelled the package name or provided an invalid glob expression. Note that glob expressions must be defined according to https://www.npmjs.com/package/micromatch.

@giselles-ai
Copy link

giselles-ai bot commented Dec 22, 2025

Finished running flow.

Step 1
🟢
On Pull Request OpenedStatus: Success Updated: Dec 22, 2025 7:38am
Step 2
🟢
Manual QAStatus: Success Updated: Dec 22, 2025 7:41am
🟢
Prompt for AI AgentsStatus: Success Updated: Dec 22, 2025 7:41am
Step 3
🟢
Create a Comment for PRStatus: Success Updated: Dec 22, 2025 7:44am
Step 4
🟢
Create Pull Request CommentStatus: Success Updated: Dec 22, 2025 7:45am

@vercel
Copy link

vercel bot commented Dec 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
giselle Ready Ready Preview, Comment Dec 22, 2025 7:40am
ui Ready Ready Preview, Comment Dec 22, 2025 7:40am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR introduces idle-nudge UX features to encourage user interaction. PlaygroundStageInput gains idle state tracking with localStorage persistence and visual feedback on inactivity. A new EditInStudioLinkWithHint component provides reusable idle-hint functionality. The playground page implements interaction tracking and hint rendering, and task headers now use the new hint component.

Changes

Cohort / File(s) Summary
Idle nudge state management
apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
Adds idle state tracking (lastInteractionAt, isIdleNudgeActive, isIdleHintVisible) with localStorage persistence. Introduces new props: interactionTick, idleNudgeDelayMs, renderIdleHint, hasRunAnyApp. Wraps existing handlers with interaction-aware variants that trigger reset timers. Applies conditional styling and renders idle hints.
Playground interaction tracking
apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
Adds interaction tracking state (interactionTick), IdleHintArrow component, and PLAYGROUND_HAS_RUN_ANY_APP_KEY constant. Implements markInteraction callbacks and global interaction listeners (pointer, focus, key events). Passes interaction state and renderIdleHint function to PlaygroundStageInput; persists app-run status to localStorage.
Idle hint component
apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
New self-contained component with idle hint display logic. Manages idle state, localStorage persistence (TASK_EDIT_IN_STUDIO_HINT_SHOWN_KEY), and renders IdleHintArrow with optional hint text after 8-second idle delay. Includes global interaction listeners and fade-in animation.
Task header integration
apps/studio.giselles.ai/components/task/task-header.tsx
Replaces inline "Edit in Studio" link with EditInStudioLinkWithHint component, preserving icon and adding hint text.

Sequence Diagram

sequenceDiagram
    participant User
    participant Component as PlaygroundStageInput/<br/>EditInStudioLinkWithHint
    participant Timer as Idle Timer
    participant Storage as localStorage
    participant UI as Hint Renderer

    User->>Component: Interact (input/click/key)
    Component->>Timer: Reset idle timer
    Component->>Storage: Update lastInteractionAt
    Component->>UI: Hide hint (if visible)

    Note over Timer: idleNudgeDelayMs passes<br/>(default: varies per component)
    Timer->>Component: Fire idle timeout
    Component->>Storage: Check hasShownEver flag
    alt First time or reset
        Storage-->>Component: false
        Component->>Storage: Set hasShownEver = true
        Component->>UI: Fade in idle hint
        Component->>UI: Apply pulsing animation
    else Already shown
        Storage-->>Component: true
        Component->>UI: Hint remains hidden
    end

    User->>Component: Resume interaction
    Component->>Timer: Reset idle timer
    Component->>UI: Fade out hint
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • PlaygroundStageInput state/effects: Review idle logic, timer lifecycle, localStorage access patterns, and handler wrapping correctness
  • EditInStudioLinkWithHint: Self-contained new component with requestAnimationFrame fade-in and global event listeners—ensure no memory leaks and proper cleanup
  • Interaction tracking integration: Verify interactionTick prop threading and event listener attachment scope across components
  • localStorage consistency: Confirm key naming conventions and falback behavior when unavailable are consistent across components

Poem

🐰 Idle hints now nudge the player awake,
With arrows pointing, for goodness sake!
State persists in localStorage's grace,
While timers reset at their rightful pace.
A nudge, a hint, a playful start—
Engagement blooms, an interactive art! 🎯

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding idle hints to the Playground and Task detail pages.
Description check ✅ Passed The PR description is comprehensive and covers all template sections with detailed implementation information, visual examples, and clear conditions for hint behavior.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/playground-idle-nudge-animation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-free-for-open-source-projects

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Silent localStorage failure

Description: localStorage access is wrapped in try-catch but fails silently, potentially causing
inconsistent state where hints may show repeatedly or not at all depending on storage
availability, leading to degraded user experience without proper fallback handling.
playground-stage-input.tsx [92-112]

Referred Code
useEffect(() => {
	try {
		const hasVisited = window.localStorage.getItem(
			PLAYGROUND_HAS_VISITED_KEY,
		);
		setIsFirstVisit(!hasVisited);
		window.localStorage.setItem(PLAYGROUND_HAS_VISITED_KEY, "1");

		const shown = window.localStorage.getItem(
			PLAYGROUND_HAS_SHOWN_IDLE_HINT_KEY,
		);
		setHasShownIdleHintEver(Boolean(shown));

		const ran = window.localStorage.getItem(PLAYGROUND_HAS_RUN_ANY_APP_KEY);
		setHasRunAnyAppEver(Boolean(ran));
	} catch {
		// localStorage might be unavailable (privacy mode). In that case, disable the hint.
		setIsFirstVisit(false);
		setHasShownIdleHintEver(true);
		setHasRunAnyAppEver(true);
	}
Silent localStorage failure

Description: localStorage access fails silently by setting hasShownEver to true on error, which
permanently disables the hint feature for users in privacy mode or with storage
restrictions without any user notification.
edit-in-studio-link-with-hint.tsx [50-61]

Referred Code
useEffect(() => {
	try {
		setHasShownEver(
			Boolean(
				window.localStorage.getItem(TASK_EDIT_IN_STUDIO_HINT_SHOWN_KEY),
			),
		);
	} catch {
		// localStorage might be unavailable (privacy mode). In that case, disable the hint.
		setHasShownEver(true);
	}
}, []);
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces subtle idle hints to guide first-time users toward key actions in the Playground and Task detail pages. The hints appear after 8 seconds of inactivity and are shown only once per user via localStorage flags.

  • Adds an idle hint in /playground that encourages users to "Run a sample app"
  • Adds an idle hint in /tasks/[taskId] near "Edit in Studio" with text "Explore how this app works"
  • Implements persistence logic to ensure hints only appear on first visit and never after a run has been executed

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
apps/studio.giselles.ai/components/task/task-header.tsx Replaces inline Link component with new EditInStudioLinkWithHint component
apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx New component implementing idle hint logic for Edit in Studio button
apps/studio.giselles.ai/app/(main)/playground/page.client.tsx Adds interaction tracking, localStorage persistence for run state, and renders idle hint via renderIdleHint prop
apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx Implements comprehensive idle hint logic with localStorage checks, interaction tracking, and visual nudge animations
Comments suppressed due to low confidence (1)

apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx:1

  • The overflow property changed from 'overflow-hidden' to 'overflow-visible'. This change appears necessary for the idle hint positioning, but may have unintended side effects if content was previously being clipped intentionally. Consider adding a comment explaining why overflow-visible is needed here.
"use client";

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +365 to +367
className={`pointer-events-none absolute right-[-100px] top-[-12px] z-20 flex flex-col items-start text-left text-[#B8E8F4] origin-top-right rotate-[20deg] translate-y-[6px] transition-opacity duration-300 ease-out ${
isVisible ? "opacity-100" : "opacity-0"
}`}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template literal with ternary for className should use clsx for consistency with the rest of the codebase. The clsx utility is already imported and used elsewhere in this file for similar conditional styling.

Suggested change
className={`pointer-events-none absolute right-[-100px] top-[-12px] z-20 flex flex-col items-start text-left text-[#B8E8F4] origin-top-right rotate-[20deg] translate-y-[6px] transition-opacity duration-300 ease-out ${
isVisible ? "opacity-100" : "opacity-0"
}`}
className={clsx(
"pointer-events-none absolute right-[-100px] top-[-12px] z-20 flex flex-col items-start text-left text-[#B8E8F4] origin-top-right rotate-[20deg] translate-y-[6px] transition-opacity duration-300 ease-out",
isVisible ? "opacity-100" : "opacity-0",
)}

Copilot uses AI. Check for mistakes.
@qodo-free-for-open-source-projects

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Abstract idle hint logic into a reusable hook

Refactor the duplicated idle hint logic from PlaygroundStageInput and
EditInStudioLinkWithHint into a single, reusable useIdleHint custom hook. This
will centralize state management, timers, and localStorage interactions,
improving maintainability and reducing code duplication.

Examples:

apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx [83-180]
	// Idle nudge: after a short inactivity window, subtly animate the next action.
	// This must be handled in an effect (timer) and should stop on any interaction.
	const [lastInteractionAt, setLastInteractionAt] = useState(() => Date.now());
	const [isIdleNudgeActive, setIsIdleNudgeActive] = useState(false);
	const [isIdleHintVisible, setIsIdleHintVisible] = useState(false);
	const [isFirstVisit, setIsFirstVisit] = useState(false);
	const [hasShownIdleHintEver, setHasShownIdleHintEver] = useState(false);
	const [hasRunAnyAppEver, setHasRunAnyAppEver] = useState(false);

	useEffect(() => {

 ... (clipped 88 lines)
apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx [44-103]
	const [lastInteractionAt, setLastInteractionAt] = useState(() => Date.now());
	const [hasShownOnce, setHasShownOnce] = useState(false);
	const [shouldBeVisible, setShouldBeVisible] = useState(false);
	const [isVisible, setIsVisible] = useState(false);
	const [hasShownEver, setHasShownEver] = useState(false);

	useEffect(() => {
		try {
			setHasShownEver(
				Boolean(

 ... (clipped 50 lines)

Solution Walkthrough:

Before:

// In PlaygroundStageInput.tsx
function PlaygroundStageInput(...) {
  const [lastInteractionAt, setLastInteractionAt] = useState(...);
  const [isIdleNudgeActive, setIsIdleNudgeActive] = useState(false);
  const [isIdleHintVisible, setIsIdleHintVisible] = useState(false);
  // ... more state and localStorage checks

  useEffect(() => {
    // ... logic with setTimeout to set isIdleNudgeActive
  }, [...]);

  useEffect(() => {
    // ... logic with requestAnimationFrame to set isIdleHintVisible
    // ... and set localStorage item
  }, [...]);

  const markInteraction = useCallback(() => { ... });
  // ... wrap all event handlers with `markInteraction`
}

// In EditInStudioLinkWithHint.tsx
function EditInStudioLinkWithHint(...) {
  const [lastInteractionAt, setLastInteractionAt] = useState(...);
  const [shouldBeVisible, setShouldBeVisible] = useState(false);
  const [isVisible, setIsVisible] = useState(false);
  // ... localStorage checks

  useEffect(() => {
    // ... logic with setTimeout to set shouldBeVisible
    // ... and set localStorage item
  }, [...]);

  useEffect(() => {
    // ... logic with requestAnimationFrame to set isVisible
  }, [...]);
}

After:

// In a new file: use-idle-hint.ts
function useIdleHint({ storageKey, delay, enabled }) {
  const [isVisible, setIsVisible] = useState(false);
  const [hasShownEver, setHasShownEver] = useState(true);

  useEffect(() => {
    // Read from localStorage
  }, []);

  const markInteraction = useCallback(() => { ... });

  useEffect(() => {
    // ... combined logic for setTimeout and requestAnimationFrame
    // ... sets localStorage item
  }, [enabled, hasShownEver, ...]);

  return { isVisible, markInteraction };
}

// In PlaygroundStageInput.tsx and EditInStudioLinkWithHint.tsx
function MyComponent(...) {
  const { isVisible, markInteraction } = useIdleHint({ ... });

  // ... use isVisible for rendering
  // ... use markInteraction for event handlers
}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies significant logic duplication for the idle hint feature across two components, which is a major part of this PR. Creating a reusable useIdleHint hook is a high-impact architectural improvement that would greatly enhance code quality and maintainability.

High
Possible issue
Add pointer event listener for hint

Add a global pointerdown event listener to the useEffect hook to ensure the hint
is dismissed on mouse clicks or taps, in addition to the existing keydown and
focusin listeners.

apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx [95-103]

 	useEffect(() => {
 		// Any interaction on the page should dismiss the hint immediately.
 		window.addEventListener("keydown", markInteraction, { capture: true });
 		window.addEventListener("focusin", markInteraction, { capture: true });
+		window.addEventListener("pointerdown", markInteraction, { capture: true });
 		return () => {
 			window.removeEventListener("keydown", markInteraction, { capture: true });
 			window.removeEventListener("focusin", markInteraction, { capture: true });
+			window.removeEventListener("pointerdown", markInteraction, {
+				capture: true,
+			});
 		};
 	}, [markInteraction]);
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that the interaction handling is incomplete by missing pointer events, which degrades the user experience of the new hint feature.

Low
Learned
best practice
Extract duplicate SVG component

The IdleHintArrow SVG component is duplicated in both page.client.tsx and
edit-in-studio-link-with-hint.tsx. Extract this component to a shared location
to avoid duplication and ensure consistency.

apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx [14-31]

-function IdleHintArrow({ className }: { className?: string }) {
+// shared/components/idle-hint-arrow.tsx
+export function IdleHintArrow({ className }: { className?: string }) {
 	return (
-		<svg
-			className={className}
-			width="77"
-			height="127"
-			viewBox="0 0 77 127"
-			fill="none"
-			xmlns="http://www.w3.org/2000/svg"
-			aria-hidden="true"
-		>
-			<path
-				d="M76.1042 19.8128C76.0142 15.4228..."
-				fill="currentColor"
-			/>
+		<svg className={className} ...>
+			<path d="M76.1042 19.8128..." fill="currentColor" />
 		</svg>
 	);
 }
 
+// edit-in-studio-link-with-hint.tsx
+import { IdleHintArrow } from "../shared/components/idle-hint-arrow";
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Centralize duplicate definitions and configurations into a single source of truth. Import shared constants, provider definitions, or utility functions from a central location rather than redefining them across multiple files.

Low
Centralize duplicate localStorage keys

The localStorage keys and idle delay constant are duplicated across multiple
files (playground-stage-input.tsx, page.client.tsx, and
edit-in-studio-link-with-hint.tsx). Extract these shared constants to a central
configuration file to maintain a single source of truth.

apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx [17-21]

-const DEFAULT_IDLE_NUDGE_DELAY_MS = 8000;
-const PLAYGROUND_HAS_VISITED_KEY = "giselle:playground:hasVisited";
-const PLAYGROUND_HAS_SHOWN_IDLE_HINT_KEY =
-	"giselle:playground:hasShownIdleHint";
-const PLAYGROUND_HAS_RUN_ANY_APP_KEY = "giselle:playground:hasRunAnyApp";
+// shared/idle-hint-config.ts
+export const IDLE_NUDGE_DELAY_MS = 8000;
+export const PLAYGROUND_HAS_VISITED_KEY = "giselle:playground:hasVisited";
+export const PLAYGROUND_HAS_SHOWN_IDLE_HINT_KEY = "giselle:playground:hasShownIdleHint";
+export const PLAYGROUND_HAS_RUN_ANY_APP_KEY = "giselle:playground:hasRunAnyApp";
 
+// playground-stage-input.tsx
+import { IDLE_NUDGE_DELAY_MS, PLAYGROUND_HAS_VISITED_KEY, ... } from "./shared/idle-hint-config";
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Centralize duplicate definitions and configurations into a single source of truth. Import shared constants from a central location rather than redefining them across multiple files.

Low
  • More

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/studio.giselles.ai/app/(main)/playground/page.client.tsx (1)

499-527: Unreachable code due to duplicate condition.

Line 499 (apps.length === 0) and line 523 (apps.length === 0) have the same condition. The second branch (lines 523-526) will never execute.

🔎 Proposed fix

The second condition likely intended to check if the filtered apps are empty when there's a search query:

-              ) : apps.length === 0 ? (
-                <p className="text-sm text-muted-foreground max-w-[960px] mx-auto w-full">
-                  No apps match your search.
-                </p>
+              ) : apps.length === 0 && appSearchQuery.trim().length > 0 ? (
+                <p className="text-sm text-muted-foreground max-w-[960px] mx-auto w-full px-4">
+                  No apps match your search.
+                </p>

However, since data.apps is checked in the first condition, you may need to restructure the logic to differentiate between "no apps at all" vs "no apps matching search".

♻️ Duplicate comments (1)
apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx (1)

17-21: Duplicate localStorage key constants.

These constants are also defined in page.client.tsx. Centralizing them would improve maintainability. Already flagged in the page.client.tsx review.

🧹 Nitpick comments (5)
apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx (3)

14-31: Duplicated IdleHintArrow component across files.

This exact SVG component is also defined in page.client.tsx (lines 37-54). Consider extracting it into a shared module to avoid duplication and ensure consistency.

🔎 Suggested extraction

Create a shared component, e.g., apps/studio.giselles.ai/components/idle-hint-arrow.tsx:

export function IdleHintArrow({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      width="77"
      height="127"
      viewBox="0 0 77 127"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      aria-hidden="true"
    >
      <path
        d="M76.1042 19.8128C76.0142..."
        fill="currentColor"
      />
    </svg>
  );
}

Then import it in both edit-in-studio-link-with-hint.tsx and page.client.tsx.


17-21: Duplicated localStorage key constant.

PLAYGROUND_HAS_RUN_ANY_APP_KEY is defined in playground-stage-input.tsx (line 21) and page.client.tsx (line 56) with the same value. While not used in this file, the pattern suggests localStorage keys could be centralized to prevent drift.

Also applies to: 56-56


44-48: Consider consolidating related boolean states.

Five separate useState calls for hint visibility logic (lines 44-48) increases cognitive overhead. These could be combined into a single state object or a reducer if the logic grows more complex.

apps/studio.giselles.ai/app/(main)/playground/page.client.tsx (1)

56-56: Duplicate localStorage key definition.

PLAYGROUND_HAS_RUN_ANY_APP_KEY is also defined in playground-stage-input.tsx (line 21) with the same value. Centralize these constants to avoid potential inconsistencies during future maintenance.

🔎 Suggested centralization

Create a shared constants file, e.g., apps/studio.giselles.ai/app/(main)/playground/constants.ts:

export const PLAYGROUND_HAS_RUN_ANY_APP_KEY = "giselle:playground:hasRunAnyApp";
export const PLAYGROUND_HAS_VISITED_KEY = "giselle:playground:hasVisited";
export const PLAYGROUND_HAS_SHOWN_IDLE_HINT_KEY = "giselle:playground:hasShownIdleHint";

Then import in both files.

apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx (1)

136-136: Redundant boolean coercion.

Boolean(hasRunAnyApp) is unnecessary since hasRunAnyApp is already a boolean (boolean | undefined). The || operator handles falsy values correctly.

🔎 Simplified condition
-    if (hasRunAnyAppEver || Boolean(hasRunAnyApp)) return false;
+    if (hasRunAnyAppEver || hasRunAnyApp) return false;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c60332 and 34cb6cb.

📒 Files selected for processing (4)
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
  • apps/studio.giselles.ai/components/task/task-header.tsx
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Favor clear, descriptive names and type annotations over clever tricks
If you need a multi-paragraph comment, refactor until intent is obvious

**/*.{ts,tsx,js,jsx}: Use async/await and proper error handling
Variables and functions should use camelCase naming
Booleans and functions should use is, has, can, should prefixes
Function names should clearly indicate purpose

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development-guide.mdc)

**/*.{ts,tsx}: MUST run pnpm biome check --write [filename] after EVERY code modification
All code changes must be formatted using Biome before being committed
Use Biome for formatting with tab indentation and double quotes
Follow organized imports pattern (enabled in biome.json)
Use TypeScript for type safety; avoid any types
Use Next.js patterns for web applications
Use async/await for asynchronous code rather than promises
Error handling: use try/catch blocks and propagate errors appropriately
Use kebab-case for all filenames
Use PascalCase for React components and classes
Use camelCase for variables, functions, and methods
Use prefixes like is, has, can, should for boolean variables (e.g., isEnabled, hasPermission)
Use prefixes like is, has, can, should for boolean functions (e.g., isTriggerRequiringCallsign(), hasActiveSubscription()) instead of imperative verbs
Use verbs or verb phrases for function naming that clearly indicate purpose (e.g., calculateTotalPrice(), not process())

Use PascalCase for React component and class names

Use TypeScript and avoid any

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/development-guide.mdc)

Use functional components with React hooks

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
**/*.{js,ts,tsx,jsx,py,java,cs,cpp,c,go,rb,php,swift,kt,scala,rs,dart}

📄 CodeRabbit inference engine (.cursor/rules/language-support.mdc)

Write all code comments in English

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
**/*

📄 CodeRabbit inference engine (.cursor/rules/naming-guide.mdc)

Use kebab-case for file names (e.g., user-profile.ts, api-client.tsx)

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/naming-guide.mdc)

**/*.{js,ts,jsx,tsx}: Use camelCase for variable names, functions, and methods
Use verbs or verb phrases for function names to clearly indicate what the function does (e.g., calculateTotalPrice(), validateUserInput())
Use nouns or noun phrases for variable names to describe what the variable represents
Use boolean prefixes (is, has, can, should) for boolean variables and functions returning boolean values (e.g., isEnabled, hasPermission, isTriggerRequiringCallsign())

**/*.{js,ts,jsx,tsx}: Run pnpm biome check --write [filename] after every code change
All code must be formatted with Biome before commit

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
**/{components,pages}/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Components should use React hooks and Next.js patterns

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
**/*.{ts,tsx,js,jsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Files should use kebab-case naming

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Components should use PascalCase naming

Files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
🧠 Learnings (21)
📚 Learning: 2025-11-25T03:05:31.051Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/edit-workspace-tour.mdc:0-0
Timestamp: 2025-11-25T03:05:31.051Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/workspace-tour/workspace-tour.tsx : Update the `TourGlobalStyles` component in `workspace-tour.tsx` for animation changes

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
📚 Learning: 2025-11-25T03:05:31.051Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/edit-workspace-tour.mdc:0-0
Timestamp: 2025-11-25T03:05:31.051Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/workspace-tour/workspace-tour.tsx : Import new images at the top of `workspace-tour.tsx` and update the `imageSrc` prop in the appropriate step component to use the new image

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
📚 Learning: 2025-12-03T05:18:36.138Z
Learnt from: shige
Repo: giselles-ai/giselle PR: 2344
File: apps/studio.giselles.ai/app/stage/ui/navigation-rail/navigation-rail-expanded.tsx:14-20
Timestamp: 2025-12-03T05:18:36.138Z
Learning: In apps/studio.giselles.ai/app/stage/ui/navigation-rail/navigation-rail-expanded.tsx, "nav-action-history" is intentionally included in stageOnlyItemIds to hide it when the stage flag is disabled because the link destination is not yet implemented, even though it appears in the "Studio - Build Apps" section of navigation-items.ts.

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
📚 Learning: 2025-11-25T03:07:19.740Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: internal-packages/workflow-designer-ui/src/new-editor/AGENTS.md:0-0
Timestamp: 2025-11-25T03:07:19.740Z
Learning: Applies to internal-packages/workflow-designer-ui/src/new-editor/**/*.{ts,tsx} : Replace context-wide reads from `src/editor/v2` with fine-grained selectors against the Zustand store

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
📚 Learning: 2025-11-25T03:07:19.740Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: internal-packages/workflow-designer-ui/src/new-editor/AGENTS.md:0-0
Timestamp: 2025-11-25T03:07:19.740Z
Learning: Applies to internal-packages/workflow-designer-ui/src/new-editor/**/*.{ts,tsx} : Lift actions into the store (e.g., `updateNode`) and call them from components needing mutations instead of passing mutation callbacks as props

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
📚 Learning: 2025-11-25T03:05:31.051Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/edit-workspace-tour.mdc:0-0
Timestamp: 2025-11-25T03:05:31.051Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/workspace-tour/assets/** : Replace GIFs and images in the `assets/` directory and update imports and references in `workspace-tour.tsx`

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
📚 Learning: 2025-11-25T03:07:07.498Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: internal-packages/workflow-designer-ui/src/editor/properties-panel/trigger-node-properties-panel/providers/github-trigger/AGENTS.md:0-0
Timestamp: 2025-11-25T03:07:07.498Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/properties-panel/trigger-node-properties-panel/providers/github-trigger/components/{callsign,labels}*.tsx : Hide the 'Back' button in `input-callsign` and `input-labels` steps during reconfiguration mode to simplify UX.

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
📚 Learning: 2025-11-25T03:05:31.051Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/edit-workspace-tour.mdc:0-0
Timestamp: 2025-11-25T03:05:31.051Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/workspace-tour/steps.ts : Modify the `tourSteps` array in `steps.ts` to change text, target elements, or placement for the workspace tour

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
📚 Learning: 2025-11-25T03:05:31.051Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/edit-workspace-tour.mdc:0-0
Timestamp: 2025-11-25T03:05:31.051Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/workspace-tour/workspace-tour.tsx : Modify CSS positioning in step components by adjusting Tailwind classes like `mt-[140px]` (margin-top) and `mr-8` (margin-right) in `workspace-tour.tsx`

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
📚 Learning: 2025-11-25T03:05:31.051Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/edit-workspace-tour.mdc:0-0
Timestamp: 2025-11-25T03:05:31.051Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/workspace-tour/workspace-tour.tsx : Modify the `CARD_STYLES` constants in `workspace-tour.tsx` to change step styling

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
📚 Learning: 2025-06-23T12:31:58.286Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/edit-workspace-tour.mdc:0-0
Timestamp: 2025-06-23T12:31:58.286Z
Learning: The workspace tour feature in `internal-packages/workflow-designer-ui/src/editor/workspace-tour/` is implemented using React and relies on a `TourStep` interface that defines each step's target (CSS selector), title, content, and placement.

Applied to files:

  • apps/studio.giselles.ai/components/task/task-header.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
📚 Learning: 2025-06-23T12:31:52.270Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/development-guide.mdc:0-0
Timestamp: 2025-06-23T12:31:52.270Z
Learning: Use functional components with React hooks for building React components.

Applied to files:

  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
📚 Learning: 2025-11-25T03:05:21.219Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/development-guide.mdc:0-0
Timestamp: 2025-11-25T03:05:21.219Z
Learning: Applies to **/*.tsx : Use functional components with React hooks

Applied to files:

  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
📚 Learning: 2025-11-25T03:06:27.023Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T03:06:27.023Z
Learning: Applies to **/{components,pages}/**/*.{tsx,ts} : Components should use React hooks and Next.js patterns

Applied to files:

  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
📚 Learning: 2025-11-25T03:07:07.498Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: internal-packages/workflow-designer-ui/src/editor/properties-panel/trigger-node-properties-panel/providers/github-trigger/AGENTS.md:0-0
Timestamp: 2025-11-25T03:07:07.498Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/properties-panel/trigger-node-properties-panel/providers/github-trigger/**/*.tsx : Wrap all state updates in `startTransition` for consistent UI behavior during configuration and reconfiguration flows.

Applied to files:

  • apps/studio.giselles.ai/app/(main)/playground/page.client.tsx
  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
📚 Learning: 2025-11-25T03:07:19.740Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: internal-packages/workflow-designer-ui/src/new-editor/AGENTS.md:0-0
Timestamp: 2025-11-25T03:07:19.740Z
Learning: Applies to internal-packages/workflow-designer-ui/src/new-editor/**/*.{ts,tsx} : In selectors, avoid selecting full state; select by id (e.g., `s.nodesById[id]`) and derive small, stable shapes

Applied to files:

  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
📚 Learning: 2025-11-25T03:07:19.740Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: internal-packages/workflow-designer-ui/src/new-editor/AGENTS.md:0-0
Timestamp: 2025-11-25T03:07:19.740Z
Learning: Applies to internal-packages/workflow-designer-ui/src/new-editor/components/**/*.{ts,tsx} : Wrap presentational components in `React.memo` only when their props are stable and derived via selectors

Applied to files:

  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
📚 Learning: 2025-08-14T02:34:56.156Z
Learnt from: gentamura
Repo: giselles-ai/giselle PR: 1590
File: internal-packages/workflow-designer-ui/src/editor/hooks/use-keyboard-shortcuts.ts:79-80
Timestamp: 2025-08-14T02:34:56.156Z
Learning: In the giselle codebase, React namespace types (React.KeyboardEvent, React.MouseEvent, React.ChangeEvent, etc.) are used consistently throughout the project without importing React itself, and verbatimModuleSyntax is not enabled in TypeScript configs. This pattern should be maintained for consistency rather than suggesting isolated changes to direct type imports.

Applied to files:

  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
📚 Learning: 2025-11-25T03:07:07.498Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: internal-packages/workflow-designer-ui/src/editor/properties-panel/trigger-node-properties-panel/providers/github-trigger/AGENTS.md:0-0
Timestamp: 2025-11-25T03:07:07.498Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/properties-panel/trigger-node-properties-panel/providers/github-trigger/**/*.tsx : Manage setup wizard state progression through steps: `select-event` → `select-repository` → `confirm-repository` → optionally `input-callsign` → optionally `input-labels` → `configured`.

Applied to files:

  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
📚 Learning: 2025-11-25T03:07:19.740Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: internal-packages/workflow-designer-ui/src/new-editor/AGENTS.md:0-0
Timestamp: 2025-11-25T03:07:19.740Z
Learning: Applies to internal-packages/workflow-designer-ui/src/new-editor/components/node/**/*.{ts,tsx} : In Node component, select exactly the node data, derived connection ids, and UI flags needed, providing a custom equality function that combines strict equality for references with `shallow` for arrays

Applied to files:

  • apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx
📚 Learning: 2025-11-25T03:05:31.051Z
Learnt from: CR
Repo: giselles-ai/giselle PR: 0
File: .cursor/rules/edit-workspace-tour.mdc:0-0
Timestamp: 2025-11-25T03:05:31.051Z
Learning: Applies to internal-packages/workflow-designer-ui/src/editor/workspace-tour/{steps.ts,workspace-tour.tsx} : Add new tour steps to the `tourSteps` array in `steps.ts`, create corresponding components in `workspace-tour.tsx` following the pattern of existing steps, and add the new step to the switch statement in the main `WorkspaceTour` component

Applied to files:

  • apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx
🧬 Code graph analysis (3)
apps/studio.giselles.ai/components/task/task-header.tsx (1)
apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx (1)
  • EditInStudioLinkWithHint (33-143)
apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx (2)
apps/studio.giselles.ai/app/(main)/playground/types.ts (1)
  • StageApp (8-24)
apps/studio.giselles.ai/app/(main)/stores/stage-app-selection-store.ts (1)
  • StageAppSelectionScope (8-8)
apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx (1)
internal-packages/ui/components/status-badge.tsx (1)
  • StatusBadge (42-90)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: check
🔇 Additional comments (18)
apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx (3)

68-83: Timer may fire after expected conditions change.

The timeout fires after IDLE_HINT_DELAY_MS but only checks lastInteractionAt inside the callback (line 71). If hasShownOnce or hasShownEver change during the delay, the early return on line 69 won't prevent the scheduled callback from running. However, the inner check ensures correctness. The logic is sound but slightly indirect.


95-103: Global event listeners are cleaned up correctly.

The keydown and focusin listeners with { capture: true } are properly removed in the cleanup function. The markInteraction callback is stable due to useCallback with no dependencies.


105-143: Component renders correctly with proper accessibility attributes.

The hint container uses aria-hidden="true" and pointer-events-none, which is appropriate for decorative hints. The link includes target="_blank" with rel="noreferrer" for security.

apps/studio.giselles.ai/components/task/task-header.tsx (2)

10-10: Import added correctly.

The relative import path is correct for the sibling file location.


110-116: Clean integration of the new hint component.

The EditInStudioLinkWithHint component is properly integrated with the required workspaceId, leftIcon, and optional hintText props. The icon styling is preserved with transition classes for hover state consistency with the parent group.

apps/studio.giselles.ai/app/(main)/playground/page.client.tsx (5)

264-276: Initialization and interaction tracking are correct.

The markInteraction callback increments a tick counter to signal interactions. The useEffect correctly initializes hasRunAnyApp from localStorage with a fallback to true if storage is unavailable (disabling hints gracefully).


278-326: Run submission correctly persists state and marks interaction.

The handler calls markInteraction() first, then persists to localStorage. The catch block sets hasRunAnyApp to true in both success and error cases, ensuring the hint won't reappear. Dependency array correctly includes markInteraction.


334-339: Interaction capture events applied correctly.

Using onPointerDownCapture, onKeyDownCapture, and onFocusCapture ensures interactions are detected during the capture phase before any child handlers prevent propagation.


340-340: Minor: overflow-visible implied by parent, confirm intentional.

The class change to overflow-visible on this container allows the idle hint to render outside bounds. Verify this doesn't cause unintended visual overflow in other scenarios.


363-375: Idle hint render prop implemented correctly.

The renderIdleHint function receives isVisible and returns the hint UI with appropriate transitions and aria-hidden="true".

apps/studio.giselles.ai/app/(main)/components/stage-input/playground-stage-input.tsx (8)

28-40: Extended props interface is well-typed.

The new optional props (interactionTick, idleNudgeDelayMs, renderIdleHint, hasRunAnyApp) are properly typed with sensible defaults. The ReactNode return type for renderIdleHint provides flexibility for the caller.


83-113: localStorage initialization with proper fallbacks.

The effect correctly handles unavailable localStorage by setting conservative defaults that disable hints. Setting hasVisited immediately after reading ensures the first-visit flag works correctly.


120-123: External interactionTick triggers internal interaction mark.

The effect syncs the external interactionTick prop to the internal markInteraction callback. This allows the parent component to signal interactions that occur outside this component.


125-145: Memoized conditions for idle nudge and hint visibility.

shouldShowIdleNudge and shouldAllowIdleHint correctly gate when the nudge/hint can appear based on running state, input presence, first visit, and prior display. The dependency arrays are complete.


147-160: Timer correctly resets on dependency changes.

The effect cleans up the previous timeout when lastInteractionAt, shouldShowIdleNudge, or idleNudgeDelayMs change, preventing stale timers from firing.


162-180: RAF pattern for fade-in transition stability.

Using requestAnimationFrame before setting isIdleHintVisible ensures the browser paints the initial opacity: 0 state, allowing the CSS transition to animate smoothly. The localStorage update inside the RAF could theoretically be moved outside for clarity, but the current approach works correctly.


182-222: Interaction-aware handler wrappers implemented consistently.

All relevant handlers (input change, key down, attachment click, submit, drop, remove file) are wrapped to call markInteraction() before delegating. The wrapper pattern using Parameters<typeof handler> preserves type safety.


229-233: Idle nudge visual feedback applied correctly.

The conditional ring-1 ring-blue-muted/30 class on the input container and motion-safe:animate-pulse on the submit button and hint text provide subtle visual cues during idle nudge. Using motion-safe: respects user's reduced motion preferences.

Also applies to: 286-290, 344-348

@giselles-ai
Copy link

giselles-ai bot commented Dec 22, 2025

## 🔍 QA Testing Assistant by Giselle

### 📋 Manual QA Checklist

Based on the changes in this PR, here are the key areas to test manually:

  • /playground Page Idle Hint: Verify hint appears after 8 seconds of inactivity on initial visit, and disappears upon typing.
  • /playground Page Idle Hint: Confirm hint is not shown if user interacts before 8 seconds or on subsequent visits.
  • /playground Page Idle Hint: Ensure hint is permanently disabled after running an app for the first time.
  • /tasks/[taskId] Page Idle Hint: Verify hint appears after 8 seconds of inactivity on task detail pages, and disappears on keyboard interaction or focus change.
  • /tasks/[taskId] Page Idle Hint: Confirm hint does not disappear on mouse down event.
  • /tasks/[taskId] Page Idle Hint: Ensure hint is not shown on subsequent visits to task detail pages.

### ✨ Prompt for AI Agents

Use the following prompts with Cursor or Claude Code to automate E2E testing:

📝 E2E Test Generation Prompt

```
You are an expert QA engineer tasked with creating automated end-to-end (E2E) tests for a new feature using Playwright. Your goal is to write clean, maintainable, and reliable tests that can be integrated into a CI/CD pipeline.

Based on the context, test scenarios, and implementation details below, please generate the necessary Playwright test code.

1. Context Summary

The PR introduces a new feature: subtle "idle hints" on two key pages to guide new users towards their next action. These hints are non-blocking and appear only after a period of user inactivity.

  • Functionality: Display a text and arrow hint after 8 seconds of inactivity.
  • Affected User Flows:
    1. A new user visiting the Playground (/playground) page for the first time.
    2. A user viewing a Task Detail (/tasks/[taskId]) page for the first time.
  • Critical Paths:
    • Verifying the hint appears correctly after the idle timeout.
    • Verifying the hint is dismissed upon user interaction.
    • Verifying the hint's appearance is controlled by localStorage flags and does not show up for returning users or after certain actions have been taken.

2. Test Scenarios

Create two separate test files: playground-idle-hint.spec.ts and task-detail-idle-hint.spec.ts.

A. Playground Page (/playground)

File: e2e/tests/playground-idle-hint.spec.ts

  • Scenario 1: Happy Path - Hint Appears for New User

    1. Setup: Before the test, ensure all relevant localStorage keys are cleared.
    2. Action: Navigate to the /playground page.
    3. Action: Do nothing. Wait for just over 8 seconds.
    4. Assert: The hint "Run a sample app." becomes visible.
    5. Assert: The submit button (with the ArrowUpIcon) has the motion-safe:animate-pulse class.
    6. Assert: The localStorage key giselle:playground:hasShownIdleHint is set to "1".
  • Scenario 2: Negative - Hint Does Not Appear on Interaction

    1. Setup: Clear localStorage.
    2. Action: Navigate to /playground.
    3. Action: Immediately type "hello" into the main textarea.
    4. Action: Wait for 9 seconds.
    5. Assert: The hint "Run a sample app." is not visible.
  • Scenario 3: Negative - Hint is Dismissed on Interaction

    1. Setup: Clear localStorage.
    2. Action: Navigate to /playground and wait for the hint to appear (8+ seconds).
    3. Assert: The hint is visible.
    4. Action: Type a key into the main textarea.
    5. Assert: The hint immediately becomes hidden.
  • Scenario 4: Persistence - Hint Does Not Appear for Returning User

    1. Setup: Clear localStorage, then set giselle:playground:hasVisited to "1".
    2. Action: Navigate to /playground.
    3. Action: Wait for 9 seconds.
    4. Assert: The hint is not visible.
  • Scenario 5: Persistence - Hint Does Not Appear After a Run

    1. Setup: Clear localStorage, then set giselle:playground:hasRunAnyApp to "1".
    2. Action: Navigate to /playground.
    3. Action: Wait for 9 seconds.
    4. Assert: The hint is not visible.

B. Task Detail Page (/tasks/[taskId])

File: e2e/tests/task-detail-idle-hint.spec.ts
(Assume a mock task exists at /tasks/mock-task-id)

  • Scenario 1: Happy Path - Hint Appears for New User

    1. Setup: Clear localStorage.
    2. Action: Navigate to /tasks/mock-task-id.
    3. Action: Do nothing. Wait for 8+ seconds.
    4. Assert: The hint "Explore how this app works." becomes visible.
    5. Assert: The localStorage key giselle:tasks:editInStudioHintShown is set to "1".
  • Scenario 2: Negative - Hint Does Not Appear on Interaction

    1. Setup: Clear localStorage.
    2. Action: Navigate to /tasks/mock-task-id.
    3. Action: Immediately press the 'Tab' key to trigger focusin.
    4. Action: Wait for 9 seconds.
    5. Assert: The hint is not visible.
  • Scenario 3: Negative - Hint is Dismissed on Interaction

    1. Setup: Clear localStorage.
    2. Action: Navigate to /tasks/mock-task-id and wait for the hint to appear.
    3. Assert: The hint is visible.
    4. Action: Press the 'Escape' key.
    5. Assert: The hint immediately becomes hidden.
  • Scenario 4: Negative - Hint is NOT Dismissed on PointerDown

    1. Setup: Clear localStorage.
    2. Action: Navigate to /tasks/mock-task-id and wait for the hint to appear.
    3. Assert: The hint is visible.
    4. Action: Click on the main page body (e.g., the task title h3).
    5. Assert: The hint remains visible.
  • Scenario 5: Persistence - Hint Does Not Appear if Already Shown

    1. Setup: Clear localStorage, then set giselle:tasks:editInStudioHintShown to "1".
    2. Action: Navigate to /tasks/mock-task-id.
    3. Action: Wait for 9 seconds.
    4. Assert: The hint is not visible.

3. Playwright Implementation Instructions

  • Test Setup: Use test.beforeEach to manage localStorage. It's critical for test isolation.

    ```typescript
    test.beforeEach(async ({ page }) => {
    // Clear all relevant storage to ensure a clean slate for each test
    await page.evaluate(() => {
    localStorage.clear();
    });
    });
    ```

  • Controlling Time: To avoid slow tests, use Playwright's built-in clock mocking. This is much faster and more reliable than page.waitForTimeout().

    ```typescript
    // At the start of a test
    await page.clock.install();

    // To advance time
    await page.clock.fastForward('00:00:08.1'); // 8.1 seconds

    // At the end of the test
    await page.clock.uninstall();
    ```

  • Selectors:

    • Playground Hint: page.getByText('Run a sample app.')
    • Playground Submit Button: page.locator('button > .lucide-arrow-up').locator('..') or a more stable test-id if available.
    • Playground Textarea: page.getByPlaceholder(/select an app/i)
    • Task Detail Hint: page.getByText('Explore how this app works.')
    • Edit in Studio Link: page.getByRole('link', { name: 'Edit in Studio' })
    • Task Title: page.locator('h3.text-inverse')
  • Assertions:

    • Use expect(locator).toBeVisible() and expect(locator).toBeHidden(). The logic uses rAF and opacity transitions, so toBeVisible() is the most reliable assertion.
    • To check localStorage, use page.evaluate():
      ```typescript
      await expect(page.evaluate(() =>
      localStorage.getItem('giselle:playground:hasShownIdleHint')
      )).resolves.toBe('1');
      ```
    • To check for the pulsing class on the Playground submit button:
      ```typescript
      const submitButton = page.locator('...'); // your selector for the button
      await expect(submitButton).toHaveClass(/motion-safe:animate-pulse/);
      ```

4. MCP Integration Guidelines (For Future Reference)

While these scenarios are primarily page-level, the PlaygroundStageInput and EditInStudioLinkWithHint components could be tested with Playwright MCP. A prompt for that would look like this:

  • Command: `pnpm mcp --component apps/studio.giselles.ai/components/task/edit-in-studio-link-with-hint.tsx --scenario shows-hint-on-idle`
  • MCP Test Logic:
    1. Render the component with necessary props (`workspaceId`).
    2. Use MCP's `clock` fixture to fast-forward time.
    3. Assert the hint text becomes visible.
    4. Simulate a `keydown` event on the page and assert the hint is hidden.

5. CI-Ready Code Requirements

  • Test Organization: Place tests in the specified files (`playground-idle-hint.spec.ts`, `task-detail-idle-hint.spec.ts`). Group related tests using `test.describe()`.
  • Naming Conventions: Use descriptive names for tests, e.g., `test('should not appear if user interacts with the page before timeout', ...)`
  • Error Handling: Playwright's `expect` assertions handle this automatically. No special `try/catch` is needed.
  • Parallelization: The tests modify global state (`localStorage`). By using `test.beforeEach` to clear storage, they are safe for parallel execution across files (as each file gets its own browser context). If multiple tests within the same file need to run in order, use `test.describe.serial()`.
    ```

---

}
});
return () => window.cancelAnimationFrame(raf);
}, [isIdleNudgeActive, renderIdleHint, shouldAllowIdleHint]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idle hint immediately hides after showing due to circular state

The idle hint in the playground will flash briefly and immediately disappear. Inside the requestAnimationFrame callback, setHasShownIdleHintEver(true) is called, which causes shouldAllowIdleHint (computed via useMemo) to become false on the next render. Since shouldAllowIdleHint is a dependency of this effect, it re-runs immediately and the condition !shouldAllowIdleHint becomes true, triggering setIsIdleHintVisible(false). The hint is visible for only one render cycle before being hidden. The task detail component avoids this by keeping visibility state separate from the "has shown" flags.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Member

@shige shige left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaochannel154

Sorry, we've decided not to merge this PR.

Here are the two reasons:

  • If we need to show this hint on /playground, it means users wouldn't know what to do, which indicates that the first view composition of the /playground page has failed.
  • We would need to adjust the animation code that handles localStorage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants