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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## Unreleased

### Added
- **`devorch --continue` / `-c`**: continue the latest conversation started in this directory. Sessions now record
their directory (older sessions have none and are not matched by `--continue`).
- **`devorch --resume` with no ID** opens a session picker (this directory first, with an option for all); **`/resume`**
switches session inside the REPL.
- **`devorch --version` / `-V`**.
- **`devorch doctor`** (and `/doctor`): a read-only health report (providers, keys, permissions, project trust,
terminal). It never prints key values, and it warns when `edit`/`filesystem` are pre-approved.
- `devorch sessions list --here`, and a Directory column in session listings.

### Fixed
- Messages in a resumed session could come back out of order when saved within the same second; they are now loaded
in the order they were saved.
- A stray `pytest` run no longer overwrites your real `~/.devorch` (it replaced saved permissions with
`edit: allow` / `filesystem: allow`); each test also starts with an empty home.

## 0.5.0

A large feature release: hooks, checkpoints, project settings, image input, streaming, prompt caching,
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ cd DevOrch && pip install -e .
devorch # Interactive setup on first run
devorch -p openai # Use a specific provider
devorch -p local # Use Ollama (local models)
devorch --resume abc123 # Resume a previous session
devorch --continue # Continue the latest conversation started in this directory (-c)
devorch --resume # Pick a previous session from a list (this directory's first)
devorch --resume abc123 # Resume a specific session by ID
devorch --version # Print the version (-V)
devorch doctor # Check your setup: providers, keys, permissions, project trust, terminal

# Non-interactive (scripting / CI)
devorch ask "explain this project"
Expand Down Expand Up @@ -262,6 +266,15 @@ pricing:
cache_write: 3.75 # optional, defaults to the input price
```

### Continuing and resuming sessions

Every session remembers the directory it was started in. `devorch --continue` (`-c`) reopens the most recent
non-empty conversation from the current directory; if there is none it tells you and starts a new one. A bare
`devorch --resume` shows a picker of this directory's sessions (labelled by what you first asked) with an option to
widen it to every directory, and `/resume` does the same without leaving DevOrch. Sessions created by earlier versions have
no recorded directory, so `--continue` cannot find them, but they still appear in the picker's "all directories"
view and can be resumed by ID. `devorch sessions list --here` lists only this directory's sessions.

### Prompt Shortcuts

| Key | Action |
Expand Down Expand Up @@ -487,7 +500,9 @@ overrides a project's `default_mode`. In the REPL, `/auto` and `/ask` switch the
| `/mcp stop <name>` | Disconnect a server and remove its tools |
| `/config` | Show configuration settings |
| `/permissions` | Show permissions; change them with `/permissions allow\|deny [--session] <tool> <pattern>`, `set <tool> <allow\|deny\|ask>`, `reset` |
| `/sessions` | `list [n]`, `show <id>`, `delete <id>` (resume with `devorch --resume <id>`) |
| `/sessions` | `list [n] [here]`, `show <id>`, `delete <id>` |
| `/resume` | Switch to another saved session (`/resume` opens a picker, `/resume <id>` goes straight to it) |
| `/doctor` | Check your setup (same as `devorch doctor`) |
| `/hooks` | Show active hooks and whether project hooks are trusted |
| `/trust` / `/untrust` | Trust (or stop trusting) this project's `.devorch` settings and hooks |
| `/cost` | Estimated session cost per model |
Expand Down
4 changes: 3 additions & 1 deletion cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
"/session": "Show current session ID, model, and message count",
"/config": "Show configuration settings and provider status",
"/permissions": "Show or change permissions (/permissions allow|deny|set|reset)",
"/sessions": "List, show or delete chat sessions (/sessions list|show <id>|delete <id>)",
"/sessions": "List, show or delete chat sessions (/sessions list [here]|show <id>|delete <id>)",
"/resume": "Switch to another saved session (/resume or /resume <id>)",
"/doctor": "Check your setup: providers, keys, permissions, project trust, terminal",
"/hooks": "Show active hooks and whether project hooks are trusted",
"/trust": "Trust this project's .devorch settings and hooks (MCP, allow rules, hooks)",
"/untrust": "Revoke trust for this project's .devorch files",
Expand Down
124 changes: 119 additions & 5 deletions cli/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import os
import sys
import time
from pathlib import Path

import questionary
import typer
from prompt_toolkit import prompt as pt_prompt
from prompt_toolkit.history import FileHistory
from rich.markup import escape
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

from cli.commands._shared import ( # noqa: E402
SKIP_PERMISSIONS_HELP,
Expand All @@ -26,6 +29,7 @@
PROMPT_STYLE,
QUESTIONARY_STYLE,
SLASH_COMMANDS,
VERSION,
SlashCommandCompleter,
print_banner,
)
Expand All @@ -42,13 +46,17 @@
)
from cli.prompt_ui import prompt_message as build_prompt_message
from cli.repl_commands import ( # noqa: E402
PICK_SESSION,
handle_hooks,
handle_permissions,
handle_resume,
handle_sessions,
handle_trust,
handle_untrust,
last_exchange_preview,
print_session_details,
print_sessions_table,
resolve_startup_session,
)
from config.permissions import (
PERMISSIONS_FILE,
Expand Down Expand Up @@ -683,6 +691,7 @@ def start_repl(
show_banner: bool = True,
mode: str | None = None,
skip_permissions: bool = False,
continue_session: bool = False,
):
"""Start the interactive REPL session."""
if show_banner:
Expand All @@ -698,7 +707,14 @@ def start_repl(

context_summary = None

# Handle session resumption
# Handle session resumption: --resume <id>, a bare --resume (picker), or --continue
choice = resolve_startup_session(session_manager, resume, continue_session)
if choice.cancelled:
print_info("No session chosen.")
raise typer.Exit(0)
if choice.notice:
print_info(choice.notice)
resume = choice.session_id
if resume:
try:
session_info, messages = session_manager.load_session(resume)
Expand All @@ -708,6 +724,8 @@ def start_repl(

print_success(f"Resumed session: {resume}")
print_info(f"Provider: {provider} | Model: {model} | Messages: {len(messages)}")
for line in last_exchange_preview(messages):
console.print(f" [dim]{escape(line)}[/dim]")

if context_summary:
print_info("Session has context from previous conversation")
Expand Down Expand Up @@ -960,6 +978,7 @@ def prompt_gutter(width, line_number, wrap_count):
"/tokens",
"/cost",
"/sessions",
"/resume",
"/copy",
"/paste",
"/undo",
Expand All @@ -976,6 +995,7 @@ def prompt_gutter(width, line_number, wrap_count):
"/tasks",
"/config",
"/permissions",
"/doctor",
"/hooks",
"/trust",
"/untrust",
Expand Down Expand Up @@ -1200,6 +1220,14 @@ def prompt_gutter(width, line_number, wrap_count):
)
continue

elif cmd == "resume":
handle_resume(cmd_arg, session_manager, agent, executor)
continue

elif cmd == "doctor":
render_doctor()
continue

elif cmd == "hooks":
handle_hooks()
continue
Expand Down Expand Up @@ -2021,16 +2049,88 @@ def prompt_gutter(width, line_number, wrap_count):
console.print("[dim] - Make sure Ollama is running: ollama serve[/dim]")


def _version_callback(value: bool):
if value:
console.print(f"devorch {VERSION}", highlight=False)
raise typer.Exit()


def normalize_resume_argv(argv: list[str]) -> list[str]:
"""Turn a bare ``--resume`` / ``-r`` (no session id) into ``--resume __pick__``.

Typer cannot express an option with an optional value: a bare ``--resume`` errors, and
``--resume -c`` would swallow ``-c`` as the id. A value-less ``--resume`` (at the end, or followed
by another option) therefore becomes the internal picker marker.
"""
out: list[str] = []
for i, arg in enumerate(argv):
out.append(arg)
if arg in ("--resume", "-r") and (i + 1 == len(argv) or argv[i + 1].startswith("-")):
out.append(PICK_SESSION)
return out


def render_doctor() -> int:
"""Print the doctor report and return the process exit code (1 if any check failed)."""
from core.doctor import FAIL, INFO, OK, WARN, exit_code, run_checks

rows = run_checks()
icons = {OK: ("✓", "green"), WARN: ("!", "yellow"), FAIL: ("✗", "red"), INFO: ("·", "dim")}
table = Table(title="DevOrch doctor", show_header=False, box=None, pad_edge=False)
table.add_column(width=2)
table.add_column(style="bold", no_wrap=True)
table.add_column(overflow="fold") # fold, don't cut: full paths matter in a bug report
for r in rows:
icon, color = icons[r.status]
table.add_row(Text(icon, style=color), Text(r.name), Text(r.detail))
if r.hint:
table.add_row("", "", Text(f"→ {r.hint}", style="dim"))
console.print(table)
summary = {s: sum(1 for r in rows if r.status == s) for s in (OK, WARN, FAIL)}
console.print(
f"\n[green]{summary[OK]} ok[/green], [yellow]{summary[WARN]} warnings[/yellow], "
f"[red]{summary[FAIL]} problems[/red]\n"
)
return exit_code(rows)


@app.command("doctor")
def doctor_command():
"""
Check your setup (providers, keys, permissions, project trust, terminal) for problems.
"""
raise typer.Exit(render_doctor())


@app.callback()
def main_callback(
ctx: typer.Context,
provider: str = typer.Option(None, "--provider", "-p", help="LLM Provider"),
model: str = typer.Option(None, "--model", "-m", help="Model name"),
resume: str = typer.Option(None, "--resume", "-r", help="Resume session by ID"),
resume: str = typer.Option(
None,
"--resume",
"-r",
help="Resume a session by ID; use --resume on its own to pick from a list",
),
continue_session: bool = typer.Option(
False,
"--continue",
"-c",
help="Continue the most recent conversation started in this directory",
),
mode: str = typer.Option(None, "--mode", help="Start in this mode: ask, auto, plan"),
skip_permissions: bool = typer.Option(
False, "--dangerously-skip-permissions", help=SKIP_PERMISSIONS_HELP
),
version: bool = typer.Option(
False,
"--version",
"-V",
callback=_version_callback,
is_eager=True,
help="Show the DevOrch version and exit",
),
):
"""
DevOrch - Your AI Coding Assistant
Expand Down Expand Up @@ -2060,14 +2160,26 @@ def main_callback(
resume=resume,
mode=mode,
skip_permissions=skip_permissions,
continue_session=continue_session,
)


@app.command()
def chat(
provider: str = typer.Option(None, "--provider", "-p", help="LLM Provider"),
model: str = typer.Option(None, "--model", "-m", help="Model name"),
resume: str = typer.Option(None, "--resume", "-r", help="Resume session by ID"),
resume: str = typer.Option(
None,
"--resume",
"-r",
help="Resume a session by ID; use --resume on its own to pick from a list",
),
continue_session: bool = typer.Option(
False,
"--continue",
"-c",
help="Continue the most recent conversation started in this directory",
),
message_limit: int = typer.Option(
DEFAULT_MESSAGE_LIMIT, "--limit", "-l", help="Messages before auto-summarization"
),
Expand Down Expand Up @@ -2095,6 +2207,7 @@ def chat(
message_limit=message_limit,
mode=mode,
skip_permissions=skip_permissions,
continue_session=continue_session,
)


Expand Down Expand Up @@ -2352,11 +2465,12 @@ def skills_list():
@sessions_app.command("list")
def sessions_list(
limit: int = typer.Option(20, "--limit", "-n", help="Number of sessions to show"),
here: bool = typer.Option(False, "--here", help="Only sessions started in this directory"),
):
"""
List recent chat sessions.
"""
print_sessions_table(SessionManager(), limit)
print_sessions_table(SessionManager(), limit, here=here)
console.print("\n[dim]Resume a session: devorch --resume <id>[/dim]")


Expand Down Expand Up @@ -2552,7 +2666,7 @@ def init_project(


def main():
app()
app(args=normalize_resume_argv(sys.argv[1:]))


if __name__ == "__main__":
Expand Down
Loading
Loading