Skip to content

Commit 1681bd8

Browse files
authored
Merge pull request #838 from benodiwal/fix/preload-sidebar-status
Fix/preload sidebar status
2 parents c3c4091 + fc50d65 commit 1681bd8

5 files changed

Lines changed: 149 additions & 0 deletions

File tree

pkg/app/app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ func (a *App) ResolveCommand(ctx context.Context, userInput string) string {
5252
return runtime.ResolveCommand(ctx, a.runtime, userInput)
5353
}
5454

55+
// EmitStartupInfo emits initial agent, team, and toolset information to the provided channel
56+
func (a *App) EmitStartupInfo(ctx context.Context, events chan runtime.Event) {
57+
a.runtime.EmitStartupInfo(ctx, events)
58+
}
59+
5560
// Run one agent loop
5661
func (a *App) Run(ctx context.Context, cancel context.CancelFunc, message string) {
5762
a.cancel = cancel

pkg/runtime/remote_runtime.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ func (r *RemoteRuntime) CurrentWelcomeMessage(ctx context.Context) string {
7878
return r.readCurrentAgentConfig(ctx).WelcomeMessage
7979
}
8080

81+
// EmitStartupInfo emits initial agent, team, and toolset information for immediate sidebar display
82+
func (r *RemoteRuntime) EmitStartupInfo(ctx context.Context, events chan Event) {
83+
agentConfig := r.readCurrentAgentConfig(ctx)
84+
85+
// Emit agent information for sidebar display
86+
modelID := agentConfig.Model // In v2 config, Model is already a string
87+
events <- AgentInfo(r.currentAgent, modelID, agentConfig.Description)
88+
89+
// Emit team information
90+
availableAgents := r.team.AgentNames()
91+
events <- TeamInfo(availableAgents, r.currentAgent)
92+
93+
// For remote runtime, we estimate toolset count from config
94+
toolsetCount := len(agentConfig.Toolsets)
95+
events <- ToolsetInfo(toolsetCount, r.currentAgent)
96+
}
97+
8198
func (r *RemoteRuntime) readCurrentAgentConfig(ctx context.Context) latest.AgentConfig {
8299
cfg, err := r.client.GetAgent(ctx, r.agentFilename)
83100
if err != nil {

pkg/runtime/runtime.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ type Runtime interface {
7070
CurrentAgentCommands(ctx context.Context) map[string]string
7171
// CurrentWelcomeMessage returns the welcome message for the active agent
7272
CurrentWelcomeMessage(ctx context.Context) string
73+
// EmitStartupInfo emits initial agent, team, and toolset information for immediate display
74+
EmitStartupInfo(ctx context.Context, events chan Event)
7375
// RunStream starts the agent's interaction loop and returns a channel of events
7476
RunStream(ctx context.Context, sess *session.Session) <-chan Event
7577
// Run starts the agent's interaction loop and returns the final messages
@@ -93,6 +95,7 @@ type LocalRuntime struct {
9395
modelsStore modelStore
9496
sessionCompaction bool
9597
managedOAuth bool
98+
startupInfoEmitted bool // Track if startup info has been emitted to avoid unnecessary duplication
9699
elicitationRequestCh chan ElicitationResult // Channel for receiving elicitation responses
97100
elicitationEventsChannel chan Event // Current events channel for sending elicitation requests
98101
elicitationEventsChannelMux sync.RWMutex // Protects elicitationEventsChannel
@@ -196,6 +199,43 @@ func (r *LocalRuntime) CurrentAgent() *agent.Agent {
196199
return current
197200
}
198201

202+
// EmitStartupInfo emits initial agent, team, and toolset information for immediate sidebar display
203+
func (r *LocalRuntime) EmitStartupInfo(ctx context.Context, events chan Event) {
204+
// Prevent duplicate emissions
205+
if r.startupInfoEmitted {
206+
return
207+
}
208+
209+
a := r.CurrentAgent()
210+
211+
// Emit agent information for sidebar display
212+
var modelID string
213+
if model := a.Model(); model != nil {
214+
modelID = model.ID()
215+
}
216+
events <- AgentInfo(a.Name(), modelID, a.Description())
217+
218+
// Emit team information
219+
availableAgents := r.team.AgentNames()
220+
events <- TeamInfo(availableAgents, r.currentAgent)
221+
222+
// Emit agent warnings (if any)
223+
r.emitAgentWarnings(a, events)
224+
225+
agentTools, err := a.Tools(ctx)
226+
if err != nil {
227+
slog.Warn("Failed to get agent tools during startup", "agent", a.Name(), "error", err)
228+
// Emit toolset info with 0 tools if we can't get them
229+
events <- ToolsetInfo(0, r.currentAgent)
230+
r.startupInfoEmitted = true
231+
return
232+
}
233+
234+
// Emit toolset information
235+
events <- ToolsetInfo(len(agentTools), r.currentAgent)
236+
r.startupInfoEmitted = true
237+
}
238+
199239
// registerDefaultTools registers the default tool handlers
200240
func (r *LocalRuntime) registerDefaultTools() {
201241
slog.Debug("Registering default tools")

pkg/runtime/runtime_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,3 +726,53 @@ func TestProcessToolCalls_UnknownTool_NoToolResultMessage(t *testing.T) {
726726
}
727727
require.False(t, sawToolMsg, "no tool result should be added for unknown tool; this reproduces invalid sequencing state")
728728
}
729+
730+
func TestEmitStartupInfo(t *testing.T) {
731+
// Create a simple agent with mock provider
732+
prov := &mockProvider{id: "test/startup-model", stream: &mockStream{}}
733+
root := agent.New("startup-test-agent", "You are a startup test agent",
734+
agent.WithModel(prov),
735+
agent.WithDescription("This is a startup test agent"))
736+
other := agent.New("other-agent", "You are another agent",
737+
agent.WithModel(prov),
738+
agent.WithDescription("This is another agent"))
739+
tm := team.New(team.WithAgents(root, other))
740+
741+
rt, err := New(tm, WithCurrentAgent("startup-test-agent"), WithModelStore(mockModelStore{}))
742+
require.NoError(t, err)
743+
744+
// Create a channel to collect events
745+
events := make(chan Event, 10)
746+
747+
// Call EmitStartupInfo
748+
rt.EmitStartupInfo(t.Context(), events)
749+
close(events)
750+
751+
// Collect events
752+
var collectedEvents []Event
753+
for event := range events {
754+
collectedEvents = append(collectedEvents, event)
755+
}
756+
757+
// Verify expected events are emitted
758+
expectedEvents := []Event{
759+
AgentInfo("startup-test-agent", "test/startup-model", "This is a startup test agent"),
760+
TeamInfo([]string{"startup-test-agent", "other-agent"}, "startup-test-agent"),
761+
ToolsetInfo(0, "startup-test-agent"), // No tools configured
762+
}
763+
764+
require.Equal(t, expectedEvents, collectedEvents)
765+
766+
// Test that calling EmitStartupInfo again doesn't emit duplicate events
767+
events2 := make(chan Event, 10)
768+
rt.EmitStartupInfo(t.Context(), events2)
769+
close(events2)
770+
771+
var collectedEvents2 []Event
772+
for event := range events2 {
773+
collectedEvents2 = append(collectedEvents2, event)
774+
}
775+
776+
// Should be empty due to deduplication
777+
require.Empty(t, collectedEvents2, "EmitStartupInfo should not emit duplicate events")
778+
}

pkg/tui/tui.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func (a *appModel) Init() tea.Cmd {
104104
cmds := []tea.Cmd{
105105
a.dialog.Init(),
106106
a.chatPage.Init(),
107+
a.emitStartupInfo(),
107108
}
108109

109110
if firstMessage := a.application.FirstMessage(); firstMessage != nil {
@@ -117,6 +118,31 @@ func (a *appModel) Init() tea.Cmd {
117118
return tea.Batch(cmds...)
118119
}
119120

121+
// emitStartupInfo creates a command that emits startup events for immediate sidebar display
122+
func (a *appModel) emitStartupInfo() tea.Cmd {
123+
return func() tea.Msg {
124+
// a buffered channel to collect startup events
125+
events := make(chan runtime.Event, 10)
126+
127+
go func() {
128+
defer close(events)
129+
a.application.EmitStartupInfo(context.Background(), events)
130+
}()
131+
132+
var collectedEvents []runtime.Event
133+
for event := range events {
134+
collectedEvents = append(collectedEvents, event)
135+
}
136+
137+
return StartupEventsMsg{Events: collectedEvents}
138+
}
139+
}
140+
141+
// StartupEventsMsg carries startup events to be processed by the UI
142+
type StartupEventsMsg struct {
143+
Events []runtime.Event
144+
}
145+
120146
// Help returns help information
121147
func (a *appModel) Help() help.KeyMap {
122148
return core.NewSimpleHelp(a.Bindings())
@@ -138,6 +164,17 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
138164
a.dialog = u.(dialog.Manager)
139165
return a, dialogCmd
140166

167+
case StartupEventsMsg:
168+
var cmds []tea.Cmd
169+
for _, event := range msg.Events {
170+
updated, cmd := a.chatPage.Update(event)
171+
a.chatPage = updated.(chat.Page)
172+
if cmd != nil {
173+
cmds = append(cmds, cmd)
174+
}
175+
}
176+
return a, tea.Batch(cmds...)
177+
141178
case tea.WindowSizeMsg:
142179
a.wWidth, a.wHeight = msg.Width, msg.Height
143180
cmd := a.handleWindowResize(msg.Width, msg.Height)

0 commit comments

Comments
 (0)