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

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—offers a minimal API for serializing and deserializing draft content, while 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 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:

// 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:

// 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:

// 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 (around line 70), the 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 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 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, as confirmed by the repository file structure and referenced in 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →