-
Notifications
You must be signed in to change notification settings - Fork 180
feat(tui): add interactive plan mode and fix compilation #639
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,29 +2,141 @@ | |
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "strings" | ||
|
|
||
| tea "charm.land/bubbletea/v2" | ||
|
|
||
| "github.com/Gitlawb/zero/internal/agent" | ||
| "github.com/Gitlawb/zero/internal/planmode" | ||
|
Check failure on line 12 in internal/tui/plan_command.go
|
||
| "github.com/Gitlawb/zero/internal/tools" | ||
| ) | ||
|
|
||
| type currentPlanReader interface { | ||
| CurrentPlan() []tools.PlanItem | ||
| } | ||
|
|
||
| // handlePlanCommand toggles plan mode on the current session, in the style of | ||
| // openclaude's /plan: | ||
| // | ||
| // /plan toggle plan mode on/off; when on, show the current plan | ||
| // /plan open open the session's plan file in $VISUAL/$EDITOR | ||
| // /plan off exit plan mode (alias: /plan exit) | ||
| // | ||
| // Plan mode is read-only: tool advertisement (see agent.toolAdvertisedInPlan) | ||
| // only exposes read tools, update_plan, and ask_user, so the agent cannot | ||
| // mutate the workspace while planning. | ||
| func (m model) handlePlanCommand(text string) (tea.Model, tea.Cmd) { | ||
| if _, ok := m.registry.Get("update_plan"); !ok { | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendSystem, text: "No plan is active."}) | ||
| return m, nil | ||
| } | ||
|
|
||
| arg := strings.ToLower(strings.TrimSpace(text)) | ||
| switch arg { | ||
| case "off", "exit": | ||
| if m.permissionMode != agent.PermissionModePlan { | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendSystem, text: "Plan mode is not active."}) | ||
| return m, nil | ||
| } | ||
| m.permissionMode = agent.PermissionModeAuto | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendSystem, text: "Exited plan mode. The agent can now implement."}) | ||
| return m, nil | ||
| case "open": | ||
| return m.openPlanInEditor() | ||
| } | ||
|
|
||
| // No subcommand: toggle plan mode, then surface the current plan. | ||
| if m.permissionMode == agent.PermissionModePlan { | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendSystem, text: m.planText()}) | ||
| return m, nil | ||
|
Comment on lines
+50
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Make bare When plan mode is already active, this branch only displays the plan and never exits the mode. That conflicts with the documented 🤖 Prompt for AI Agents |
||
| } | ||
| if m.pending || m.exiting { | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendError, text: "Cannot enter plan mode while a run is active."}) | ||
| return m, nil | ||
| } | ||
| m.permissionMode = agent.PermissionModePlan | ||
| textToShow := planEnterText(m) + "\n\n" + m.planText() | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendSystem, text: textToShow}) | ||
| return m, nil | ||
| } | ||
|
|
||
| // openPlanInEditor writes the session plan file (if missing) and suspends the | ||
| // TUI to launch $VISUAL/$EDITOR on it, resuming on exit. | ||
| func (m model) openPlanInEditor() (tea.Model, tea.Cmd) { | ||
| if m.permissionMode != agent.PermissionModePlan { | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendSystem, text: "Enter plan mode (/plan) before opening the plan file."}) | ||
| return m, nil | ||
| } | ||
| path, err := planmode.PlanFilePath(m.cwd, m.activeSession.SessionID) | ||
| if err != nil { | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendError, text: "plan path error: " + err.Error()}) | ||
| return m, nil | ||
| } | ||
| if _, ok := fileExists(path); !ok { | ||
| if _, err := planmode.WritePlan(m.cwd, m.activeSession.SessionID, ""); err != nil { | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendError, text: "plan write error: " + err.Error()}) | ||
| return m, nil | ||
| } | ||
| } | ||
| editor := strings.TrimSpace(os.Getenv("VISUAL")) | ||
| if editor == "" { | ||
| editor = strings.TrimSpace(os.Getenv("EDITOR")) | ||
| } | ||
| if editor == "" { | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendSystem, text: "Set $VISUAL or $EDITOR to open the plan file:\n" + path}) | ||
| return m, nil | ||
| } | ||
| if m.program == nil { | ||
| // No live program (e.g. under test): just report the path. | ||
| m.transcript = reduceTranscript(m.transcript, transcriptAction{kind: actionAppendSystem, text: "Plan file: " + path}) | ||
| return m, nil | ||
| } | ||
| parts := strings.Fields(editor) | ||
| cmd := exec.Command(parts[0], append(parts[1:], path)...) //nolint:gosec // editor path from $VISUAL/$EDITOR | ||
| cmd.Stdin = os.Stdin | ||
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
| return m, tea.ExecProcess(cmd, func(err error) tea.Msg { | ||
| return nil | ||
| }) | ||
| } | ||
|
|
||
| func planEnterText(m model) string { | ||
| path, err := planmode.PlanFilePath(m.cwd, m.activeSession.SessionID) | ||
| planNote := "" | ||
| if err == nil && path != "" { | ||
| planNote = "\nPlan file: " + path | ||
| } | ||
| return "Entered plan mode. The agent can inspect the workspace and shape the plan with update_plan, but cannot edit files or run commands until you exit.\n" + | ||
| "Use /plan to view the plan, /plan open to edit it, or /plan off to implement." + planNote | ||
| } | ||
|
|
||
| func (m model) planText() string { | ||
| // Prefer the session plan file when present. | ||
| if path, err := planmode.PlanFilePath(m.cwd, m.activeSession.SessionID); err == nil && path != "" { | ||
| if content, exists, err := planmode.ReadPlan(m.cwd, m.activeSession.SessionID); err == nil && exists { | ||
| header := "Current Plan (plan mode)" | ||
| if path != "" { | ||
| header += "\n" + path | ||
| } | ||
| return header + "\n" + strings.TrimRight(content, "\n") | ||
| } | ||
| } | ||
|
|
||
| // Fall back to the update_plan list the agent has been building. | ||
| tool, ok := m.registry.Get("update_plan") | ||
| if !ok { | ||
| return "No plan is active." | ||
| return "Plan mode is active. No plan written yet. Use update_plan to outline steps, or /plan open to draft the plan file." | ||
| } | ||
|
|
||
| reader, ok := tool.(currentPlanReader) | ||
| if !ok { | ||
| return "No plan is active." | ||
| return "Plan mode is active. No plan written yet." | ||
| } | ||
|
|
||
| plan := reader.CurrentPlan() | ||
| if len(plan) == 0 { | ||
| return "No plan is active." | ||
| return "Plan mode is active. No plan written yet. Use update_plan to outline steps, or /plan open to draft the plan file." | ||
| } | ||
|
|
||
| lines := make([]string, 0, len(plan)+1) | ||
|
|
@@ -38,3 +150,11 @@ | |
| } | ||
| return strings.Join(lines, "\n") | ||
| } | ||
|
|
||
| func fileExists(path string) (struct{}, bool) { | ||
| _, err := os.Stat(path) | ||
| if err != nil { | ||
| return struct{}{}, false | ||
| } | ||
| return struct{}{}, true | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,6 +57,7 @@ func Run(ctx context.Context, options Options) int { | |
| initialModel.mouseCapture = true | ||
| } | ||
| program = tea.NewProgram(initialModel, programOpts...) | ||
| initialModel.program = program | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
rg -n -C 3 'tea\.NewProgram\(|initialModel\.program|m\.program|tea\.ExecProcess' internal/tuiRepository: Gitlawb/zero Length of output: 1613 Initialize 🤖 Prompt for AI Agents |
||
|
|
||
| if _, err := program.Run(); err != nil { | ||
| // Surface the failure: exiting 1 with zero diagnostics left users | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift
Restore the missing
planmodepackage or correct this import.Current checks fail because
github.com/Gitlawb/zero/internal/planmodeis not provided by the module. This blocks compilation of the TUI; commit the package implementingPlanFilePath,ReadPlan, andWritePlan, or import the existing package path.🧰 Tools
🪛 GitHub Check: Security & code health
[failure] 12-12:
could not import github.com/Gitlawb/zero/internal/planmode (invalid package name: "")
[failure] 12-12:
no required module provides package github.com/Gitlawb/zero/internal/planmode; to add it:
🤖 Prompt for AI Agents
Source: Linters/SAST tools