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
19 changes: 5 additions & 14 deletions cmd/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import (
// Tool-registry construction (essential/optional tools) is in chat_tools.go
// The Bubble Tea event loop (Update, applyPromptArrowKey) is in chat_update.go

const workInputPlaceholder = `Ask Hawk to inspect, edit, or run something... (Shift+Enter for newline)`

func genID() string {
b := make([]byte, 8)
_, _ = cryptorand.Read(b)
Expand Down Expand Up @@ -181,7 +183,6 @@ func newChatModel(ref *progRef, systemPrompt string, settings hawkconfig.Setting
m.connStatusVal = m.buildConnectionStatusPlain()
m.connStatusKey = m.connStatusFingerprint()
}
m.phase = initialUIPhase(m.hasChatMessages(), promptFlag != "")
m.invalidateInputLayoutCache()
(&m).refreshInputLayoutIfNeeded()
m = m.syncViewportMouseWheel().withSyncedLayout()
Expand Down Expand Up @@ -298,13 +299,8 @@ func newChatModel(ref *progRef, systemPrompt string, settings hawkconfig.Setting
ok := sandbox.DockerAvailable()
dockerRunning = &ok
}
m.welcomeCache = buildWelcomeMessage(sess, sid, registry, saved, settings, false, initWidth, dockerRunning, m.phase == phaseWelcomeGate)
// Welcome scrollback only when skipping the gate (resume / -p). Gate users already saw the splash.
if m.phase == phaseWork {
m.messages = append(m.messages, displayMsg{role: "welcome", content: m.welcomeCache})
}
m.openConfigOnStart = hawkconfig.NeedsFirstRunSetup(context.Background()) &&
(saved == nil || len(saved.Messages) == 0)
m.welcomeCache = buildWelcomeMessage(sess, sid, registry, saved, settings, false, initWidth, initHeight, dockerRunning)
m.messages = append(m.messages, displayMsg{role: "welcome", content: m.welcomeCache})

// Wire permission system
sess.PermSvc().SetPermissionFn(func(req engine.PermissionRequest) {
Expand Down Expand Up @@ -419,12 +415,7 @@ func (m chatModel) Init() tea.Cmd {
cwd, _ := os.Getwd()
cmds = append(cmds, bootContainerCmd(cwd))
}
if m.phase == phaseWork {
cmds = append(cmds, m.input.Focus())
}
if m.phase == phaseWork && m.openConfigOnStart {
cmds = append(cmds, func() tea.Msg { return autoOpenConfigMsg{} })
}
cmds = append(cmds, m.input.Focus())
return tea.Batch(cmds...)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/chat_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ var slashDescriptions = map[string]string{
"/explain": "Trace code back to the commit that created it",
"/export": "Export session",
"/follow": "Toggle stream follow (auto-scroll)",
"/home": "Scroll to welcome screen",
"/home": "Jump to top of chat and welcome header",
"/feedback": "Submit feedback about hawk",
"/fast": "Toggle fast mode",
"/files": "Show modified files",
Expand Down Expand Up @@ -188,7 +188,7 @@ var slashDescriptions = map[string]string{
"/usage": "Show cost summary",
"/version": "Show hawk version",
"/vim": "Toggle vim mode",
"/welcome": "Show welcome screen",
"/welcome": "Re-print the welcome header",
"/ecosystem": "Show eyrie, yaad, and tok integration status",
"/path": "Developer path readiness (setup, security, sandbox)",
"/yaad": "Show yaad memory (use /yaad search <query> to search)",
Expand Down
2 changes: 0 additions & 2 deletions cmd/chat_config_hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import (
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
)

type autoOpenConfigMsg struct{}

func (m chatModel) openConfigPanel() (chatModel, tea.Cmd) {
return m.openConfigAtTab(-1)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/chat_focus.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func formatSessionContextUsage(m *chatModel) string {
} else {
b.WriteString("Stream follow: off (/follow on)\n")
}
b.WriteString("Tab: prompt ↔ scrollback · /home: welcome · /export: save transcript")
b.WriteString("Tab: prompt ↔ scrollback · /home: top of chat · /export: save transcript")
if pct >= engine.DefaultAutoCompactThresholdPct {
b.WriteString("\n" + icons.Alert() + " Approaching auto-compact threshold — consider /compact")
}
Expand Down
5 changes: 1 addition & 4 deletions cmd/chat_layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const minChatViewportLines = 4

// fixedWelcomeLineCount reserves room for the branded welcome pane above chat.
func (m chatModel) fixedWelcomeLineCount() int {
if strings.TrimSpace(m.welcomeCache) == "" || m.onWelcomeGate() {
if strings.TrimSpace(m.welcomeCache) == "" {
return 0
}
lines := strings.Split(strings.TrimRight(m.welcomeCache, "\n"), "\n")
Expand Down Expand Up @@ -48,9 +48,6 @@ func (m chatModel) withSyncedLayout() chatModel {
welcomeH := m.fixedWelcomeLineCount()
// View() draws welcome text then a newline; the next row is the first chat line.
vpH := m.height - bottomH - welcomeH
if m.onWelcomeGate() {
vpH = minChatViewportLines
}
if vpH < minChatViewportLines {
vpH = minChatViewportLines
}
Expand Down
2 changes: 0 additions & 2 deletions cmd/chat_layout_mouse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ func TestView_LineCountMatchesHeight(t *testing.T) {
input: textarea.New(),
viewport: viewport.New(80, 8),
ghostText: NewGhostText(),
phase: phaseWork,
}
m = m.withSyncedLayout()
got := m.View()
Expand Down Expand Up @@ -57,7 +56,6 @@ func TestMouseWheelDelta_SGRUsesZeroBasedY(t *testing.T) {
height: 24,
width: 80,
uiFocus: focusPrompt,
phase: phaseWork,
}
m = m.withSyncedLayout()
before := m.viewport.YOffset
Expand Down
4 changes: 0 additions & 4 deletions cmd/chat_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,6 @@ type chatModel struct {
sessionStartedAt time.Time // whole chat session (footer duration)
toolStartTime time.Time
welcomeCache string
welcomeDismissed bool
phase uiPhase
sandboxReadyPending bool // defer sandbox system line until after welcome gate
openConfigOnStart bool // first-run: open /config after welcome gate (Enter)
viewDirty bool
layoutKey int // input lines + slash menu height fingerprint
cachedBottomBarLines int // memoized chatBottomBarLines; refresh via refreshInputLayoutIfNeeded
Expand Down
3 changes: 0 additions & 3 deletions cmd/chat_mouse_scroll_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ func runMouseScrollSplitPanePass(t *testing.T, pass int) {
height: 24,
width: 80,
uiFocus: focusPrompt,
phase: phaseWork,
}
m = m.syncViewportMouseWheel().withSyncedLayout()
before := m.viewport.YOffset
Expand Down Expand Up @@ -83,7 +82,6 @@ func TestUpdate_MouseMotionDoesNotReflowLayout(t *testing.T) {
height: 24,
width: 80,
uiFocus: focusPrompt,
phase: phaseWork,
cachedBottomBarLines: 10,
layoutKey: 65536,
}
Expand Down Expand Up @@ -112,7 +110,6 @@ func TestUpdate_InputHistoryWhileWaiting(t *testing.T) {
height: 24,
width: 80,
uiFocus: focusPrompt,
phase: phaseWork,
waiting: true,
history: []string{"first", "second"},
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/chat_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,14 @@ func TestWelcomeDockerRunning_States(t *testing.T) {

func TestBuildWelcomeMessage_IncludesDockerWhenEnabled(t *testing.T) {
running := true
msg := buildWelcomeMessage(nil, "", nil, nil, hawkconfig.Settings{}, false, 80, &running, false)
msg := buildWelcomeMessage(nil, "", nil, nil, hawkconfig.Settings{}, false, 80, 24, &running)
if !strings.Contains(msg, "Docker") {
t.Fatalf("expected Docker indicator in welcome, got snippet without it")
}
}

func TestBuildWelcomeMessage_OmitsDockerWhenDisabled(t *testing.T) {
msg := buildWelcomeMessage(nil, "", nil, nil, hawkconfig.Settings{}, false, 80, nil, false)
msg := buildWelcomeMessage(nil, "", nil, nil, hawkconfig.Settings{}, false, 80, 24, nil)
if strings.Contains(msg, "Docker") {
t.Fatal("expected no Docker indicator when container mode disabled")
}
Expand Down Expand Up @@ -237,7 +237,7 @@ func TestShowWelcomeBanner_WithMessages(t *testing.T) {

func TestBuildWelcomeMessage_UsesDisplayVersion(t *testing.T) {
SetVersion("dev")
msg := buildWelcomeMessage(nil, "", nil, nil, hawkconfig.Settings{}, false, 80, nil, false)
msg := buildWelcomeMessage(nil, "", nil, nil, hawkconfig.Settings{}, false, 80, 24, nil)
if strings.Contains(msg, "vdev") {
t.Fatal("welcome should not show vdev; DisplayVersion should read VERSION file or dev")
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/chat_subcommand_welcome.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import (
)

// welcomeSubcommand implements the /welcome slash command. It
// re-prints the startup welcome message.
// re-prints the inline welcome header at the top of chat.
type welcomeSubcommand struct{}

func (w *welcomeSubcommand) Name() string { return "welcome" }
func (w *welcomeSubcommand) Aliases() []string { return nil }
func (w *welcomeSubcommand) Description() string { return "re-print the startup welcome message" }
func (w *welcomeSubcommand) Description() string { return "re-print the welcome header" }
func (w *welcomeSubcommand) Usage() string { return "" }
func (w *welcomeSubcommand) Handle(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
m.messages = append(m.messages, displayMsg{role: "welcome", content: m.welcomeCache})
Expand Down
20 changes: 2 additions & 18 deletions cmd/chat_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,6 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, tea.Batch(cmds...)

case autoOpenConfigMsg:
if !m.openConfigOnStart || m.configOpen {
return m, nil
}
m.openConfigOnStart = false
return m.openConfigPanel()
case tea.KeyMsg:
// Ctrl+\ enters native terminal selection mode. Available in every UI
// state (welcome gate, permissions, prompt, scrollback) so users always
Expand All @@ -128,10 +122,6 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, nil
}
if next, cmd, handled := m.handleWelcomeGateKey(msg); handled {
return next, cmd
}

// Command palette (Ctrl+K) — intercept all input when open
if m.commandPalette != nil && m.commandPalette.IsOpen() {
action, handled := m.commandPalette.Update(msg)
Expand Down Expand Up @@ -693,9 +683,7 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
if !m.onWelcomeGate() {
m.input.SetWidth(msg.Width - 4)
}
m.input.SetWidth(msg.Width - 4)
m.invalidateInputLayoutCache()
m.rebuildWelcomeCache(false)
m.viewDirty = true
Expand Down Expand Up @@ -735,11 +723,7 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.session.PermSvc().Autonomy() == 0 {
m.session.PermSvc().SetAutonomy(DefaultContainerAutonomy)
}
if m.phase == phaseWelcomeGate {
m.sandboxReadyPending = true
} else {
m.messages = append(m.messages, displayMsg{role: "system", content: formatSandboxReadyAutonomyMessage(m.session.PermSvc().Autonomy())})
}
m.messages = append(m.messages, displayMsg{role: "system", content: formatSandboxReadyAutonomyMessage(m.session.PermSvc().Autonomy())})
m.invalidateConnStatus()
}
if msg.err != nil {
Expand Down
31 changes: 3 additions & 28 deletions cmd/chat_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ func (m chatModel) showWelcomeBanner() bool {
return strings.TrimSpace(m.welcomeCache) != ""
}

func (m chatModel) hasChatMessages() bool {
for _, msg := range m.messages {
switch msg.role {
case "user", "assistant", "tool_use", "tool_result":
return true
}
}
return false
}

func renderSetupCompleteMessage(model string) string {
success := lipgloss.NewStyle().Foreground(doneGreen).Bold(true).Inline(true)
muted := configMutedStyle().Inline(true)
Expand Down Expand Up @@ -202,7 +192,7 @@ func wrapText(text string, width int, prefixWidth int) string {

// chatBottomBarLines counts fixed rows below the chat viewport (must stay in sync with View).
func (m chatModel) chatBottomBarLines() int {
if m.onWelcomeGate() || m.configOpen {
if m.configOpen {
return 0
}
if m.cachedBottomBarLines > 0 {
Expand Down Expand Up @@ -247,11 +237,7 @@ func (m *chatModel) updateViewportContent() {
}
m.viewDirty = false

if m.onWelcomeGate() {
return
}

// /config overlay: config panel only (welcome was on the gate, not repeated here).
// /config overlay: config panel only.
if m.configOpen {
var content strings.Builder
content.WriteString(m.configPanelView())
Expand Down Expand Up @@ -285,24 +271,13 @@ func (m chatModel) View() string {
m = m.withSyncedLayout()

viewWidth := m.width
viewHeight := m.height
if viewWidth <= 0 {
if w, h, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
viewWidth = w
if h > 0 {
viewHeight = h
}
} else {
viewWidth = 80
}
}
if viewHeight <= 0 {
viewHeight = 24
}

if m.onWelcomeGate() {
return m.renderWelcomeGate(viewWidth, viewHeight)
}

// Build the fixed bottom bar
var bottomBar strings.Builder
Expand Down
2 changes: 1 addition & 1 deletion cmd/chat_viewport.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func (m chatModel) shouldRouteMouseToViewport(msg tea.Msg) bool {
if !tea.MouseEvent(mouse).IsWheel() {
return m.inScrollbackFocus()
}
if m.configOpen || m.onWelcomeGate() {
if m.configOpen {
return false
}
if m.inScrollbackFocus() {
Expand Down
4 changes: 2 additions & 2 deletions cmd/chat_viewport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestShouldRouteMouseToViewport_SplitPaneUX(t *testing.T) {
func TestSyncViewportMouseWheel_ManualRouting(t *testing.T) {
t.Setenv("HAWK_MOUSE", "")
vp := viewport.New(80, 10)
m := chatModel{viewport: vp, uiFocus: focusPrompt, phase: phaseWork}
m := chatModel{viewport: vp, uiFocus: focusPrompt}
m = m.syncViewportMouseWheel()
if m.viewport.MouseWheelEnabled {
t.Fatal("viewport auto-wheel must stay off; hawk routes wheel by pane")
Expand All @@ -97,7 +97,7 @@ func TestSyncViewportMouseWheel_DisabledWithOptOut(t *testing.T) {
t.Setenv("HAWK_MOUSE", "0")
vp := viewport.New(80, 10)
disabled := false
m := chatModel{viewport: vp, uiFocus: focusPrompt, phase: phaseWork, settings: hawkconfig.Settings{TuiMouse: &disabled}}
m := chatModel{viewport: vp, uiFocus: focusPrompt, settings: hawkconfig.Settings{TuiMouse: &disabled}}
m = m.syncViewportMouseWheel()
if m.viewport.MouseWheelEnabled {
t.Fatal("wheel should be disabled when mouse capture is off")
Expand Down
Loading
Loading