// @ts-nocheck
import { createSignal, createMemo, createEffect, on, For, Show, batch } from "solid-js"
import { createStore, produce } from "solid-js/store"
import type {
  Message,
  UserMessage,
  AssistantMessage,
  Part,
  TextPart,
  ReasoningPart,
  ToolPart,
  FilePart,
  AgentPart,
} from "@opencode-ai/sdk/v2"
import { DataProvider } from "../context/data"
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
import { SessionTurn } from "./session-turn"

// ---------------------------------------------------------------------------
// ID helpers
// ---------------------------------------------------------------------------
let seq = 0
const uid = () => `pg-${++seq}-${Date.now().toString(36)}`

// ---------------------------------------------------------------------------
// Lorem ipsum content
// ---------------------------------------------------------------------------
const LOREM = [
  "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
  "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
  "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
  "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
  "Cras justo odio, dapibus ut facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper.",
]

// ---------------------------------------------------------------------------
// User message variants
// ---------------------------------------------------------------------------
const USER_VARIANTS = {
  short: {
    label: "short",
    text: "Fix the bug in the login form",
    parts: [] as Part[],
  },
  medium: {
    label: "medium",
    text: "Can you update the session timeline component to support lazy loading? The current implementation loads everything eagerly which causes jank on large sessions.",
    parts: [] as Part[],
  },
  long: {
    label: "long",
    text: `I need you to refactor the message rendering pipeline. Currently the timeline renders all messages synchronously which blocks first paint. Here's what I want:

1. Implement virtual scrolling for the message list
2. Defer-mount older messages using requestAnimationFrame batching
3. Add content-visibility: auto to each turn container
4. Make sure the scroll-to-bottom behavior still works correctly after these changes

Please also add appropriate CSS containment hints and make sure we don't break the sticky header behavior for the session title.`,
    parts: [] as Part[],
  },
  "with @file": {
    label: "with @file",
    text: "Update @src/components/session-turn.tsx to fix the spacing issue between parts",
    parts: (() => {
      const id = `static-file-${Date.now()}`
      return [
        {
          id,
          type: "file",
          mime: "text/plain",
          filename: "session-turn.tsx",
          url: "src/components/session-turn.tsx",
          source: {
            type: "file",
            path: "src/components/session-turn.tsx",
            text: {
              value: "@src/components/session-turn.tsx",
              start: 7,
              end: 38,
            },
          },
        } as FilePart,
      ]
    })(),
  },
  "with @agent": {
    label: "with @agent",
    text: "Use @explore to find all CSS files related to the timeline, then fix the spacing",
    parts: (() => {
      return [
        {
          id: `static-agent-${Date.now()}`,
          type: "agent",
          name: "explore",
          source: { start: 4, end: 12 },
        } as AgentPart,
      ]
    })(),
  },
  "with image": {
    label: "with image",
    text: "Here's a screenshot of the bug I'm seeing",
    parts: (() => {
      // 1x1 blue pixel PNG as data URI for a realistic attachment
      const pixel =
        "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
      return [
        {
          id: `static-img-${Date.now()}`,
          type: "file",
          mime: "image/png",
          filename: "screenshot.png",
          url: pixel,
        } as FilePart,
      ]
    })(),
  },
  "with file attachment": {
    label: "with file attachment",
    text: "Check this config file for issues",
    parts: (() => {
      return [
        {
          id: `static-attach-${Date.now()}`,
          type: "file",
          mime: "application/json",
          filename: "tsconfig.json",
          url: "data:application/json;base64,e30=",
        } as FilePart,
      ]
    })(),
  },
  "multi attachment": {
    label: "multi attachment",
    text: "Look at these files and the screenshot, then fix the layout",
    parts: (() => {
      const pixel =
        "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
      return [
        {
          id: `static-multi-img-${Date.now()}`,
          type: "file",
          mime: "image/png",
          filename: "layout-bug.png",
          url: pixel,
        } as FilePart,
        {
          id: `static-multi-file-${Date.now()}`,
          type: "file",
          mime: "text/css",
          filename: "session-turn.css",
          url: "data:text/css;base64,LyogZW1wdHkgKi8=",
        } as FilePart,
        {
          id: `static-multi-ref-${Date.now()}`,
          type: "file",
          mime: "text/plain",
          filename: "session-turn.tsx",
          url: "src/components/session-turn.tsx",
          source: {
            type: "file",
            path: "src/components/session-turn.tsx",
            text: { value: "@src/components/session-turn.tsx", start: 0, end: 0 },
          },
        } as FilePart,
      ]
    })(),
  },
} satisfies Record<string, { label: string; text: string; parts: Part[] }>

const MARKDOWN_SAMPLES = {
  headings: `# Heading 1
## Heading 2
### Heading 3
#### Heading 4

Some paragraph text after headings.`,

  lists: `Here's a list of changes:

- First item with some explanation
- Second item that is a bit longer and wraps to the next line when the viewport is narrow
- Third item
  - Nested item A
  - Nested item B

1. Numbered first
2. Numbered second
3. Numbered third`,

  code: `Here's an inline \`variable\` reference and a code block:

\`\`\`typescript
export function sum(values: number[]) {
  return values.reduce((total, value) => total + value, 0)
}

export function average(values: number[]) {
  if (values.length === 0) return 0
  return sum(values) / values.length
}
\`\`\`

And some text after the code block.`,

  mixed: `## Implementation Plan

I'll make the following changes:

1. **Update the schema** - Add new fields to the database model
2. **Create the API endpoint** - Handle validation and persistence
3. **Add frontend components** - Build the form and display views

Here's the key change:

\`\`\`typescript
const table = sqliteTable("session", {
  id: text().primaryKey(),
  project_id: text().notNull(),
  created_at: integer().notNull(),
})
\`\`\`

> Note: This is a breaking change that requires a migration.

The migration will handle existing data by setting \`project_id\` to the default workspace.

---

For more details, see the [documentation](https://example.com/docs).`,

  table: `## Comparison

| Feature | Before | After |
|---------|--------|-------|
| Speed | 120ms | 45ms |
| Memory | 256MB | 128MB |
| Bundle | 1.2MB | 890KB |

The improvements are significant across all metrics.`,

  blockquote: `## Summary

> This is a blockquote that contains important information about the implementation approach.
>
> It spans multiple lines and contains **bold** and \`code\` elements.

The approach above was chosen for its simplicity.`,

  links: `Check out these resources:

- [SolidJS docs](https://solidjs.com)
- [TypeScript handbook](https://www.typescriptlang.org/docs/handbook)
- The API is at \`https://api.example.com/v2\`

You can also visit https://example.com/docs for more info.`,

  images: `## Screenshot

Here's what the output looks like:

![Alt text](https://via.placeholder.com/400x200)

And below is the final result.`,
}

const REASONING_SAMPLES = [
  `**Analyzing the request**

The user wants to add a new feature to the session timeline. I need to understand the existing component structure first.

Let me look at the key files involved:
- \`session-turn.tsx\` handles individual turns
- \`message-part.tsx\` renders different part types
- The data flows through the \`DataProvider\` context`,

  `**Considering approaches**

I could either modify the existing SessionTurn component or create a wrapper. The wrapper approach is cleaner because it doesn't touch the core rendering logic.

The trade-off is that we'd need to pass additional props through, but that's acceptable for this use case.`,

  `**Planning the implementation**

I'll need to:
1. Create the data generators
2. Wire up the context providers
3. Add CSS variable controls
4. Implement the export functionality

This should be straightforward given the existing component architecture.`,
]

const TOOL_SAMPLES = {
  read: {
    tool: "read",
    input: { filePath: "src/components/session-turn.tsx", offset: 1, limit: 50 },
    output: "export function SessionTurn(props) {\n  // component implementation\n  return <div>...</div>\n}",
    title: "Read src/components/session-turn.tsx",
    metadata: {},
  },
  glob: {
    tool: "glob",
    input: { pattern: "**/*.tsx", path: "src/components" },
    output: "src/components/button.tsx\nsrc/components/card.tsx\nsrc/components/session-turn.tsx",
    title: "Found 3 files",
    metadata: {},
  },
  grep: {
    tool: "grep",
    input: { pattern: "SessionTurn", path: "src", include: "*.tsx" },
    output: "src/components/session-turn.tsx:141\nsrc/pages/session/timeline.tsx:987",
    title: "Found 2 matches",
    metadata: {},
  },
  bash: {
    tool: "bash",
    input: { command: "bun test --filter session" },
    output:
      "bun test v1.3.14\n\n✓ session-turn.test.tsx (3 tests) 45ms\n✓ message-part.test.tsx (7 tests) 120ms\n\nTest Suites: 2 passed, 2 total\nTests:       10 passed, 10 total\nTime:        0.89s",
    title: "bun test --filter session",
    metadata: { command: "bun test --filter session" },
  },
  edit: {
    tool: "edit",
    input: {
      filePath: "src/components/session-turn.tsx",
      oldString: "gap: 12px",
      newString: "gap: 18px",
    },
    output: "File edited successfully",
    title: "Edit src/components/session-turn.tsx",
    metadata: {
      filediff: {
        file: "src/components/session-turn.tsx",
        before: "  gap: 12px;\n  display: flex;",
        after: "  gap: 18px;\n  display: flex;",
        additions: 1,
        deletions: 1,
      },
    },
  },
  write: {
    tool: "write",
    input: {
      filePath: "src/utils/helpers.ts",
      content:
        "export function clamp(value: number, min: number, max: number) {\n  return Math.min(Math.max(value, min), max)\n}\n",
    },
    output: "File written successfully",
    title: "Write src/utils/helpers.ts",
    metadata: {},
  },
  task: {
    tool: "task",
    input: { description: "Explore components", subagent_type: "explore", prompt: "Find all session components" },
    output: "Found 12 session-related components across 3 directories.",
    title: "Agent (Explore)",
    metadata: { sessionId: "sub-session-1" },
  },
  webfetch: {
    tool: "webfetch",
    input: { url: "https://solidjs.com/docs/latest/api" },
    output: "# SolidJS API Reference\n\nCore primitives for building reactive applications...",
    title: "Fetch https://solidjs.com/docs/latest/api",
    metadata: {},
  },
  websearch: {
    tool: "websearch",
    input: { query: "SolidJS createStore performance" },
    output:
      "https://solidjs.com/docs/latest/api#createstore\nhttps://dev.to/solidjs/understanding-solid-reactivity\nhttps://github.com/solidjs/solid/discussions/1234",
    title: "Search: SolidJS createStore performance",
    metadata: {},
  },
  question: {
    tool: "question",
    input: {
      questions: [
        {
          question: "Which approach do you prefer?",
          header: "Approach",
          options: [
            { label: "Wrapper component", description: "Create a new wrapper around SessionTurn" },
            { label: "Direct modification", description: "Modify SessionTurn directly" },
          ],
        },
      ],
    },
    output: "",
    title: "Question",
    metadata: { answers: [["Wrapper component"]] },
  },
  skill: {
    tool: "skill",
    input: { name: "playwriter" },
    output: "Skill loaded successfully",
    title: "playwriter",
    metadata: {},
  },
  todowrite: {
    tool: "todowrite",
    input: {
      todos: [
        { content: "Create data generators", status: "completed", priority: "high" },
        { content: "Build UI controls", status: "in_progress", priority: "high" },
        { content: "Add CSS export", status: "pending", priority: "medium" },
      ],
    },
    output: "",
    title: "Todos",
    metadata: {
      todos: [
        { content: "Create data generators", status: "completed", priority: "high" },
        { content: "Build UI controls", status: "in_progress", priority: "high" },
        { content: "Add CSS export", status: "pending", priority: "medium" },
      ],
    },
  },
}

// ---------------------------------------------------------------------------
// Fake data generators
// ---------------------------------------------------------------------------
const SESSION_ID = "playground-session"
const DEFAULT_SESSION = { id: SESSION_ID, title: "Timeline Playground" }

function record(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === "object" && !Array.isArray(value)
}

function normalize(raw: unknown) {
  if (Array.isArray(raw)) {
    const info = raw.find((row) => record(row) && row.type === "session" && record(row.data))?.data
    if (!record(info) || typeof info.id !== "string") {
      throw new Error("No session found in JSON")
    }

    const part = new Map<string, Part[]>()
    const messages = raw.flatMap((row) => {
      if (!record(row) || !record(row.data)) return []
      if (row.type === "part" && typeof row.data.messageID === "string") {
        const list = part.get(row.data.messageID) ?? []
        list.push(row.data as Part)
        part.set(row.data.messageID, list)
        return []
      }
      if (row.type !== "message" || typeof row.data.id !== "string") return []
      return [{ info: row.data as Message, parts: [] as Part[] }]
    })

    return {
      info,
      messages: messages.map((msg) => ({
        info: msg.info,
        parts: part.get(msg.info.id) ?? [],
      })),
    }
  }

  if (!record(raw) || !record(raw.info) || typeof raw.info.id !== "string" || !Array.isArray(raw.messages)) {
    throw new Error("Expected an `opencode export` JSON file")
  }

  return {
    info: raw.info,
    messages: raw.messages.flatMap((row) => {
      if (!record(row) || !record(row.info) || typeof row.info.id !== "string") return []
      return [{ info: row.info as Message, parts: Array.isArray(row.parts) ? (row.parts as Part[]) : [] }]
    }),
  }
}

function mkUser(text: string, extra: Part[] = [], sessionID = SESSION_ID): { message: UserMessage; parts: Part[] } {
  const id = uid()
  return {
    message: {
      id,
      sessionID,
      role: "user",
      time: { created: Date.now() },
      agent: "code",
      model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
    } as UserMessage,
    parts: [
      { id: uid(), type: "text", text, time: { created: Date.now() } } as TextPart,
      // Clone extra parts with fresh ids so each user message owns unique part instances
      ...extra.map((p) => ({ ...p, id: uid() })),
    ],
  }
}

function mkAssistant(parentID: string, sessionID = SESSION_ID): AssistantMessage {
  return {
    id: uid(),
    sessionID,
    role: "assistant",
    time: { created: Date.now(), completed: Date.now() + 3000 },
    parentID,
    modelID: "claude-sonnet-4-20250514",
    providerID: "anthropic",
    mode: "default",
    agent: "code",
    path: { cwd: "/project", root: "/project" },
    cost: 0.003,
    tokens: { input: 1200, output: 800, reasoning: 200, cache: { read: 0, write: 0 } },
  } as AssistantMessage
}

function textPart(text: string): TextPart {
  return { id: uid(), type: "text", text, time: { created: Date.now() } } as TextPart
}

function reasoningPart(text: string): ReasoningPart {
  return { id: uid(), type: "reasoning", text, time: { start: Date.now(), end: Date.now() + 500 } } as ReasoningPart
}

function toolPart(sample: (typeof TOOL_SAMPLES)[keyof typeof TOOL_SAMPLES], status = "completed"): ToolPart {
  const base = {
    id: uid(),
    type: "tool" as const,
    callID: uid(),
    tool: sample.tool,
  }
  if (status === "completed") {
    return {
      ...base,
      state: {
        status: "completed",
        input: sample.input,
        output: sample.output,
        title: sample.title,
        metadata: sample.metadata ?? {},
        time: { start: Date.now(), end: Date.now() + 1000 },
      },
    } as ToolPart
  }
  if (status === "running") {
    return {
      ...base,
      state: {
        status: "running",
        input: sample.input,
        title: sample.title,
        metadata: sample.metadata ?? {},
        time: { start: Date.now() },
      },
    } as ToolPart
  }
  return {
    ...base,
    state: { status: "pending", input: sample.input, raw: "" },
  } as ToolPart
}

// ---------------------------------------------------------------------------
// CSS Controls definition
// ---------------------------------------------------------------------------

// Source file basenames inside the UI component packages.
const MD = "markdown.css"
const MP = "message-part.css"
const ST = "session-turn.css"
const CL = "collapsible.css"
const BT = "basic-tool.css"

/**
 * Source mapping for a CSS control.
 * - `anchor`: immutable text near the property (comment, selector, etc.) that
 *   won't change when values change — used to locate the right rule block.
 * - `prop`: the CSS property name whose value gets replaced.
 * - `format`: turns the slider number into a CSS value string.
 */
type CSSSource = {
  file: string
  anchor: string
  prop: string
  format: (v: string) => string
}

type CSSControl = {
  key: string
  label: string
  group: string
  type: "range" | "color" | "select"
  initial: string
  selector: string
  property: string
  min?: string
  max?: string
  step?: string
  options?: string[]
  unit?: string
  source?: CSSSource
}

const px = (v: string) => `${v}px`
const pxZero = (v: string) => `${v}px 0`
const pct = (v: string) => `${v}%`

const CSS_CONTROLS: CSSControl[] = [
  // --- Timeline spacing ---
  {
    key: "turn-gap",
    label: "Above user messages",
    group: "Timeline Spacing",
    type: "range",
    initial: "32",
    selector: '[data-slot="session-turn-list"]',
    property: "gap",
    min: "0",
    max: "80",
    step: "1",
    unit: "px",
    source: { file: ST, anchor: '[data-slot="session-turn-list"]', prop: "gap", format: px },
  },
  {
    key: "container-gap",
    label: "Below user messages",
    group: "Timeline Spacing",
    type: "range",
    initial: "0",
    selector: '[data-slot="session-turn-message-container"]',
    property: "gap",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: ST, anchor: '[data-slot="session-turn-message-container"]', prop: "gap", format: px },
  },
  {
    key: "assistant-gap",
    label: "Assistant parts gap",
    group: "Timeline Spacing",
    type: "range",
    initial: "12",
    selector: '[data-slot="session-turn-assistant-content"]',
    property: "gap",
    min: "0",
    max: "40",
    step: "1",
    unit: "px",
    source: { file: ST, anchor: '[data-slot="session-turn-assistant-content"]', prop: "gap", format: px },
  },
  {
    key: "text-part-margin",
    label: "Text part margin-top",
    group: "Timeline Spacing",
    type: "range",
    initial: "24",
    selector: '[data-component="text-part"]',
    property: "margin-top",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: MP, anchor: '[data-component="text-part"]', prop: "margin-top", format: px },
  },

  // --- Markdown typography ---
  {
    key: "md-font-size",
    label: "Font size",
    group: "Markdown Typography",
    type: "range",
    initial: "14",
    selector: '[data-component="markdown"]',
    property: "font-size",
    min: "10",
    max: "22",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Reset & Base Typography */", prop: "font-size", format: px },
  },
  {
    key: "md-line-height",
    label: "Line height",
    group: "Markdown Typography",
    type: "range",
    initial: "180",
    selector: '[data-component="markdown"]',
    property: "line-height",
    min: "100",
    max: "300",
    step: "5",
    unit: "%",
    source: { file: MD, anchor: "/* Reset & Base Typography */", prop: "line-height", format: pct },
  },

  // --- Markdown headings ---
  {
    key: "md-heading-margin-top",
    label: "Heading margin-top",
    group: "Markdown Headings",
    type: "range",
    initial: "32",
    selector: '[data-component="markdown"] :is(h1,h2,h3,h4,h5,h6)',
    property: "margin-top",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Headings:", prop: "margin-top", format: px },
  },
  {
    key: "md-heading-margin-bottom",
    label: "Heading margin-bottom",
    group: "Markdown Headings",
    type: "range",
    initial: "12",
    selector: '[data-component="markdown"] :is(h1,h2,h3,h4,h5,h6)',
    property: "margin-bottom",
    min: "0",
    max: "40",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Headings:", prop: "margin-bottom", format: px },
  },
  {
    key: "md-heading-font-size",
    label: "Heading font size",
    group: "Markdown Headings",
    type: "range",
    initial: "14",
    selector: '[data-component="markdown"] :is(h1,h2,h3,h4,h5,h6)',
    property: "font-size",
    min: "12",
    max: "28",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Headings:", prop: "font-size", format: px },
  },

  // --- Markdown paragraphs ---
  {
    key: "md-p-margin-bottom",
    label: "Paragraph margin-bottom",
    group: "Markdown Paragraphs",
    type: "range",
    initial: "16",
    selector: '[data-component="markdown"] p',
    property: "margin-bottom",
    min: "0",
    max: "40",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Paragraphs */", prop: "margin-bottom", format: px },
  },

  // --- Markdown lists ---
  {
    key: "md-list-margin-top",
    label: "List margin-top",
    group: "Markdown Lists",
    type: "range",
    initial: "8",
    selector: '[data-component="markdown"] :is(ul,ol)',
    property: "margin-top",
    min: "0",
    max: "40",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Lists */", prop: "margin-top", format: px },
  },
  {
    key: "md-list-margin-bottom",
    label: "List margin-bottom",
    group: "Markdown Lists",
    type: "range",
    initial: "16",
    selector: '[data-component="markdown"] :is(ul,ol)',
    property: "margin-bottom",
    min: "0",
    max: "40",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Lists */", prop: "margin-bottom", format: px },
  },
  {
    key: "md-list-padding-left",
    label: "List padding-left",
    group: "Markdown Lists",
    type: "range",
    initial: "24",
    selector: '[data-component="markdown"] :is(ul,ol)',
    property: "padding-left",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Lists */", prop: "padding-left", format: px },
  },
  {
    key: "md-li-margin-bottom",
    label: "List item margin-bottom",
    group: "Markdown Lists",
    type: "range",
    initial: "8",
    selector: '[data-component="markdown"] li',
    property: "margin-bottom",
    min: "0",
    max: "20",
    step: "1",
    unit: "px",
    // Anchor on `li {` to skip the `ul,ol` margin-bottom above
    source: { file: MD, anchor: "\n  li {", prop: "margin-bottom", format: px },
  },

  // --- Markdown code blocks ---
  {
    key: "md-pre-margin-top",
    label: "Code block margin-top",
    group: "Markdown Code",
    type: "range",
    initial: "32",
    selector: '[data-component="markdown"] pre',
    property: "margin-top",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "\n  pre {", prop: "margin-top", format: px },
  },
  {
    key: "md-pre-margin-bottom",
    label: "Code block margin-bottom",
    group: "Markdown Code",
    type: "range",
    initial: "32",
    selector: '[data-component="markdown"] pre',
    property: "margin-bottom",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "\n  pre {", prop: "margin-bottom", format: px },
  },
  {
    key: "md-shiki-font-size",
    label: "Code font size",
    group: "Markdown Code",
    type: "range",
    initial: "13",
    selector: '[data-component="markdown"] .shiki',
    property: "font-size",
    min: "10",
    max: "20",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: ".shiki {", prop: "font-size", format: px },
  },
  {
    key: "md-shiki-padding",
    label: "Code padding",
    group: "Markdown Code",
    type: "range",
    initial: "12",
    selector: '[data-component="markdown"] .shiki',
    property: "padding",
    min: "0",
    max: "32",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: ".shiki {", prop: "padding", format: px },
  },
  {
    key: "md-shiki-radius",
    label: "Code border-radius",
    group: "Markdown Code",
    type: "range",
    initial: "6",
    selector: '[data-component="markdown"] .shiki',
    property: "border-radius",
    min: "0",
    max: "16",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: ".shiki {", prop: "border-radius", format: px },
  },

  // --- Markdown blockquotes ---
  {
    key: "md-blockquote-margin",
    label: "Blockquote margin",
    group: "Markdown Blockquotes",
    type: "range",
    initial: "24",
    selector: '[data-component="markdown"] blockquote',
    property: "margin-block",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Blockquotes */", prop: "margin", format: pxZero },
  },
  {
    key: "md-blockquote-padding-left",
    label: "Blockquote padding-left",
    group: "Markdown Blockquotes",
    type: "range",
    initial: "8",
    selector: '[data-component="markdown"] blockquote',
    property: "padding-left",
    min: "0",
    max: "40",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Blockquotes */", prop: "padding-left", format: px },
  },
  {
    key: "md-blockquote-border-width",
    label: "Blockquote border width",
    group: "Markdown Blockquotes",
    type: "range",
    initial: "2",
    selector: '[data-component="markdown"] blockquote',
    property: "border-left-width",
    min: "0",
    max: "8",
    step: "1",
    unit: "px",
    source: {
      file: MD,
      anchor: "/* Blockquotes */",
      prop: "border-left",
      format: (v) => `${v}px solid var(--border-weak-base)`,
    },
  },

  // --- Markdown tables ---
  {
    key: "md-table-margin",
    label: "Table margin",
    group: "Markdown Tables",
    type: "range",
    initial: "24",
    selector: '[data-component="markdown"] table',
    property: "margin-block",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Tables */", prop: "margin", format: pxZero },
  },
  {
    key: "md-td-padding",
    label: "Cell padding",
    group: "Markdown Tables",
    type: "range",
    initial: "12",
    selector: '[data-component="markdown"] :is(th,td)',
    property: "padding",
    min: "0",
    max: "24",
    step: "1",
    unit: "px",
    // Anchor on td selector to skip other padding rules
    source: { file: MD, anchor: "th,\n  td {", prop: "padding", format: px },
  },

  // --- Markdown HR ---
  {
    key: "md-hr-margin",
    label: "HR margin",
    group: "Markdown HR",
    type: "range",
    initial: "40",
    selector: '[data-component="markdown"] hr',
    property: "margin-block",
    min: "0",
    max: "80",
    step: "1",
    unit: "px",
    source: { file: MD, anchor: "/* Horizontal Rule", prop: "margin", format: pxZero },
  },

  // --- Reasoning part ---
  {
    key: "reasoning-md-font-size",
    label: "Reasoning font size",
    group: "Reasoning Part",
    type: "range",
    initial: "14",
    selector: '[data-component="reasoning-part"] [data-component="markdown"]',
    property: "font-size",
    min: "10",
    max: "22",
    step: "1",
    unit: "px",
    source: { file: MP, anchor: '[data-component="reasoning-part"]', prop: "font-size", format: px },
  },
  {
    key: "reasoning-md-margin-top",
    label: "Reasoning markdown margin-top",
    group: "Reasoning Part",
    type: "range",
    initial: "24",
    selector: '[data-component="reasoning-part"] [data-component="markdown"]',
    property: "margin-top",
    min: "0",
    max: "60",
    step: "1",
    unit: "px",
    source: { file: MP, anchor: '[data-component="reasoning-part"]', prop: "margin-top", format: px },
  },

  // --- User message ---
  {
    key: "user-msg-padding",
    label: "User bubble padding",
    group: "User Message",
    type: "range",
    initial: "12",
    selector: '[data-slot="user-message-text"]',
    property: "padding",
    min: "0",
    max: "32",
    step: "1",
    unit: "px",
    source: { file: MP, anchor: '[data-slot="user-message-text"]', prop: "padding", format: px },
  },
  {
    key: "user-msg-radius",
    label: "User bubble border-radius",
    group: "User Message",
    type: "range",
    initial: "6",
    selector: '[data-slot="user-message-text"]',
    property: "border-radius",
    min: "0",
    max: "24",
    step: "1",
    unit: "px",
    source: { file: MP, anchor: '[data-slot="user-message-text"]', prop: "border-radius", format: px },
  },

  // --- Tool parts ---
  {
    key: "tool-subtitle-font-size",
    label: "Subtitle font size",
    group: "Tool Parts",
    type: "range",
    initial: "14",
    selector: '[data-slot="basic-tool-tool-subtitle"]',
    property: "font-size",
    min: "10",
    max: "22",
    step: "1",
    unit: "px",
    source: { file: BT, anchor: '[data-slot="basic-tool-tool-subtitle"]', prop: "font-size", format: px },
  },
  {
    key: "exa-output-font-size",
    label: "Search output font size",
    group: "Tool Parts",
    type: "range",
    initial: "14",
    selector: '[data-component="exa-tool-output"]',
    property: "font-size",
    min: "10",
    max: "22",
    step: "1",
    unit: "px",
    source: { file: MP, anchor: '[data-component="exa-tool-output"]', prop: "font-size", format: px },
  },
  {
    key: "tool-content-gap",
    label: "Trigger/content gap",
    group: "Tool Parts",
    type: "range",
    initial: "4",
    selector: '[data-component="collapsible"].tool-collapsible',
    property: "--tool-content-gap",
    min: "0",
    max: "24",
    step: "1",
    unit: "px",
    source: { file: CL, anchor: "&.tool-collapsible {", prop: "--tool-content-gap", format: px },
  },
  {
    key: "context-tool-gap",
    label: "Explored tool gap",
    group: "Explored Group",
    type: "range",
    initial: "4",
    selector: '[data-component="context-tool-group-list"]',
    property: "gap",
    min: "0",
    max: "40",
    step: "1",
    unit: "px",
    source: { file: MP, anchor: '[data-component="context-tool-group-list"]', prop: "gap", format: px },
  },
  {
    key: "context-tool-indent",
    label: "Explored indent",
    group: "Explored Group",
    type: "range",
    initial: "0",
    selector: '[data-component="context-tool-group-list"]',
    property: "padding-left",
    min: "0",
    max: "48",
    step: "1",
    unit: "px",
    source: { file: MP, anchor: '[data-component="context-tool-group-list"]', prop: "padding-left", format: px },
  },
  {
    key: "bash-max-height",
    label: "Shell output max-height",
    group: "Tool Parts",
    type: "range",
    initial: "240",
    selector: '[data-slot="bash-scroll"]',
    property: "max-height",
    min: "100",
    max: "600",
    step: "10",
    unit: "px",
    source: { file: MP, anchor: '[data-slot="bash-scroll"]', prop: "max-height", format: px },
  },
]

// ---------------------------------------------------------------------------
// Playground component
// ---------------------------------------------------------------------------
function FileStub() {
  return <div style={{ padding: "8px", color: "var(--text-weak)", "font-size": "13px" }}>File viewer stub</div>
}

function Playground() {
  // ---- Messages & parts state ----
  const [state, setState] = createStore<{
    messages: Message[]
    parts: Record<string, Part[]>
  }>({
    messages: [],
    parts: {},
  })
  const [session, setSession] = createSignal({ ...DEFAULT_SESSION })
  const [loaded, setLoaded] = createSignal("")
  const [issue, setIssue] = createSignal("")

  // ---- CSS overrides ----
  const [css, setCss] = createStore<Record<string, string>>({})
  const [defaults, setDefaults] = createStore<Record<string, string>>({})
  let styleEl: HTMLStyleElement | undefined
  let previewRef: HTMLDivElement | undefined
  let pick: HTMLInputElement | undefined

  const sample = (ctrl: CSSControl) => {
    if (!ctrl.group.startsWith("Markdown")) return ctrl.selector
    return ctrl.selector.replace(
      '[data-component="markdown"]',
      '[data-component="text-part"] [data-component="markdown"]',
    )
  }

  /** Read computed styles from the DOM to seed slider defaults */
  const readDefaults = () => {
    const root = previewRef
    if (!root) return
    const next: Record<string, string> = {}
    for (const ctrl of CSS_CONTROLS) {
      const el = (root.querySelector(sample(ctrl)) ?? root.querySelector(ctrl.selector)) as HTMLElement | null
      if (!el) continue
      const styles = getComputedStyle(el)
      const raw = ctrl.property.startsWith("--")
        ? styles.getPropertyValue(ctrl.property).trim()
        : ((styles as any)[ctrl.property] as string)
      if (!raw) continue
      // Shorthands may return "24px 0px" — take the first value
      const num = parseFloat(raw.split(" ")[0])
      if (!Number.isFinite(num)) continue
      // line-height returns px — convert back to % relative to font-size
      if (ctrl.unit === "%") {
        const fs = parseFloat(styles.fontSize)
        if (fs > 0) {
          next[ctrl.key] = String(Math.round((num / fs) * 100))
          continue
        }
      }
      next[ctrl.key] = String(Math.round(num))
    }
    setDefaults(next)
  }

  const updateStyle = () => {
    const rules: string[] = []
    for (const ctrl of CSS_CONTROLS) {
      const val = css[ctrl.key]
      if (val === undefined) continue
      const value = ctrl.unit ? `${val}${ctrl.unit}` : val
      rules.push(`${ctrl.selector} { ${ctrl.property}: ${value} !important; }`)
    }
    if (styleEl) styleEl.textContent = rules.join("\n")
  }

  const setCssValue = (key: string, value: string) => {
    setCss(key, value)
    updateStyle()
  }

  const resetCss = () => {
    batch(() => {
      for (const ctrl of CSS_CONTROLS) {
        setCss(ctrl.key, undefined as any)
      }
    })
    if (styleEl) styleEl.textContent = ""
  }

  // ---- Derived ----
  const userMessages = createMemo(() => state.messages.filter((m): m is UserMessage => m.role === "user"))

  const data = createMemo(() => ({
    session: [session()],
    session_status: {},
    session_diff: {},
    message: { [session().id]: state.messages },
    part: state.parts,
    provider: {
      all: new Map([
        ["anthropic", { id: "anthropic", models: { "claude-sonnet-4-20250514": { name: "Claude Sonnet" } } }],
      ]),
      connected: ["anthropic"],
      default: {},
    },
  }))

  // Read computed defaults once DOM has turn elements to query
  createEffect(
    on(
      () => userMessages().length,
      (len) => {
        if (len === 0) return
        // Wait a frame for the DOM to settle after render
        requestAnimationFrame(readDefaults)
      },
    ),
  )

  // ---- Find or create the last assistant message to append parts to ----
  const lastAssistantID = createMemo(() => {
    for (let i = state.messages.length - 1; i >= 0; i--) {
      if (state.messages[i].role === "assistant") return state.messages[i].id
    }
    return undefined
  })

  /** Ensure a turn (user + assistant) exists and return the assistant message id */
  const ensureTurn = (): string => {
    const id = lastAssistantID()
    if (id) return id
    // Create a minimal placeholder turn
    const user = mkUser("...", [], session().id)
    const asst = mkAssistant(user.message.id, session().id)
    setState(
      produce((draft) => {
        draft.messages.push(user.message)
        draft.messages.push(asst)
        draft.parts[user.message.id] = user.parts
        draft.parts[asst.id] = []
      }),
    )
    return asst.id
  }

  /** Append parts to the last assistant message */
  const appendParts = (parts: Part[]) => {
    const id = ensureTurn()
    setState(
      produce((draft) => {
        const existing = draft.parts[id] ?? []
        draft.parts[id] = [...existing, ...parts]
      }),
    )
  }

  // ---- User message helpers ----
  const addUser = (variant: keyof typeof USER_VARIANTS) => {
    const v = USER_VARIANTS[variant]
    const user = mkUser(v.text, v.parts, session().id)
    const asst = mkAssistant(user.message.id, session().id)
    setState(
      produce((draft) => {
        draft.messages.push(user.message)
        draft.messages.push(asst)
        draft.parts[user.message.id] = user.parts
        draft.parts[asst.id] = []
      }),
    )
  }

  // ---- Part helpers (append to last turn) ----
  const addText = (variant: keyof typeof MARKDOWN_SAMPLES) => {
    appendParts([textPart(MARKDOWN_SAMPLES[variant])])
  }

  const addReasoning = () => {
    const idx = Math.floor(Math.random() * REASONING_SAMPLES.length)
    appendParts([reasoningPart(REASONING_SAMPLES[idx])])
  }

  const addTool = (name: keyof typeof TOOL_SAMPLES) => {
    appendParts([toolPart(TOOL_SAMPLES[name])])
  }

  // ---- Composite helpers (create full turns with user + assistant) ----
  const addFullTurn = (userText: string, parts: Part[]) => {
    const user = mkUser(userText, [], session().id)
    const asst = mkAssistant(user.message.id, session().id)
    setState(
      produce((draft) => {
        draft.messages.push(user.message)
        draft.messages.push(asst)
        draft.parts[user.message.id] = user.parts
        draft.parts[asst.id] = parts
      }),
    )
  }

  const addContextGroupTurn = () => {
    addFullTurn("Read some files", [
      toolPart(TOOL_SAMPLES.read),
      toolPart(TOOL_SAMPLES.glob),
      toolPart(TOOL_SAMPLES.grep),
      textPart("After gathering context, here's what I found:\n\n" + LOREM[2]),
    ])
  }

  const addReasoningFullTurn = () => {
    addFullTurn("Make the changes described above", [
      reasoningPart(REASONING_SAMPLES[0]),
      toolPart(TOOL_SAMPLES.read),
      toolPart(TOOL_SAMPLES.glob),
      toolPart(TOOL_SAMPLES.grep),
      toolPart(TOOL_SAMPLES.edit),
      toolPart(TOOL_SAMPLES.bash),
      textPart(MARKDOWN_SAMPLES.mixed),
    ])
  }

  const addKitchenSink = () => {
    // User message variants
    addUser("short")
    appendParts([textPart(MARKDOWN_SAMPLES.headings)])
    addUser("medium")
    appendParts([textPart(MARKDOWN_SAMPLES.lists)])
    addUser("long")
    appendParts([textPart(MARKDOWN_SAMPLES.code)])
    addUser("with @file")
    appendParts([textPart(MARKDOWN_SAMPLES.mixed)])
    addUser("with image")
    appendParts([reasoningPart(REASONING_SAMPLES[0]), textPart(MARKDOWN_SAMPLES.table)])
    addUser("multi attachment")
    appendParts([
      toolPart(TOOL_SAMPLES.read),
      toolPart(TOOL_SAMPLES.glob),
      toolPart(TOOL_SAMPLES.grep),
      toolPart(TOOL_SAMPLES.edit),
      toolPart(TOOL_SAMPLES.bash),
      textPart(MARKDOWN_SAMPLES.blockquote),
    ])
    addContextGroupTurn()
    addReasoningFullTurn()
  }

  const interrupt = () => {
    const user = userMessages().at(-1)
    if (!user) return
    const now = Date.now()

    setState(
      produce((draft) => {
        const msg = draft.messages.findLast(
          (item): item is AssistantMessage => item.role === "assistant" && item.parentID === user.id,
        )

        if (msg) {
          const time = msg.time ?? { created: now }
          msg.time = { ...time, completed: time.completed ?? now }
          msg.error = { name: "MessageAbortedError", message: "Interrupted" }
          return
        }

        const asst = mkAssistant(user.id, session().id)
        asst.time = { created: now, completed: now }
        asst.error = { name: "MessageAbortedError", message: "Interrupted" }
        draft.messages.push(asst)
        draft.parts[asst.id] = []
      }),
    )
  }

  const load = (raw: unknown, name: string) => {
    const next = normalize(raw)
    const id = typeof next.info.id === "string" && next.info.id ? next.info.id : SESSION_ID
    const messages = next.messages.map((msg) => ({
      ...msg.info,
      sessionID: typeof msg.info.sessionID === "string" ? msg.info.sessionID : id,
    }))
    const parts = Object.fromEntries(
      next.messages.map((msg, idx) => {
        const info = messages[idx]
        return [
          info.id,
          msg.parts.map((part) => ({
            ...part,
            messageID: typeof part.messageID === "string" ? part.messageID : info.id,
            sessionID: typeof part.sessionID === "string" ? part.sessionID : info.sessionID,
          })),
        ]
      }),
    )

    batch(() => {
      setSession({
        ...DEFAULT_SESSION,
        ...next.info,
        id,
        title: typeof next.info.title === "string" && next.info.title ? next.info.title : name,
      })
      setState({ messages, parts })
      setLoaded(name)
      setIssue("")
    })
  }

  const importFile = async (event: Event) => {
    const input = event.currentTarget as HTMLInputElement
    const file = input.files?.[0]
    if (!file) return

    setIssue("")

    try {
      load(JSON.parse(await file.text()), file.name)
    } catch (err) {
      setIssue(err instanceof Error ? err.message : String(err))
    } finally {
      input.value = ""
    }
  }

  const clearAll = () => {
    batch(() => {
      setState({ messages: [], parts: {} })
      setSession({ ...DEFAULT_SESSION })
      setLoaded("")
      setIssue("")
      seq = 0
    })
  }

  // ---- CSS export ----
  const exportCss = () => {
    const lines: string[] = ["/* Timeline Playground CSS Overrides */", ""]
    const groups = new Map<string, string[]>()

    for (const ctrl of CSS_CONTROLS) {
      const val = css[ctrl.key]
      if (val === undefined) continue
      const value = ctrl.unit ? `${val}${ctrl.unit}` : val
      const group = ctrl.group
      if (!groups.has(group)) groups.set(group, [])
      groups.get(group)!.push(`/* ${ctrl.label}: ${value} */`)
      groups.get(group)!.push(`${ctrl.selector} { ${ctrl.property}: ${value}; }`)
    }

    if (groups.size === 0) {
      lines.push("/* No overrides applied */")
    } else {
      for (const [group, rules] of groups) {
        lines.push(`/* --- ${group} --- */`)
        lines.push(...rules)
        lines.push("")
      }
    }

    const text = lines.join("\n")
    navigator.clipboard.writeText(text).catch(() => {})
    return text
  }

  const [exported, setExported] = createSignal("")

  // ---- Apply to source files ----
  const [applying, setApplying] = createSignal(false)
  const [applyResult, setApplyResult] = createSignal("")

  const changedControls = createMemo(() => CSS_CONTROLS.filter((ctrl) => css[ctrl.key] !== undefined && ctrl.source))

  const applyToSource = async () => {
    const controls = changedControls()
    if (controls.length === 0) return

    setApplying(true)
    setApplyResult("")

    const edits = controls.map((ctrl) => {
      const src = ctrl.source!
      return { file: src.file, anchor: src.anchor, prop: src.prop, value: src.format(css[ctrl.key]!) }
    })

    try {
      const resp = await fetch("/__playground/apply-css", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ edits }),
      })
      const data = await resp.json()
      const ok = data.results?.filter((r: any) => r.ok).length ?? 0
      const fail = data.results?.filter((r: any) => !r.ok) ?? []
      const lines = [`Applied ${ok}/${edits.length} edits`]
      for (const f of fail) {
        lines.push(`  FAIL ${f.file} ${f.prop}: ${f.error}`)
      }
      setApplyResult(lines.join("\n"))

      if (ok === edits.length) {
        batch(() => {
          for (const ctrl of controls) {
            setDefaults(ctrl.key, css[ctrl.key]!)
            setCss(ctrl.key, undefined as any)
          }
        })
        updateStyle()
        // Wait for Vite HMR then re-read computed defaults
        setTimeout(readDefaults, 500)
      }
    } catch (err) {
      setApplyResult(`Error: ${err}`)
    } finally {
      setApplying(false)
    }
  }

  // ---- Panel collapse state ----
  const [panels, setPanels] = createStore({
    generators: true,
    css: true,
    export: false,
  })

  // ---- Group collapse state for CSS ----
  const [collapsed, setCollapsed] = createStore<Record<string, boolean>>({})
  const groups = createMemo(() => {
    const result = new Map<string, CSSControl[]>()
    for (const ctrl of CSS_CONTROLS) {
      if (!result.has(ctrl.group)) result.set(ctrl.group, [])
      result.get(ctrl.group)!.push(ctrl)
    }
    return result
  })

  // ---- Shared button styles ----
  const sectionLabel = {
    "font-size": "11px",
    color: "var(--text-weak)",
    "margin-bottom": "4px",
    "text-transform": "uppercase",
    "letter-spacing": "0.5px",
  } as const
  const btnStyle = {
    padding: "4px 8px",
    "border-radius": "4px",
    border: "1px solid var(--border-weak-base)",
    background: "var(--surface-base)",
    cursor: "pointer",
    "font-size": "12px",
    color: "var(--text-base)",
  } as const
  const btnAccent = {
    ...btnStyle,
    border: "1px solid var(--border-interactive-base)",
    background: "var(--surface-interactive-weak)",
    "font-weight": "500",
    color: "var(--text-interactive-base)",
  } as const
  const btnDanger = {
    ...btnStyle,
    border: "1px solid var(--border-critical-base)",
    background: "transparent",
    color: "var(--text-on-critical-base)",
  } as const

  return (
    <div style={{ display: "flex", height: "calc(100vh - 48px)", gap: "0", overflow: "hidden", margin: "-24px" }}>
      {/* Inject dynamic style element */}
      <style ref={styleEl!} />

      {/* Left sidebar: controls */}
      <div
        style={{
          width: "320px",
          "min-width": "320px",
          "border-right": "1px solid var(--border-weak-base)",
          overflow: "auto",
          "background-color": "var(--background-stronger)",
          "scrollbar-width": "none",
        }}
      >
        {/* Generate section */}
        <div style={{ "border-bottom": "1px solid var(--border-weak-base)" }}>
          <button
            style={{
              width: "100%",
              display: "flex",
              "align-items": "center",
              "justify-content": "space-between",
              padding: "10px 12px",
              background: "none",
              border: "none",
              cursor: "pointer",
              "font-weight": "500",
              "font-size": "13px",
              color: "var(--text-strong)",
            }}
            onClick={() => setPanels("generators", (v) => !v)}
          >
            Generate Messages
            <span>{panels.generators ? "−" : "+"}</span>
          </button>
          <Show when={panels.generators}>
            <div style={{ padding: "0 12px 12px", display: "flex", "flex-direction": "column", gap: "6px" }}>
              {/* ---- Session import ---- */}
              <div style={sectionLabel}>Import session</div>
              <div style={{ "font-size": "10px", color: "var(--text-weaker)", "margin-bottom": "2px" }}>
                Replaces the current timeline with an `opencode export` JSON file
              </div>
              <div style={{ display: "flex", "flex-wrap": "wrap", gap: "4px" }}>
                <button style={btnAccent} onClick={() => pick?.click()}>
                  Import session
                </button>
                <input
                  ref={pick!}
                  type="file"
                  accept=".json,application/json"
                  onChange={importFile}
                  style={{ display: "none" }}
                />
              </div>
              <Show when={loaded()}>
                <div style={{ "font-size": "10px", color: "var(--text-weaker)", "line-height": "1.4" }}>
                  {loaded()} • {session().title || session().id} • {state.messages.length} message
                  {state.messages.length === 1 ? "" : "s"}
                </div>
              </Show>
              <Show when={issue()}>
                <div style={{ "font-size": "10px", color: "var(--text-on-critical-base)", "line-height": "1.4" }}>
                  {issue()}
                </div>
              </Show>

              {/* ---- User messages ---- */}
              <div style={sectionLabel}>User messages</div>
              <div style={{ "font-size": "10px", color: "var(--text-weaker)", "margin-bottom": "2px" }}>
                Creates a new turn (user + empty assistant)
              </div>
              <div style={{ display: "flex", "flex-wrap": "wrap", gap: "4px" }}>
                <For each={Object.keys(USER_VARIANTS) as (keyof typeof USER_VARIANTS)[]}>
                  {(key) => (
                    <button style={btnStyle} onClick={() => addUser(key)}>
                      {USER_VARIANTS[key].label}
                    </button>
                  )}
                </For>
              </div>
              <div style={{ display: "flex", "flex-wrap": "wrap", gap: "4px" }}>
                <button
                  style={{
                    ...btnDanger,
                    opacity: userMessages().length === 0 ? "0.5" : "1",
                    cursor: userMessages().length === 0 ? "not-allowed" : "pointer",
                  }}
                  disabled={userMessages().length === 0}
                  onClick={interrupt}
                >
                  Interrupt last
                </button>
              </div>

              {/* ---- Text and reasoning blocks ---- */}
              <div style={{ ...sectionLabel, "margin-top": "8px" }}>Text and reasoning blocks</div>
              <div style={{ "font-size": "10px", color: "var(--text-weaker)", "margin-bottom": "2px" }}>
                Appends to the last turn's assistant parts
              </div>
              <div style={{ display: "flex", "flex-wrap": "wrap", gap: "4px" }}>
                <For each={Object.keys(MARKDOWN_SAMPLES) as (keyof typeof MARKDOWN_SAMPLES)[]}>
                  {(key) => (
                    <button style={btnStyle} onClick={() => addText(key)}>
                      {key}
                    </button>
                  )}
                </For>
                <button style={btnStyle} onClick={addReasoning}>
                  reasoning
                </button>
              </div>

              {/* ---- Tool calls ---- */}
              <div style={{ ...sectionLabel, "margin-top": "8px" }}>Tool calls</div>
              <div style={{ "font-size": "10px", color: "var(--text-weaker)", "margin-bottom": "2px" }}>
                Appends to the last turn's assistant parts
              </div>
              <div style={{ display: "flex", "flex-wrap": "wrap", gap: "4px" }}>
                <For each={Object.keys(TOOL_SAMPLES) as (keyof typeof TOOL_SAMPLES)[]}>
                  {(key) => (
                    <button style={btnStyle} onClick={() => addTool(key)}>
                      {key}
                    </button>
                  )}
                </For>
              </div>

              {/* ---- Composite (full turns) ---- */}
              <div style={{ ...sectionLabel, "margin-top": "8px" }}>Composite turns</div>
              <div style={{ "font-size": "10px", color: "var(--text-weaker)", "margin-bottom": "2px" }}>
                Creates complete user + assistant turns
              </div>
              <div style={{ display: "flex", "flex-wrap": "wrap", gap: "4px" }}>
                <button style={btnStyle} onClick={addContextGroupTurn}>
                  context group
                </button>
                <button style={btnStyle} onClick={addReasoningFullTurn}>
                  full turn
                </button>
                <button style={btnAccent} onClick={addKitchenSink}>
                  kitchen sink
                </button>
              </div>

              <div style={{ "margin-top": "8px" }}>
                <button style={btnDanger} onClick={clearAll}>
                  Clear all
                </button>
              </div>
            </div>
          </Show>
        </div>

        {/* CSS Controls section */}
        <div style={{ "border-bottom": "1px solid var(--border-weak-base)" }}>
          <button
            style={{
              width: "100%",
              display: "flex",
              "align-items": "center",
              "justify-content": "space-between",
              padding: "10px 12px",
              background: "none",
              border: "none",
              cursor: "pointer",
              "font-weight": "500",
              "font-size": "13px",
              color: "var(--text-strong)",
            }}
            onClick={() => setPanels("css", (v) => !v)}
          >
            CSS Controls
            <span>{panels.css ? "−" : "+"}</span>
          </button>
          <Show when={panels.css}>
            <div style={{ padding: "0 12px 12px" }}>
              <button
                style={{
                  padding: "4px 8px",
                  "border-radius": "4px",
                  border: "1px solid var(--border-weak-base)",
                  background: "var(--surface-base)",
                  cursor: "pointer",
                  "font-size": "11px",
                  color: "var(--text-base)",
                  "margin-bottom": "8px",
                }}
                onClick={resetCss}
              >
                Reset all
              </button>

              <For each={[...groups().entries()]}>
                {([group, controls]) => (
                  <div style={{ "margin-bottom": "4px" }}>
                    <button
                      style={{
                        width: "100%",
                        display: "flex",
                        "align-items": "center",
                        "justify-content": "space-between",
                        padding: "6px 0",
                        background: "none",
                        border: "none",
                        "border-bottom": "1px solid var(--border-weaker-base)",
                        cursor: "pointer",
                        "font-size": "11px",
                        "font-weight": "500",
                        color: "var(--text-base)",
                        "text-transform": "uppercase",
                        "letter-spacing": "0.5px",
                      }}
                      onClick={() => setCollapsed(group, (v) => !v)}
                    >
                      {group}
                      <span style={{ "font-size": "10px" }}>{collapsed[group] ? "+" : "−"}</span>
                    </button>
                    <Show when={!collapsed[group]}>
                      <div style={{ padding: "6px 0", display: "flex", "flex-direction": "column", gap: "8px" }}>
                        <For each={controls}>
                          {(ctrl) => (
                            <div style={{ display: "flex", "flex-direction": "column", gap: "2px" }}>
                              <div
                                style={{ display: "flex", "justify-content": "space-between", "align-items": "center" }}
                              >
                                <label
                                  style={{
                                    "font-size": "11px",
                                    color: "var(--text-base)",
                                  }}
                                >
                                  {ctrl.label}
                                </label>
                                <span
                                  style={{
                                    "font-size": "11px",
                                    color:
                                      css[ctrl.key] !== undefined ? "var(--text-interactive-base)" : "var(--text-weak)",
                                    "font-family": "var(--font-family-mono)",
                                    "min-width": "40px",
                                    "text-align": "right",
                                  }}
                                >
                                  {css[ctrl.key] ?? defaults[ctrl.key] ?? ctrl.initial}
                                  {ctrl.unit ?? ""}
                                </span>
                              </div>
                              <input
                                type="range"
                                min={ctrl.min ?? "0"}
                                max={ctrl.max ?? "100"}
                                step={ctrl.step ?? "1"}
                                value={css[ctrl.key] ?? defaults[ctrl.key] ?? ctrl.initial}
                                onInput={(e) => setCssValue(ctrl.key, e.currentTarget.value)}
                                style={{
                                  width: "100%",
                                  height: "4px",
                                  "accent-color": "var(--text-interactive-base)",
                                  cursor: "pointer",
                                }}
                              />
                            </div>
                          )}
                        </For>
                      </div>
                    </Show>
                  </div>
                )}
              </For>
            </div>
          </Show>
        </div>

        {/* Export section */}
        <div style={{ "border-bottom": "1px solid var(--border-weak-base)" }}>
          <button
            style={{
              width: "100%",
              display: "flex",
              "align-items": "center",
              "justify-content": "space-between",
              padding: "10px 12px",
              background: "none",
              border: "none",
              cursor: "pointer",
              "font-weight": "500",
              "font-size": "13px",
              color: "var(--text-strong)",
            }}
            onClick={() => setPanels("export", (v) => !v)}
          >
            Export CSS
            <span>{panels.export ? "−" : "+"}</span>
          </button>
          <Show when={panels.export}>
            <div style={{ padding: "0 12px 12px", display: "flex", "flex-direction": "column", gap: "8px" }}>
              <button style={btnAccent} onClick={() => setExported(exportCss())}>
                Copy CSS to clipboard
              </button>
              <button
                style={{
                  ...btnAccent,
                  opacity: changedControls().length === 0 || applying() ? "0.5" : "1",
                  cursor: changedControls().length === 0 || applying() ? "not-allowed" : "pointer",
                }}
                disabled={changedControls().length === 0 || applying()}
                onClick={applyToSource}
              >
                {applying()
                  ? "Applying..."
                  : `Apply ${changedControls().length} edit${changedControls().length === 1 ? "" : "s"} to source`}
              </button>
              <Show when={changedControls().length > 0}>
                <div
                  style={{
                    "font-size": "10px",
                    color: "var(--text-weaker)",
                    "line-height": "1.4",
                  }}
                >
                  <For each={changedControls()}>
                    {(ctrl) => (
                      <div>
                        {ctrl.source!.file}: {ctrl.property} = {css[ctrl.key]}
                        {ctrl.unit}
                      </div>
                    )}
                  </For>
                </div>
              </Show>
              <Show when={applyResult()}>
                <pre
                  style={{
                    padding: "8px",
                    "border-radius": "4px",
                    background: "var(--surface-inset-base)",
                    border: "1px solid var(--border-weak-base)",
                    "font-size": "11px",
                    "font-family": "var(--font-family-mono)",
                    "line-height": "1.5",
                    "white-space": "pre-wrap",
                    "word-break": "break-all",
                    "max-height": "200px",
                    "overflow-y": "auto",
                    color: "var(--text-base)",
                  }}
                >
                  {applyResult()}
                </pre>
              </Show>
              <Show when={exported()}>
                <pre
                  style={{
                    padding: "8px",
                    "border-radius": "4px",
                    background: "var(--surface-inset-base)",
                    border: "1px solid var(--border-weak-base)",
                    "font-size": "11px",
                    "font-family": "var(--font-family-mono)",
                    "line-height": "1.5",
                    "white-space": "pre-wrap",
                    "word-break": "break-all",
                    "max-height": "300px",
                    "overflow-y": "auto",
                    color: "var(--text-base)",
                  }}
                >
                  {exported()}
                </pre>
              </Show>
            </div>
          </Show>
        </div>
      </div>

      {/* Main area: timeline preview */}
      <div
        ref={previewRef!}
        style={{ flex: "1", overflow: "auto", "min-width": "0", "background-color": "var(--background-stronger)" }}
      >
        <DataProvider data={data()} directory="/project">
          <FileComponentProvider component={FileStub}>
            <div
              style={{
                "max-width": "800px",
                margin: "0 auto",
                padding: "16px 0",
              }}
            >
              <Show
                when={userMessages().length > 0}
                fallback={
                  <div
                    style={{
                      display: "flex",
                      "align-items": "center",
                      "justify-content": "center",
                      height: "400px",
                      color: "var(--text-weak)",
                      "font-size": "14px",
                    }}
                  >
                    Click a generator button or import a session
                  </div>
                }
              >
                <div
                  role="log"
                  data-slot="session-turn-list"
                  style={{ display: "flex", "flex-direction": "column", width: "100%", padding: "0 20px" }}
                >
                  <For each={userMessages()}>
                    {(msg) => (
                      <div style={{ width: "100%" }}>
                        <SessionTurn
                          sessionID={session().id}
                          messageID={msg.id}
                          messages={state.messages}
                          active={false}
                          showReasoningSummaries={true}
                          shellToolDefaultOpen={true}
                          editToolDefaultOpen={true}
                          classes={{
                            root: "min-w-0 w-full relative",
                            content: "flex flex-col justify-between !overflow-visible",
                            container: "w-full",
                          }}
                        />
                      </div>
                    )}
                  </For>
                </div>
              </Show>
            </div>
          </FileComponentProvider>
        </DataProvider>
      </div>
    </div>
  )
}

// ---------------------------------------------------------------------------
// Story export
// ---------------------------------------------------------------------------
export default {
  title: "Playground/Timeline",
  id: "playground-timeline",
  parameters: {
    layout: "fullscreen",
  },
}

export const Basic = {
  render: () => <Playground />,
}
