@@ -21,8 +21,10 @@ import (
2121 "github.com/docker/cagent/pkg/paths"
2222 "github.com/docker/cagent/pkg/runtime"
2323 "github.com/docker/cagent/pkg/session"
24+ "github.com/docker/cagent/pkg/sessiontitle"
2425 "github.com/docker/cagent/pkg/teamloader"
2526 "github.com/docker/cagent/pkg/telemetry"
27+ "github.com/docker/cagent/pkg/tui/background"
2628 "github.com/docker/cagent/pkg/tui/styles"
2729)
2830
@@ -204,45 +206,38 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s
204206 out .Println ("Recording mode enabled, cassette: " + cassettePath )
205207 }
206208
207- var (
208- rt runtime.Runtime
209- sess * session.Session
210- cleanup func ()
211- )
209+ // Remote runtime
212210 if f .remoteAddress != "" {
213- rt , sess , err = f .createRemoteRuntimeAndSession (ctx , agentFileName )
214- if err != nil {
215- return err
216- }
217- cleanup = func () {} // Remote runtime doesn't need local cleanup
218- } else {
219- agentSource , err := config .Resolve (agentFileName , f .runConfig .EnvProvider ())
211+ rt , sess , err := f .createRemoteRuntimeAndSession (ctx , agentFileName )
220212 if err != nil {
221213 return err
222214 }
215+ return f .launchTUI (ctx , out , rt , sess , args , tui )
216+ }
223217
224- loadResult , err := f .loadAgentFrom (ctx , agentSource )
225- if err != nil {
226- return err
227- }
218+ // Local runtime
219+ agentSource , err := config .Resolve (agentFileName , f .runConfig .EnvProvider ())
220+ if err != nil {
221+ return err
222+ }
228223
229- rt , sess , err = f .createLocalRuntimeAndSession (ctx , loadResult )
230- if err != nil {
231- return err
232- }
224+ loadResult , err : = f .loadAgentFrom (ctx , agentSource )
225+ if err != nil {
226+ return err
227+ }
233228
234- // Setup cleanup for local runtime
235- cleanup = func () {
236- // Use a fresh context for cleanup since the original may be canceled
237- cleanupCtx := context .WithoutCancel (ctx )
238- if err := loadResult .Team .StopToolSets (cleanupCtx ); err != nil {
239- slog .Error ("Failed to stop tool sets" , "error" , err )
240- }
229+ rt , sess , err := f .createLocalRuntimeAndSession (ctx , loadResult )
230+ if err != nil {
231+ return err
232+ }
233+ cleanup := func () {
234+ cleanupCtx := context .WithoutCancel (ctx )
235+ if err := loadResult .Team .StopToolSets (cleanupCtx ); err != nil {
236+ slog .Error ("Failed to stop tool sets" , "error" , err )
241237 }
242238 }
243239 defer cleanup ()
244240
245- // Apply theme before TUI starts
246241 if tui {
247242 applyTheme ()
248243 }
@@ -256,7 +251,25 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s
256251 return f .handleExecMode (ctx , out , rt , sess , args )
257252 }
258253
259- return f .handleRunMode (ctx , rt , sess , args )
254+ opts , err := f .buildAppOpts (args )
255+ if err != nil {
256+ return err
257+ }
258+
259+ // Concurrent agents: use multi-session TUI
260+ if background .IsEnabled () {
261+ var sessStore session.Store
262+ switch typedRt := rt .(type ) {
263+ case * runtime.LocalRuntime :
264+ sessStore = typedRt .SessionStore ()
265+ case * runtime.PersistentRuntime :
266+ sessStore = typedRt .SessionStore ()
267+ }
268+
269+ return runMultiSessionTUI (ctx , rt , sess , f .createSessionSpawner (agentSource , sessStore ), cleanup , opts ... )
270+ }
271+
272+ return runTUI (ctx , rt , sess , opts ... )
260273}
261274
262275func (f * runExecFlags ) loadAgentFrom (ctx context.Context , agentSource config.Source ) (* teamloader.LoadResult , error ) {
@@ -456,12 +469,34 @@ func readInitialMessage(args []string) (*string, error) {
456469 return & args [1 ], nil
457470}
458471
459- func (f * runExecFlags ) handleRunMode (ctx context.Context , rt runtime.Runtime , sess * session.Session , args []string ) error {
460- firstMessage , err := readInitialMessage (args )
472+ func (f * runExecFlags ) launchTUI (ctx context.Context , out * cli.Printer , rt runtime.Runtime , sess * session.Session , args []string , tui bool ) error {
473+ if tui {
474+ applyTheme ()
475+ }
476+
477+ if f .dryRun {
478+ out .Println ("Dry run mode enabled. Agent initialized but will not execute." )
479+ return nil
480+ }
481+
482+ if ! tui {
483+ return f .handleExecMode (ctx , out , rt , sess , args )
484+ }
485+
486+ opts , err := f .buildAppOpts (args )
461487 if err != nil {
462488 return err
463489 }
464490
491+ return runTUI (ctx , rt , sess , opts ... )
492+ }
493+
494+ func (f * runExecFlags ) buildAppOpts (args []string ) ([]app.Opt , error ) {
495+ firstMessage , err := readInitialMessage (args )
496+ if err != nil {
497+ return nil , err
498+ }
499+
465500 var opts []app.Opt
466501 if firstMessage != nil {
467502 opts = append (opts , app .WithFirstMessage (* firstMessage ))
@@ -472,8 +507,74 @@ func (f *runExecFlags) handleRunMode(ctx context.Context, rt runtime.Runtime, se
472507 if f .exitAfterResponse {
473508 opts = append (opts , app .WithExitAfterFirstResponse ())
474509 }
510+ return opts , nil
511+ }
475512
476- return runTUI (ctx , rt , sess , opts ... )
513+ // createSessionSpawner creates a function that can spawn new sessions with different working directories.
514+ func (f * runExecFlags ) createSessionSpawner (agentSource config.Source , sessStore session.Store ) background.SessionSpawner {
515+ return func (spawnCtx context.Context , workingDir string ) (* app.App , * session.Session , func (), error ) {
516+ // Create a copy of the runtime config with the new working directory
517+ runConfigCopy := f .runConfig .Clone ()
518+ runConfigCopy .WorkingDir = workingDir
519+
520+ // Load team with the new working directory
521+ loadResult , err := teamloader .LoadWithConfig (spawnCtx , agentSource , runConfigCopy , teamloader .WithModelOverrides (f .modelOverrides ))
522+ if err != nil {
523+ return nil , nil , nil , err
524+ }
525+
526+ team := loadResult .Team
527+ agent , err := team .Agent (f .agentName )
528+ if err != nil {
529+ return nil , nil , nil , err
530+ }
531+
532+ // Create model switcher config
533+ modelSwitcherCfg := & runtime.ModelSwitcherConfig {
534+ Models : loadResult .Models ,
535+ Providers : loadResult .Providers ,
536+ ModelsGateway : runConfigCopy .ModelsGateway ,
537+ EnvProvider : runConfigCopy .EnvProvider (),
538+ AgentDefaultModels : loadResult .AgentDefaultModels ,
539+ }
540+
541+ // Create the local runtime
542+ localRt , err := runtime .New (team ,
543+ runtime .WithSessionStore (sessStore ),
544+ runtime .WithCurrentAgent (f .agentName ),
545+ runtime .WithTracer (otel .Tracer (AppName )),
546+ runtime .WithModelSwitcherConfig (modelSwitcherCfg ),
547+ )
548+ if err != nil {
549+ return nil , nil , nil , err
550+ }
551+
552+ // Create a new session
553+ newSess := session .New (
554+ session .WithMaxIterations (agent .MaxIterations ()),
555+ session .WithToolsApproved (f .autoApprove ),
556+ session .WithThinking (agent .ThinkingConfigured ()),
557+ session .WithWorkingDir (workingDir ),
558+ )
559+
560+ // Create cleanup function
561+ cleanup := func () {
562+ cleanupCtx := context .WithoutCancel (spawnCtx )
563+ _ = team .StopToolSets (cleanupCtx )
564+ }
565+
566+ // Create the app
567+ var appOpts []app.Opt
568+ if pr , ok := localRt .(* runtime.PersistentRuntime ); ok {
569+ if model := pr .CurrentAgent ().Model (); model != nil {
570+ appOpts = append (appOpts , app .WithTitleGenerator (sessiontitle .New (model )))
571+ }
572+ }
573+
574+ a := app .New (spawnCtx , localRt , newSess , appOpts ... )
575+
576+ return a , newSess , cleanup , nil
577+ }
477578}
478579
479580// applyTheme applies the theme from user config, or the built-in default.
0 commit comments