Skip to content

Commit 1abe368

Browse files
committed
refactor: simplify rho symbol rebrand runtime
1 parent dd2bed4 commit 1abe368

23 files changed

Lines changed: 410 additions & 266 deletions

‎cmd/chat.go‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
rhostorage "github.com/GrayCodeAI/rho/internal/storage"
4343
"github.com/GrayCodeAI/rho/internal/system/staleness"
4444
"github.com/GrayCodeAI/rho/internal/tool"
45+
"github.com/GrayCodeAI/rho/internal/tui"
4546
"github.com/GrayCodeAI/rho/internal/ui/icons"
4647
)
4748

@@ -330,6 +331,9 @@ func newChatModelWithRegistry(ref *progRef, systemPrompt string, settings rhocon
330331
Enabled: true,
331332
MaxAutoApprove: safety.AutonomySemi,
332333
ConfirmFn: func(req engine.ApprovalRequest) engine.ApprovalResponse {
334+
if sess.PermSvc() != nil && sess.PermSvc().RuntimeState().Autonomy == safety.AutonomyYOLO {
335+
return engine.ApprovalApprove
336+
}
333337
response := make(chan engine.ApprovalResponse, 1)
334338
ref.Send(approvalAskMsg{req: req, response: response})
335339
select {
@@ -528,11 +532,11 @@ func (m *chatModel) refreshInputPlaceholder() {
528532
}
529533
switch work {
530534
case engine.WorkModePlan:
531-
m.input.Placeholder = "Design architecture or draft plan... · / commands · ? help"
535+
m.input.Placeholder = "Design architecture or draft plan... · /select copy · ? help"
532536
case engine.WorkModeReview:
533-
m.input.Placeholder = "Audit diffs, security, or PRs... · / commands · ? help"
537+
m.input.Placeholder = "Audit diffs, security, or PRs... · /select copy · ? help"
534538
default:
535-
m.input.Placeholder = "Build, refactor, or run commands... · / commands · ? help"
539+
m.input.Placeholder = "Build, refactor, or run commands... · /select copy · ? help"
536540
}
537541
}
538542

@@ -726,16 +730,24 @@ func runChat() error {
726730

727731
finalModel, err := p.Run()
728732
writeTerminalMouse(disableMouseCSI)
733+
// Kitty graphics are not scoped to Bubble Tea's alternate screen. Remove
734+
// the optional welcome mascot before the farewell is written so shutdown
735+
// leaves only the final text message in the terminal.
736+
_ = tui.ClearGraphics(os.Stderr)
729737
if err != nil {
730738
return err
731739
}
732-
fm, ok := finalModel.(chatModel)
733-
if !ok {
734-
return fmt.Errorf("unexpected final model type: %T", finalModel)
740+
fm, err := finalChatModel(finalModel)
741+
if err != nil {
742+
return err
735743
}
736744
if fm.quitting {
737745
fm.saveSession()
738-
fmt.Print(formatQuitResumeMessage(fm.sessionID))
746+
// Bubble Tea restores the previous screen before returning. Clear that
747+
// frame and any terminal-persistent mascot so shutdown has one clean,
748+
// predictable final state.
749+
fmt.Print("\x1b[2J\x1b[H")
750+
fmt.Println("Thank you for using Rho!")
739751
return nil
740752
}
741753
rhoC := ansiOrange
@@ -790,3 +802,18 @@ func runChat() error {
790802
}
791803
return nil
792804
}
805+
806+
// finalChatModel normalizes Bubble Tea's shutdown model. Bubble Tea may return
807+
// either the value passed to NewProgram or its pointer after updates; both are
808+
// valid representations of the same chat state.
809+
func finalChatModel(model tea.Model) (chatModel, error) {
810+
switch m := model.(type) {
811+
case chatModel:
812+
return m, nil
813+
case *chatModel:
814+
if m != nil {
815+
return *m, nil
816+
}
817+
}
818+
return chatModel{}, fmt.Errorf("unexpected final model type: %T", model)
819+
}

‎cmd/chat_runtime_test.go‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package cmd
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
tea "charm.land/bubbletea/v2"
8+
)
9+
10+
type unexpectedFinalChatModel struct{}
11+
12+
func (unexpectedFinalChatModel) Init() tea.Cmd { return nil }
13+
14+
func (m unexpectedFinalChatModel) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil }
15+
16+
func (unexpectedFinalChatModel) View() tea.View { return tea.View{} }
17+
18+
func TestFinalChatModelAcceptsValueAndPointer(t *testing.T) {
19+
want := chatModel{sessionID: "session-1", quitting: true}
20+
21+
for name, input := range map[string]tea.Model{
22+
"value": want,
23+
"pointer": &want,
24+
} {
25+
t.Run(name, func(t *testing.T) {
26+
got, err := finalChatModel(input)
27+
if err != nil {
28+
t.Fatalf("finalChatModel() error = %v", err)
29+
}
30+
if got.sessionID != want.sessionID || got.quitting != want.quitting {
31+
t.Fatalf("finalChatModel() = %+v, want %+v", got, want)
32+
}
33+
})
34+
}
35+
}
36+
37+
func TestFinalChatModelRejectsUnexpectedModel(t *testing.T) {
38+
_, err := finalChatModel(unexpectedFinalChatModel{})
39+
if err == nil || !strings.Contains(err.Error(), "unexpected final model type") {
40+
t.Fatalf("finalChatModel() error = %v, want unexpected model error", err)
41+
}
42+
}

‎cmd/chat_scrollbar.go‎

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ func (m chatModel) chatViewportWidth(totalWidth int) int {
5050
if totalWidth <= 0 {
5151
return 0
5252
}
53-
if mainWidth := m.chatMainWidth(totalWidth); mainWidth != totalWidth {
54-
totalWidth = mainWidth
55-
}
5653
if m.chatHasOverflow() && totalWidth > scrollbarWidth {
5754
return totalWidth - scrollbarWidth
5855
}
@@ -171,13 +168,13 @@ func (m chatModel) renderChatPane() string {
171168

172169
if !m.chatScrollbarVisible() {
173170
chatView = padToHeight(chatView, origVpH)
174-
return m.joinChatSidebar(chatView, origVpH)
171+
return chatView
175172
}
176173

177174
scrollbar := m.renderScrollbarHeight(origVpH)
178175
if scrollbar == "" {
179176
chatView = padToHeight(chatView, origVpH)
180-
return m.joinChatSidebar(chatView, origVpH)
177+
return chatView
181178
}
182179

183180
targetW := m.viewport.Width()
@@ -205,33 +202,5 @@ func (m chatModel) renderChatPane() string {
205202
out.WriteByte('\n')
206203
}
207204
}
208-
return m.joinChatSidebar(out.String(), origVpH)
209-
}
210-
211-
func (m chatModel) joinChatSidebar(chatView string, height int) string {
212-
sidebarW := m.chatSidebarWidth(m.width)
213-
if sidebarW == 0 {
214-
return chatView
215-
}
216-
mainW := m.viewport.Width()
217-
if mainW <= 0 {
218-
mainW = m.chatMainWidth(m.width)
219-
}
220-
lines := strings.Split(padToHeight(chatView, height), "\n")
221-
sidebar := strings.Split(m.renderSessionSidebar(sidebarW, height), "\n")
222-
divider := scrollbarTrackStyle().Render("│")
223-
var out strings.Builder
224-
for i := 0; i < height; i++ {
225-
if i > 0 {
226-
out.WriteByte('\n')
227-
}
228-
line := lines[i]
229-
if w := visibleWidth(line); w < mainW {
230-
line += strings.Repeat(" ", mainW-w)
231-
}
232-
out.WriteString(line)
233-
out.WriteString(divider)
234-
out.WriteString(sidebar[i])
235-
}
236205
return out.String()
237206
}

‎cmd/chat_scrollbar_test.go‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,23 @@ func TestChatViewportWidth_NarrowTerminalClampsWithoutJump(t *testing.T) {
6060
}
6161
}
6262

63+
func TestRenderChatPaneUsesFullWidthWithoutSessionSidebar(t *testing.T) {
64+
m := chatModel{
65+
viewport: viewportWithSize(120, 4),
66+
contentLines: 2,
67+
width: 120,
68+
}
69+
m.viewport.SetContent("hello\nworld")
70+
71+
got := m.renderChatPane()
72+
if strings.Contains(got, "SESSION") || strings.Contains(got, "CONTEXT") {
73+
t.Fatalf("chat pane still renders the removed session sidebar: %q", got)
74+
}
75+
if !strings.Contains(got, "hello") || !strings.Contains(got, "world") {
76+
t.Fatalf("chat pane lost conversation content: %q", got)
77+
}
78+
}
79+
6380
func TestRenderScrollbar_TopAndBottom(t *testing.T) {
6481
m := chatModel{
6582
viewport: viewportWithSize(80, 10),

‎cmd/chat_sidebar.go‎

Lines changed: 0 additions & 161 deletions
This file was deleted.

‎cmd/chat_subcommand_simple.go‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,17 @@ func init() {
820820
},
821821
})
822822

823+
// /permission — focused control plane for access decisions and folder trust.
824+
subcommandRegistry.Register(&delegatingCommand{
825+
name: "permission",
826+
description: "show or change permissions and folder trust",
827+
usage: "/permission [status|trust|reset]",
828+
handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
829+
next, cmd := m.handlePolicyCommand(args)
830+
return &next, cmd
831+
},
832+
})
833+
823834
// /add <file...> — add file content to context
824835
subcommandRegistry.Register(&delegatingCommand{
825836
name: "add",

0 commit comments

Comments
 (0)