Skip to content

Commit 481e198

Browse files
cpcloudcursoragent
andcommitted
feat(ui): right-align mag notation to preserve original display width
Pad mag output with leading spaces so it matches the display width of the original value. Prevents columns from shifting when toggling mag mode on/off. Closes #192 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c83f6d0 commit 481e198

File tree

7 files changed

+118
-48
lines changed

7 files changed

+118
-48
lines changed

internal/app/chat.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -779,14 +779,16 @@ func (m *Model) handleSQLResult(msg sqlResultMsg) tea.Cmd {
779779

780780
// The SQL is already stored in the assistant message's SQL field.
781781
// Stage 2: summarize results via streaming LLM call.
782+
// Always send unformatted numbers to the LLM so the stored response
783+
// contains regular dollar amounts. Client-side magTransformText handles
784+
// mag notation at render time, making it toggleable.
782785
resultsTable := llm.FormatResultsTable(msg.Columns, msg.Rows)
783786
summaryPrompt := llm.BuildSummaryPrompt(
784787
msg.Question,
785788
msg.SQL,
786789
resultsTable,
787790
time.Now(),
788791
m.llmExtraContext,
789-
m.magMode,
790792
)
791793

792794
messages := []llm.Message{
@@ -1098,7 +1100,6 @@ func (m *Model) buildFallbackMessages(question string) []llm.Message {
10981100
dataDump,
10991101
time.Now(),
11001102
m.llmExtraContext,
1101-
m.magMode,
11021103
)
11031104

11041105
messages := []llm.Message{
@@ -1241,7 +1242,11 @@ func (m *Model) renderChatMessages() string {
12411242

12421243
// Show response if available.
12431244
if text != "" {
1244-
parts = append(parts, renderMarkdown(text, innerW-2))
1245+
display := text
1246+
if m.magMode {
1247+
display = magTransformText(display)
1248+
}
1249+
parts = append(parts, renderMarkdown(display, innerW-2))
12451250
}
12461251

12471252
// Determine what to show on the label line.

internal/app/chat_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ func TestLateChatChunkAfterCancellationIsDropped(t *testing.T) {
319319
"partial content should have been removed by cancellation")
320320
}
321321

322-
// TestChatMagModeToggle verifies that ctrl+m toggles the global mag mode
322+
// TestChatMagModeToggle verifies that ctrl+o toggles the global mag mode
323323
// even when the chat overlay is active.
324324
func TestChatMagModeToggle(t *testing.T) {
325325
m := newTestModel()
@@ -336,3 +336,44 @@ func TestChatMagModeToggle(t *testing.T) {
336336
sendKey(m, "ctrl+o")
337337
assert.False(t, m.magMode)
338338
}
339+
340+
// TestChatMagModeTogglesRenderedOutput verifies that toggling mag mode
341+
// updates dollar amounts in already-displayed LLM responses.
342+
func TestChatMagModeTogglesRenderedOutput(t *testing.T) {
343+
m := newTestModel()
344+
m.width = 120
345+
m.height = 40
346+
m.openChat()
347+
348+
// Simulate a completed assistant response with dollar amounts.
349+
m.chat.Messages = []chatMessage{
350+
{Role: roleUser, Content: "how much did I spend?"},
351+
{Role: roleAssistant, Content: "You spent $5,234.23 on kitchen renovations."},
352+
}
353+
m.refreshChatViewport()
354+
355+
// Mag mode off: original dollar amount should appear in the viewport.
356+
vpContent := m.chat.Viewport.View()
357+
assert.Contains(t, vpContent, "$5,234.23",
358+
"dollar amount should appear verbatim with mag mode off")
359+
assert.NotContains(t, vpContent, magArrow,
360+
"mag arrow should not appear with mag mode off")
361+
362+
// Toggle mag mode on from within chat.
363+
sendKey(m, "ctrl+o")
364+
assert.True(t, m.magMode)
365+
366+
vpContent = m.chat.Viewport.View()
367+
assert.NotContains(t, vpContent, "$5,234.23",
368+
"original dollar amount should be replaced with mag mode on")
369+
assert.Contains(t, vpContent, magArrow,
370+
"mag arrow should appear with mag mode on")
371+
372+
// Toggle back off.
373+
sendKey(m, "ctrl+o")
374+
assert.False(t, m.magMode)
375+
376+
vpContent = m.chat.Viewport.View()
377+
assert.Contains(t, vpContent, "$5,234.23",
378+
"dollar amount should reappear after toggling mag mode off")
379+
}

internal/app/mag.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package app
66
import (
77
"fmt"
88
"math"
9+
"regexp"
910
"strconv"
1011
"strings"
1112

@@ -63,7 +64,6 @@ func magFormat(c cell, includeUnit bool) string {
6364
if f == 0 {
6465
return fmt.Sprintf("%s%s%s0", sign, unit, magArrow)
6566
}
66-
6767
mag := int(math.Round(math.Log10(math.Abs(f))))
6868
return fmt.Sprintf("%s%s%s%d", sign, unit, magArrow, mag)
6969
}
@@ -82,6 +82,21 @@ func magOptionalCents(cents *int64) string {
8282
return magCents(*cents)
8383
}
8484

85+
// magMoneyRe matches dollar amounts like $1,234.56 or -$5.00 in prose.
86+
var magMoneyRe = regexp.MustCompile(`-?\$[\d,]+(?:\.\d+)?`)
87+
88+
// magTransformText replaces dollar amounts in free-form text with magnitude
89+
// notation. Used to post-process LLM responses when mag mode is on.
90+
// Does not pad (no width preservation needed in prose).
91+
func magTransformText(s string) string {
92+
return magMoneyRe.ReplaceAllStringFunc(s, func(match string) string {
93+
c := cell{Value: match, Kind: cellMoney}
94+
result := magFormat(c, true)
95+
// Strip leading padding -- prose doesn't need right-alignment.
96+
return strings.TrimLeft(result, " ")
97+
})
98+
}
99+
85100
// magTransformCells returns a copy of the cell grid with numeric values
86101
// replaced by their order-of-magnitude representation. Dollar prefixes are
87102
// stripped because the column header carries the unit annotation instead.

internal/app/mag_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
func TestMagFormatMoneyWithUnit(t *testing.T) {
1313
// Used by magCents for dashboard (input still has $ from FormatCents).
14+
// No internal padding; rendering layer handles alignment.
1415
tests := []struct {
1516
name string
1617
value string
@@ -32,8 +33,9 @@ func TestMagFormatMoneyWithUnit(t *testing.T) {
3233
}
3334

3435
func TestMagFormatBareMoney(t *testing.T) {
35-
// Table cells now carry $ from FormatCents. With includeUnit=false
36+
// Table cells carry $ from FormatCents. With includeUnit=false
3637
// the mag output strips the $ (header carries the unit instead).
38+
// No internal padding; table renderer handles alignment.
3739
tests := []struct {
3840
name string
3941
value string
@@ -149,6 +151,45 @@ func TestMagTransformCells(t *testing.T) {
149151
assert.Equal(t, "$5,234.23", rows[0][2].Value)
150152
}
151153

154+
func TestMagTransformText(t *testing.T) {
155+
tests := []struct {
156+
name string
157+
input string
158+
want string
159+
}{
160+
{
161+
"dollar amount",
162+
"You spent $5,234.23 on kitchen.",
163+
"You spent $ \U0001F8214 on kitchen.",
164+
},
165+
{
166+
"multiple amounts",
167+
"Budget is $10,000.00 and actual is $8,500.00.",
168+
"Budget is $ \U0001F8214 and actual is $ \U0001F8214.",
169+
},
170+
{
171+
"negative amount",
172+
"Loss of -$500.00 this month.",
173+
"Loss of -$ \U0001F8213 this month.",
174+
},
175+
{
176+
"no amounts",
177+
"The project is underway.",
178+
"The project is underway.",
179+
},
180+
{
181+
"small amount",
182+
"Just $5.00.",
183+
"Just $ \U0001F8211.",
184+
},
185+
}
186+
for _, tt := range tests {
187+
t.Run(tt.name, func(t *testing.T) {
188+
assert.Equal(t, tt.want, magTransformText(tt.input))
189+
})
190+
}
191+
}
192+
152193
func TestMagModeToggle(t *testing.T) {
153194
m := newTestModel()
154195
assert.False(t, m.magMode)

internal/app/model.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ func (m *Model) handleCommonKeys(key tea.KeyMsg) (tea.Cmd, bool) {
377377
return nil, true
378378
case "ctrl+o":
379379
m.magMode = !m.magMode
380+
if m.chat != nil && m.chat.Visible {
381+
m.refreshChatViewport()
382+
}
380383
return nil, true
381384
case "h", "left":
382385
if tab := m.effectiveTab(); tab != nil {

internal/llm/prompt.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ func BuildSummaryPrompt(
5757
question, sql, resultsTable string,
5858
now time.Time,
5959
extraContext string,
60-
magMode bool,
6160
) string {
6261
var b strings.Builder
6362
b.WriteString(summarySystemPreamble)
@@ -70,9 +69,6 @@ func BuildSummaryPrompt(
7069
b.WriteString(resultsTable)
7170
b.WriteString("\n```\n\n")
7271
b.WriteString(summaryGuidelines)
73-
if magMode {
74-
b.WriteString(magModeGuideline)
75-
}
7672
if extraContext != "" {
7773
b.WriteString("\n\n## Additional context\n\n")
7874
b.WriteString(extraContext)
@@ -88,7 +84,6 @@ func BuildSystemPrompt(
8884
dataSummary string,
8985
now time.Time,
9086
extraContext string,
91-
magMode bool,
9287
) string {
9388
var b strings.Builder
9489
b.WriteString(fallbackPreamble)
@@ -106,9 +101,6 @@ func BuildSystemPrompt(
106101
}
107102
b.WriteString("\n\n")
108103
b.WriteString(fallbackGuidelines)
109-
if magMode {
110-
b.WriteString(magModeGuideline)
111-
}
112104
if extraContext != "" {
113105
b.WriteString("\n\n## Additional context\n\n")
114106
b.WriteString(extraContext)
@@ -316,9 +308,6 @@ const summaryGuidelines = `RULES:
316308
5. Do NOT show raw SQL or table formatting. Speak naturally.
317309
6. Do NOT invent data that isn't in the results.`
318310

319-
const magModeGuideline = `
320-
7. MAGNITUDE MODE: Format ALL numeric values using order-of-magnitude notation with the 🠡 symbol. Examples: $🠡3 for thousands, $🠡6 for millions, 🠡2 for hundreds. Even single numbers must use this format. Calculate the magnitude as floor(log10(abs(value))). For zero, use 🠡0.`
321-
322311
// ---------- Fallback (single-stage) ----------
323312

324313
const fallbackPreamble = `You are micasa-assistant, a factual Q&A bot for a home management app. ` +

internal/llm/prompt_test.go

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ var testNow = time.Date(2026, 2, 13, 10, 0, 0, 0, time.UTC)
3434
// --- BuildSystemPrompt (fallback) ---
3535

3636
func TestBuildSystemPromptIncludesSchema(t *testing.T) {
37-
prompt := BuildSystemPrompt(testTables, "", testNow, "", false)
37+
prompt := BuildSystemPrompt(testTables, "", testNow, "")
3838
assert.Contains(t, prompt, "projects")
3939
assert.Contains(t, prompt, "id integer PK")
4040
assert.Contains(t, prompt, "title text NOT NULL")
@@ -48,39 +48,27 @@ func TestBuildSystemPromptIncludesData(t *testing.T) {
4848
"### projects (3 rows)\n\n- id: 1, title: Fix roof\n",
4949
testNow,
5050
"",
51-
false,
5251
)
5352
assert.Contains(t, prompt, "Fix roof")
5453
assert.Contains(t, prompt, "Current Data")
5554
}
5655

5756
func TestBuildSystemPromptOmitsDataWhenEmpty(t *testing.T) {
58-
prompt := BuildSystemPrompt(nil, "", testNow, "", false)
57+
prompt := BuildSystemPrompt(nil, "", testNow, "")
5958
assert.NotContains(t, prompt, "Current Data")
6059
}
6160

6261
func TestBuildSystemPromptIncludesCurrentDate(t *testing.T) {
63-
prompt := BuildSystemPrompt(nil, "", testNow, "", false)
62+
prompt := BuildSystemPrompt(nil, "", testNow, "")
6463
assert.Contains(t, prompt, "Friday, February 13, 2026")
6564
}
6665

6766
func TestBuildSystemPromptIncludesExtraContext(t *testing.T) {
68-
prompt := BuildSystemPrompt(nil, "", testNow, "House is a 1920s craftsman.", false)
67+
prompt := BuildSystemPrompt(nil, "", testNow, "House is a 1920s craftsman.")
6968
assert.Contains(t, prompt, "Additional context")
7069
assert.Contains(t, prompt, "1920s craftsman")
7170
}
7271

73-
func TestBuildSystemPromptIncludesMagGuideline(t *testing.T) {
74-
prompt := BuildSystemPrompt(nil, "", testNow, "", true)
75-
assert.Contains(t, prompt, "MAGNITUDE MODE")
76-
assert.Contains(t, prompt, "\U0001F821") // 🠡
77-
}
78-
79-
func TestBuildSystemPromptOmitsMagGuidelineWhenOff(t *testing.T) {
80-
prompt := BuildSystemPrompt(nil, "", testNow, "", false)
81-
assert.NotContains(t, prompt, "MAGNITUDE MODE")
82-
}
83-
8472
// --- BuildSQLPrompt ---
8573

8674
func TestBuildSQLPromptIncludesDDL(t *testing.T) {
@@ -125,7 +113,6 @@ func TestBuildSummaryPromptIncludesAllParts(t *testing.T) {
125113
"count\n3\n",
126114
testNow,
127115
"",
128-
false,
129116
)
130117
assert.Contains(t, prompt, "How many projects?")
131118
assert.Contains(t, prompt, "SELECT COUNT(*)")
@@ -134,27 +121,16 @@ func TestBuildSummaryPromptIncludesAllParts(t *testing.T) {
134121
}
135122

136123
func TestBuildSummaryPromptIncludesCurrentDate(t *testing.T) {
137-
prompt := BuildSummaryPrompt("test", "SELECT 1", "1\n", testNow, "", false)
124+
prompt := BuildSummaryPrompt("test", "SELECT 1", "1\n", testNow, "")
138125
assert.Contains(t, prompt, "Friday, February 13, 2026")
139126
}
140127

141128
func TestBuildSummaryPromptIncludesExtraContext(t *testing.T) {
142-
prompt := BuildSummaryPrompt("test", "SELECT 1", "1\n", testNow, "Currency is CAD.", false)
129+
prompt := BuildSummaryPrompt("test", "SELECT 1", "1\n", testNow, "Currency is CAD.")
143130
assert.Contains(t, prompt, "Additional context")
144131
assert.Contains(t, prompt, "Currency is CAD")
145132
}
146133

147-
func TestBuildSummaryPromptIncludesMagGuideline(t *testing.T) {
148-
prompt := BuildSummaryPrompt("test", "SELECT 1", "1\n", testNow, "", true)
149-
assert.Contains(t, prompt, "MAGNITUDE MODE")
150-
assert.Contains(t, prompt, "\U0001F821") // 🠡
151-
}
152-
153-
func TestBuildSummaryPromptOmitsMagGuidelineWhenOff(t *testing.T) {
154-
prompt := BuildSummaryPrompt("test", "SELECT 1", "1\n", testNow, "", false)
155-
assert.NotContains(t, prompt, "MAGNITUDE MODE")
156-
}
157-
158134
// --- FormatResultsTable ---
159135

160136
func TestFormatResultsTableWithRows(t *testing.T) {
@@ -214,7 +190,7 @@ func TestBuildSQLPromptIncludesEntityRelationships(t *testing.T) {
214190
}
215191

216192
func TestBuildSystemPromptIncludesEntityRelationships(t *testing.T) {
217-
prompt := BuildSystemPrompt(testTables, "", testNow, "", false)
193+
prompt := BuildSystemPrompt(testTables, "", testNow, "")
218194
assert.Contains(t, prompt, "## Entity Relationships")
219195
assert.Contains(t, prompt, "Foreign key relationships")
220196
assert.Contains(t, prompt, "projects.project_type_id")

0 commit comments

Comments
 (0)