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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ and [`agents/peggy/CHANGELOG.md`](agents/peggy/CHANGELOG.md).

## Unreleased

- **`glue run --continue` reopens the most recently updated session.**
Resume previously existed only inside the TUI (`/resume` picker);
from the CLI you had to remember the `--id` you used. `--continue`
asks the store for the newest session, prints which one it picked
(id, message count, last-updated), and works for both the TUI and
one-shot `--prompt` runs. Mutually exclusive with `--id`. (#366)

- **`find_files`, `grep`, and the TUI `@`-picker respect `.gitignore`
(`tools/fs.IgnoreMatcher`).** The recursive walkers previously
skipped only `.git`, so real projects burned tokens walking
Expand Down
2 changes: 1 addition & 1 deletion cmd/glue/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func printVersion(w io.Writer) {

func printUsage(w io.Writer) {
fmt.Fprint(w, `Usage:
glue run --prompt <text> [--provider <name>] [--id <id>] [--model <model>] [--store <dir>] [--work <dir>] [--coding] [--env <path>]
glue run --prompt <text> [--provider <name>] [--id <id> | --continue] [--model <model>] [--store <dir>] [--work <dir>] [--coding] [--env <path>]
glue goal "<objective>" [--provider <name>] [--model <model>] [--store <dir>] [--work <dir>] [--coding] [--yolo] [--worktree] [--max-iterations <n>] [--budget <tokens>] [--env <path>]
glue goal --resume [<id>] | --list [--store <dir>]
glue serve [--provider <name>] [--listen 127.0.0.1:0] [--metadata <path>] [--model <model>] [--store <dir>] [--work <dir>] [--coding] [--env <path>]
Expand Down
33 changes: 30 additions & 3 deletions cmd/glue/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func runCommand(ctx context.Context, args []string, stdin io.Reader, stdout io.W
flags.SetOutput(stderr)

id := flags.String("id", "default", "session id")
continueLast := flags.Bool("continue", false, "resume the most recently updated session in the store (mutually exclusive with --id)")
prompt := flags.String("prompt", "", "prompt text")
provider := flags.String("provider", defaultProvider, "provider name: codex, gemini, nvidia, or openrouter")
model := flags.String("model", "", "model id (default: the provider's default model); gemini/<model> accepted")
Expand All @@ -73,6 +74,17 @@ func runCommand(ctx context.Context, args []string, stdin io.Reader, stdout io.W
if err := validateUsagePricing(*usagePricing); err != nil {
return err
}
if *continueLast {
idExplicit := false
flags.Visit(func(f *flag.Flag) {
if f.Name == "id" {
idExplicit = true
}
})
if idExplicit {
return errors.New("--continue and --id are mutually exclusive (--continue picks the session for you)")
}
}

agentName := "default"
if flags.NArg() > 0 {
Expand Down Expand Up @@ -193,6 +205,21 @@ func runCommand(ctx context.Context, args []string, stdin io.Reader, stdout io.W
return err
}
storeImpl := filestore.New(*storeDir)
// --continue resolves to the store's most recently updated session.
// The store orders by UpdatedAt descending, so the first row wins.
effectiveID := *id
if *continueLast {
summaries, err := storeImpl.ListSessions(ctx, glue.ListSessionsOptions{Limit: 1})
if err != nil {
return fmt.Errorf("--continue: list sessions: %w", err)
}
if len(summaries) == 0 {
return fmt.Errorf("--continue: no sessions found in %s", *storeDir)
}
effectiveID = summaries[0].ID
fmt.Fprintf(stderr, "glue run: continuing session %q (%d messages, last updated %s)\n",
effectiveID, summaries[0].Messages, summaries[0].UpdatedAt.Local().Format("2006-01-02 15:04"))
}
systemPrompt, autoContinue := capabilityDefaults(providerName, tools, *coding)
// Coding runs get the refreshable environment block (cwd, git
// state, date) so the model doesn't spend turns rediscovering it.
Expand Down Expand Up @@ -234,7 +261,7 @@ func runCommand(ctx context.Context, args []string, stdin io.Reader, stdout io.W
}
return tui.Run(ctx, tui.Config{
Agent: agent,
SessionID: *id,
SessionID: effectiveID,
Provider: providerName,
Model: effectiveModel,
WorkDir: *workDir,
Expand All @@ -245,13 +272,13 @@ func runCommand(ctx context.Context, args []string, stdin io.Reader, stdout io.W
BuildTools: buildToolsAt,
})
}
session, err := agent.Session(ctx, *id)
session, err := agent.Session(ctx, effectiveID)
if err != nil {
return err
}

if jsonMode {
return runOneShotJSON(ctx, session, effectivePrompt, *id, providerName, effectiveModel, stdout, *showUsage, usagePricing, stderr)
return runOneShotJSON(ctx, session, effectivePrompt, effectiveID, providerName, effectiveModel, stdout, *showUsage, usagePricing, stderr)
}

wroteDelta := false
Expand Down
61 changes: 61 additions & 0 deletions cmd/glue/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,64 @@ func TestRunCLIMissingPrompt(t *testing.T) {
t.Fatalf("stderr = %q, want missing prompt", stderr.String())
}
}

func TestRunCLIContinuePicksMostRecentSession(t *testing.T) {
t.Parallel()

storeDir := t.TempDir()

// Seed two sessions; "older" first so "newer" has the later UpdatedAt.
for _, id := range []string{"older", "newer"} {
p := &scriptedProvider{turns: [][]glue.ProviderEvent{textTurn("seed " + id)}}
var stdout, stderr bytes.Buffer
if code := runCLI(context.Background(), []string{
"run", "--id", id, "--prompt", "seed", "--store", storeDir,
}, &stdout, &stderr, fakeFactory(p)); code != 0 {
t.Fatalf("seed %s code = %d stderr=%q", id, code, stderr.String())
}
}

cont := &scriptedProvider{turns: [][]glue.ProviderEvent{textTurn("continued")}}
var stdout, stderr bytes.Buffer
code := runCLI(context.Background(), []string{
"run", "--continue", "--prompt", "and then", "--store", storeDir,
}, &stdout, &stderr, fakeFactory(cont))
if code != 0 {
t.Fatalf("continue code = %d stderr=%q", code, stderr.String())
}
if !strings.Contains(stderr.String(), `continuing session "newer"`) {
t.Fatalf("stderr = %q, want continuing session \"newer\"", stderr.String())
}
// Resumed transcript (user+assistant) plus the new user message.
if got := len(cont.requests[0].Messages); got != 3 {
t.Fatalf("request msg count = %d, want 3", got)
}
}

func TestRunCLIContinueRejectsExplicitID(t *testing.T) {
t.Parallel()
var stdout, stderr bytes.Buffer
code := runCLI(context.Background(), []string{
"run", "--continue", "--id", "x", "--prompt", "hi", "--store", t.TempDir(),
}, &stdout, &stderr, fakeFactory(&scriptedProvider{}))
if code == 0 {
t.Fatal("code = 0, want nonzero for --continue with --id")
}
if !strings.Contains(stderr.String(), "mutually exclusive") {
t.Fatalf("stderr = %q, want mutually exclusive error", stderr.String())
}
}

func TestRunCLIContinueEmptyStoreErrors(t *testing.T) {
t.Parallel()
var stdout, stderr bytes.Buffer
code := runCLI(context.Background(), []string{
"run", "--continue", "--prompt", "hi", "--store", t.TempDir(),
}, &stdout, &stderr, fakeFactory(&scriptedProvider{}))
if code == 0 {
t.Fatal("code = 0, want nonzero for --continue with empty store")
}
if !strings.Contains(stderr.String(), "no sessions found") {
t.Fatalf("stderr = %q, want no sessions found", stderr.String())
}
}
Loading