# How pi-web Handles Draft Persistence Using the draft-store Module

> Discover how pi-web uses the draft-store module to save chat drafts in localStorage for automatic recovery. Learn about this lightweight persistence solution for seamless user input.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-07-26

---

**The `draft-store` module provides a lightweight, type-safe persistence layer that saves chat drafts to the browser's `localStorage` using session-scoped keys, enabling automatic recovery of user input across page reloads.**

The agegr/pi-web repository implements **draft persistence** through a client-side storage strategy that requires no server round-trips. The `draft-store` module—located at [`lib/draft-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/draft-store.ts)—offers a minimal API for serializing and deserializing draft content, while [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx) orchestrates the read and write lifecycle. This approach ensures that unsent messages survive browser refreshes and accidental tab closures.

## Core Architecture of the draft-store Module

The `draft-store` module exports strictly typed interfaces and utility functions that abstract `localStorage` operations. According to the source code, the module defines the shape of draft data and provides three primary methods for managing persistence.

### Data Models: ChatDraft and ChatDraftImage

The module defines two TypeScript interfaces that structure the draft data:

- **`ChatDraftImage`**: Describes attached images with a `src` property and an optional `alt` attribute.
- **`ChatDraft`**: Contains the text `content` string and an optional array of `ChatDraftImage` objects.

These types ensure that both message text and media attachments persist consistently across sessions.

### Storage API: getDraft, setDraft, and clearDraft

The module exposes three functions that handle all interaction with `localStorage`:

- **`getDraft(key)`**: Retrieves the JSON string from `localStorage`, parses it, and returns a `ChatDraft` object. Returns `null` if no entry exists for the provided key.
- **`setDraft(key, draft)`**: Accepts a session key and `ChatDraft` object, serializes the data to JSON, and writes it to `localStorage`.
- **`clearDraft(key)`**: Permanently removes the draft entry associated with the session key from `localStorage`.

Each function uses a unique key per chat session to prevent collision between different conversation contexts.

## Integrating Draft Persistence in ChatInput.tsx

The [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx) file implements the draft lifecycle by invoking the `draft-store` methods at critical user interaction points. This integration ensures that drafts restore automatically when users return to the chat and clear immediately after successful message delivery.

### Restoring Drafts on Component Mount

When the chat input component initializes, a `useEffect` hook retrieves any previously saved draft for the current `sessionId`:

```tsx
// components/ChatInput.tsx – loading a saved draft
useEffect(() => {
  const saved = getDraft(sessionId);
  if (saved) {
    setContent(saved.content);
    setImages(saved.images ?? []);
  }
}, [sessionId]);

```

This pattern ensures that users returning to the page see their unsent text and attached images exactly as they left them.

### Persisting Drafts in Real-Time

To minimize data loss, the component persists the draft on every keystroke. The `onChange` handler calls `setDraft` immediately after updating local state:

```tsx
// components/ChatInput.tsx – persisting on each change
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  const newContent = e.target.value;
  setContent(newContent);
  // Persist the draft immediately
  setDraft(sessionId, { content: newContent, images });
};

```

This synchronous write to `localStorage` guarantees that even abrupt page closures preserve the latest user input.

### Clearing Drafts After Message Send

Once the user successfully sends a message, the component invokes `clearDraft` to remove the persisted data and reset the input field:

```tsx
// components/ChatInput.tsx – clearing after send
const sendMessage = async () => {
  await postMessage(content, images);
  clearDraft(sessionId);          // remove persisted draft
  setContent('');                 // reset UI
  setImages([]);
};

```

As noted in [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) (around line 70), the [`draft-store.ts`](https://github.com/agegr/pi-web/blob/main/draft-store.ts) utility is listed among the repository's core modules, confirming its integral role in the application architecture.

## Benefits of Client-Side Draft Storage

Using `localStorage` through the `draft-store` module offers several architectural benefits:

- **Zero Server Overhead**: Drafts never traverse the network, reducing database load and eliminating latency for persistence operations.
- **Instant Recovery**: Data retrieval happens synchronously from the browser store, enabling immediate UI restoration without async loading states.
- **Session Isolation**: Unique keys per `sessionId` ensure drafts remain scoped to specific conversations, preventing cross-contamination between chat contexts.

## Summary

- The `draft-store` module at [`lib/draft-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/draft-store.ts) provides type-safe `localStorage` wrappers for chat draft persistence.
- **Three core functions**—`getDraft`, `setDraft`, and `clearDraft`—manage the full lifecycle of draft data using session-scoped keys.
- [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx) consumes these utilities to **restore** drafts on mount, **persist** changes in real-time, and **clear** storage after successful message transmission.
- The implementation relies entirely on **client-side storage**, eliminating server dependencies while surviving page reloads and navigation events.

## Frequently Asked Questions

### How does pi-web ensure drafts don't leak between different chat sessions?

The `draft-store` module accepts a unique `key` parameter (typically the `sessionId`) for every operation. Each draft writes to a distinct `localStorage` key scoped to that specific session, ensuring complete isolation between separate conversations.

### What happens to drafts if the user disables localStorage in their browser?

If `localStorage` is unavailable or disabled, the `getDraft` function returns `null`, and `setDraft`/`clearDraft` operations fail silently. The `ChatInput` component handles this gracefully by falling back to empty initial states, allowing the chat to function normally without persistence.

### Where is the draft-store module located in the pi-web repository?

The implementation resides in [`lib/draft-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/draft-store.ts), as confirmed by the repository file structure and referenced in [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) around line 70. This location follows the project's convention of housing utility modules in the top-level `lib` directory.

### Does the draft persistence support rich media like images?

Yes. The `ChatDraft` interface includes an optional `images` array containing `ChatDraftImage` objects with `src` and `alt` properties. The `setDraft` function serializes the entire object—including image metadata—to `localStorage`, and `getDraft` restores it when the component remounts.