You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Issue:#660 (scoped from #457 point .5) Depends on: Issue #4 (PreviewSession and View Isolation Audit) - completed Status: Planning
1. Design Principles
These principles guide every decision in this issue. When facing a design
fork, test both options against this list.
Nav-view consistency - the tweak home screen follows the same
Bubble Tea patterns as highway and porthole views. Where the home
screen has a distinct purpose (menu-driven entry points), the
interaction style prefers charm huh for forms and wizards.
Charm-first UI - all tweak UI elements use charm.sh libraries.
Forms use huh, the main loop uses bubbletea, and static styling
uses lipgloss with predefined themes.
Safe colour boundary - user-selected colours apply only to the
embedded navigation preview, never to the tweak UI itself. The tweak
UI (home screen, menus, forms) uses predefined charm themes so it
remains legible regardless of the user's colour experiments.
Additive only - no existing file is modified. New files are
created alongside existing ones.
Skeleton first - all four entry points (gradient workshop, palette
editor, bindings, import) are visible on the home screen but are
non-functional. Their models and screens are implemented in later
issues.
2. Scope
Delivers the tweak command wiring, coordinator skeleton with three-layer
state model, and the home screen TUI with entry points visible but not
functional.
TweakCoordinator lives in src/app/controller/ alongside the
existing Coordinator. The two are independent structs with no shared
code beyond common imports (pref, core, contract, report).
tweakHomeModel (the Bubble Tea model for the tweak home screen)
lives in src/app/controller/ alongside the coordinator for this
skeleton. In later issues, sub-screens may move to dedicated packages
under src/prism/tweak/.
The home screen model is unexported (tweakHomeModel). The TweakCoordinator owns the model and the tea.Program.
4.2 State Model Location
The three-layer state model (raw/upscaled/working) and dirty tracking
live inside TweakCoordinator. They are not extracted into a separate
package because:
They are tightly coupled to coordinator lifecycle.
huh is a new direct dependency. Use the latest stable version
compatible with the existing bubbletea/v2 dependency (v2.0.7).
4.4 Tweak UI Theme
The tweak home screen and all forms use a fixed internal style set
built with lipgloss at construction time, derived from a small
hardcoded palette of high-contrast, universally legible colours. These
styles are defined as private constants/helpers in tweak-home-model.go and are never user-configurable.
The navigation preview embedded alongside the home screen (or toggled)
uses the user's working-state palette via contract.NewTheme and ThemeUpdateMsg. This ensures the colour experiment boundary is clear
and enforced by construction.
4.5 Auto-Restart Loop
The auto-restart loop lives in TweakCoordinator, not in the model.
The coordinator launches a goroutine that:
Creates a fresh PreviewSession.
Runs an agenor traversal against the configured preview path.
On End lifecycle event, checks context cancellation.
If context is still alive, re-arms with a new PreviewSession and
repeats.
When context is cancelled (user presses Ctrl-C or selects Quit),
the goroutine exits and MarkComplete is never called.
The model is not aware of the traversal lifecycle. It receives only
messages (node events, completion events) via program.Send().
4.6 Exit Flow Ownership
The exit flow (checking dirty flags, showing confirmation prompts)
lives in TweakCoordinator, not the model. When the model receives a
Quit keypress, it sends an internal quitRequestedMsg to the
coordinator, which checks state and either:
Exits immediately (no changes).
Shows a prompt (changes pending) via a sub-model or huh form.
Cancels the exit and returns to the home screen.
4.7 No Signal Setup in TweakCoordinator
Ctrl-C is handled by the caller context, not by signal.Notify inside TweakCoordinator. This matches the PreviewSession pattern
established in Issue #4. The caller (the tweak command handler)
creates a cancellable context; Ctrl-C cancels it, which terminates the
auto-restart loop and triggers a clean exit.
4.8 No ThemeUpdateMsg Arms Yet
The ThemeUpdateMsgUpdate arms in highway and porthole models are
deferred to Issue #5 (this issue) but only as a presence check - the case is added but the body simply returns the model unchanged or
logs. Real hot-swap implementation is part of Issue #6 when the
gradient workshop produces palette changes.
5. Required Changes
5.1 go.mod - New Dependency
github.com/charmbracelet/huhv<latest-compatible>
Picked to be compatible with the existing bubbletea/v2 dependency.
Run go get github.com/charmbracelet/huh@latest and verify no version
conflicts.
5.2 src/app/command/tweak-cmd.go
package command
import (
"github.com/snivilised/jaywalk/src/app/controller""github.com/snivilised/li18ngo""github.com/snivilised/jaywalk/locale""github.com/snivilised/mamba/assist"
)
// tweakState holds param-sets owned exclusively by the tweak command.typetweakStatestruct {
previewPathPs*assist.ParamSet[whatever]
}
func (b*Bootstrap) buildTweakCommand(container*assist.CobraContainer) {
tweakCmd:=&cobra.Command{
Use: "tweak",
Short: li18ngo.Text(locale.TweakCmdShortDescTemplData{}),
Long: li18ngo.Text(locale.TweakCmdLongDescTemplData{}),
Args: cobra.NoArgs,
RunE: b.runTweak,
}
// --preview-path flag (optional, overrides jay.ui.yml tweak.preview-path)b.tweak.previewPathPs= assist.NewParamSet[store.PreviewParameterSet](cmd)
// ... bind preview-path flag ...container.MustRegisterRootedCommand(tweakCmd)
}
func (b*Bootstrap) runTweak(cmd*cobra.Command, _ []string) error {
// 1. Resolve preview path: flag > jay.ui.yml tweak.preview-path > $HOMEpreviewPath:=resolvePreviewPath(b.tweak.previewPathPs, b.UI)
// 2. Create cancellable context for clean Ctrl-Cctx, cancel:=context.WithCancel(cmd.Context())
defercancel()
// 3. Create TweakCoordinator with working palettecoordinator:=controller.NewTweakCoordinator(
controller.TweakCoordinatorOptions{
PreviewPath: previewPath,
Palette: currentPalette,
Logger: b.logger,
},
)
// 4. Run the tweak TUI (blocking until user quits)returncoordinator.Run(ctx)
}
Note: i18n template data structs for locale strings are defined in locale/ and follow the conventions from .claude/skills/go-i18n/SKILL.md.
5.3 src/app/controller/tweak-coordinator.go
package controller
import (
"context""sync/atomic""time""github.com/snivilised/jaywalk/src/agenor""github.com/snivilised/jaywalk/src/agenor/core""github.com/snivilised/jaywalk/src/agenor/pref""github.com/snivilised/jaywalk/src/prism/contract"
tea "charm.land/bubbletea/v2"
)
// TweakCoordinatorOptions configures the coordinator.typeTweakCoordinatorOptionsstruct {
PreviewPathstringPalette contract.PaletteLogger*slog.Logger
}
// tweakDirty tracks whether the user has made changes.typetweakDirtystruct {
upscaleCreativebool// layer 1 differs from layer 0creativebool// layer 2 differs from layer 1
}
// TweakCoordinator manages the tweak TUI lifecycle, state model,// dirty tracking, undo, exit flow, and the perpetual preview// traversal auto-restart loop.typeTweakCoordinatorstruct {
previewPathstring// three-layer state modellayer0 contract.Palette// raw loaded (read-only)layer1 contract.Palette// upscaled (read-only)layer2 contract.Palette// working (mutable)dirtytweakDirty// program is the Bubble Tea program for the tweak home screen.program*tea.Program// cancel is the context cancellation function for the preview// traversal auto-restart loop.cancel context.CancelFunclogger*slog.Logger
}
// NewTweakCoordinator creates a TweakCoordinator with the palette// loaded into layer 0. Layer 1 is produced by upscaling layer 0.// Layer 2 is a copy of layer 1 (no creative changes yet).funcNewTweakCoordinator(optsTweakCoordinatorOptions) *TweakCoordinator {
return&TweakCoordinator{
previewPath: opts.PreviewPath,
layer0: opts.Palette,
layer1: opts.Palette, // upscaled in Issue #1layer2: opts.Palette, // copy of layer1logger: opts.Logger,
}
}
// Run starts the tweak TUI and blocks until the user exits.func (tc*TweakCoordinator) Run(ctx context.Context) error {
ctx, tc.cancel=context.WithCancel(ctx)
defertc.cancel()
model:=newTweakHomeModel(tc)
tc.program=tea.NewProgram(model)
// Start the preview traversal auto-restart loop.gotc.runPreviewLoop(ctx)
// Run the TUI (blocking)._, err:=tc.program.Run()
returnerr
}
// runPreviewLoop runs agenor traversals in a perpetual loop until// the context is cancelled.func (tc*TweakCoordinator) runPreviewLoop(ctx context.Context) {
for {
select {
case<-ctx.Done():
returndefault:
tc.runSinglePreview(ctx)
}
}
}
// runSinglePreview runs one agenor traversal with PreviewSession.func (tc*TweakCoordinator) runSinglePreview(ctx context.Context) {
session:=NewPreviewSession()
defersession.MarkComplete()
// Build a pref.Facade using the working palette's theme.// The callback sends node events to the tea program.traversal:=// ... build agenor traversal with session ..._=traversal.Navigate(ctx)
// On completion, re-arm (loop will call again).
}
// ---------------------------------------------------------------------------// State model operations// ---------------------------------------------------------------------------// WorkingPalette returns the current working state palette.func (tc*TweakCoordinator) WorkingPalette() contract.Palette {
returntc.layer2
}
// Undo resets layer 2 to a copy of layer 1.func (tc*TweakCoordinator) Undo() {
tc.layer2=tc.layer1tc.dirty.creative=false// re-evaluate upscale dirty after undo
}
// IsDirty returns true if either dirty flag is set.func (tc*TweakCoordinator) IsDirty() bool {
returntc.dirty.upscaleCreative||tc.dirty.creative
}
// ExitFlow handles the exit prompt sequence based on dirty state.// Returns true if the application should exit, false if cancelled.func (tc*TweakCoordinator) ExitFlow() bool {
switch {
casetc.dirty.creative:
// "You have unsaved changes. [S] Save [D] Discard [C] Cancel"casetc.dirty.upscaleCreative:
// "No changes to save, but your theme has been enriched..."default:
returntrue// exit silently
}
returnfalse
}
TweakCoordinator Design Notes
TweakCoordinator does not implement report.Presenter. The
existing Presenter interface is for production traversal events.
The tweak coordinator uses tea.Program.Send() directly.
The three-layer state model mirrors the design doc sections 5.1-5.3.
Layer 1 upscaling is a no-op until Issue chore(gh-actions): apply auto-check edits #1 implements UpscalePalette. For the skeleton, all three layers hold the same
palette.
Exit flow prompts use huh forms for consistent charm styling.
The preview loop receives the working palette theme and sends it to
the embedded nav view via ThemeUpdateMsg on each palette change.
5.4 src/app/controller/tweak-home-model.go
package controller
import (
tea "charm.land/bubbletea/v2""github.com/charmbracelet/huh""github.com/snivilised/jaywalk/src/prism/contract"
)
// ---------------------------------------------------------------------------// Messages// ---------------------------------------------------------------------------// entrySelectedMsg is sent when the user selects a home screen entry.// For the skeleton, all entries are non-functional; the message is// defined so Issue #6-9 can handle it.typeentrySelectedMsgstruct {
entryint// 1=gradient workshop, 2=palette editor, 3=bindings, 4=import
}
// exitRequestedMsg is sent when the user triggers an exit flow.typeexitRequestedMsgstruct{}
// ---------------------------------------------------------------------------// Model constants// ---------------------------------------------------------------------------// home screen style definitions use a fixed high-contrast palette.// These styles are never user-configurable (see principle 3).var (
homeTitleStyle=lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FFF")).
Background(lipgloss.Color("#333"))
homeItemStyle=lipgloss.NewStyle().
Foreground(lipgloss.Color("#DDD"))
homeKeyStyle=lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#7C3AED")) // purple accent
)
// ---------------------------------------------------------------------------// tweakHomeModel// ---------------------------------------------------------------------------// tweakHomeModel is the Bubble Tea model for the tweak home screen.// It displays the four entry points, the theme name, and keyboard// shortcuts for file operations, undo, and quit.typetweakHomeModelstruct {
coordinator*TweakCoordinatorthemeNamestringwidthintheightint
}
funcnewTweakHomeModel(tc*TweakCoordinator) tweakHomeModel {
returntweakHomeModel{
coordinator: tc,
themeName: "starship", // from current palette name
}
}
func (mtweakHomeModel) Init() tea.Cmd {
returnnil
}
func (mtweakHomeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switchmsg:=msg.(type) {
case tea.WindowSizeMsg:
m.width=msg.Widthm.height=msg.Heightcase tea.KeyMsg:
switchmsg.String() {
case"1", "2", "3", "4":
// Entry points non-functional in skeleton.// In later issues, sends entrySelectedMsg{entry: N}.returnm, nilcase"f", "F":
// File menu (open/new/save/save-as) - non-functional.returnm, nilcase"z", "Z":
m.coordinator.Undo()
returnm, nilcase"q", "Q":
ifm.coordinator.ExitFlow() {
returnm, tea.Quit
}
returnm, nilcase"ctrl+c":
returnm, tea.Quit
}
}
returnm, nil
}
func (mtweakHomeModel) View() tea.View {
varb strings.Builder// Title bartitle:=fmt.Sprintf(" jay tweak [theme: %s] ", m.themeName)
b.WriteString(homeTitleStyle.Render(title))
b.WriteString("\n\n")
// Entry pointsentries:= []struct {
keystringnamestringdescstring
}{
{"1", "Gradient Workshop", "Define seed gradient, harvest steps to palette roles"},
{"2", "Palette Editor", "Edit palette roles directly"},
{"3", "Bindings", "Map component slots to gradients"},
{"4", "Import Theme", "Convert iTerm2 / VS Code / Alacritty / Warp themes"},
}
for_, e:=rangeentries {
b.WriteString(homeKeyStyle.Render(" "+e.key+". "))
b.WriteString(homeItemStyle.Render(e.name))
b.WriteString("\n")
b.WriteString(" ")
b.WriteString(mutedStyle.Render(e.desc))
b.WriteString("\n\n")
}
// Footer shortcutsb.WriteString(homeDividerStyle.Render(strings.Repeat("─", m.width)))
b.WriteString("\n")
b.WriteString(homeKeyStyle.Render(" F"))
b.WriteString(homeItemStyle.Render(" File"))
b.WriteString(" ")
b.WriteString(homeKeyStyle.Render("Z"))
b.WriteString(homeItemStyle.Render(" Undo"))
b.WriteString(" ")
b.WriteString(homeKeyStyle.Render("Q"))
b.WriteString(homeItemStyle.Render(" Quit"))
b.WriteString("\n")
returntea.NewView(b.String())
}
Tweak Home Model Design Notes
The home screen is a single Bubble Tea model with no child widgets
(unlike highway which has track/status). Complexity is minimal.
Style definitions are package-level vars in tweak-home-model.go,
constructed once at package init. This follows a different pattern
from the nav views (which use Theme structs) because the tweak UI
styles are fixed and never user-configurable.
The huh library is imported and available for the exit flow
prompts (used by TweakCoordinator.ExitFlow), but not used in the
home screen model directly for the skeleton. In later issues, huh
forms replace manual string-building for wizards and input screens.
5.5 src/app/command/bootstrap.go Changes
Add to the Bootstrap struct:
typeBootstrapstruct {
// ... existing fields ...// tweak holds param-sets owned exclusively by the tweak command.tweaktweakState
}
Add to Root():
// In Root(), after b.buildQueryCommand(b.container):b.buildTweakCommand(b.container)
No other part of Root() changes. The tweak command shares the
persistent flags (--tui, --theme) inherited from root.
5.6 src/app/controller/tweak-coordinator_test.go
Describe("TweakCoordinator")
Describe("NewTweakCoordinator")
It("initialises all three layers to the provided palette")
It("starts with no dirty flags set")
Describe("Undo")
It("resets layer 2 to layer 1")
It("clears the creative dirty flag")
It("does not affect layer 0 or layer 1")
Describe("IsDirty")
It("returns false when no changes have been made")
It("returns true when creative changes exist")
Describe("ExitFlow - no changes")
It("returns true (exit silently) when IsDirty is false")
Describe("WorkingPalette")
It("returns layer 2")
5.7 src/app/controller/tweak-home-model_test.go
Describe("tweakHomeModel")
Describe("Init")
It("returns nil command")
Describe("Update - WindowSizeMsg")
It("stores width and height")
Describe("Update - KeyMsg")
It("handles 1/2/3/4 without error (entry points non-functional)")
It("handles Z/z by calling Undo on the coordinator")
It("sends tea.Quit on Q/q when ExitFlow returns true")
It("stays running on Q/q when ExitFlow returns false")
It("sends tea.Quit on ctrl+c")
Describe("View")
It("renders the title bar with theme name")
It("renders all four entry points")
It("renders keyboard shortcuts in the footer")
6. Testing Strategy
6.1 TweakCoordinator Tests
Unit tests only. No real filesystem, no agenor traversal, no Bubble
Tea program. The coordinator is a plain Go struct in these tests.
Use a contract.Palette fixture (e.g. contract.SystemPalette())
as the input.
Undo tests modify layer 2 directly (via a test helper or by
setting it) then verify reset.
ExitFlow tests verify the returned boolean only (prompt UI tested
separately in view model tests).
6.2 tweakHomeModel Tests
The model is tested directly via Ginkgo without running a tea.NewProgram. Tests construct the model, send tea.Msg values
through Update(), and inspect the returned state.
A spy coordinator is injected to verify Undo and ExitFlow calls.
View output is tested with Gomega matchers (ContainSubstring, HavePrefix, etc.).
huh form rendering: covered by exit flow integration tests in
later issues.
Signal handling / Ctrl-C: the caller owns the context; tested at
the command integration level.
7. Acceptance Criteria
jay tweak --help shows the command with correct description.
jay tweak launches the Bubble Tea TUI and shows the home screen.
Home screen displays all four entry points (non-functional).
Home screen displays the current theme name in the title bar.
Z/z triggers Undo (resets working state to upscaled state).
Q/q triggers exit flow:
No changes: exits silently.
Changes pending: shows prompt (deferred to later issue for actual huh form; skeleton returns true/false based on dirty flags).
Ctrl-C exits the TUI cleanly without saving.
F/f is a no-op (file menu deferred to later issues).
1/2/3/4 are no-ops (entry point screens deferred).
TweakCoordinator initialises all three layers to the provided
palette.
TweakCoordinator.Undo() resets layer 2 to layer 1 and clears
the creative dirty flag.
TweakCoordinator.IsDirty() reflects the correct state.
TweakCoordinator.ExitFlow() returns the correct boolean based
on dirty state.
All existing tests in src/app/command/ and src/app/controller/ continue to pass without modification.
go vet and linter report no issues.
go build ./... succeeds with the new huh dependency.
8. Implementation Order
go get github.com/charmbracelet/huh - add dependency, verify
compatibility with bubbletea/v2.
src/app/controller/tweak-coordinator.go - coordinator struct,
state model, dirty tracking, Undo, ExitFlow, auto-restart loop
skeleton (loop body is a no-op for now).
src/app/controller/tweak-home-model.go - home screen Bubble Tea
model with all four entry points visible.
src/app/command/bootstrap.go - add tweakState, wire command in Root().
src/app/controller/tweak-coordinator_test.go - coordinator unit
tests.
src/app/controller/tweak-home-model_test.go - home screen model
tests.
Verify: go build ./..., go vet ./..., full test suite.
Manual: jay tweak launches, home screen renders, keyboard
shortcuts work.
9. Files to Create
File
Description
src/app/command/tweak-cmd.go
Cobra adapter for jay tweak
src/app/controller/tweak-coordinator.go
TweakCoordinator with state model
src/app/controller/tweak-home-model.go
Bubble Tea model for home screen
src/app/controller/tweak-coordinator_test.go
Coordinator unit tests
src/app/controller/tweak-home-model_test.go
Home screen model tests
10. Files Modified
File
Change
Required?
go.mod, go.sum
Add huh dependency
Yes
src/app/command/bootstrap.go
Add tweakState, wire buildTweakCommand
Yes
11. Dependencies
New Library Dependencies
Library
Version
Purpose
github.com/charmbracelet/huh
Latest compatible
Forms for exit flow, wizards
Existing Library Dependencies (no change)
Library
Package
Used by
charm.land/bubbletea/v2
tea
TweakCoordinator, tweakHomeModel
charm.land/lipgloss/v2
lipgloss
tweakHomeModel styles
github.com/snivilised/jaywalk/src/agenor/core
core
PreviewSession, Session
github.com/snivilised/jaywalk/src/agenor/pref
pref
Options for preview traversal
github.com/snivilised/jaywalk/src/prism/contract
contract
Palette, Theme, ThemeUpdateMsg
github.com/snivilised/jaywalk/src/app/controller
local
PreviewSession
12. Key Integration Points
Component
Integrates With
Mechanism
tweak-cmd.go
TweakCoordinator
Creates coordinator, calls Run(ctx)
TweakCoordinator
PreviewSession
Uses NewPreviewSession for traversal
TweakCoordinator
tweakHomeModel
Owns model, creates tea.NewProgram
tweakHomeModel
TweakCoordinator
Calls Undo(), ExitFlow(), IsDirty()
TweakCoordinator
agenor traversal
runPreviewLoop -> Navigate(ctx)
TweakCoordinator
nav view models
Sends ThemeUpdateMsg via program.Send()
13. Open Questions (Resolved as Noted)
Question
Resolution
Where does huh form for exit flow live?
Inline in TweakCoordinator.ExitFlow() for now. Extracted when the prompt grows complex.
How does the home screen model get the preview nav view rendered alongside?
The skeleton does not embed a nav view. The auto-restart loop runs in a goroutine and sends messages to a nav view model if one is active. For the skeleton, the loop is a no-op body. Full preview integration is Issue #6.
What happens to the theme name on the title bar?
It comes from the palette name carried by the working state. For the skeleton, it is hardcoded to "starship" until the file management flow is implemented.
Does TweakCoordinator need its own test suite bootstrap file?
No. The existing controller-suite_test.go runs all *_test.go files in the package via RunSpecs.
Tweak Command and Coordinator Skeleton
Issue: #660 (scoped from #457 point .5)
Depends on: Issue #4 (PreviewSession and View Isolation Audit) - completed
Status: Planning
1. Design Principles
These principles guide every decision in this issue. When facing a design
fork, test both options against this list.
Bubble Tea patterns as highway and porthole views. Where the home
screen has a distinct purpose (menu-driven entry points), the
interaction style prefers charm
huhfor forms and wizards.Forms use
huh, the main loop usesbubbletea, and static stylinguses
lipglosswith predefined themes.embedded navigation preview, never to the tweak UI itself. The tweak
UI (home screen, menus, forms) uses predefined charm themes so it
remains legible regardless of the user's colour experiments.
created alongside existing ones.
editor, bindings, import) are visible on the home screen but are
non-functional. Their models and screens are implemented in later
issues.
2. Scope
Delivers the
tweakcommand wiring, coordinator skeleton with three-layerstate model, and the home screen TUI with entry points visible but not
functional.
This issue does not include:
ThemeUpdateMsgUpdatearms in view models (deferred to this issueas a stub - just the
casepresence)RegisterVisualisers()or any visualiser implementations (Issue feat: define initial structure (#2) #3)3. Deliverables
src/app/command/tweak-cmd.gojay tweaksrc/app/controller/tweak-coordinator.goTweakCoordinator- state model, dirty tracking, undo, exit flow, auto-restart loopsrc/app/controller/tweak-home-model.gogo.mod/go.sumgithub.com/charmbracelet/huhdependencysrc/app/command/bootstrap.gotweakcommand inRoot(), addtweakStatesrc/app/controller/tweak-coordinator_test.gosrc/app/controller/tweak-home-model_test.go4. Resolved Decisions
4.1 Package Layout
TweakCoordinatorlives insrc/app/controller/alongside theexisting
Coordinator. The two are independent structs with no sharedcode beyond common imports (
pref,core,contract,report).tweakHomeModel(the Bubble Tea model for the tweak home screen)lives in
src/app/controller/alongside the coordinator for thisskeleton. In later issues, sub-screens may move to dedicated packages
under
src/prism/tweak/.tweakHomeModel). TheTweakCoordinatorowns the model and thetea.Program.4.2 State Model Location
The three-layer state model (raw/upscaled/working) and dirty tracking
live inside
TweakCoordinator. They are not extracted into a separatepackage because:
4.3 Charm Dependency
huhis a new direct dependency. Use the latest stable versioncompatible with the existing
bubbletea/v2dependency (v2.0.7).4.4 Tweak UI Theme
The tweak home screen and all forms use a fixed internal style set
built with lipgloss at construction time, derived from a small
hardcoded palette of high-contrast, universally legible colours. These
styles are defined as private constants/helpers in
tweak-home-model.goand are never user-configurable.The navigation preview embedded alongside the home screen (or toggled)
uses the user's working-state palette via
contract.NewThemeandThemeUpdateMsg. This ensures the colour experiment boundary is clearand enforced by construction.
4.5 Auto-Restart Loop
The auto-restart loop lives in
TweakCoordinator, not in the model.The coordinator launches a goroutine that:
PreviewSession.Endlifecycle event, checks context cancellation.PreviewSessionandrepeats.
the goroutine exits and
MarkCompleteis never called.The model is not aware of the traversal lifecycle. It receives only
messages (node events, completion events) via
program.Send().4.6 Exit Flow Ownership
The exit flow (checking dirty flags, showing confirmation prompts)
lives in
TweakCoordinator, not the model. When the model receives aQuit keypress, it sends an internal
quitRequestedMsgto thecoordinator, which checks state and either:
huhform.4.7 No Signal Setup in TweakCoordinator
Ctrl-C is handled by the caller context, not by
signal.NotifyinsideTweakCoordinator. This matches thePreviewSessionpatternestablished in Issue #4. The caller (the
tweakcommand handler)creates a cancellable context; Ctrl-C cancels it, which terminates the
auto-restart loop and triggers a clean exit.
4.8 No ThemeUpdateMsg Arms Yet
The
ThemeUpdateMsgUpdatearms in highway and porthole models aredeferred to Issue #5 (this issue) but only as a presence check - the
caseis added but the body simply returns the model unchanged orlogs. Real hot-swap implementation is part of Issue #6 when the
gradient workshop produces palette changes.
5. Required Changes
5.1
go.mod- New DependencyPicked to be compatible with the existing
bubbletea/v2dependency.Run
go get github.com/charmbracelet/huh@latestand verify no versionconflicts.
5.2
src/app/command/tweak-cmd.goNote: i18n template data structs for locale strings are defined in
locale/and follow the conventions from.claude/skills/go-i18n/SKILL.md.5.3
src/app/controller/tweak-coordinator.goTweakCoordinator Design Notes
TweakCoordinatordoes not implementreport.Presenter. Theexisting
Presenterinterface is for production traversal events.The tweak coordinator uses
tea.Program.Send()directly.Layer 1 upscaling is a no-op until Issue chore(gh-actions): apply auto-check edits #1 implements
UpscalePalette. For the skeleton, all three layers hold the samepalette.
huhforms for consistent charm styling.the embedded nav view via
ThemeUpdateMsgon each palette change.5.4
src/app/controller/tweak-home-model.goTweak Home Model Design Notes
(unlike highway which has track/status). Complexity is minimal.
tweak-home-model.go,constructed once at package init. This follows a different pattern
from the nav views (which use
Themestructs) because the tweak UIstyles are fixed and never user-configurable.
huhlibrary is imported and available for the exit flowprompts (used by
TweakCoordinator.ExitFlow), but not used in thehome screen model directly for the skeleton. In later issues,
huhforms replace manual string-building for wizards and input screens.
5.5
src/app/command/bootstrap.goChangesAdd to the
Bootstrapstruct:Add to
Root():No other part of
Root()changes. The tweak command shares thepersistent flags (
--tui,--theme) inherited from root.5.6
src/app/controller/tweak-coordinator_test.go5.7
src/app/controller/tweak-home-model_test.go6. Testing Strategy
6.1 TweakCoordinator Tests
Unit tests only. No real filesystem, no agenor traversal, no Bubble
Tea program. The coordinator is a plain Go struct in these tests.
contract.Palettefixture (e.g.contract.SystemPalette())as the input.
Undotests modify layer 2 directly (via a test helper or bysetting it) then verify reset.
ExitFlowtests verify the returned boolean only (prompt UI testedseparately in view model tests).
6.2 tweakHomeModel Tests
The model is tested directly via Ginkgo without running a
tea.NewProgram. Tests construct the model, sendtea.Msgvaluesthrough
Update(), and inspect the returned state.UndoandExitFlowcalls.ContainSubstring,HavePrefix, etc.).6.3 What Is Not Tested Here
in Issue feat: add prototype tapable code (#4) #6 when real preview content is needed.
ThemeUpdateMsgdispatch: covered by Issue feat: add prototype tapable code (#4) #6.huhform rendering: covered by exit flow integration tests inlater issues.
the command integration level.
7. Acceptance Criteria
jay tweak --helpshows the command with correct description.jay tweaklaunches the Bubble Tea TUI and shows the home screen.Z/ztriggers Undo (resets working state to upscaled state).Q/qtriggers exit flow:huhform; skeleton returns true/false based on dirty flags).F/fis a no-op (file menu deferred to later issues).1/2/3/4are no-ops (entry point screens deferred).TweakCoordinatorinitialises all three layers to the providedpalette.
TweakCoordinator.Undo()resets layer 2 to layer 1 and clearsthe creative dirty flag.
TweakCoordinator.IsDirty()reflects the correct state.TweakCoordinator.ExitFlow()returns the correct boolean basedon dirty state.
src/app/command/andsrc/app/controller/continue to pass without modification.go vetand linter report no issues.go build ./...succeeds with the newhuhdependency.8. Implementation Order
go get github.com/charmbracelet/huh- add dependency, verifycompatibility with
bubbletea/v2.src/app/controller/tweak-coordinator.go- coordinator struct,state model, dirty tracking, Undo, ExitFlow, auto-restart loop
skeleton (loop body is a no-op for now).
src/app/controller/tweak-home-model.go- home screen Bubble Teamodel with all four entry points visible.
src/app/command/tweak-cmd.go- cobra adapter wiring.src/app/command/bootstrap.go- addtweakState, wire command inRoot().src/app/controller/tweak-coordinator_test.go- coordinator unittests.
src/app/controller/tweak-home-model_test.go- home screen modeltests.
go build ./...,go vet ./..., full test suite.jay tweaklaunches, home screen renders, keyboardshortcuts work.
9. Files to Create
src/app/command/tweak-cmd.gojay tweaksrc/app/controller/tweak-coordinator.goTweakCoordinatorwith state modelsrc/app/controller/tweak-home-model.gosrc/app/controller/tweak-coordinator_test.gosrc/app/controller/tweak-home-model_test.go10. Files Modified
go.mod,go.sumhuhdependencysrc/app/command/bootstrap.gotweakState, wirebuildTweakCommand11. Dependencies
New Library Dependencies
github.com/charmbracelet/huhExisting Library Dependencies (no change)
charm.land/bubbletea/v2teaTweakCoordinator,tweakHomeModelcharm.land/lipgloss/v2lipglosstweakHomeModelstylesgithub.com/snivilised/jaywalk/src/agenor/corecorePreviewSession,Sessiongithub.com/snivilised/jaywalk/src/agenor/prefprefgithub.com/snivilised/jaywalk/src/prism/contractcontractPalette,Theme,ThemeUpdateMsggithub.com/snivilised/jaywalk/src/app/controllerPreviewSession12. Key Integration Points
tweak-cmd.goTweakCoordinatorRun(ctx)TweakCoordinatorPreviewSessionNewPreviewSessionfor traversalTweakCoordinatortweakHomeModeltea.NewProgramtweakHomeModelTweakCoordinatorUndo(),ExitFlow(),IsDirty()TweakCoordinatorrunPreviewLoop->Navigate(ctx)TweakCoordinatorThemeUpdateMsgviaprogram.Send()13. Open Questions (Resolved as Noted)
huhform for exit flow live?TweakCoordinator.ExitFlow()for now. Extracted when the prompt grows complex."starship"until the file management flow is implemented.TweakCoordinatorneed its own test suite bootstrap file?controller-suite_test.goruns all*_test.gofiles in the package viaRunSpecs.