v0.0.5: context compaction, minimal config, retry/search/code symbols/diff/status/test commands
This commit is contained in:
+111
-25
@@ -28,6 +28,7 @@ type Options struct {
|
||||
Yolo bool
|
||||
ThemeMode ThemeMode
|
||||
ModelManager ModelManager
|
||||
WorkingDir string
|
||||
}
|
||||
|
||||
type ModelManager interface {
|
||||
@@ -69,13 +70,14 @@ type message struct {
|
||||
}
|
||||
|
||||
type model struct {
|
||||
ctx context.Context
|
||||
agent *agent.Agent
|
||||
modelName string
|
||||
yolo bool
|
||||
models ModelManager
|
||||
theme theme
|
||||
styles styles
|
||||
ctx context.Context
|
||||
agent *agent.Agent
|
||||
modelName string
|
||||
yolo bool
|
||||
models ModelManager
|
||||
workingDir string
|
||||
theme theme
|
||||
styles styles
|
||||
|
||||
viewport viewport.Model
|
||||
input textarea.Model
|
||||
@@ -84,11 +86,12 @@ type model struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
messages []message
|
||||
status string
|
||||
running bool
|
||||
cancel context.CancelFunc
|
||||
events <-chan tea.Msg
|
||||
messages []message
|
||||
status string
|
||||
contextUsage string
|
||||
running bool
|
||||
cancel context.CancelFunc
|
||||
events <-chan tea.Msg
|
||||
|
||||
// Session picker state
|
||||
picking bool
|
||||
@@ -119,6 +122,7 @@ type slashCommand struct {
|
||||
|
||||
var slashCommands = []slashCommand{
|
||||
{Name: "/clear", Description: "reset context"},
|
||||
{Name: "/compact", Description: "compress context"},
|
||||
{Name: "/exit", Description: "quit"},
|
||||
{Name: "/quit", Description: "quit"},
|
||||
{Name: "/model", Description: "model/provider/thinking"},
|
||||
@@ -126,6 +130,9 @@ var slashCommands = []slashCommand{
|
||||
{Name: "/resume", Description: "resume session"},
|
||||
{Name: "/rename", Description: "rename session"},
|
||||
{Name: "/new", Description: "new session"},
|
||||
{Name: "/status", Description: "show changed files"},
|
||||
{Name: "/diff", Description: "show unstaged diff"},
|
||||
{Name: "/test", Description: "run project tests"},
|
||||
}
|
||||
|
||||
func newModel(ctx context.Context, assistant *agent.Agent, opts Options) model {
|
||||
@@ -154,19 +161,22 @@ func newModel(ctx context.Context, assistant *agent.Agent, opts Options) model {
|
||||
vp.Style = st.Viewport
|
||||
vp.SetContent("")
|
||||
|
||||
return model{
|
||||
ctx: ctx,
|
||||
agent: assistant,
|
||||
modelName: opts.ModelName,
|
||||
yolo: opts.Yolo,
|
||||
models: opts.ModelManager,
|
||||
theme: th,
|
||||
styles: st,
|
||||
viewport: vp,
|
||||
input: input,
|
||||
spinner: spin,
|
||||
status: "ready",
|
||||
m := model{
|
||||
ctx: ctx,
|
||||
agent: assistant,
|
||||
modelName: opts.ModelName,
|
||||
yolo: opts.Yolo,
|
||||
models: opts.ModelManager,
|
||||
workingDir: opts.WorkingDir,
|
||||
theme: th,
|
||||
styles: st,
|
||||
viewport: vp,
|
||||
input: input,
|
||||
spinner: spin,
|
||||
status: "ready",
|
||||
}
|
||||
m.refreshContextUsage()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
@@ -272,6 +282,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.models != nil {
|
||||
_ = m.models.Save()
|
||||
}
|
||||
m.refreshContextUsage()
|
||||
m.resize()
|
||||
m.refreshViewport(true)
|
||||
cmds = append(cmds, textarea.Blink)
|
||||
@@ -338,13 +349,14 @@ func (m *model) submit() (tea.Model, tea.Cmd) {
|
||||
return *m, tea.Quit
|
||||
case "/clear":
|
||||
m.agent.Clear()
|
||||
m.refreshContextUsage()
|
||||
m.messages = []message{{role: roleSystem, content: "Context cleared."}}
|
||||
m.input.SetValue("")
|
||||
m.afterInputChanged()
|
||||
m.status = "ready"
|
||||
m.refreshViewport(true)
|
||||
return *m, nil
|
||||
case "/model":
|
||||
case "/model", "/compact":
|
||||
return m.runLocalCommand(input)
|
||||
}
|
||||
|
||||
@@ -541,6 +553,9 @@ func (m model) modelMetaView() string {
|
||||
parts = append(parts, "thinking "+currentThinking)
|
||||
}
|
||||
}
|
||||
if m.contextUsage != "" {
|
||||
parts = append(parts, m.contextUsage)
|
||||
}
|
||||
parts = append([]string{modelName, mode}, parts...)
|
||||
parts = append(parts, "theme "+string(m.theme.Mode))
|
||||
line := fitLine(strings.Join(parts, " · "), max(1, m.width-2))
|
||||
@@ -700,6 +715,58 @@ func (m model) modelInfo() string {
|
||||
return fmt.Sprintf("model: %s\nmode: %s\ntheme: %s", m.modelName, mode, m.theme.Mode)
|
||||
}
|
||||
|
||||
func (m *model) refreshContextUsage() {
|
||||
m.contextUsage = contextUsageMeta(m.agent)
|
||||
}
|
||||
|
||||
func contextUsageMeta(assistant *agent.Agent) string {
|
||||
if assistant == nil {
|
||||
return ""
|
||||
}
|
||||
info := assistant.RuntimeInfo()
|
||||
if info.ContextTokens <= 0 && info.MaxContextTokens <= 0 {
|
||||
return ""
|
||||
}
|
||||
used := formatTokenCount(info.ContextTokens)
|
||||
if info.MaxContextTokens <= 0 {
|
||||
return "ctx ~" + used
|
||||
}
|
||||
return fmt.Sprintf("ctx ~%s/%s (%s)", used, formatTokenCount(info.MaxContextTokens), contextPercent(info.ContextTokens, info.MaxContextTokens))
|
||||
}
|
||||
|
||||
func contextPercent(used int, limit int) string {
|
||||
if limit <= 0 {
|
||||
return ""
|
||||
}
|
||||
if used <= 0 {
|
||||
return "0%"
|
||||
}
|
||||
percent := used * 100 / limit
|
||||
if percent == 0 {
|
||||
return "<1%"
|
||||
}
|
||||
return fmt.Sprintf("%d%%", percent)
|
||||
}
|
||||
|
||||
func formatTokenCount(tokens int) string {
|
||||
if tokens < 0 {
|
||||
tokens = 0
|
||||
}
|
||||
if tokens < 1000 {
|
||||
return fmt.Sprintf("%d", tokens)
|
||||
}
|
||||
if tokens < 1_000_000 {
|
||||
if tokens%1000 == 0 {
|
||||
return fmt.Sprintf("%dk", tokens/1000)
|
||||
}
|
||||
return fmt.Sprintf("%.1fk", float64(tokens)/1000)
|
||||
}
|
||||
if tokens%1_000_000 == 0 {
|
||||
return fmt.Sprintf("%dm", tokens/1_000_000)
|
||||
}
|
||||
return fmt.Sprintf("%.1fm", float64(tokens)/1_000_000)
|
||||
}
|
||||
|
||||
func fitLine(text string, width int) string {
|
||||
width = max(1, width)
|
||||
if lipgloss.Width(text) <= width {
|
||||
@@ -741,6 +808,7 @@ func (m *model) runLocalCommand(input string) (tea.Model, tea.Cmd) {
|
||||
m.status = status
|
||||
// Rebuild display from agent history (matters after /resume and /new)
|
||||
m.syncMessages()
|
||||
m.refreshContextUsage()
|
||||
// Append command output after synced history
|
||||
m.messages = append(m.messages, message{role: role, content: output})
|
||||
m.refreshViewport(true)
|
||||
@@ -753,6 +821,8 @@ func (m model) handleLocalCommand(input string) (string, error) {
|
||||
return "", fmt.Errorf("empty command")
|
||||
}
|
||||
switch fields[0] {
|
||||
case "/compact":
|
||||
return m.handleCompactCommand()
|
||||
case "/model":
|
||||
return m.handleModelCommand(fields[1:])
|
||||
case "/sessions":
|
||||
@@ -763,11 +833,27 @@ func (m model) handleLocalCommand(input string) (string, error) {
|
||||
return m.handleRenameCommand(fields[1:])
|
||||
case "/new":
|
||||
return m.handleNewCommand()
|
||||
case "/status":
|
||||
return m.handleStatusCommand(fields[1:])
|
||||
case "/diff":
|
||||
return m.handleDiffCommand(fields[1:])
|
||||
case "/test":
|
||||
return m.handleTestCommand(fields[1:])
|
||||
default:
|
||||
return "", fmt.Errorf("unknown command: %s", fields[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) handleCompactCommand() (string, error) {
|
||||
if m.agent == nil {
|
||||
return "", fmt.Errorf("agent is not configured")
|
||||
}
|
||||
if err := m.agent.Compact(m.ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Context compacted.", nil
|
||||
}
|
||||
|
||||
func (m model) handleModelCommand(args []string) (string, error) {
|
||||
if m.models == nil {
|
||||
if len(args) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user