Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ func newChatModelWithRegistry(ref *progRef, systemPrompt string, settings hawkco
m.welcomeAgentsOK = quickSnapshot.agentsOK
m.welcomeCache = buildWelcomeMessageWithSnapshot(sess, sid, registry, saved, settings, 0, connectedMCPCount(registry), false, initWidth, initHeight, nil, quickSnapshot, false, "")
m.messages = append(m.messages, displayMsg{role: "welcome", content: m.welcomeCache})
// First-session control-plane tip (skip when resuming history).
if saved == nil {
m.messages = append(m.messages, displayMsg{role: "system", content: controlPlaneOnboardingHint(sess)})
}
startup.EndPhase("newChatModel:welcome")

// Wire permission system
Expand Down
12 changes: 9 additions & 3 deletions cmd/chat_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func ResetSlashCache() {
}

var allSlashCommands = []string{
"/add", "/add-dir", "/agents", "/agents-init", "/audit", "/autonomy", "/branch", "/branches", "/bughunter", "/clean", "/clear",
"/add", "/add-dir", "/agents", "/agents-init", "/audit", "/auto-commit", "/autonomy", "/branch", "/branch-agent", "/branches", "/bughunter", "/clean", "/clear",
"/check", "/color", "/commit", "/compact", "/compress", "/config", "/context", "/council", "/design",
"/copy", "/cost", "/cron", "/ctx", "/diff", "/doctor", "/drop", "/effort", "/env", "/exit", "/explain",
"/export", "/fast", "/feedback", "/files", "/focus", "/follow", "/fork", "/help", "/history", "/home", "/hooks", "/init",
Expand All @@ -79,8 +79,9 @@ var allSlashCommands = []string{
"/power", "/pr-comments", "/provider-status", "/quit", "/recipe", "/recover", "/reflect", "/refresh-model-catalog", "/release-notes",
"/image", "/reload-plugins", "/remote-env", "/rename", "/render", "/research", "/resume", "/retry", "/review", "/rewind",
"/run", "/btw", "/brainstorm", "/checkpoint", "/dream", "/away", "/investigate", "/search", "/security-review", "/session", "/share", "/skills", "/snapshot", "/soul", "/spec", "/stale", "/stats",
"/mouse", "/select", "/status", "/statusline", "/summary", "/tag", "/taste", "/tasks", "/test", "/theme", "/think", "/thinkback", "/thinkback-play", "/tokens", "/tools", "/ultrareview", "/undo", "/upgrade", "/usage",
"/mouse", "/select", "/start", "/status", "/statusline", "/summary", "/tag", "/taste", "/tasks", "/test", "/theme", "/think", "/thinkback", "/thinkback-play", "/tokens", "/tools", "/trust", "/ultrareview", "/undo", "/upgrade", "/usage",
"/version", "/vibe", "/vim", "/voice", "/welcome", "/ecosystem", "/path", "/yaad",
"/isolation",
"/scroll-speed", "/scroll-invert", "/scroll-mode", "/terminal-setup", "/pager-config", "/prompt-queue",
}

Expand Down Expand Up @@ -240,7 +241,12 @@ var slashDescriptions = map[string]string{
"/skills": "List skills or manage: search, install, trending, info, remove, update, feedback, publish, audit",
"/learn": "LLM-powered skill advisor (/learn deep for source analysis)",
"/stats": "Show analytics stats",
"/status": "Show session info",
"/status": "Show session info (mode, isolation, trust, cost)",
"/start": "Guided setup: trust, mode, branch, first tasks",
"/trust": "Folder trust status / add / remove",
"/isolation": "Isolation profile: dev|workspace|strict|container",
"/branch-agent": "Create hawk/agent-* branch if on main/master",
"/auto-commit": "Toggle git auto-commit after Write/Edit (on|off)",
"/summary": "Summarize the session",
"/tasks": "Show task list",
"/test": "Run tests, add failures to context",
Expand Down
53 changes: 53 additions & 0 deletions cmd/chat_subcommand_auto_commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package cmd

import (
"fmt"
"strings"

tea "charm.land/bubbletea/v2"
)

// autoCommitSubcommand toggles git auto-commit after Write/Edit.
type autoCommitSubcommand struct{}

func (c *autoCommitSubcommand) Name() string { return "auto-commit" }
func (c *autoCommitSubcommand) Aliases() []string { return []string{"autocommit"} }
func (c *autoCommitSubcommand) Description() string {
return "auto-commit after Write/Edit: on|off|status"
}
func (c *autoCommitSubcommand) Usage() string { return "/auto-commit [on|off|status]" }

func (c *autoCommitSubcommand) Handle(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
if m.session == nil {
m.messages = append(m.messages, displayMsg{role: "error", content: "No session"})
return m, nil
}
action := "status"
if len(args) > 0 {
action = strings.ToLower(args[0])
}
switch action {
case "status", "show", "":
state := "off"
if m.session.AutoCommit() {
state = "on"
}
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf(
"Auto-commit: %s\nWhen on, successful Write/Edit/StructuredEdit run `git add` + `git commit` for that file.\nTip: use /branch-agent first so commits stay off main.",
state,
)})
case "on", "enable", "true", "1":
m.session.SetAutoCommit(true)
m.messages = append(m.messages, displayMsg{role: "system", content: "Auto-commit → on (Write/Edit will commit)"})
case "off", "disable", "false", "0":
m.session.SetAutoCommit(false)
m.messages = append(m.messages, displayMsg{role: "system", content: "Auto-commit → off"})
default:
m.messages = append(m.messages, displayMsg{role: "error", content: "Usage: /auto-commit [on|off|status]"})
}
return m, nil
}

func init() {
subcommandRegistry.Register(&autoCommitSubcommand{})
}
49 changes: 49 additions & 0 deletions cmd/chat_subcommand_branch_agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"fmt"

tea "charm.land/bubbletea/v2"

"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/GrayCodeAI/hawk/internal/ui/icons"
)

// branchAgentSubcommand creates a hawk/agent-* branch from main/master.
type branchAgentSubcommand struct{}

func (c *branchAgentSubcommand) Name() string { return "branch-agent" }
func (c *branchAgentSubcommand) Aliases() []string { return []string{"agent-branch"} }
func (c *branchAgentSubcommand) Description() string {
return "create hawk/agent-* branch if on main/master"
}
func (c *branchAgentSubcommand) Usage() string { return "/branch-agent" }

func (c *branchAgentSubcommand) Handle(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
info := engine.InspectGitBranch("")
if !info.HasRepo {
m.messages = append(m.messages, displayMsg{role: "error", content: "Not a git repository."})
return m, nil
}
if !info.OnDefault {
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf(
"Already on `%s` (not a default branch). No change.", info.Branch,
)})
return m, nil
}
name, err := engine.EnsureAgentBranch("")
if err != nil {
m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()})
return m, nil
}
m.refreshStatusBarLeft(true)
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf(
"%s Checked out `%s` — agent edits stay off %s.\nTip: `/commit` when ready.",
icons.CheckBold(), name, info.Branch,
)})
return m, nil
}

func init() {
subcommandRegistry.Register(&branchAgentSubcommand{})
}
4 changes: 2 additions & 2 deletions cmd/chat_subcommand_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func helpCategory(cmdName string) string {
switch cmdName {
case "/help", "/model", "/config", "/quit", "/exit", "/clear", "/compact", "/undo", "/snapshot", "/recover", "/new", "/copy", "/welcome":
return "Core"
case "/review", "/commit", "/test", "/lint", "/diff", "/status", "/audit", "/security-review", "/check", "/bughunter", "/hunt", "/ultrareview":
case "/review", "/commit", "/test", "/lint", "/diff", "/status", "/audit", "/security-review", "/check", "/bughunter", "/hunt", "/ultrareview", "/start", "/branch-agent", "/auto-commit":
return "Workflow"
case "/agents", "/agents-init", "/mission", "/exec", "/research", "/loop", "/council", "/dream", "/investigate", "/vibe":
return "Agent"
Expand All @@ -25,7 +25,7 @@ func helpCategory(cmdName string) string {
return "Tools"
case "/doctor", "/cost", "/usage", "/metrics", "/stats", "/integrity", "/stale", "/tokens", "/provider-status":
return "Diagnostics"
case "/autonomy", "/spec", "/vim", "/theme", "/color", "/mouse", "/select", "/focus", "/follow", "/output-style", "/statusline", "/keybindings", "/voice", "/remote-env", "/refresh-model-catalog":
case "/autonomy", "/spec", "/vim", "/theme", "/color", "/mouse", "/select", "/focus", "/follow", "/output-style", "/statusline", "/keybindings", "/voice", "/remote-env", "/refresh-model-catalog", "/mode", "/isolation", "/trust":
return "Settings"
default:
return "Other"
Expand Down
61 changes: 61 additions & 0 deletions cmd/chat_subcommand_isolation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cmd

import (
"fmt"
"strings"

tea "charm.land/bubbletea/v2"

"github.com/GrayCodeAI/hawk/internal/engine"
)

// isolationSubcommand sets the unified IsolationProfile: OS sandbox + container story.
type isolationSubcommand struct{}

func (c *isolationSubcommand) Name() string { return "isolation" }
func (c *isolationSubcommand) Aliases() []string { return []string{"iso"} }
func (c *isolationSubcommand) Description() string {
return "isolation profile: dev|workspace|strict|container"
}

func (c *isolationSubcommand) Usage() string {
return "/isolation [dev|workspace|strict|container|os=workspace,container=1]"
}

func (c *isolationSubcommand) Handle(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
if m.session == nil {
m.messages = append(m.messages, displayMsg{role: "error", content: "No session"})
return m, nil
}
if len(args) == 0 {
p := m.session.Isolation()
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf(
"Isolation: %s\n OS sandbox: %s\n Container required: %v\nPresets: dev | workspace | strict | container",
p.String(), p.OSMode, p.ContainerRequired,
)})
return m, nil
}
raw := strings.Join(args, " ")
p, err := engine.ParseIsolationProfile(raw)
if err != nil {
m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()})
return m, nil
}
m.session.ApplyIsolationProfile(p)
detail := fmt.Sprintf("Isolation → %s\n OS sandbox: %s\n Container required: %v",
p.String(), p.OSMode, p.ContainerRequired)
switch {
case p.ContainerRequired:
detail += "\n Tools wait until Docker sandbox is ready."
case p.OSMode == "workspace" || p.OSMode == "strict":
detail += "\n Bash/PowerShell use OS wrap when a backend is available (seatbelt/unshare)."
default:
detail += "\n Host shell (no OS wrap). Prefer workspace for safer agent runs."
}
m.messages = append(m.messages, displayMsg{role: "system", content: detail})
return m, nil
}

func init() {
subcommandRegistry.Register(&isolationSubcommand{})
}
64 changes: 55 additions & 9 deletions cmd/chat_subcommand_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,84 @@ import (

tea "charm.land/bubbletea/v2"

"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/GrayCodeAI/hawk/internal/feature/shellmode"
)

// modeSubcommand implements the /mode slash command. It shows or
// changes the chat's mode (auto, shell, agent, or toggle).
// modeSubcommand implements the /mode slash command.
//
// Two layers:
// - Work modes (plan|act|review): tool visibility + read-only bash (control plane)
// - Shell modes (auto|shell|agent|toggle): input routing for the TUI
type modeSubcommand struct{}

func (mo *modeSubcommand) Name() string { return "mode" }
func (mo *modeSubcommand) Aliases() []string { return nil }
func (mo *modeSubcommand) Description() string {
return "show or change the chat mode (auto|shell|agent|toggle)"
return "work mode (plan|act|review) or shell mode (auto|shell|agent|toggle)"
}
func (mo *modeSubcommand) Usage() string { return "/mode [auto|shell|agent|toggle]" }

func (mo *modeSubcommand) Usage() string {
return "/mode [plan|act|review|auto|shell|agent|toggle]"
}

func (mo *modeSubcommand) Handle(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
if len(args) == 0 {
current := m.modeManager.Current()
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Mode: %s (auto | shell | agent)", current.String())})
shell := m.modeManager.Current().String()
work := engine.WorkModeAct
if m.session != nil {
work = m.session.WorkMode()
}
iso := "dev"
if m.session != nil {
iso = m.session.Isolation().String()
}
tr := engine.ProjectTrust("")
ac := "off"
if m.session != nil && m.session.AutoCommit() {
ac = "on"
}
visible := 0
if m.session != nil && m.session.Tools() != nil && m.session.Tools().Registry() != nil {
visible = len(m.session.Tools().Registry().EyrieTools())
}
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf(
"Work mode: %s (plan | act | review)\nShell mode: %s (auto | shell | agent)\nIsolation: %s\nTrust: %s\nAuto-commit: %s\nTools visible: %d\n\n/start · /isolation · /trust · /branch-agent · /auto-commit",
work, shell, iso, tr.String(), ac, visible,
)})
return m, nil
}
arg := strings.ToLower(args[0])

// Work modes first (product control plane).
if wm, err := engine.ParseWorkMode(arg); err == nil &&
(arg == "plan" || arg == "act" || arg == "review" ||
arg == "planning" || arg == "research" || arg == "build" ||
arg == "inspect" || arg == "readonly" || arg == "read-only" || arg == "ro") {
if m.session == nil {
m.messages = append(m.messages, displayMsg{role: "error", content: "No session for work mode"})
return m, nil
}
if err := m.session.SetWorkMode(wm); err != nil {
m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()})
return m, nil
}
m.messages = append(m.messages, displayMsg{role: "system", content: workModeSwitchSummary(m.session, wm)})
return m, nil
}

if arg == "toggle" {
newMode := m.modeManager.Toggle()
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Mode → %s", newMode.String())})
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Shell mode → %s", newMode.String())})
return m, nil
}
mode, ok := shellmode.ParseMode(arg)
if !ok {
m.messages = append(m.messages, displayMsg{role: "error", content: "Usage: /mode [auto|shell|agent|toggle]"})
m.messages = append(m.messages, displayMsg{role: "error", content: "Usage: /mode [plan|act|review|auto|shell|agent|toggle]"})
return m, nil
}
m.modeManager.Set(mode)
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Mode → %s", mode.String())})
m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Shell mode → %s", mode.String())})
return m, nil
}

Expand Down
Loading
Loading