@@ -122,6 +122,22 @@ func registerUtilityActionCommands(r *Registry) {
122122 Category : "utility" ,
123123 Handler : cmdUndoFiles ,
124124 })
125+
126+ r .Register (Command {
127+ Name : "/scope" ,
128+ Aliases : []string {},
129+ Description : "focus agent on specific files/dirs: /scope path1 path2 ..." ,
130+ Category : "utility" ,
131+ Handler : cmdScope ,
132+ })
133+
134+ r .Register (Command {
135+ Name : "/perf" ,
136+ Aliases : []string {},
137+ Description : "show token usage breakdown per conversation turn" ,
138+ Category : "utility" ,
139+ Handler : cmdPerf ,
140+ })
125141}
126142
127143func cmdContext (ctx Context ) Result {
@@ -650,3 +666,138 @@ func cmdMap(ctx Context) Result {
650666
651667 return Result {Handled : true }
652668}
669+
670+ // cmdScope focuses the agent on a specific set of files or directories by
671+ // injecting a scoped context message and optionally prepending a system note.
672+ // Usage: /scope path1 path2 ...
673+ // With no args, shows the current scope or clears it.
674+ func cmdScope (ctx Context ) Result {
675+ if ctx .Agent == nil {
676+ PrintError ("no agent available" )
677+ return Result {Handled : true }
678+ }
679+
680+ if ! ctx .HasArg (1 ) {
681+ // Show current scope: look for a scope marker in recent messages.
682+ for i := len (ctx .Agent .Messages ) - 1 ; i >= 0 ; i -- {
683+ if strings .HasPrefix (ctx .Agent .Messages [i ].Content , "[Scope]" ) {
684+ snippet := ctx .Agent .Messages [i ].Content
685+ if len (snippet ) > 200 {
686+ snippet = snippet [:200 ] + "…"
687+ }
688+ fmt .Printf ("%s%s%s\n \n " , ColorDim , snippet , ColorReset )
689+ return Result {Handled : true }
690+ }
691+ }
692+ fmt .Printf ("%sNo scope set. Use /scope path1 path2 ... to focus the agent.%s\n \n " , ColorDim , ColorReset )
693+ return Result {Handled : true }
694+ }
695+
696+ paths := ctx .Parts [1 :]
697+ var verified []string
698+ for _ , p := range paths {
699+ abs := p
700+ if ! strings .HasPrefix (p , "/" ) {
701+ abs = filepath .Join (ctx .RepoPath , p )
702+ }
703+ if _ , err := os .Stat (abs ); err == nil {
704+ rel , _ := filepath .Rel (ctx .RepoPath , abs )
705+ verified = append (verified , rel )
706+ } else {
707+ fmt .Printf ("%s warning: %s not found%s\n " , ColorDim , p , ColorReset )
708+ }
709+ }
710+
711+ if len (verified ) == 0 {
712+ PrintError ("no valid paths found" )
713+ return Result {Handled : true }
714+ }
715+
716+ scopeMsg := fmt .Sprintf ("[Scope] Focus exclusively on the following paths for all changes and analysis:\n %s\n \n Only read, edit, or reference files within these paths unless explicitly asked otherwise." ,
717+ " - " + strings .Join (verified , "\n - " ))
718+
719+ ctx .Agent .Messages = append (ctx .Agent .Messages , iteragent .NewUserMessage (scopeMsg ))
720+ PrintSuccess ("scope set to: %s" , strings .Join (verified , ", " ))
721+ return Result {Handled : true }
722+ }
723+
724+ // cmdPerf shows a per-turn breakdown of estimated token usage in the conversation.
725+ // Since most providers return only final usage totals, this estimates per-message
726+ // cost from content length with a clear approximation disclaimer.
727+ func cmdPerf (ctx Context ) Result {
728+ if ctx .Agent == nil || len (ctx .Agent .Messages ) == 0 {
729+ fmt .Println ("No conversation to profile." )
730+ return Result {Handled : true }
731+ }
732+
733+ const charPerToken = 4
734+ type turnStat struct {
735+ idx int
736+ role string
737+ chars int
738+ tokens int
739+ }
740+
741+ var turns []turnStat
742+ totalChars := 0
743+ for i , m := range ctx .Agent .Messages {
744+ if m .Role == "system" {
745+ continue
746+ }
747+ chars := len (m .Content )
748+ totalChars += chars
749+ turns = append (turns , turnStat {
750+ idx : i ,
751+ role : m .Role ,
752+ chars : chars ,
753+ tokens : chars / charPerToken ,
754+ })
755+ }
756+
757+ if len (turns ) == 0 {
758+ fmt .Println ("No non-system messages." )
759+ return Result {Handled : true }
760+ }
761+
762+ maxTokens := 0
763+ for _ , t := range turns {
764+ if t .tokens > maxTokens {
765+ maxTokens = t .tokens
766+ }
767+ }
768+
769+ fmt .Printf ("%s── Token Profile (approx ~1 tok/4 chars) ─────────────%s\n " , ColorDim , ColorReset )
770+ for _ , t := range turns {
771+ roleColor := ColorDim
772+ roleLabel := "assistant"
773+ if t .role == "user" {
774+ roleColor = ColorCyan
775+ roleLabel = "user "
776+ }
777+
778+ barWidth := 20
779+ barFill := 0
780+ if maxTokens > 0 {
781+ barFill = t .tokens * barWidth / maxTokens
782+ }
783+ bar := strings .Repeat ("▪" , barFill ) + strings .Repeat ("·" , barWidth - barFill )
784+
785+ snippet := strings .ReplaceAll (strings .TrimSpace (ctx .Agent .Messages [t .idx ].Content ), "\n " , " " )
786+ if len (snippet ) > 40 {
787+ snippet = snippet [:40 ] + "…"
788+ }
789+
790+ fmt .Printf (" %s%-9s%s [%s%s%s] %s~%d tok%s %s%s%s\n " ,
791+ roleColor , roleLabel , ColorReset ,
792+ ColorBold , bar , ColorReset ,
793+ ColorDim , t .tokens , ColorReset ,
794+ ColorDim , snippet , ColorReset )
795+ }
796+
797+ totalApprox := totalChars / charPerToken
798+ fmt .Printf ("%s────────────────────────────────────────────────────%s\n " , ColorDim , ColorReset )
799+ fmt .Printf (" Total: ~%s (%d messages)\n \n " ,
800+ formatTokenCount (totalApprox ), len (turns ))
801+
802+ return Result {Handled : true }
803+ }
0 commit comments