Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e059d76
tui3: a home panel stands where its content puts it (#1046)
ZeroPoint95 Sep 15, 2026
a95a31e
tui3: the resting box keeps a caret on the cell the first letter land…
ZeroPoint95 Sep 15, 2026
3f6ff3c
tui3, demo-home: a standing row names enter only where there is a con…
ZeroPoint95 Sep 15, 2026
af00d86
tui3: the middle column is what the selected row says about itself
ZeroPoint95 Sep 15, 2026
2bd4439
tui3: needs you keeps its question under its row
ZeroPoint95 Sep 15, 2026
d62265a
tui3: the description column is every row's note, aligned to its row
ZeroPoint95 Sep 15, 2026
137fd6d
tui3: a row's keys are its own, and enter on a home-made watch opens …
ZeroPoint95 Sep 15, 2026
63bc1a2
tui3: a question home raises is drawn in the description column
ZeroPoint95 Sep 15, 2026
f805de3
tui3: a watch asked for in another project opens from here
ZeroPoint95 Sep 15, 2026
93bfba0
tui3: a question raised by the launch draws in the column too
ZeroPoint95 Sep 15, 2026
00f12c7
tui3: the ? leads the question, not the row that asked it
ZeroPoint95 Sep 15, 2026
b36a97a
tui3: a note in the description column stands in three cells, not two
ZeroPoint95 Sep 15, 2026
dea2629
demo-home: the fixture has been looked at before, so `since you left`…
ZeroPoint95 Sep 15, 2026
f3fa007
tui3: a question card is drawn whole or not at all, and the foot answ…
ZeroPoint95 Sep 15, 2026
dd347ec
tui3: needs you is its words alone — no counts, no clause, and the la…
ZeroPoint95 Sep 15, 2026
8d2362f
tui3: every row of home's field rests on the one foot, and ctrl+o is …
ZeroPoint95 Sep 15, 2026
ab3721c
tui3: the right margin of every field row is a time, and the rest is …
ZeroPoint95 Sep 16, 2026
39b785d
tui3: a home panel's fold is a toggle — N more opens the panel, N few…
ZeroPoint95 Sep 16, 2026
d825da6
tui3: a next up row describes what it found, not when it wakes
ZeroPoint95 Sep 16, 2026
448adeb
tui3: next up is scheduled — one word, no clock at the right, one sen…
ZeroPoint95 Sep 16, 2026
6a3564b
tui3: the collapse coming back is a fold, not the find word
ZeroPoint95 Sep 16, 2026
006fbd6
tui3: a scheduled row says its sentence at every width, a fold names …
ZeroPoint95 Sep 16, 2026
74d9707
tui3: a question's door word is said under the cursor, not on every row
ZeroPoint95 Sep 16, 2026
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
29 changes: 25 additions & 4 deletions cmd/codeaf-demo-home/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,24 @@ never opened.
// safe is somebody typing --into over a directory that is not a demo home at
// all. So an existing directory is only accepted when it is empty, or when the
// caller says --keep and the directory looks like something this program wrote.
// THE DIRECTORY IS RESOLVED THROUGH ITS SYMLINKS BEFORE ANYTHING IS WRITTEN IN
// IT, and on a Mac that is not a nicety: `/tmp` is a symlink to `/private/tmp`,
// os.MkdirTemp hands back the `/tmp` spelling, and a codeaf launched in the
// fixture resolves its own working directory to the `/private/tmp` one. The two
// spellings are two PROJECT BUCKETS for one folder — the fixture's conversations
// under `-tmp-…-codeaf` and the window's own under `-private-tmp-…-codeaf` — so
// home drew the demo project twice, and `enter` on a standing order refused
// because the conversation that asked for it was "elsewhere"
// ([app.homeItemEnter]'s bucket guard). Every path this fixture writes is the
// resolved one, so the fixture and the binary agree about what the folder is
// called.
func demoDir(into string, reuse bool) (dir string, fresh bool, err error) {
if into == "" {
made, err := os.MkdirTemp("", "codeaf-demo-home-")
if err != nil {
return "", false, fmt.Errorf("make a directory to build in: %w", err)
}
return made, true, nil
return resolveDemoDir(made), true, nil
}
dir, err = filepath.Abs(into)
if err != nil {
Expand All @@ -145,21 +156,21 @@ func demoDir(into string, reuse bool) (dir string, fresh bool, err error) {
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", false, fmt.Errorf("make %s: %w", dir, err)
}
return dir, true, nil
return resolveDemoDir(dir), true, nil
}
if err != nil {
return "", false, fmt.Errorf("read %s: %w", dir, err)
}
if len(entries) == 0 {
return dir, true, nil
return resolveDemoDir(dir), true, nil
}
if !reuse {
return "", false, fmt.Errorf("%s is not empty; pass --keep to reuse a demo home that is already there", dir)
}
if _, err := os.Stat(filepath.Join(dir, ".codeaf", "v3", "projects")); err != nil {
return "", false, fmt.Errorf("%s is not empty and does not hold a demo home", dir)
}
return dir, false, nil
return resolveDemoDir(dir), false, nil
}

// launchAgainst runs the surface against the demo home and keeps the live
Expand Down Expand Up @@ -208,3 +219,13 @@ func launchAgainst(dir, binary string) error {
func demoEnviron() []string {
return env.EnvironWithout("HOME", home.EnvVar)
}

// resolveDemoDir is the directory with its symlinks followed, and the directory
// itself where they cannot be — a path that does not resolve is not a reason to
// refuse to build a fixture in it.
func resolveDemoDir(dir string) string {
if real, err := filepath.EvalSymlinks(dir); err == nil {
return real
}
return dir
}
21 changes: 20 additions & 1 deletion cmd/codeaf-demo-home/seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func seedDemoHome(dir string, now time.Time) (builtHome, error) {
}
built.Nodes, built.Jobs = nodes, jobs

orders, err := writeStanding(filepath.Join(root, "v3", "standing"), projects, now)
orders, err := writeStanding(filepath.Join(root, "v3", "standing"), projects, ids, now)
if err != nil {
return built, err
}
Expand All @@ -134,9 +134,28 @@ func seedDemoHome(dir string, now time.Time) (builtHome, error) {
built.UsageLines = lines

built.Artifacts = writeArtifacts(filepath.Join(root, "v3", session.ArtifactsIndexName), projects, ids, now)

// AND THE MACHINE HAS BEEN LOOKED AT BEFORE, half a day ago.
//
// Without this stamp `since you left` draws its whisper on a fixture full of
// landed work, and correctly: the look stamp is the ORIGIN news is measured
// from, and a machine that has never had home closed on it has no origin —
// "the first look marks NOTHING as news, rather than everything"
// (internal/session's look.go). A demo home with no stamp is therefore a
// demo home where one of the seven panels can never have a row, which is the
// one thing this fixture exists to prevent.
//
// TWELVE HOURS IS THE FIGURE THE DESIGN DRAWS (`since you left · 12h`,
// docs/design/home-mission-control/DESIGN.md §1) and it is far enough back
// that the landed work and the made files above it are all inside the
// window.
session.NoteLook(filepath.Join(root, "v3", "projects"), now.Add(-demoLastLook))
return built, nil
}

// demoLastLook is how long ago this fixture's person last closed home.
const demoLastLook = 12 * time.Hour

// ── the projects ────────────────────────────────────────────────────────────

// demoProject is one workspace on the demo machine.
Expand Down
27 changes: 26 additions & 1 deletion cmd/codeaf-demo-home/seed_standing.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package main

import (
"fmt"
"path/filepath"
"time"

"github.com/Agent-Field/codeaf/internal/standing"
Expand All @@ -27,6 +28,16 @@ type demoOrder struct {
// as the ticker applies it, because Create refuses to make anything that is
// not active and has never run.
after func(item *standing.Item, now time.Time)
// asked is the TITLE of the conversation this order was made in, and it
// becomes the item's [standing.Origin] — the door home opens on `enter`,
// which answers "why did I get this?".
//
// EVERY ORDER NEEDS ONE. An item with no origin is a row whose `enter`
// refuses, and a fixture full of them is a fixture that never exercises the
// door at all — which is the join discipline seed.go already states for
// tasks and ledger lines: a join that points at nothing draws a row a person
// cannot open.
asked string
// spent is the ledger this order has left behind: one entry per element,
// oldest first, counted back in days from today.
spent []demoFiring
Expand All @@ -52,6 +63,7 @@ var demoOrders = []demoOrder{
// The one that is ASKING. Its latest run stopped on a question, which is
// the field home sorts on and the reason this row is drawn first.
project: firstProjectName,
asked: "Standing Up the Watches",
item: standing.Item{
ID: "watch-ci",
Words: "tell me when CI goes red on master",
Expand Down Expand Up @@ -82,6 +94,7 @@ var demoOrders = []demoOrder{
// The one that FIRED TODAY and has come back clean three times running,
// which is the count the rope column's middle rung is drawn from.
project: firstProjectName,
asked: "Standing Up the Watches",
item: standing.Item{
ID: "morning-sweep",
Words: "sweep the repo every morning at nine and tell me what changed under me",
Expand Down Expand Up @@ -118,6 +131,7 @@ var demoOrders = []demoOrder{
// card in a conversation and only a yes makes an item — so the pause is a
// Save afterwards, exactly as the surface's own `p` writes it.
project: "infra",
asked: "The Backup Window",
item: standing.Item{
ID: "deps-weekly",
Words: "check the dependency advisories every Monday morning",
Expand All @@ -140,6 +154,7 @@ var demoOrders = []demoOrder{
// The RULE. A hold never wakes, so it can never spend, which is why it
// alone needs no rails and no action — and why it leaves no ledger line.
project: firstProjectName,
asked: "The Manual's Missing Page",
item: standing.Item{
ID: "public-api-hold",
Words: "never change the public API without telling me first",
Expand All @@ -152,7 +167,7 @@ var demoOrders = []demoOrder{

// writeStanding creates the four orders, writes the quiet half onto them, and
// leaves the day ledgers their cost-per-firing is summed from.
func writeStanding(root string, projects map[string]*demoProject, now time.Time) (int, error) {
func writeStanding(root string, projects map[string]*demoProject, ids map[string]string, now time.Time) (int, error) {
orders, err := standing.Open(root)
if err != nil {
return 0, fmt.Errorf("open the standing store: %w", err)
Expand All @@ -165,6 +180,16 @@ func writeStanding(root string, projects map[string]*demoProject, now time.Time)
}
item := order.item
item.Workspace = project.dir
// THE ORDER REMEMBERS WHERE IT WAS ASKED FOR, so `enter` on its home row
// has a conversation to open ([app.homeItemEnter]).
id, ok := ids[order.asked]
if !ok {
return written, fmt.Errorf("the standing order %q was asked for in %q, which is not a conversation this fixture writes", item.ID, order.asked)
}
item.Origin = standing.Origin{
SessionID: id,
Transcript: filepath.Join(project.bucket, id, "transcript.jsonl"),
}
made, err := orders.Create(item)
if err != nil {
return written, fmt.Errorf("create the standing order %q: %w", item.ID, err)
Expand Down
10 changes: 7 additions & 3 deletions cmd/codeaf-demo-home/seed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,15 @@ func TestTheDemoHomeRefusesADirectoryThatIsNotItsOwn(t *testing.T) {
}

// An empty directory, and one this program has already built, are the two it
// will take.
// will take — and what comes back is the directory with its SYMLINKS
// FOLLOWED. On a Mac t.TempDir() is under `/var`, which is a symlink to
// `/private/var`, and the fixture has to write the spelling a codeaf launched
// inside it will resolve for itself or the two disagree about what the
// project folder is called ([resolveDemoDir]).
empty := t.TempDir()
dir, fresh, err := demoDir(empty, false)
if err != nil || !fresh || dir != empty {
t.Fatalf("an empty directory came back as (%q, fresh=%v, %v)", dir, fresh, err)
if err != nil || !fresh || dir != resolveDemoDir(empty) {
t.Fatalf("an empty directory came back as (%q, fresh=%v, %v), want %q", dir, fresh, err, resolveDemoDir(empty))
}
if _, err := seedDemoHome(empty, time.Now()); err != nil {
t.Fatal(err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/codeaf-demo-home/seed_work.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ var demoTasks = []demoTask{
},
},
// AND A GROUP OF LANDINGS NOBODY HAS CHECKED, ACROSS THREE PROJECTS. Home's
// `needs you` panel holds them as its `to check` group (homepanel_needs.go),
// `needs you` panel holds them as its `unread` group (homepanel_needs.go),
// and a fixture with ONE of them draws a group of one — which is the shape a
// group line, a count, a clause and a fold all fail to be tested by. These
// are deliberately unlike each other: one wrote seventeen files, one wrote
Expand Down
Loading
Loading