Skip to content

Commit f1da4bd

Browse files
committed
feat: full herm-style container mode with TUI status bar
- Container boots async on launch with --container flag - Status bar shows container lifecycle: checking → starting → <id> - "Docker is not running" error shown like herm when Docker unavailable - BashTool routes through ContainerExecutor when container is active - DevEnvTool lets agent read/write/build Dockerfiles for hot-swap - ContainerSandbox exposes ContainerID() for status display - No permission prompts in container mode (full isolation)
1 parent bf33cc5 commit f1da4bd

4 files changed

Lines changed: 74 additions & 0 deletions

File tree

‎cmd/chat_model.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
hawkconfig "github.com/GrayCodeAI/hawk/config"
1717
"github.com/GrayCodeAI/hawk/engine"
1818
"github.com/GrayCodeAI/hawk/plugin"
19+
"github.com/GrayCodeAI/hawk/sandbox"
1920
"github.com/GrayCodeAI/hawk/session"
2021
"github.com/GrayCodeAI/hawk/tool"
2122
)
@@ -138,6 +139,13 @@ type chatModel struct {
138139
welcomeCache string
139140
viewDirty bool
140141
activeSkills map[string]plugin.SmartSkill // per-session activated skills
142+
143+
// Container mode (herm-style hermetic execution)
144+
containerEnabled bool
145+
containerStatus string // "checking docker…", "pulling image…", "starting…", "<id>", "docker not running"
146+
containerReady bool
147+
containerErr error
148+
containerSandbox *sandbox.ContainerSandbox
141149
}
142150

143151
func blinkTickCmd() tea.Cmd {

‎cmd/chat_view.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ func (m chatModel) View() string {
349349
leftBold := permissionModeLabel(m.session)
350350
leftDim := permissionModeHint(m.session)
351351
rightStatus := fmt.Sprintf("%s %s", m.session.Provider(), m.session.Model())
352+
if m.containerEnabled && m.containerStatus != "" {
353+
rightStatus = fmt.Sprintf("container: %s %s", m.containerStatus, rightStatus)
354+
}
352355
leftVisLen := len(leftBold) + len(leftDim)
353356
gap := totalW - leftVisLen - len(rightStatus)
354357
if gap < 1 {

‎cmd/container_boot.go‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
tea "github.com/charmbracelet/bubbletea"
9+
10+
"github.com/GrayCodeAI/hawk/sandbox"
11+
)
12+
13+
// containerStatusMsg carries container lifecycle updates to the TUI.
14+
type containerStatusMsg struct {
15+
status string
16+
ready bool
17+
err error
18+
sandbox *sandbox.ContainerSandbox
19+
}
20+
21+
// bootContainerCmd starts the container in the background and sends status
22+
// updates to the TUI (herm-style async boot with progress feedback).
23+
func bootContainerCmd(projectDir string) tea.Cmd {
24+
return func() tea.Msg {
25+
cs := sandbox.NewContainerSandbox(projectDir)
26+
27+
if !sandbox.DockerAvailable() {
28+
return containerStatusMsg{
29+
status: "docker not running",
30+
err: fmt.Errorf("Docker is not running. Start Docker and try again."),
31+
}
32+
}
33+
34+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
35+
defer cancel()
36+
37+
if err := cs.Start(ctx); err != nil {
38+
return containerStatusMsg{
39+
status: "start failed",
40+
err: fmt.Errorf("container start failed: %w", err),
41+
}
42+
}
43+
44+
cid := cs.ContainerID()
45+
shortID := cid
46+
if len(shortID) > 12 {
47+
shortID = shortID[:12]
48+
}
49+
50+
return containerStatusMsg{
51+
status: shortID,
52+
ready: true,
53+
sandbox: cs,
54+
}
55+
}
56+
}

‎sandbox/container.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ func (c *ContainerSandbox) Running() bool {
118118
return c.running
119119
}
120120

121+
// ContainerID returns the full container ID (empty if not running).
122+
func (c *ContainerSandbox) ContainerID() string {
123+
c.mu.Lock()
124+
defer c.mu.Unlock()
125+
return c.containerID
126+
}
127+
121128
// BuildFromDockerfile builds a new image from a Dockerfile in the project.
122129
// Returns the image tag that can be used for subsequent Start calls.
123130
func (c *ContainerSandbox) BuildFromDockerfile(ctx context.Context, dockerfile string) (string, error) {

0 commit comments

Comments
 (0)