Skip to content

Commit 87b5ec7

Browse files
idoubiclaude
andcommitted
feat: /copy /stats /retry commands, plan mode indicator, MCP status
- /copy: copy last response to system clipboard - /stats: detailed session statistics (messages, tools, cost) - /retry: re-send the last user message - Status bar: plan mode indicator, MCP connection count - Status bar: improved permission mode display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2003e9c commit 87b5ec7

3 files changed

Lines changed: 161 additions & 2 deletions

File tree

internal/slash/commands.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,133 @@ func (h *Handler) themeCmd(args []string) Result {
758758
}
759759
}
760760

761+
// ─── /copy ────────────────────────────────────────
762+
763+
func (h *Handler) copyCmd(args []string) Result {
764+
a := h.app.GetAgent()
765+
if a == nil {
766+
return Result{Message: "No conversation to copy from."}
767+
}
768+
769+
msgs := a.GetMessages()
770+
// Find last assistant message
771+
var lastText string
772+
for i := len(msgs) - 1; i >= 0; i-- {
773+
if msgs[i].Role == "assistant" {
774+
for _, block := range msgs[i].Content {
775+
if block.Text != "" {
776+
lastText = block.Text
777+
break
778+
}
779+
}
780+
if lastText != "" {
781+
break
782+
}
783+
}
784+
}
785+
786+
if lastText == "" {
787+
return Result{Message: "No assistant response to copy."}
788+
}
789+
790+
// Copy to clipboard
791+
var cmd *exec.Cmd
792+
switch runtime.GOOS {
793+
case "darwin":
794+
cmd = exec.Command("pbcopy")
795+
case "linux":
796+
cmd = exec.Command("xclip", "-selection", "clipboard")
797+
default:
798+
return Result{Message: "Clipboard not supported on this platform."}
799+
}
800+
801+
cmd.Stdin = strings.NewReader(lastText)
802+
if err := cmd.Run(); err != nil {
803+
return Result{Message: fmt.Sprintf("Failed to copy: %v", err)}
804+
}
805+
806+
preview := lastText
807+
if len(preview) > 80 {
808+
preview = preview[:80] + "..."
809+
}
810+
return Result{Message: fmt.Sprintf("✓ Copied to clipboard (%d chars)\n %s", len(lastText), preview)}
811+
}
812+
813+
// ─── /stats ───────────────────────────────────────
814+
815+
func (h *Handler) statsCmd(args []string) Result {
816+
a := h.app.GetAgent()
817+
if a == nil {
818+
return Result{Message: "No active session."}
819+
}
820+
821+
var b strings.Builder
822+
b.WriteString("Session statistics:\n\n")
823+
824+
msgs := a.GetMessages()
825+
userMsgs := 0
826+
assistantMsgs := 0
827+
toolCalls := 0
828+
toolTypes := make(map[string]int)
829+
830+
for _, msg := range msgs {
831+
switch msg.Role {
832+
case "user":
833+
userMsgs++
834+
case "assistant":
835+
assistantMsgs++
836+
for _, block := range msg.Content {
837+
if block.Type == "tool_use" {
838+
toolCalls++
839+
toolTypes[block.Name]++
840+
}
841+
}
842+
}
843+
}
844+
845+
b.WriteString(fmt.Sprintf(" Messages: %d user, %d assistant\n", userMsgs, assistantMsgs))
846+
b.WriteString(fmt.Sprintf(" Tool calls: %d total\n", toolCalls))
847+
848+
if len(toolTypes) > 0 {
849+
b.WriteString(" By tool:\n")
850+
for name, count := range toolTypes {
851+
b.WriteString(fmt.Sprintf(" %-12s %d\n", name, count))
852+
}
853+
}
854+
855+
b.WriteString(fmt.Sprintf("\n Cost: $%.4f\n", h.app.GetCost()))
856+
b.WriteString(fmt.Sprintf(" Tokens in: %d\n", h.app.GetTokensIn()))
857+
b.WriteString(fmt.Sprintf(" Tokens out: %d\n", h.app.GetTokensOut()))
858+
859+
return Result{Message: b.String()}
860+
}
861+
862+
// ─── /retry ───────────────────────────────────────
863+
864+
func (h *Handler) retryCmd(args []string) Result {
865+
a := h.app.GetAgent()
866+
if a == nil {
867+
return Result{Message: "No conversation."}
868+
}
869+
870+
msgs := a.GetMessages()
871+
// Find last user message
872+
for i := len(msgs) - 1; i >= 0; i-- {
873+
if msgs[i].Role == "user" {
874+
for _, block := range msgs[i].Content {
875+
if block.Text != "" {
876+
return Result{
877+
Message: "Retrying last message...",
878+
SkillPrompt: block.Text,
879+
}
880+
}
881+
}
882+
}
883+
}
884+
885+
return Result{Message: "No previous user message to retry."}
886+
}
887+
761888
// ─── helpers ──────────────────────────────────────
762889

763890
func min(a, b int) int {

internal/slash/slash.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ func AllCommands() []CommandDef {
8585
{Name: "/logout", Description: "Remove stored API key"},
8686
// Theme
8787
{Name: "/theme", Description: "Switch color theme", HasArgs: true},
88+
// Utilities
89+
{Name: "/copy", Description: "Copy last response to clipboard"},
90+
{Name: "/stats", Description: "Detailed session statistics"},
91+
{Name: "/retry", Description: "Retry last message"},
8892
}
8993
}
9094

@@ -198,6 +202,12 @@ func (h *Handler) Handle(input string) Result {
198202
return h.logoutCmd(args)
199203
case "/theme":
200204
return h.themeCmd(args)
205+
case "/copy":
206+
return h.copyCmd(args)
207+
case "/stats":
208+
return h.statsCmd(args)
209+
case "/retry":
210+
return h.retryCmd(args)
201211
default:
202212
// Try skill invocation
203213
if result, ok := h.HandleSkillInvocation(cmd, args); ok {

internal/tui/model.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,11 @@ func (m *Model) renderSlashSuggestions() string {
907907
func (m *Model) renderStatusBar() string {
908908
var leftParts []string
909909

910+
// Plan mode indicator
911+
if m.planMode {
912+
leftParts = append(leftParts, theme.SecondaryText.Render("📋 PLAN"))
913+
}
914+
910915
// Permission mode indicator
911916
mode := m.cfg.PermissionMode
912917
if mode == "" {
@@ -916,11 +921,11 @@ func (m *Model) renderStatusBar() string {
916921
case "bypassPermissions":
917922
leftParts = append(leftParts, theme.WarningText.Render("⚡ bypass"))
918923
case "acceptEdits":
919-
leftParts = append(leftParts, theme.SuccessText.Render("✎ acceptEdits"))
924+
leftParts = append(leftParts, theme.SuccessText.Render("✎ auto"))
920925
case "plan":
921926
leftParts = append(leftParts, theme.SecondaryText.Render("📋 plan"))
922927
default:
923-
leftParts = append(leftParts, theme.MutedStyle.Render("🔒 default"))
928+
leftParts = append(leftParts, theme.MutedStyle.Render("🔒"))
924929
}
925930

926931
// Cost
@@ -933,6 +938,23 @@ func (m *Model) renderStatusBar() string {
933938
leftParts = append(leftParts, theme.MutedStyle.Render(fmt.Sprintf("↑%d ↓%d", m.totalTokensIn, m.totalTokensOut)))
934939
}
935940

941+
// MCP connections
942+
if m.agent != nil {
943+
client := m.agent.MCPClient()
944+
if client != nil {
945+
conns := client.AllConnections()
946+
if len(conns) > 0 {
947+
connected := 0
948+
for _, c := range conns {
949+
if c.Status == "connected" {
950+
connected++
951+
}
952+
}
953+
leftParts = append(leftParts, theme.MutedStyle.Render(fmt.Sprintf("MCP %d/%d", connected, len(conns))))
954+
}
955+
}
956+
}
957+
936958
left := strings.Join(leftParts, theme.DimText.Render(" │ "))
937959

938960
// Right side: scroll position

0 commit comments

Comments
 (0)