Skip to content

Commit 145c239

Browse files
binarylogicclaude
andcommitted
feat: selectable log events in drawer detail views submit context-specific prompts
Selecting a log event (Enter) in any drawer detail view closes the drawer and submits a tab-specific prompt to the chat. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0b9890c commit 145c239

9 files changed

Lines changed: 129 additions & 17 deletions

File tree

‎internal/app/app.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
269269
m.statusBar.CloseDrawer()
270270
}
271271

272+
case appmsg.DrawerPrompt:
273+
// A drawer tab wants to submit a prompt to the chat.
274+
m.statusBar.CloseDrawer()
275+
return m, func() tea.Msg { return msgs.UserSubmittedInput{Text: msg.Text} }
276+
272277
case msgs.UserSubmittedInput:
273278
// Intercept "exit" and "quit" commands
274279
text := strings.TrimSpace(msg.Text)

‎internal/app/msgs/drawer.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package msgs
2+
3+
// DrawerPrompt is emitted by a statusbar drawer tab to submit a
4+
// context-specific prompt to the chat. The app model closes the
5+
// drawer and forwards the text as a UserSubmittedInput.
6+
type DrawerPrompt struct {
7+
Text string
8+
}

‎internal/app/statusbar/compliance/compliance.go‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,21 @@ func (m *Model) HandleKeyPress(msg tea.KeyPressMsg) tea.Cmd {
204204
return nil
205205
}
206206

207-
// Detail mode: backspace returns to category list (esc handled by statusbar).
207+
// Detail mode: navigate policies or go back.
208208
if m.detail != nil {
209-
if key.Matches(msg, keymap.DrawerBack) {
209+
switch {
210+
case key.Matches(msg, keymap.DrawerBack):
210211
m.detail = nil
212+
case key.Matches(msg, keymap.DrawerUp):
213+
if m.detail.cursor > 0 {
214+
m.detail.cursor--
215+
}
216+
case key.Matches(msg, keymap.DrawerDown):
217+
if m.detail.cursor < len(m.detail.policies)-1 {
218+
m.detail.cursor++
219+
}
220+
case key.Matches(msg, keymap.DrawerSelect):
221+
return m.detail.Prompt()
211222
}
212223
return nil
213224
}

‎internal/app/statusbar/compliance/detail.go‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"fmt"
55
"strings"
66

7+
tea "charm.land/bubbletea/v2"
78
"charm.land/lipgloss/v2"
89

10+
appmsg "github.com/usetero/cli/internal/app/msgs"
911
"github.com/usetero/cli/internal/domain"
1012
"github.com/usetero/cli/internal/format"
1113
"github.com/usetero/cli/internal/styles"
@@ -17,6 +19,7 @@ type detail struct {
1719
theme styles.Theme
1820
category domain.ComplianceCategorySummary
1921
policies []domain.CompliancePolicy
22+
cursor int
2023
}
2124

2225
// newDetail creates a detail view for the given category and pre-fetched policies.
@@ -28,6 +31,19 @@ func newDetail(theme styles.Theme, category domain.ComplianceCategorySummary, po
2831
}
2932
}
3033

34+
// Prompt returns a tea.Cmd that emits a DrawerPrompt for the selected policy.
35+
func (d *detail) Prompt() tea.Cmd {
36+
if len(d.policies) == 0 {
37+
return nil
38+
}
39+
p := d.policies[d.cursor]
40+
text := fmt.Sprintf(
41+
"Tell me about the %s compliance issue for the %q log event in the %s service.",
42+
displayCategoryName(d.category.Category), p.LogEventName, p.ServiceName,
43+
)
44+
return func() tea.Msg { return appmsg.DrawerPrompt{Text: text} }
45+
}
46+
3147
// View renders the detail: a header with category summary, then a policy table.
3248
func (d *detail) View(width int) string {
3349
var lines []string
@@ -79,16 +95,25 @@ func (d *detail) renderTable(width int) string {
7995
tbl.Headers("Log Event", "Service", "Volume", "Status")
8096
tbl.SetWidth(width)
8197

82-
for _, p := range d.policies {
98+
accent := lipgloss.NewStyle().Foreground(d.theme.Accent).Background(d.theme.Bg)
99+
100+
for i, p := range d.policies {
101+
name := p.LogEventName
102+
if i == d.cursor {
103+
name = accent.Render("▶ " + name)
104+
} else {
105+
name = d.observedDot(p.AnyObserved) + " " + name
106+
}
107+
83108
vol := "—"
84109
if p.VolumePerHour != nil {
85110
vol = format.Volume(*p.VolumePerHour) + " evt/hr"
86111
}
87112

88-
status := d.observedDot(p.AnyObserved) + " " + d.formatSensitiveTypes(p.Fields, 3)
113+
status := d.formatSensitiveTypes(p.Fields, 3)
89114

90115
tbl.Row(
91-
p.LogEventName,
116+
name,
92117
p.ServiceName,
93118
vol,
94119
status,

‎internal/app/statusbar/services/detail.go‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"fmt"
55
"strings"
66

7+
tea "charm.land/bubbletea/v2"
78
"charm.land/lipgloss/v2"
89

10+
appmsg "github.com/usetero/cli/internal/app/msgs"
911
"github.com/usetero/cli/internal/domain"
1012
"github.com/usetero/cli/internal/format"
1113
"github.com/usetero/cli/internal/styles"
@@ -18,6 +20,7 @@ type detail struct {
1820
theme styles.Theme
1921
service domain.ServiceStatus
2022
logEvents []domain.LogEventStatus
23+
cursor int
2124
}
2225

2326
// newDetail creates a detail view for the given service and pre-fetched log events.
@@ -29,6 +32,17 @@ func newDetail(theme styles.Theme, service domain.ServiceStatus, logEvents []dom
2932
}
3033
}
3134

35+
// Prompt returns a tea.Cmd that emits a DrawerPrompt for the selected log event.
36+
func (d *detail) Prompt() tea.Cmd {
37+
if len(d.logEvents) == 0 {
38+
return nil
39+
}
40+
le := d.logEvents[d.cursor]
41+
svc := d.service.Name
42+
text := fmt.Sprintf("Tell me about the %q log event in the %s service.", le.Name, svc)
43+
return func() tea.Msg { return appmsg.DrawerPrompt{Text: text} }
44+
}
45+
3246
// View renders the detail: a header with service summary, then a log event table.
3347
func (d *detail) View(width int) string {
3448
var lines []string
@@ -79,10 +93,13 @@ func (d *detail) renderTable(width int) string {
7993
tbl.SetWidth(width)
8094

8195
muted := lipgloss.NewStyle().Foreground(d.theme.TextMuted).Background(d.theme.Bg)
96+
accent := lipgloss.NewStyle().Foreground(d.theme.Accent).Background(d.theme.Bg)
8297

83-
for _, le := range d.logEvents {
98+
for i, le := range d.logEvents {
8499
name := le.Name
85-
if le.PendingPolicyCount > 0 {
100+
if i == d.cursor {
101+
name = accent.Render("▶ " + name)
102+
} else if le.PendingPolicyCount > 0 {
86103
warn := lipgloss.NewStyle().Foreground(d.theme.Warning).Background(d.theme.Bg)
87104
name = warn.Render("●") + " " + name
88105
} else {

‎internal/app/statusbar/services/services.go‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,21 @@ func (m *Model) HandleKeyPress(msg tea.KeyPressMsg) tea.Cmd {
176176
return nil
177177
}
178178

179-
// Detail mode: backspace returns to service list (esc handled by statusbar).
179+
// Detail mode: navigate log events or go back.
180180
if m.detail != nil {
181-
if key.Matches(msg, keymap.DrawerBack) {
181+
switch {
182+
case key.Matches(msg, keymap.DrawerBack):
182183
m.detail = nil
184+
case key.Matches(msg, keymap.DrawerUp):
185+
if m.detail.cursor > 0 {
186+
m.detail.cursor--
187+
}
188+
case key.Matches(msg, keymap.DrawerDown):
189+
if m.detail.cursor < len(m.detail.logEvents)-1 {
190+
m.detail.cursor++
191+
}
192+
case key.Matches(msg, keymap.DrawerSelect):
193+
return m.detail.Prompt()
183194
}
184195
return nil
185196
}

‎internal/app/statusbar/statusbar.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,19 @@ func (m *Model) HandleEsc() bool {
199199
func (m *Model) ShortHelp() []key.Binding {
200200
if m.activeTab == TabCompliance && m.complianceStatus.HasData() {
201201
if m.complianceStatus.InDetail() {
202-
return []key.Binding{keymap.DrawerBack, keymap.NextTab}
202+
return []key.Binding{keymap.DrawerUp, keymap.DrawerDown, keymap.DrawerSelect, keymap.DrawerBack, keymap.NextTab}
203203
}
204204
return []key.Binding{keymap.DrawerUp, keymap.DrawerDown, keymap.DrawerSelect, keymap.NextTab, keymap.CloseDrawer}
205205
}
206206
if m.activeTab == TabWaste && m.wasteStatus.HasData() {
207207
if m.wasteStatus.InDetail() {
208-
return []key.Binding{keymap.DrawerBack, keymap.NextTab}
208+
return []key.Binding{keymap.DrawerUp, keymap.DrawerDown, keymap.DrawerSelect, keymap.DrawerBack, keymap.NextTab}
209209
}
210210
return []key.Binding{keymap.DrawerUp, keymap.DrawerDown, keymap.DrawerSelect, keymap.NextTab, keymap.CloseDrawer}
211211
}
212212
if m.activeTab == TabServices && m.servicesStatus.HasData() {
213213
if m.servicesStatus.InDetail() {
214-
return []key.Binding{keymap.DrawerBack, keymap.NextTab}
214+
return []key.Binding{keymap.DrawerUp, keymap.DrawerDown, keymap.DrawerSelect, keymap.DrawerBack, keymap.NextTab}
215215
}
216216
return []key.Binding{keymap.DrawerUp, keymap.DrawerDown, keymap.DrawerSelect, keymap.NextTab, keymap.CloseDrawer}
217217
}

‎internal/app/statusbar/waste/detail.go‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"fmt"
55
"strings"
66

7+
tea "charm.land/bubbletea/v2"
78
"charm.land/lipgloss/v2"
89

10+
appmsg "github.com/usetero/cli/internal/app/msgs"
911
"github.com/usetero/cli/internal/domain"
1012
"github.com/usetero/cli/internal/format"
1113
"github.com/usetero/cli/internal/styles"
@@ -17,6 +19,7 @@ type detail struct {
1719
theme styles.Theme
1820
category domain.PolicyCategoryStatus
1921
policies []domain.WastePolicy
22+
cursor int
2023
}
2124

2225
// newDetail creates a detail view for the given category and pre-fetched policies.
@@ -28,6 +31,19 @@ func newDetail(theme styles.Theme, category domain.PolicyCategoryStatus, policie
2831
}
2932
}
3033

34+
// Prompt returns a tea.Cmd that emits a DrawerPrompt for the selected policy.
35+
func (d *detail) Prompt() tea.Cmd {
36+
if len(d.policies) == 0 {
37+
return nil
38+
}
39+
p := d.policies[d.cursor]
40+
text := fmt.Sprintf(
41+
"Pull up the %q policy for the %q log event in the %s service.",
42+
d.category.DisplayName(), p.LogEventName, p.ServiceName,
43+
)
44+
return func() tea.Msg { return appmsg.DrawerPrompt{Text: text} }
45+
}
46+
3147
// View renders the detail: a header with category summary, then a policy table.
3248
func (d *detail) View(width int) string {
3349
var lines []string
@@ -81,9 +97,17 @@ func (d *detail) renderTable(width int) string {
8197
}
8298
tbl.SetWidth(width)
8399

100+
accent := lipgloss.NewStyle().Foreground(d.theme.Accent).Background(d.theme.Bg)
84101
dot := lipgloss.NewStyle().Foreground(d.theme.Warning).Background(d.theme.Bg).Render("●")
85102

86-
for _, p := range d.policies {
103+
for i, p := range d.policies {
104+
name := p.LogEventName
105+
if i == d.cursor {
106+
name = accent.Render("▶ " + name)
107+
} else {
108+
name = dot + " " + name
109+
}
110+
87111
bytes := "—"
88112
if p.BytesPerHour != nil {
89113
bytes = format.Bytes(*p.BytesPerHour) + "/hr"
@@ -95,9 +119,9 @@ func (d *detail) renderTable(width int) string {
95119
if p.VolumePerHour != nil {
96120
vol = format.Volume(*p.VolumePerHour) + " evt/hr"
97121
}
98-
tbl.Row(dot+" "+p.LogEventName, p.ServiceName, vol, bytes, savings)
122+
tbl.Row(name, p.ServiceName, vol, bytes, savings)
99123
} else {
100-
tbl.Row(dot+" "+p.LogEventName, p.ServiceName, bytes, savings)
124+
tbl.Row(name, p.ServiceName, bytes, savings)
101125
}
102126
}
103127

‎internal/app/statusbar/waste/waste.go‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,21 @@ func (m *Model) HandleKeyPress(msg tea.KeyPressMsg) tea.Cmd {
195195
return nil
196196
}
197197

198-
// Detail mode: backspace returns to category list (esc handled by statusbar).
198+
// Detail mode: navigate policies or go back.
199199
if m.detail != nil {
200-
if key.Matches(msg, keymap.DrawerBack) {
200+
switch {
201+
case key.Matches(msg, keymap.DrawerBack):
201202
m.detail = nil
203+
case key.Matches(msg, keymap.DrawerUp):
204+
if m.detail.cursor > 0 {
205+
m.detail.cursor--
206+
}
207+
case key.Matches(msg, keymap.DrawerDown):
208+
if m.detail.cursor < len(m.detail.policies)-1 {
209+
m.detail.cursor++
210+
}
211+
case key.Matches(msg, keymap.DrawerSelect):
212+
return m.detail.Prompt()
202213
}
203214
return nil
204215
}

0 commit comments

Comments
 (0)