docs: design for new-file naming dialog & editor filename header (TASK-046)
07a496f33b9caf1b97d0a47dde119e276e311383
humdrum-tiv <45084903+humdrum-tiv@users.noreply.github.com> · 2026-07-30 17:34
parent fe49582b
docs: design for new-file naming dialog & editor filename header (TASK-046) Ask for the filename up front in a centered dialog instead of deferring to a bottom-bar save-as prompt, and show the filename in editor mode as an uneditable header bar styled like preview's title bar. One dialog component (new internal/dialog package) serves new-file, name-this-buffer, and F2 rename; ModeSaveAs goes away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 files changed
- → New-file-naming-dialog-editor-filename-header.md +29 −0
@@ -0,0 +1,29 @@
+---
+id: TASK-046
+title: New-file naming dialog + editor filename header
+status: "\U0001F7E6 Backlog"
+assignee: []
+created_date: '2026-07-31 00:34'
+labels:
+ - feature
+dependencies: []
+priority: medium
+ordinal: 45000
+---
+
+## Description
+
+<!-- SECTION:DESCRIPTION:BEGIN -->
+Ask for the filename up front in a centered dialog whenever a new file is created, and show the filename in editor mode as an uneditable header bar styled like preview's title bar. Collapses save-as/new-file into one dialog component (new internal/dialog package) and reuses it for F2 rename. Design: docs/superpowers/specs/2026-07-30-new-file-dialog-design.md
+<!-- SECTION:DESCRIPTION:END -->
+
+## Acceptance Criteria
+<!-- AC:BEGIN -->
+- [ ] #1 Ctrl+N/Ctrl+B, picker Ctrl+N (prefilled with query), and 'glint -n' with no name all open a centered naming dialog
+- [ ] #2 Enter creates the file immediately and opens it; existing name opens that file; slashes nest subdirs; empty name reports 'Type a name first'
+- [ ] #3 Esc cancels back to the previous mode with nothing written
+- [ ] #4 Editor mode shows the basename (minus .md) as a full-width header bar in the top pad, costing no text rows
+- [ ] #5 Pathless buffer shows muted 'Untitled'; dirty buffer appends a bullet
+- [ ] #6 F2 renames the current file on disk, rebinds the path, and reopens Harper under the new URI
+- [ ] #7 ModeSaveAs/saveInput/saveBar removed; Ctrl+S on a pathless buffer opens the same dialog
+<!-- AC:END -->
docs/superpowers/specs/2026-07-30-new-file-dialog-design.md +127 −0
@@ -0,0 +1,127 @@
+# New-file naming dialog & editor filename header
+
+Date: 2026-07-30
+
+## Problem
+
+Creating a note in glint leaves the buffer unnamed. The name is asked for later, at
+the first Ctrl+S, through a one-line prompt in the bottom status bar (`ModeSaveAs`).
+Two consequences:
+
+- The name is an afterthought. A new note has no identity until it is saved, and the
+ Harper language server sees it under an untitled URI until then.
+- Editor mode never shows the filename anywhere. Preview mode does — it renders a
+ full-width title bar above the body — so the two modes disagree about whether the
+ document has a visible name.
+
+## Goals
+
+1. Ask for the filename up front, in a centered dialog, whenever a new file is made.
+2. Show the filename in editor mode as an uneditable header bar, styled like the
+ title bar preview already renders.
+3. Collapse the two naming UIs (save-as, new-file) into one component, and reuse it
+ for renaming an existing file.
+
+## Design
+
+### Approach
+
+A new `internal/dialog` package holds the centered prompt as a self-contained
+Bubbletea sub-model: prompt title, target-directory hint, text input, validation,
+and Enter/Esc handling. `App` owns a `*dialog.Model` and a new `ModeNamePrompt`.
+
+`legibleText` and `hexToRGB` move from `internal/preview` into `internal/theme`, so
+the editor header and the preview title bar derive their colors from one place and
+cannot drift apart.
+
+Alternatives considered and rejected:
+
+- **Everything inline in `app.go`.** Fewest files, but `app.go` is already the
+ largest file in the repo (1182 lines) and the dialog would only be testable by
+ driving the whole app. The color logic would also be duplicated against preview's.
+- **Header owned by the `editor` package.** Rejected: the header is document chrome,
+ not buffer text. `editor` already carries wrap, scroll, and undercurl geometry, and
+ the header's text comes from `App.path`, which `editor` does not know.
+
+### The dialog
+
+A centered bordered box on the canvas, in the same visual family as `helpOverlay`:
+rounded border in `theme.Heading`, themed background.
+
+```
+ ╭────────────────────────────────╮
+ │ New note │
+ │ in ~/Humdrum/Inbox/ │
+ │ │
+ │ > my-new-note▌ │
+ │ │
+ │ Enter to create · Esc cancel │
+ ╰────────────────────────────────╯
+```
+
+One component, two uses:
+
+| Use | Title | Prefill | Confirm |
+| ------ | ----------- | --------------------- | -------------------------------------- |
+| New | `New note` | picker query, if any | create empty file, open it |
+| Rename | `Rename` | current basename | `os.Rename`, rebind path |
+
+Behavior:
+
+- The target directory is displayed, so it is clear whether Ctrl+N (current dir) or
+ Ctrl+B (inbox) is in play.
+- Enter on an existing name **opens** that file (new) or refuses with `Name taken`
+ (rename). This matches today's `openNoteAt`.
+- Slashes nest subdirectories, via the existing `picker.NewNotePath` plus `MkdirAll`.
+- Enter with an empty input reports `Type a name first` and keeps the dialog open.
+- Esc returns to the previous mode. Nothing is written, and no unnamed buffer is left
+ behind.
+
+### The header
+
+The header occupies the existing three-row top pad (`canvasTopPad = 3`), so it costs
+no text rows and the editor's height is unchanged:
+
+```
+row 0 (blank pad)
+row 1 ██ my-note ████████████████████ ← header bar
+row 2 (blank pad)
+row 3+ document text (unchanged height)
+```
+
+- Spans the content column, aligned inside the canvas margins — the same width as the
+ text, matching preview's `titleBar`.
+- Bold, `theme.Heading` background, `theme.LegibleText(Heading)` foreground, one space
+ of padding each side, truncated with `…` when the name overflows.
+- Text is the basename minus `.md`. A frontmatter `title:` property does **not**
+ override it: edit mode shows what is on disk.
+- A pathless buffer renders `Untitled` in `theme.Muted` rather than the heading bar,
+ so "unsaved and unnamed" reads at a glance.
+- A dirty buffer appends a bullet: `my-note •`. The status bar keeps its own
+ indicator.
+- Rendered in `ModeEditor` and the overlay modes that keep the editor visible beneath
+ (find, goto, spell). Not in the picker, not in help (the body is replaced), and not
+ in preview (which has its own title bar).
+
+### Wiring
+
+- `ModeNamePrompt` is added. `ModeSaveAs`, `saveInput`, and `saveBar` are deleted;
+ `saveAs()` folds into the dialog's confirm handler.
+- `newFile()` opens the dialog instead of calling `startBlankIn()`. The dirty-buffer
+ discard confirmation stays in front of the dialog: press the same key again to
+ discard, then the dialog opens.
+- Picker Ctrl+N opens the dialog prefilled with the typed query, cursor at the end.
+ (Today it creates the file with no prompt.)
+- `glint -n` with no name boots straight into the dialog over an empty editor.
+- `F2` opens the rename dialog, prefilled with the current basename. On confirm:
+ `os.Rename`, rebind `App.path`, reopen Harper under the new URI via `grammarOpen`,
+ and update the preview title. On a pathless buffer, F2 names and writes it.
+- Ctrl+S on a pathless buffer opens the same dialog — one naming UI, not two.
+- `startBlankIn()` survives for the pathless case.
+
+### Testing
+
+- `internal/dialog`: unit tests for validation, prefill, Esc, and slash-nested paths.
+- `internal/app`: header text and dirty-marker rendering; the F2 rename round-trip
+ against a temp directory; the new-file flow from both the editor and the picker.
+- Existing `ModeSaveAs` tests are rewritten against the dialog.