# Prompt Queue

> Stack of follow-up prompts queued while an agent works: reorder, edit inline, remove, or Send now to steer. Renders nothing when empty.

- **Slug:** `prompt-queue` · **Category:** agent · **Status:** experimental
- **Import:** `import { PromptQueue, QueuedPrompt, QueuedPromptAttachment } from "@kura-ui/react/prompt-queue";`
- **Docs:** https://kuro.abstergo.fyi/components/prompt-queue
- **Registry item:** https://kuro.abstergo.fyi/r/prompt-queue.json
- **Tags:** agent, queue, prompts, follow-up, steer, reorder, drag, composer

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/prompt-queue.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add prompt-queue
# npm package
npm install @kura-ui/react
```

## When to use

- The user types follow-ups while an agent is mid-turn (Claude Code / Codex queued messages) and they should send in order when it finishes.
- Letting the user steer: interrupt the agent and send one queued prompt immediately.
- Directly above a composer (variant='tray') so queued prompts read as part of the input.

## When not to use

- Suggested prompts the user has not written — use prompt-suggestions.
- The prompt input itself — use composer (and put PromptQueue above it).
- Work the agent plans to do — use task-list.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `items` | `QueuedPrompt[]` | — | no | Queued prompts in send order (controlled): { id, text, attachments?: { name, type?: 'image' \| 'file' }[] }. |
| `defaultItems` | `QueuedPrompt[]` | — | no | Initial prompts when uncontrolled. |
| `onItemsChange` | `(items: QueuedPrompt[]) => void` | — | no | Called with the whole new list after a reorder, edit, remove or Send now. Fires on every step while dragging. |
| `onSendNow` | `(item: QueuedPrompt) => void` | — | no | Steer: interrupt the agent and send this prompt now. The prompt is removed from the queue first. Without it no Send now button renders. |
| `title` | `React.ReactNode` | `"Queued"` | no | Header title; the count follows it. |
| `hint` | `React.ReactNode` | `"Sends when the agent finishes this turn"` | no | Secondary header text, e.g. 'Sends when Kura finishes running pnpm test'. null hides it. |
| `variant` | `"panel" \| "tray"` | `"panel"` | no | panel = standalone card; tray = sunken, rounded on top only, meant to sit flush on top of a Composer (inset it with className='mx-3'). |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **PromptQueue** (`data-slot="prompt-queue"`) — Root with `data-variant`.
- **Header** (`data-slot="prompt-queue-header"`) — Title, count and prompt-queue-hint.
- **Item** (`data-slot="prompt-queue-item"`) — Focusable <li> (`data-state` = editing | dragging): index (grip on hover), text, attachments, actions.
- **Handle** (`data-slot="prompt-queue-handle"`) — Drag grip shown in place of the index on hover (pointer only; keyboard uses Alt+arrows or Space).
- **Text** (`data-slot="prompt-queue-text"`) — One truncated line; the full text is the title tooltip.
- **Attachments** (`data-slot="prompt-queue-attachments"`) — First attachment's icon and name in mono, then '+N'.
- **Actions** (`data-slot="prompt-queue-actions"`) — Move up, Move down, Edit, Remove and prompt-queue-send ('Send now'); shown on hover/focus, always on touch.
- **Editor** (`data-slot="prompt-queue-editor"`) — Auto-growing textarea replacing the row while editing, with Cancel/Save.

## Accessibility

- Role: `list (ol) of focusable items`
- The list is labelled by the title and described by visually hidden keyboard help. Items use a roving tabindex (one Tab stop) and are named '2 of 3: <text>'.
- A polite status region announces moves ('Moved to position 1 of 3'), removals and Send now. Actions are icon buttons with tooltips.
- Escape in the editor cancels without reaching the composer's own Escape (stop) handler.
- Reduced motion: Reorders jump into place instead of sliding; the drag still follows the pointer.

| Keys | Action |
| --- | --- |
| ArrowUp / ArrowDown | Move focus between prompts |
| Alt+ArrowUp / Alt+ArrowDown | Move the focused prompt up / down |
| Space, then arrows, then Space | Grab, move and drop (Escape restores) |
| Enter / F2 | Edit inline (Enter saves, Shift+Enter newline, Escape cancels) |
| Delete / Backspace | Remove |
| Mod+Enter | Send now |

## Notes for agents

- When the agent finishes a turn, shift the first item off `items` and send it yourself — the component never sends on its own.
- onSendNow means 'interrupt and send': stop the current turn in your harness before sending.
- Keep ids stable across edits; reordering and focus rely on them.
- Empty queue renders only a visually hidden status region, so you can leave it mounted above the composer.

## Examples

### Two queued prompts (minimal)

```tsx
"use client";

import { PromptQueue } from "@kura-ui/react/prompt-queue";

export function PromptQueueMinimal() {
  return (
    <PromptQueue
      className="w-full max-w-xl"
      defaultItems={[
        { id: "readme", text: "When tests pass, update the auth section of the README" },
        { id: "legacy", text: "Don't touch src/legacy/ — it's deleted next sprint" },
      ]}
      onSendNow={() => {}}
    />
  );
}
```

### Controlled, with steering (full)

Attachments, a prompt that arrives after mount, and Send now reported below.

```tsx
"use client";

import { CornerDownRight } from "lucide-react";
import * as React from "react";
import { PromptQueue, type QueuedPrompt } from "@kura-ui/react/prompt-queue";

const QUEUE: QueuedPrompt[] = [
  {
    id: "helper",
    text: "Use the refreshSession() helper instead of calling fetch('/api/session') directly",
    attachments: [{ name: "session-flow.png", type: "image" }],
  },
  {
    id: "legacy",
    text: "Don't touch src/legacy/ — it's being deleted next sprint",
  },
  {
    id: "readme",
    text: "When tests pass, update the auth section of the README and link the migration guide",
    attachments: [
      { name: "MIGRATION.md", type: "file" },
      { name: "auth-diagram.png", type: "image" },
    ],
  },
];

export function PromptQueueFull() {
  const [items, setItems] = React.useState<QueuedPrompt[]>(QUEUE);
  const [steered, setSteered] = React.useState<string | null>(null);

  // A teammate queues one more follow-up a moment after mount.
  React.useEffect(() => {
    const t = setTimeout(
      () =>
        setItems((list) =>
          list.some((i) => i.id === "pr")
            ? list
            : [...list, { id: "pr", text: "Open the PR as a draft and request review from @maya" }],
        ),
      1600,
    );
    return () => clearTimeout(t);
  }, []);

  return (
    <div className="flex w-full max-w-xl flex-col gap-2">
      <PromptQueue
        items={items}
        onItemsChange={setItems}
        onSendNow={(item) => setSteered(item.text)}
        hint="Sends when Kura finishes running pnpm test"
      />
      <p
        className="flex min-h-5 items-center gap-1.5 px-1 text-[12px] text-kura-fg-subtle"
        aria-live="polite"
      >
        {steered ? (
          <>
            <CornerDownRight aria-hidden className="size-3.5 shrink-0" strokeWidth={1.75} />
            <span className="truncate">
              Interrupted Kura and sent: <span className="text-kura-fg-muted">{steered}</span>
            </span>
          </>
        ) : (
          "Hover a prompt, or focus it and use Alt+↑/↓, Enter, Delete and ⌘↵."
        )}
      </p>
    </div>
  );
}
```

### Tray (variant)

Rounded on top only, tucked onto the input below.

```tsx
"use client";

import { PromptQueue, type QueuedPrompt } from "@kura-ui/react/prompt-queue";

const QUEUE: QueuedPrompt[] = [
  {
    id: "helper",
    text: "Use the refreshSession() helper instead of calling fetch('/api/session') directly",
    attachments: [{ name: "session-flow.png", type: "image" }],
  },
  {
    id: "legacy",
    text: "Don't touch src/legacy/ — it's being deleted next sprint",
  },
  {
    id: "readme",
    text: "When tests pass, update the auth section of the README and link the migration guide",
    attachments: [
      { name: "MIGRATION.md", type: "file" },
      { name: "auth-diagram.png", type: "image" },
    ],
  },
];

export function PromptQueueTray() {
  return (
    <div className="flex w-full max-w-xl flex-col">
      <PromptQueue
        variant="tray"
        className="mx-3"
        defaultItems={QUEUE.slice(0, 2)}
        onSendNow={() => {}}
      />
      <div className="h-16 rounded-kura-xl bg-kura-surface shadow-kura-sm" />
    </div>
  );
}
```

### Above a composer (composition)

Messages sent while the agent works are queued; Send now steers.

```tsx
"use client";

import { CornerDownRight } from "lucide-react";
import * as React from "react";
import { Composer } from "@kura-ui/react/composer";
import { RunHeader } from "@kura-ui/react/run-header";
import { PromptQueue, type QueuedPrompt } from "@kura-ui/react/prompt-queue";

const QUEUE: QueuedPrompt[] = [
  {
    id: "helper",
    text: "Use the refreshSession() helper instead of calling fetch('/api/session') directly",
    attachments: [{ name: "session-flow.png", type: "image" }],
  },
  {
    id: "legacy",
    text: "Don't touch src/legacy/ — it's being deleted next sprint",
  },
  {
    id: "readme",
    text: "When tests pass, update the auth section of the README and link the migration guide",
    attachments: [
      { name: "MIGRATION.md", type: "file" },
      { name: "auth-diagram.png", type: "image" },
    ],
  },
];

export function PromptQueueComposition() {
  const [items, setItems] = React.useState<QueuedPrompt[]>(QUEUE.slice(0, 2));
  const [log, setLog] = React.useState<string[]>([]);
  const [startedAt] = React.useState(() => Date.now() - 611_000);
  return (
    <div className="flex w-full max-w-xl flex-col gap-3">
      <RunHeader
        variant="bar"
        title="Run pnpm test --filter web"
        status="running"
        startedAt={startedAt}
        progress={{ done: 5, total: 7 }}
        cost={1.52}
        onStop={() => {}}
        shortcuts={false}
      />
      {log.length > 0 && (
        <ul className="flex flex-col gap-1 px-1 text-[12px] text-kura-fg-subtle">
          {log.map((l, i) => (
            <li key={i} className="flex items-center gap-1.5 truncate">
              <CornerDownRight aria-hidden className="size-3.5 shrink-0" strokeWidth={1.75} />
              Steered: <span className="truncate text-kura-fg-muted">{l}</span>
            </li>
          ))}
        </ul>
      )}
      <div className="flex flex-col">
        <PromptQueue
          variant="tray"
          className="mx-3"
          items={items}
          onItemsChange={setItems}
          onSendNow={(item) => setLog((l) => [...l, item.text])}
          hint="Sends after this turn"
        />
        <Composer
          className="relative"
          placeholder="Queue a follow-up for Kura…"
          onSubmit={(m) => setItems((list) => [...list, { id: `q-${Date.now()}`, text: m.text }])}
        />
      </div>
    </div>
  );
}
```

## Dependencies

- npm: `lucide-react`, `motion`
- Kura registry items: `kura-theme`, `button`, `icon-button`, `kura-motion`, `use-controllable-state`, `use-reduced-motion`, `utils`
- Related: `composer`, `run-header`, `prompt-suggestions`, `task-list`
