Skip to content

[Task] apps/editor: browse and edit gists from a sidebar rail, with a local sandbox #321

Description

@soroushm

Task Description ✏️

Turn the connected GitHub account (#320) into a way of working: browse your gists, open their files
in the editor, and edit them against a local sandbox that only reaches GitHub when you publish.

The rail (common/EditorSidebar) grows to three rows sharing one panel column — Files, Gists, and
the account pinned to the bottom. Picking a gist in Gists switches the panel to Files, since
choosing a gist is a request to see what is in it.

The sandbox. Every change to a gist is staged rather than sent:

  • opening a gist file tags the document with its origin ({ gistId, filename }), so Save stages
    the content instead of writing to disk — Save As still means a file on disk
  • Add file stages an addition, the row's ✕ stages a deletion, and a staged row's ↺ unstages it
  • the description is read-only with an Edit button; editing opens a multiline field with Save
    and Cancel, because a description can run to several lines and Enter belongs to the text
  • rows carry A / M / D the way a diff would mark them, with an unpublished-change count that
    includes the description
  • Publish sends the whole draft as one PATCH /gists/{id} — files and description together;
    Reset discards it, behind a confirmation, since it destroys work that exists nowhere else

Switching files must not force a choice between losing work and publishing. The unsaved-changes
prompt is therefore two buttons — Save as draft and Discard changes — where saving stages the
document as one more change in the sandbox. Several files can be edited in turn and published
together. There is no cancel: keeping the work already costs nothing, and Escape maps to keeping it
so no accidental dismissal loses anything. A document that is not from a gist gets a plain Save,
which still writes to disk.

Drafts persist to userData/gist-drafts.json, keyed by gist id — plain JSON, deliberately not
encrypted, because it is the user's own draft text rather than a secret. Quitting mid-edit does not
silently lose staged work.

Main gains fetchGists, fetchGistFiles, patchGist, draftStore, gistService and
ipc/gistHandlers; the renderer gains useGists, useGistFiles, useGistDraft, useWindowTitle,
common/GistList, common/GistFiles and EditorSidebar. Saving became origin-aware inside
useDocument, so the menu, the Save button and the prompt all go through one path.

Parent Epic: #295.

Acceptance Criteria ✅

  • The rail lists the account's gists; picking one shows its files in the panel
  • Opening a file loads it into the editor; a dirty document is never replaced silently
  • The unsaved-changes prompt offers Save as draft and Discard changes, and saving stages the
    document rather than writing it to disk or publishing it
  • Work is kept when the prompt is dismissed with Escape, and the document stays put whenever the
    save could not be completed
  • Save on a gist file stages it locally and never writes to disk; Save As still writes to disk
  • Add, delete, edit and the description are all staged — nothing reaches GitHub until Publish
  • The description reads as text until Edit is pressed, then edits over several lines; Cancel stages
    nothing, and Save on an untouched description creates no change
  • Several files can be edited and staged in turn, then published in one request
  • Publish sends one request containing the whole draft; a failed publish keeps the draft intact
  • Reset discards the draft only after a confirmation
  • Staged changes survive quitting and reopening the app
  • Staging from the editor and from the panel stay in step — saving updates the panel's change count
    without reopening it
  • The document name and the window title both follow the open file, with an unsaved marker
  • pnpm lint, pnpm typecheck, pnpm test:coverage green; 100% coverage on every touched file

Dependencies 🔗

Definition of Ready (DoR) 📋

  • Sandbox scope agreed: edits, adds, deletes and the description are all staged, rather than only
    content edits
  • Persistence agreed: drafts are written to disk, not held in renderer memory only

Definition of Done (DoD) 🏁

  • Implemented
  • Unit tests at 100% coverage on touched files
  • Verified by hand against a real gist: open several files, save each as a draft, edit the
    description, publish once

Additional Notes or Resources 📎

  • A gist cannot have zero files, so staging D on every file and publishing returns 422. The
    error message says so rather than reporting a bare status code.
  • GitHub rejects a gist file whose content is the empty string with a 422, so toFilePatch
    publishes empty content as a blank line. Without it, adding a file and publishing before typing
    anything could never succeed.
  • The truncation guard from [Task] apps/editor: connect a GitHub account with an encrypted personal access token #320's file fetch matters here: GitHub cuts inline content at 1MB, and
    loading a partial file into the editor and then publishing it would silently drop the rest.
  • A draft is { files, description? } rather than a bare file map. Drafts written before the
    description could be staged are still read as a file map, so existing staged work is not lost.
  • Only the renderer can save — it holds the content and knows where the document belongs — so
    choosing to keep the work when closing the window sends the save and leaves the window open;
    closing again then goes straight through. Worth revisiting if that extra click grates.
  • Still outstanding, worth its own task: widening the design system's SidebarItem.icon to
    IconName | ReactNode. All three rail rows are hand-rolled Pressables porting into the panel
    through SidebarContext only because icon takes a registry name.
  • No e2e coverage — the Playwright-Electron suite would need a stubbed fetch in main.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions