Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/changes/unreleased/1018-draft-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
kind: added
title: /drafts, the page over the kill ring
surface: [chat]
pr: 1018
---

Cleared drafts are ringed (the walk above in `/drafts` reads them); this page
is the browse door: enter puts one back, d lets one go, all in newest-first
order.
5 changes: 5 additions & 0 deletions internal/manual/chat/starting-aforge.md
Original file line number Diff line number Diff line change
Expand Up @@ -702,3 +702,8 @@ do?", "what does ctrl+b do?", "can you read a PDF?" and "why did you just ask me
that?" are all fair questions to type straight into the conversation. If the
manual has nothing on something, that usually means aforge does not do it, and it
will tell you so instead of inventing an answer.

## /drafts and the ↑ walk — cleared drafts come back too

`ctrl+u`, a conversation switch, any clear that empties the whole box — aforge pushes the draft onto a ring of ten (the kill ring, `draftring.go`). The usual `↑` walk, which used to answer about the sent lines, now visits the ring first, dim in front of the sent history. `/drafts` opens the same ring as its own page: a list with `enter` restorer over what is left in the box (that box, too, joins the ring before the restored line takes it), and `d` letting one go for good. A cleared draft is never lost and never keeps its place in the ring once it lands back in the box.

11 changes: 11 additions & 0 deletions internal/tui2/tokens/glyph.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ const (
// exchange unreadable at exactly the moment it matters. U+21B3 is
// East_Asian_Width=Neutral and one cell under both shipping rulers.
GlyphReplyIn = "↳"
// GlyphDraftUnsent is a message a person typed and then CLEARED the whole
// box into — the mark the /drafts page draws in front of each line that
// is still waiting to come back (internal/tui3's draftpage.go).
//
// IT IS PENCIL-SHAPED LIKE [GlyphWrite] AND NOT THE SAME PENCIL. ✎ is the
// step gutter's "a call wrote something down"; ✐ (U+2710 LOWER RIGHT PENCIL)
// is a thing the hand still holds. It is also deliberately NOT the steer
// prompt's mapped-into arrow one slot up: a draft has gone nowhere yet, it
// sits on the wrong side of the line that one draws.
GlyphDraftUnsent = "✐"

// The execution voices (5.5). A work record is four speakers and no
// labels: the model thinking, the tools it reached for, the reader
Expand Down Expand Up @@ -445,6 +455,7 @@ func Glyphs() []GlyphInfo {
{"PromptChat", GlyphPromptChat, '›', false},
{"PromptSteer", GlyphPromptSteer, '↦', false},
{"ReplyIn", GlyphReplyIn, '↳', false},
{"DraftUnsent", GlyphDraftUnsent, '✐', false},
{"Thought", GlyphThought, '✳', false},
{"Shell", GlyphShell, '$', false},
{"Search", GlyphSearch, '⌕', false},
Expand Down
4 changes: 4 additions & 0 deletions internal/tui2/tokens/glyphset.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ const (
// hears `?` for a search, and `?` is already the mark for work waiting on a
// person, which is a row the filter box is very often sitting above.
GFilter
// GDraftUnsent is the mark of a message typed and then cleared before it
// was sent: the ring of them behind /drafts and the ↑ walk's dim end
// (internal/tui3's draftring.go, draftpage.go).
GDraftUnsent
glyphIDCount
)

Expand Down
10 changes: 10 additions & 0 deletions internal/tui2/tokens/nerdfont.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,16 @@ var vocabulary = []GlyphBinding{
ID: GReplyIn, Name: "ReplyIn", Meaning: "the answer to a question put back to the asker, drawn under it",
Plain: GlyphReplyIn, UsualTint: TextTertiary, Geometry: true,
},
// A composer's own mark that is NOT geometry: the unsent draft (a message
// cleared before sending). Both sides are "being written", the outline
// pencil on the floor and the compose icon where a patched font supplies
// one — the same shape at two weights, which is the whole test of a binding.
{
ID: GDraftUnsent, Name: "DraftUnsent", Meaning: "a message typed and cleared before it was sent",
Plain: GlyphDraftUnsent, NerdFont: "\uF044", NFName: "nf-fa-pencil_square_o",
ASCII: "w",
UsualTint: TextTertiary, NFAmbiguous: true, AutoUpgrade: true,
},

// -- the execution voices (5.5) ------------------------------------------
{
Expand Down
3 changes: 3 additions & 0 deletions internal/tui2/tokens/testdata/nerdfont_glyphnames.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,8 @@
},
"nf-pl-right_soft_divider": {
"code": "e0b3"
},
"nf-fa-pencil_square_o": {
"code": "f044"
}
}
27 changes: 27 additions & 0 deletions internal/tui3/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1975,6 +1975,10 @@ type app struct {
// Nil history is a surface with no ↑, which is what --no-history is.
history History
hist recall
// drafts is the kill ring: the sentences the whole-box clears took, newest
// first (draftring.go). The same ↑ walk visits them in front of the sent
// history, drawn dim, and /drafts lists them.
drafts draftRing

// draftFile is where the unsent sentence is kept between sessions
// (draft.go); empty means it is not kept at all.
Expand Down Expand Up @@ -2319,6 +2323,10 @@ type app struct {
// anything; closed, it costs the frame nothing.
permPanel permPanel

// draftPage is the list /drafts opens over the ring of cleared-but-kept
// drafts (draftring.go): closed, it costs the frame nothing.
draftPage draftPanel

// orders is the standing place's state: the shelves of what stands here — this
// conversation's orders, this project's and the machine's (place_standing.go).
// It reads the engine's own seam, so a surface whose agent has no ambient
Expand Down Expand Up @@ -2565,6 +2573,18 @@ type app struct {
resume func(file string) (Agent, error)
}

// noteKilled is the door every whole-box clear goes through BEFORE the words
// go: the sentence in the box, if there is one, goes on the kill ring
// (draftring.go), so a clear that was a mistake is one ↑ away rather than
// retyped. Clears that hand the words on — a send, the rewind's stash — are
// not kills and do not call here. A conversation switch DOES (detach.go's
// [app.clearConversation]): the sidecar that picks the sentence up answers
// only while that conversation is still kept, and the ring is what still has
// the words if it is let go first.
func (a *app) noteKilled() {
a.drafts.push(a.input.value)
}

// landingKeysWord is the opening line of every session: the keys the status
// line has no room for. It is named because the note that writes it also names
// the chords inside it for THE PAYLOAD RULE (payload.go), and a sentence
Expand Down Expand Up @@ -6894,6 +6914,13 @@ func (a *app) slash(line string) tea.Cmd {
a.openPermissions()
return nil

case "drafts":
// The ring of cleared-but-kept drafts, as a list (draftpage.go). No
// argument form: the rows are a person's own words, and naming them is
// reading this list first.
a.openDrafts()
return nil

case "standing":
// WHAT IS ALREADY TRUE HERE, as a list, with the three keys that take one
// back on it (standingpage.go). Nothing on that page is NAMED at the command
Expand Down
1 change: 1 addition & 0 deletions internal/tui3/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ var commands = []command{
{name: "new", desc: "start another conversation in this project", alias: []string{"clear", "clean", "reset"}},
{name: "resume", desc: "open an earlier conversation", alias: []string{"sessions"}},
{name: "compact", desc: "summarize the conversation now"},
{name: "drafts", desc: "cleared-but-kept drafts · enter restores one, d lets one go"},
{name: "stop", desc: "stop the open task or selected work · asks first"},
{name: "autonomy", desc: "how questions are handled while you are away"},
{name: "autonomy", args: "<kind> <ask|recommend DURATION|decide>", desc: "change one project's question rule"},
Expand Down
8 changes: 8 additions & 0 deletions internal/tui3/detach.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,13 @@ func (a *app) clearConversation() {
// The box goes with the conversation it was typed at: the sidecar is holding
// it, and the arriving conversation has its own.
//
// AND ON THE KILL RING FIRST (draftring.go). The sidecar's answer to where
// the sentence is lasts only as long as the conversation is kept — a tab
// closed, a conversation evicted — and the ring is the net that still has
// the words after that. The push dedupes, so an ordinary switch away and
// back does not fill the walk with copies of one sentence.
a.noteKilled()
//
// ALL OF THE BOXES, which is what [app.forgetComposers] adds (recipient.go):
// the compact pastes the tokens in the sentence stood for, and every task
// page's own unsent line. A stash carried across would be words addressed to
Expand Down Expand Up @@ -447,6 +454,7 @@ func (a *app) closeForSwitch() {
// about to start work in a conversation they are no longer in
// (subharness.go).
a.subPage.close()
a.draftPage.close()
a.roster.close()
a.shelf.close()
a.closeLists()
Expand Down
Loading
Loading