Skip to content

Commit 337389d

Browse files
idoubiclaude
andcommitted
feat: plan mode, subagents, teams, worktrees - 49 commands
Major features: - Plan mode: /plan toggles no-execution mode, permission callback respects it - Subagent support: SDK v0.3.0 auto-registers AgentTool with spawner - 3 built-in types: general-purpose, explore, plan - Child agents inherit parent's API config, permissions, cost tracking - Team system: /team create/add/delete/send/inbox - File-based mailbox at ~/.codeany/teams/<name>/inboxes/ - Async inter-agent messaging with read tracking - Worktree isolation: /worktree enter/exit - Creates git worktree at ~/.codeany/worktrees/ - Tracks original CWD, branch, session - --remove flag to cleanup on exit - 49 slash commands total, 5600+ lines Go Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent beef892 commit 337389d

File tree

6 files changed

+514
-4
lines changed

6 files changed

+514
-4
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/charmbracelet/bubbletea v1.3.5
88
github.com/charmbracelet/glamour v0.9.1
99
github.com/charmbracelet/lipgloss v1.1.0
10-
github.com/codeany-ai/open-agent-sdk-go v0.2.0
10+
github.com/codeany-ai/open-agent-sdk-go v0.3.0
1111
github.com/muesli/reflow v0.3.0
1212
github.com/spf13/cobra v1.9.1
1313
gopkg.in/yaml.v3 v3.0.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payR
3232
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
3333
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
3434
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
35-
github.com/codeany-ai/open-agent-sdk-go v0.2.0 h1:q/8dL16Iq116OmBSUwPOMZkgqhwD3dCRjf+dMmcEGQI=
36-
github.com/codeany-ai/open-agent-sdk-go v0.2.0/go.mod h1:j9P9/i2oWLD0iMB73vZ6IllaQ9FzgG0LAdPmnqb9VOw=
35+
github.com/codeany-ai/open-agent-sdk-go v0.3.0 h1:Levqy7k0fGFSDhNFUjJakWlqM28dfgq5Y/1YVzwodkk=
36+
github.com/codeany-ai/open-agent-sdk-go v0.3.0/go.mod h1:j9P9/i2oWLD0iMB73vZ6IllaQ9FzgG0LAdPmnqb9VOw=
3737
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
3838
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
3939
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=

internal/slash/commands.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"github.com/codeany-ai/codeany/internal/plugins"
1717
"github.com/codeany-ai/codeany/internal/session"
1818
"github.com/codeany-ai/codeany/internal/skills"
19+
"github.com/codeany-ai/codeany/internal/team"
20+
"github.com/codeany-ai/codeany/internal/worktree"
1921
"github.com/codeany-ai/open-agent-sdk-go/mcp"
2022
)
2123

@@ -1107,6 +1109,151 @@ func newRNG() *rand.Rand {
11071109
return rand.New(rand.NewSource(time.Now().UnixNano()))
11081110
}
11091111

1112+
// ─── /team ────────────────────────────────────────
1113+
1114+
func (h *Handler) teamCmd(args []string) Result {
1115+
configDir := config.GlobalConfigDir()
1116+
1117+
if len(args) == 0 {
1118+
teams := team.ListTeams(configDir)
1119+
return Result{Message: team.FormatTeamList(teams)}
1120+
}
1121+
1122+
switch args[0] {
1123+
case "create":
1124+
if len(args) < 2 {
1125+
return Result{Message: "Usage: /team create <name> [description]"}
1126+
}
1127+
name := args[1]
1128+
desc := strings.Join(args[2:], " ")
1129+
t, err := team.Create(configDir, name, desc)
1130+
if err != nil {
1131+
return Result{Message: fmt.Sprintf("Failed to create team: %v", err)}
1132+
}
1133+
return Result{Message: fmt.Sprintf("✓ Team %q created with lead agent\n Dir: %s", t.Name, filepath.Join(team.TeamsDir(configDir), name))}
1134+
1135+
case "add":
1136+
if len(args) < 3 {
1137+
return Result{Message: "Usage: /team add <team> <agent-name> [type]"}
1138+
}
1139+
t, err := team.Load(configDir, args[1])
1140+
if err != nil {
1141+
return Result{Message: fmt.Sprintf("Team %q not found.", args[1])}
1142+
}
1143+
agentType := "general-purpose"
1144+
if len(args) > 3 {
1145+
agentType = args[3]
1146+
}
1147+
t.AddMember(args[2], agentType, "")
1148+
return Result{Message: fmt.Sprintf("✓ Added %s to team %s", args[2], t.Name)}
1149+
1150+
case "delete", "remove":
1151+
if len(args) < 2 {
1152+
return Result{Message: "Usage: /team delete <name>"}
1153+
}
1154+
if err := team.Delete(configDir, args[1]); err != nil {
1155+
return Result{Message: fmt.Sprintf("Failed to delete team: %v", err)}
1156+
}
1157+
return Result{Message: fmt.Sprintf("✓ Team %q deleted", args[1])}
1158+
1159+
case "send":
1160+
if len(args) < 4 {
1161+
return Result{Message: "Usage: /team send <team> <agent> <message>"}
1162+
}
1163+
teamName := args[1]
1164+
agentName := args[2]
1165+
msg := strings.Join(args[3:], " ")
1166+
if err := team.SendMsg(configDir, teamName, "user", agentName, msg); err != nil {
1167+
return Result{Message: fmt.Sprintf("Failed to send: %v", err)}
1168+
}
1169+
return Result{Message: fmt.Sprintf("✓ Message sent to %s in team %s", agentName, teamName)}
1170+
1171+
case "inbox":
1172+
if len(args) < 3 {
1173+
return Result{Message: "Usage: /team inbox <team> <agent>"}
1174+
}
1175+
messages := team.ReadInbox(configDir, args[1], args[2])
1176+
if len(messages) == 0 {
1177+
return Result{Message: "No unread messages."}
1178+
}
1179+
var b strings.Builder
1180+
b.WriteString(fmt.Sprintf("Inbox for %s (%d messages):\n\n", args[2], len(messages)))
1181+
for _, m := range messages {
1182+
b.WriteString(fmt.Sprintf(" [%s] %s: %s\n", m.Timestamp.Format("15:04"), m.From, m.Text))
1183+
}
1184+
return Result{Message: b.String()}
1185+
1186+
default:
1187+
return Result{Message: "Usage: /team [create|add|delete|send|inbox]\n\n/team List teams\n/team create <n> Create team\n/team add <t> <a> Add agent\n/team delete <n> Delete team\n/team send <t> <a> Send message\n/team inbox <t> <a> Read inbox"}
1188+
}
1189+
}
1190+
1191+
// ─── /worktree ────────────────────────────────────
1192+
1193+
func (h *Handler) worktreeCmd(args []string) Result {
1194+
configDir := config.GlobalConfigDir()
1195+
1196+
if len(args) == 0 {
1197+
wts := worktree.ListAll(configDir)
1198+
if len(wts) == 0 {
1199+
return Result{Message: "No worktrees.\n\nCreate one with: /worktree enter <name>"}
1200+
}
1201+
var b strings.Builder
1202+
b.WriteString("Worktrees:\n\n")
1203+
for _, wt := range wts {
1204+
b.WriteString(fmt.Sprintf(" %s → %s (branch: %s)\n", wt.Name, wt.Path, wt.Branch))
1205+
}
1206+
return Result{Message: b.String()}
1207+
}
1208+
1209+
switch args[0] {
1210+
case "enter", "create":
1211+
name := "work"
1212+
if len(args) > 1 {
1213+
name = args[1]
1214+
}
1215+
a := h.app.GetAgent()
1216+
sessionID := "unknown"
1217+
if a != nil {
1218+
sessionID = a.SessionID()
1219+
}
1220+
wt, err := worktree.Create(configDir, name, sessionID)
1221+
if err != nil {
1222+
return Result{Message: fmt.Sprintf("Failed to create worktree: %v", err)}
1223+
}
1224+
if err := wt.Enter(); err != nil {
1225+
return Result{Message: fmt.Sprintf("Failed to enter worktree: %v", err)}
1226+
}
1227+
return Result{Message: fmt.Sprintf("✓ Entered worktree %q\n Branch: %s\n Path: %s\n\nUse /worktree exit to return.", name, wt.Branch, wt.Path)}
1228+
1229+
case "exit", "leave":
1230+
a := h.app.GetAgent()
1231+
sessionID := "unknown"
1232+
if a != nil {
1233+
sessionID = a.SessionID()
1234+
}
1235+
wt := worktree.LoadActive(configDir, sessionID)
1236+
if wt == nil {
1237+
return Result{Message: "Not in a worktree."}
1238+
}
1239+
remove := false
1240+
if len(args) > 1 && (args[1] == "--remove" || args[1] == "-r") {
1241+
remove = true
1242+
}
1243+
if err := wt.Exit(remove); err != nil {
1244+
return Result{Message: fmt.Sprintf("Failed to exit worktree: %v", err)}
1245+
}
1246+
msg := fmt.Sprintf("✓ Returned to %s", wt.OriginalCWD)
1247+
if remove {
1248+
msg += " (worktree removed)"
1249+
}
1250+
return Result{Message: msg}
1251+
1252+
default:
1253+
return Result{Message: "Usage: /worktree [enter|exit]\n\n/worktree List worktrees\n/worktree enter <n> Create & enter worktree\n/worktree exit [-r] Exit (--remove to delete)"}
1254+
}
1255+
}
1256+
11101257
// ─── helpers ──────────────────────────────────────
11111258

11121259
func min(a, b int) int {

internal/slash/slash.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ func AllCommands() []CommandDef {
107107
{Name: "/vim", Description: "Toggle vim mode"},
108108
{Name: "/feedback", Description: "Report bugs or feedback"},
109109
{Name: "/tips", Description: "Show a random tip"},
110+
// Multi-agent
111+
{Name: "/team", Description: "Team management (create, add, send, inbox)", HasArgs: true},
112+
{Name: "/worktree", Description: "Git worktree isolation (enter, exit)", HasArgs: true},
110113
}
111114
}
112115

@@ -250,6 +253,10 @@ func (h *Handler) Handle(input string) Result {
250253
return h.feedbackCmd(args)
251254
case "/tips":
252255
return h.tipsCmd(args)
256+
case "/team":
257+
return h.teamCmd(args)
258+
case "/worktree":
259+
return h.worktreeCmd(args)
253260
default:
254261
// Try skill invocation
255262
if result, ok := h.HandleSkillInvocation(cmd, args); ok {
@@ -304,9 +311,13 @@ func (h *Handler) help() Result {
304311
b.WriteString(" /copy Copy last response\n")
305312
b.WriteString(" /retry Retry last message\n")
306313

314+
b.WriteString("\nMulti-agent:\n")
315+
b.WriteString(" /team Team management (create, add, send, inbox)\n")
316+
b.WriteString(" /worktree Git worktree isolation (enter, exit)\n")
317+
307318
b.WriteString("\nConfig:\n")
308319
b.WriteString(" /config Show configuration\n")
309-
b.WriteString(" /permissions Permission mode\n")
320+
b.WriteString(" /permissions Permission mode (bypass/auto/default/plan)\n")
310321
b.WriteString(" /login <key> Set API key\n")
311322
b.WriteString(" /logout Remove API key\n")
312323
b.WriteString(" /doctor Environment check\n")

0 commit comments

Comments
 (0)