Add agent components 451-465 and icon.Crystal

Fifteen components that dress an AI agent's work as an RPG party:
AgentCard, ToolCall, PlanQuest, ThinkingOrb, TokenMana, ApprovalPrompt,
AgentLog, SubAgentParty, MemoryCrystals, AgentStatus, DiffScroll,
ModelSelect, PromptBox, CostCoins and Checkpoints.

The context window is HP and the token budget MP; tool calls are spells
that CAST, glow, then land OK or FAILED; the transcript reads as a battle
log whose newest line types itself out; models line up like fighters.
Busy signs keep moving under Reduce Motion, decoration stops. Commands
and prompts never reach the log, and a veiled PromptBox casts nothing.

icon.Crystal is the new 12x12 save and memory crystal.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 12:03:44 -04:00
1 parent 5605d785de
commit d60d53ef90
29 files changed
+2489 -2

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
package gallery
import (
"strings"
"testing"
)
func TestAgentDemosLogTheirActions(t *testing.T) {
// Clicks that only focus a component may log nothing.
quiet := map[string]bool{"Prompt": true}
for _, tc := range []struct {
path string
steps []string
}{
{"/c/451-agentcard", []string{"READ A BIG FILE", "COMPACT"}}, {"/c/452-toolcall", []string{"CAST GREP"}},
{"/c/453-planquest", []string{"COMPLETE STEP"}}, {"/c/454-thinkingorb", []string{"PONDER"}},
{"/c/455-tokenmana", []string{"CAST (12K)", "DRINK ETHER"}}, {"/c/456-approvalprompt", []string{"YES", "NO"}},
{"/c/457-agentlog", []string{"NEXT STEP"}}, {"/c/458-subagentparty", []string{"SUMMON", "NEXT TURN"}},
{"/c/459-memorycrystals", []string{"Memories"}}, {"/c/462-modelselect", []string{"Models"}},
{"/c/463-promptbox", []string{"Prompt", "type:ship it", "key:Enter"}}, {"/c/464-costcoins", []string{"RUN THE AGENT", "NEW MONTH"}},
{"/c/465-checkpoints", []string{"RESTORE Tests green", "SAVE HERE"}},
} {
a, tt := open(tc.path)
slug := tc.path[3:]
for _, step := range tc.steps {
before := len(a.logs[slug])
if k, ok := strings.CutPrefix(step, "key:"); ok {
tt.Key(0, letters[k])
} else if s, ok := strings.CutPrefix(step, "type:"); ok {
tt.Type(s)
} else {
click(t, tt, step)
}
if len(a.logs[slug]) <= before && !quiet[step] {
t.Errorf("%s: %s logged nothing: %q", slug, step, a.logs[slug])
}
}
// A prompt may hold secrets: the log counts, never quotes.
if strings.Contains(strings.Join(a.logs[slug], " "), "ship it") {
t.Errorf("%s logged the prompt: %q", slug, a.logs[slug])
}
}
}
+213
View File
@@ -0,0 +1,213 @@
package gallery
import (
"fmt"
"slices"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/agent"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
var agentEntries = []Entry{
{451, "AgentCard", "智能体卡", "agent", "A party card. The context window is the HP bar: the more it reads, the less it has left.", demoAgentCard},
{452, "ToolCall", "工具调用", "agent", "Every tool call is a spell: CAST, a moment of glowing, then OK or FAILED.", demoToolCall},
{453, "PlanQuest", "计划任务", "agent", "The plan as quest objectives. Finish them all for a fanfare.", demoPlanQuest},
{454, "ThinkingOrb", "思考水晶球", "agent", "A crystal ball that swirls while the agent thinks.", demoThinkingOrb},
{455, "TokenMana", "令牌魔力", "agent", "The token budget as MP. Spend it all and the well runs dry.", demoTokenMana},
{456, "ApprovalPrompt", "授权提示", "agent", "ALLOW SPELL? The agent asks before it runs a command; risky ones come in red.", demoApprovalPrompt},
{457, "AgentLog", "智能体日志", "agent", "The transcript as a battle log. The newest line types itself out.", demoAgentLog},
{458, "SubAgentParty", "子智能体", "agent", "Agents summon helpers. The tree shows who summoned whom, and how each is doing.", demoSubAgentParty},
}
type agentDemo struct {
used, done, mana, next int
cast time.Time
told, thinking bool
log []agent.LogStep
party []agent.Member
}
func agentState(c *ui.Context) *agentDemo {
return ui.Local(c.Root(), demoKey("agent"), func() agentDemo {
return agentDemo{used: 64_000, done: 1, mana: 28_000, told: true, next: 3, log: slices.Clone(story[:3]),
party: []agent.Member{{Name: "LEAD", Status: agent.Working, Summoned: []agent.Member{
{Name: "SCOUT", Status: agent.Done}, {Name: "SMITH", Status: agent.Working, Summoned: []agent.Member{{Name: "IMP", Status: agent.Blocked}}},
}}}}
})
}
func demoAgentCard(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := agentState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
agent.AgentCard(c, agent.Agent{Name: "ROBO", Class: "CODER", Level: 12, Used: s.used, Window: 200_000}, agent.AgentCardOptions{})
agent.AgentCard(c, agent.Agent{Name: "SAGE", Class: "REVIEWER", Level: 30, Used: 182_000, Window: 200_000}, agent.AgentCardOptions{})
ui.Row(c).Gap(k.D(6)).Children(func() {
if input.Button(c, "READ A BIG FILE", input.ButtonOptions{Size: input.Small}).Clicked {
s.used = min(200_000, s.used+24_000)
log(fmt.Sprintf("ROBO used %d tokens", s.used))
}
// Compacting is a night at the inn: HP comes back.
if input.Button(c, "COMPACT", input.ButtonOptions{Size: input.Small}).Clicked {
s.used = 12_000
log("ROBO compacted its context")
}
})
})
}
func demoToolCall(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := agentState(c)
grep := agent.OK
if done := s.cast.Add(1500 * time.Millisecond); k.Now().Before(done) {
grep = agent.Casting
k.At(c, done)
} else if !s.told {
s.told = true
log("grep: OK")
}
ui.Column(c).Gap(k.D(4)).Children(func() {
agent.ToolCall(c, "read_file", agent.OK, agent.ToolCallOptions{Args: "pixel/canvas.go"})
agent.ToolCall(c, "grep", grep, agent.ToolCallOptions{Args: `"TODO" in 512 files`})
agent.ToolCall(c, "bash", agent.Failed, agent.ToolCallOptions{Args: "npm test: 2 tests fainted"})
if input.Button(c, "CAST GREP", input.ButtonOptions{Size: input.Small, Disabled: grep == agent.Casting}).Clicked {
s.cast, s.told = k.Now(), false
log("casting grep")
}
})
}
func demoPlanQuest(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := agentState(c)
steps := []agent.PlanStep{{Text: "Reproduce the save bug"}, {Text: "Write a failing test"}, {Text: "Fix the root cause"}, {Text: "Run every test"}, {Text: "Ship it"}}
for i := range s.done {
steps[i].Done = true
}
ui.Column(c).Gap(k.D(6)).Children(func() {
agent.PlanQuest(c, "The Lost Save", steps, agent.PlanQuestOptions{})
if s.done < len(steps) {
if input.Button(c, "COMPLETE STEP", input.ButtonOptions{Size: input.Small}).Clicked {
s.done++
log(fmt.Sprintf("objective %d done", s.done))
}
return
}
if input.Button(c, "NEW QUEST", input.ButtonOptions{Size: input.Small}).Clicked {
s.done = 0
log("a new quest begins")
}
})
}
func demoThinkingOrb(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := agentState(c)
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
agent.ThinkingOrb(c, "Mind", s.thinking, agent.ThinkingOrbOptions{})
label := "PONDER"
if s.thinking {
label = "SNAP OUT OF IT"
}
if input.Button(c, label, input.ButtonOptions{Size: input.Small}).Clicked {
s.thinking = !s.thinking
log(fmt.Sprintf("thinking %v", s.thinking))
}
})
}
func demoTokenMana(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := agentState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
agent.TokenMana(c, "Token budget", s.mana, 100_000, agent.TokenManaOptions{Width: 160})
ui.Row(c).Gap(k.D(6)).Children(func() {
if input.Button(c, "CAST (12K)", input.ButtonOptions{Size: input.Small}).Clicked {
s.mana += 12_000
log(fmt.Sprintf("%d tokens spent", s.mana))
}
if input.Button(c, "DRINK ETHER", input.ButtonOptions{Size: input.Small}).Clicked {
s.mana = 0
log("MP restored")
}
})
})
}
func demoApprovalPrompt(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Column(c).Gap(k.D(8)).Children(func() {
for _, cmd := range []struct {
line string
danger bool
}{{"go test ./...", false}, {"rm -rf ~/saves", true}} {
r := agent.ApprovalPrompt(c, cmd.line, cmd.line, agent.ApprovalPromptOptions{Danger: cmd.danger})
if r.Allowed {
log("allowed: " + cmd.line)
}
if r.Denied {
log("denied: " + cmd.line)
}
}
})
}
// story is what the log demo's agent does, in turn.
var story = []agent.LogStep{
{Kind: agent.Thought, Text: "the save file looks corrupt"}, {Kind: agent.Cast, Text: "read_file"},
{Kind: agent.Hit, Text: "a stray byte at 0x2A"}, {Kind: agent.Cast, Text: "edit_file"},
{Kind: agent.Miss, Text: "one test still fails"}, {Kind: agent.Cast, Text: "edit_file"},
{Kind: agent.Hit, Text: "all 64 tests pass"}, {Kind: agent.Said, Text: "Fixed! Shall I ship it?"},
}
func demoAgentLog(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := agentState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
agent.AgentLog(c, "Transcript", "ROBO", s.log, agent.AgentLogOptions{Width: 260})
if input.Button(c, "NEXT STEP", input.ButtonOptions{Size: input.Small}).Clicked {
step := story[s.next%len(story)]
s.log, s.next = append(s.log, step), s.next+1
log(fmt.Sprintf("step %d: %s", s.next, step.Text))
}
})
}
func demoSubAgentParty(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := agentState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
agent.SubAgentParty(c, "Party", s.party, agent.SubAgentPartyOptions{})
ui.Row(c).Gap(k.D(6)).Children(func() {
lead := &s.party[0]
if input.Button(c, "SUMMON", input.ButtonOptions{Size: input.Small, Disabled: len(lead.Summoned) == 4}).Clicked {
name := []string{"GOLEM", "WISP"}[len(lead.Summoned)-2]
lead.Summoned = append(lead.Summoned, agent.Member{Name: name, Status: agent.Working})
log("LEAD summons " + name)
}
// A turn passes: the blocked wake, the busy finish.
if input.Button(c, "NEXT TURN", input.ButtonOptions{Size: input.Small}).Clicked {
advance(lead.Summoned)
log("a turn passes")
}
})
})
}
// advance moves each summoned member on by a turn.
func advance(ms []agent.Member) {
for i := range ms {
switch ms[i].Status {
case agent.Blocked, agent.Idle:
ms[i].Status = agent.Working
case agent.Working:
ms[i].Status = agent.Done
}
advance(ms[i].Summoned)
}
}
+127
View File
@@ -0,0 +1,127 @@
package gallery
import (
"fmt"
"time"
"unicode/utf8"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/agent"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
var guildEntries = []Entry{
{459, "MemoryCrystals", "记忆水晶", "agent", "Memory slots as crystals. Lit ones hold something; the arrows peek inside.", demoMemoryCrystals},
{460, "AgentStatus", "状态环", "agent", "A ring that says it all: sleepy, busy, stuck or done.", demoAgentStatus},
{461, "DiffScroll", "改动卷轴", "agent", "What changed, on a scroll: each file with lines added and removed.", demoDiffScroll},
{462, "ModelSelect", "模型选择", "agent", "Pick a model like a fighter: stats on the right, a flag on your pick.", demoModelSelect},
{463, "PromptBox", "咒语输入", "agent", "Write the spell, then CAST. Enter casts; Shift+Enter starts a new line.", demoPromptBox},
{464, "CostCoins", "花费", "agent", "What it cost, in coins: gold dollars, silver dimes, bronze cents.", demoCostCoins},
{465, "Checkpoints", "存档点", "agent", "Save points along the way. RESTORE takes you back.", demoCheckpoints},
}
type guildDemo struct {
model, cents int
prompt string
saves []agent.Checkpoint
}
func guildState(c *ui.Context) *guildDemo {
return ui.Local(c.Root(), demoKey("guild"), func() guildDemo {
now := pixel.K(c).Now()
return guildDemo{model: 1, cents: 1234, saves: []agent.Checkpoint{
{Name: "Project start", At: now.Add(-50 * time.Hour)},
{Name: "Before the refactor", At: now.Add(-3 * time.Hour)},
{Name: "Tests green", At: now.Add(-5 * time.Minute)},
}}
})
}
func demoMemoryCrystals(c *ui.Context, log func(string)) {
memories := []string{"Prefers tabs", "Tests before commits", "Never force-push", "Coffee: black", "Hates dark patterns"}
if i := agent.MemoryCrystals(c, "Memories", memories, agent.MemoryCrystalsOptions{}).Picked; i >= 0 {
if i < len(memories) {
log("recalled: " + memories[i])
return
}
log(fmt.Sprintf("crystal %d is empty", i+1))
}
}
func demoAgentStatus(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(8)).Children(func() {
for _, s := range []agent.Status{agent.Idle, agent.Working, agent.Blocked, agent.Done} {
agent.AgentStatus(c, fmt.Sprintf("Robo %d", s+1), s, agent.AgentStatusOptions{})
}
})
}
func demoDiffScroll(c *ui.Context, log func(string)) {
agent.DiffScroll(c, "Changes", []agent.FileDiff{
{Path: "pixel/canvas.go", Added: 42, Removed: 7},
{Path: "agent/agentstatus.go", Added: 88},
{Path: "README.md", Added: 3, Removed: 1},
{Path: "old/clever_hack.go", Removed: 120},
}, agent.DiffScrollOptions{Width: 260})
}
func demoModelSelect(c *ui.Context, log func(string)) {
s := guildState(c)
models := []agent.Model{
{Name: "SPRITE", Portrait: icon.Bolt, Speed: 5, Power: 2, Cost: 1},
{Name: "KNIGHT", Portrait: icon.Shield, Speed: 3, Power: 4, Cost: 3},
{Name: "WIZARD", Portrait: icon.Sparkle, Speed: 2, Power: 5, Cost: 5},
{Name: "BARD", Portrait: icon.Music, Speed: 4, Power: 3, Cost: 2},
}
if agent.ModelSelect(c, "Models", models, &s.model, agent.ModelSelectOptions{}).Changed {
log(models[s.model].Name + " joins the party")
}
}
func demoPromptBox(c *ui.Context, log func(string)) {
s := guildState(c)
r := agent.PromptBox(c, "Prompt", &s.prompt, agent.PromptBoxOptions{Placeholder: "Describe the quest…"})
// Prompts can hold secrets, so the log only counts.
if r.Changed {
log(fmt.Sprintf("%d runes written", utf8.RuneCountInString(s.prompt)))
}
if r.Cast != "" {
log(fmt.Sprintf("cast %d runes", utf8.RuneCountInString(r.Cast)))
}
}
func demoCostCoins(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := guildState(c)
ui.Row(c).Gap(k.D(8)).AlignItems(ui.End).Children(func() {
agent.CostCoins(c, "Spent", s.cents, agent.CostCoinsOptions{})
ui.Column(c).Gap(k.D(4)).Children(func() {
if input.Button(c, "RUN THE AGENT", input.ButtonOptions{Size: input.Small}).Clicked {
s.cents += 37
log(fmt.Sprintf("spent $%d.%02d", s.cents/100, s.cents%100))
}
if input.Button(c, "NEW MONTH", input.ButtonOptions{Size: input.Small}).Clicked {
s.cents = 0
log("the bill is paid")
}
})
})
}
func demoCheckpoints(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := guildState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
if i := agent.Checkpoints(c, "Save points", s.saves, agent.CheckpointsOptions{}).Restored; i >= 0 {
log("restored: " + s.saves[i].Name)
}
if input.Button(c, "SAVE HERE", input.ButtonOptions{Size: input.Small}).Clicked {
s.saves = append(s.saves, agent.Checkpoint{Name: fmt.Sprintf("Save %d", len(s.saves)+1), At: k.Now()})
log("saved")
}
})
}
+1 -1
View File
@@ -56,7 +56,7 @@ var Categories = []Category{
{"tabletop", "Tabletop", "桌游", icon.Crown},
}
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries, chatEntries, mailEntries, mediaEntries, studioEntries, codeEntries, debugEntries, accountEntries, playerEntries, filesEntries, storageEntries, projectEntries, teamEntries, shopEntries, marketEntries)
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries, chatEntries, mailEntries, mediaEntries, studioEntries, codeEntries, debugEntries, accountEntries, playerEntries, filesEntries, storageEntries, projectEntries, teamEntries, shopEntries, marketEntries, agentEntries, guildEntries)
func Entries() []Entry { return entries }