Skip to main content
ClaudeWave
Slash Command1.6k repo starsupdated 3d ago

tip

Author a new contextual tip for Nimbalyst's tips system (transcript-inline tip cards with trigger/targeting rules).

Install in Claude Code
Copy
mkdir -p ~/.claude/commands && curl -fsSL https://raw.githubusercontent.com/nimbalyst/nimbalyst/HEAD/.claude/commands/tip.md -o ~/.claude/commands/tip.md
Then start a new Claude Code session; the slash command loads automatically.

tip.md

Author a new contextual tip: {{arg1}}

A **contextual tip** is a small, dismissible card Nimbalyst shows in the empty panel of a new AI session to teach users a feature they have not discovered yet. We are adding many of these to help users get the most value from Nimbalyst. This command walks you through writing one correctly.

If `{{arg1}}` is empty, ask the user what feature/behavior the tip should teach and who should see it, then proceed.

## How the tips system works (read before writing)

All code lives under `packages/electron/src/renderer/tips/`.

### Where a tip is displayed
Tips render **inline in the empty panel of a new/unused AI session transcript** — not as a floating popup (the floating `TipCard` variant exists but is dormant). The surface is wired in `SessionTranscript.tsx` via `renderEmptyExtra()`, which mounts `InlineTipDisplay` above the slash-command suggestions when `messages.length === 0`.

- The card shows: icon + title + body (basic markdown) + optional primary action button + optional secondary link.
- Footer controls: **Next** (cycles through tips, sorted by priority) and **All tips** (opens `AllTipsDialog` to browse every tip).
- Dismiss = X button; primary action = `markTipCompleted`; both clear the active tip.
- Body markdown (`parseMarkdownBody` in `TipCard.tsx`) supports only: `**bold**`, paragraph breaks (blank line), and bullet lists (`- ` / `* `). No links, headings, or inline code.

### How targeting / rules work
`TipProvider.tsx` runs an evaluation loop. A tip is **eligible** only when ALL of these hold:
1. Not shown yet this app launch (`tipShownThisSession` — in-memory, one tip per launch, resets on restart).
2. Help/tips master toggle is on (`walkthroughState.enabled`), no walkthrough active, no dialog/overlay visible, no tip already active.
3. An empty-transcript surface is mounted (`emptyTranscriptVisibleCountAtom > 0`).
4. Not previously dismissed or completed (`shouldShowTip` in `TipService.ts`) — unless `version` was bumped.
5. `trigger.screen === '*'` OR matches the current `ContentMode` (e.g. `'agent'`, `'files'`, `'tracker'`).
6. `trigger.condition(context)` returns `true`.

Among eligible tips, the highest `priority` wins; then it waits `trigger.delay` ms and re-checks the condition before showing. The loop only starts ~15s after launch and re-evaluates every 5s, reading live state from refs.

The `condition` receives a `TipTriggerContext`:
```ts
interface TipTriggerContext {
  currentMode: ContentMode;
  workspacePath?: string;
  isGitRepo: boolean;
  isWorktreesAvailable: boolean;
  featureUsage: Record<string, FeatureUsageRecord>;
  hasBeenUsed: (feature: string) => boolean;            // count > 0
  hasReachedCount: (feature: string, threshold: number) => boolean; // count >= threshold
}
```
Most tips follow the pattern "user has done X enough times but never used Y" — e.g. `hasReachedCount(SESSION_CREATED, 10) && !hasBeenUsed(WORKTREE_CREATED)`. The condition must be a **pure, synchronous** predicate (no async, no IPC).

### Persistence & analytics
Tips reuse the walkthrough store via IPC (no new tables). IDs MUST start with `tip-`. Bumping `version` re-shows a tip to users who dismissed an older version. PostHog events (`tip_shown`, `tip_dismissed`, `tip_action_clicked`, `tip_navigated`, `tip_all_tips_opened`) are captured automatically — do not add your own.

## Steps to author the tip

### 1. Pick the trigger signal
Decide what user state should surface this tip. If you can express it with an existing key in `FEATURE_USAGE_KEYS` (`packages/electron/src/shared/featureUsage.ts`), use it. Current keys include: `SESSION_CREATED`, `SESSION_COMPLETED`, `APP_LAUNCH`, `AI_PROMPT_SUBMITTED`, `EXCALIDRAW_OPENED`, `MOCKUP_OPENED`, `SPREADSHEET_OPENED`, `DATAMODEL_OPENED`, `TRACKER_USED`, `THEME_CHANGED`, `KEYBOARD_SHORTCUT_USED`, `FILE_CREATED`, `WORKTREE_CREATED`.

If no key fits, you must add a new one (see step 4) AND wire a `recordUsage` call at the feature's call site — otherwise the condition can never flip and the tip will never show. Confirm with the user before introducing a new key.

### 2. Create the definition file
`packages/electron/src/renderer/tips/definitions/<kebab-name>.tsx`. Match the style of a nearby tip such as `worktree-session.tsx`. Prefer `<MaterialSymbol icon="..." size={16} />` for the icon. Drive actions through atoms/IPC/`dialogRef` like the existing tips do — never reach into DOM.

**Action options.** A tip's `content.action` can either:
- run code via `onClick` (navigate, open a dialog, set an atom), or
- seed the composer via `insertPrompt: '/some-command '` — drops that text into the session input so the user can review and submit. Use this to point at a slash command (e.g. a planning-extension command). `insertPrompt` only works in the inline surface of a **claude-code** session; the button is hidden automatically where there's nowhere to insert, so keep the body meaningful without it. See `session-cleanup.tsx`.

```tsx
import { MaterialSymbol } from '@nimbalyst/runtime';
import { store } from '@nimbalyst/runtime/store';
import { FEATURE_USAGE_KEYS } from '../../../shared/featureUsage';
import type { TipDefinition } from '../types';

const Icon = <MaterialSymbol icon="lightbulb" size={16} />;

export const myFeatureTip: TipDefinition = {
  id: 'tip-my-feature',          // MUST start with 'tip-'
  name: 'My Feature Suggestion', // human-readable, used in analytics
  version: 1,                    // bump later to re-show after dismissal
  trigger: {
    screen: '*',                 // or 'agent' | 'files' | 'tracker' | ...
    condition: (ctx) =>
      ctx.hasReachedCount(FEATURE_USAGE_KEYS.SESSION_CREATED, 5) &&
      !ctx.hasBeenUsed(FEATURE_USAGE_KEYS.MY_FEATURE),
    delay: 2500,                 // ms after eligible before showing (default 2000)
    priority: 6,                 // higher shows first (existing tips ~5-10)
  },
  content: {
    icon: Icon,
    title: 'Short, benefit-led title',         //