xdog-coding 0.57.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xdog/coding/__init__.py +3 -0
- xdog/coding/cli/__init__.py +1 -0
- xdog/coding/cli/args.py +148 -0
- xdog/coding/cli/config_selector.py +74 -0
- xdog/coding/cli/file_processor.py +80 -0
- xdog/coding/cli/initial_message.py +55 -0
- xdog/coding/cli/list_models.py +27 -0
- xdog/coding/cli/session_picker.py +45 -0
- xdog/coding/config.py +274 -0
- xdog/coding/core/__init__.py +1 -0
- xdog/coding/core/agent_session.py +281 -0
- xdog/coding/core/bash_executor.py +65 -0
- xdog/coding/core/compaction/__init__.py +1 -0
- xdog/coding/core/compaction/compaction.py +106 -0
- xdog/coding/core/compaction/utils.py +80 -0
- xdog/coding/core/defaults.py +35 -0
- xdog/coding/core/event_bus.py +56 -0
- xdog/coding/core/exec_utils.py +127 -0
- xdog/coding/core/extensions/__init__.py +1 -0
- xdog/coding/core/extensions/loader.py +116 -0
- xdog/coding/core/extensions/runner.py +79 -0
- xdog/coding/core/extensions/types.py +95 -0
- xdog/coding/core/keybindings.py +64 -0
- xdog/coding/core/messages.py +192 -0
- xdog/coding/core/prompt_templates.py +65 -0
- xdog/coding/core/resource_loader.py +137 -0
- xdog/coding/core/sdk.py +169 -0
- xdog/coding/core/session_manager.py +156 -0
- xdog/coding/core/settings_manager.py +93 -0
- xdog/coding/core/skills.py +75 -0
- xdog/coding/core/slash_commands.py +208 -0
- xdog/coding/core/system_prompt.py +115 -0
- xdog/coding/core/timings.py +84 -0
- xdog/coding/core/tools/__init__.py +36 -0
- xdog/coding/core/tools/bash.py +74 -0
- xdog/coding/core/tools/edit.py +104 -0
- xdog/coding/core/tools/find.py +91 -0
- xdog/coding/core/tools/grep.py +148 -0
- xdog/coding/core/tools/ls.py +99 -0
- xdog/coding/core/tools/path_utils.py +94 -0
- xdog/coding/core/tools/read.py +97 -0
- xdog/coding/core/tools/truncate.py +58 -0
- xdog/coding/core/tools/write.py +62 -0
- xdog/coding/main.py +191 -0
- xdog/coding/modes/__init__.py +1 -0
- xdog/coding/modes/interactive/__init__.py +1 -0
- xdog/coding/modes/interactive/components/__init__.py +1 -0
- xdog/coding/modes/interactive/components/assistant_message.py +22 -0
- xdog/coding/modes/interactive/components/chat_log.py +99 -0
- xdog/coding/modes/interactive/components/custom_editor.py +255 -0
- xdog/coding/modes/interactive/components/footer.py +47 -0
- xdog/coding/modes/interactive/components/tool_execution.py +216 -0
- xdog/coding/modes/interactive/components/user_message.py +23 -0
- xdog/coding/modes/interactive/interactive_mode.py +526 -0
- xdog/coding/modes/interactive/theme.py +183 -0
- xdog/coding/modes/print_mode.py +106 -0
- xdog/coding/modes/rpc/__init__.py +1 -0
- xdog/coding/modes/rpc/rpc_mode.py +189 -0
- xdog/coding/py.typed +0 -0
- xdog/coding/utils/__init__.py +1 -0
- xdog/coding/utils/frontmatter.py +72 -0
- xdog/coding/utils/git.py +90 -0
- xdog/coding/utils/mime.py +60 -0
- xdog/coding/utils/shell.py +77 -0
- xdog/coding/utils/sleep.py +49 -0
- xdog_coding-0.57.1.dist-info/METADATA +46 -0
- xdog_coding-0.57.1.dist-info/RECORD +71 -0
- xdog_coding-0.57.1.dist-info/WHEEL +4 -0
- xdog_coding-0.57.1.dist-info/entry_points.txt +2 -0
- xdog_coding-0.57.1.dist-info/licenses/LICENSE +662 -0
- xdog_coding-0.57.1.dist-info/licenses/LICENSE-EXCEPTION.md +82 -0
xdog/coding/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI sub-package for argument parsing and interactive selectors."""
|
xdog/coding/cli/args.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""CLI argument parsing via click."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from xdog.coding.cli.list_models import list_models_command
|
|
10
|
+
from xdog.coding.cli.session_picker import pick_session_command
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
|
|
14
|
+
@click.option(
|
|
15
|
+
"-m", "--model",
|
|
16
|
+
default=None,
|
|
17
|
+
help="Model to use (e.g. sonnet, opus, haiku, or a full model id).",
|
|
18
|
+
)
|
|
19
|
+
@click.option(
|
|
20
|
+
"-r", "--resume",
|
|
21
|
+
is_flag=True,
|
|
22
|
+
default=False,
|
|
23
|
+
help="Resume the most recent session.",
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--resume-id",
|
|
27
|
+
default=None,
|
|
28
|
+
help="Resume a specific session by ID.",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"-p", "--prompt",
|
|
32
|
+
default=None,
|
|
33
|
+
help="Initial prompt to send (non-interactive).",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--print",
|
|
37
|
+
"print_mode",
|
|
38
|
+
is_flag=True,
|
|
39
|
+
default=False,
|
|
40
|
+
help="Run in non-interactive print mode.",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--output-format",
|
|
44
|
+
type=click.Choice(["text", "json", "markdown"]),
|
|
45
|
+
default="text",
|
|
46
|
+
help="Output format for print mode.",
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--working-dir",
|
|
50
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
51
|
+
default=None,
|
|
52
|
+
help="Working directory for the agent.",
|
|
53
|
+
)
|
|
54
|
+
@click.option(
|
|
55
|
+
"--config",
|
|
56
|
+
"config_path",
|
|
57
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
58
|
+
default=None,
|
|
59
|
+
help="Path to a config file.",
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--thinking-level",
|
|
63
|
+
type=click.Choice(["none", "normal", "deep", "ultrathink"]),
|
|
64
|
+
default=None,
|
|
65
|
+
help="Thinking / reasoning level.",
|
|
66
|
+
)
|
|
67
|
+
@click.option(
|
|
68
|
+
"--list-models",
|
|
69
|
+
is_flag=True,
|
|
70
|
+
default=False,
|
|
71
|
+
help="List available models and exit.",
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"--pick-session",
|
|
75
|
+
is_flag=True,
|
|
76
|
+
default=False,
|
|
77
|
+
help="Interactively pick a session to resume.",
|
|
78
|
+
)
|
|
79
|
+
@click.option(
|
|
80
|
+
"--rpc",
|
|
81
|
+
is_flag=True,
|
|
82
|
+
default=False,
|
|
83
|
+
help="Run in RPC mode for IDE integration.",
|
|
84
|
+
)
|
|
85
|
+
@click.option(
|
|
86
|
+
"--verbose",
|
|
87
|
+
is_flag=True,
|
|
88
|
+
default=False,
|
|
89
|
+
help="Enable verbose logging.",
|
|
90
|
+
)
|
|
91
|
+
@click.argument("files", nargs=-1, type=click.Path(exists=True, path_type=Path))
|
|
92
|
+
def cli(
|
|
93
|
+
model: str | None,
|
|
94
|
+
resume: bool,
|
|
95
|
+
resume_id: str | None,
|
|
96
|
+
prompt: str | None,
|
|
97
|
+
print_mode: bool,
|
|
98
|
+
output_format: str,
|
|
99
|
+
working_dir: Path | None,
|
|
100
|
+
config_path: Path | None,
|
|
101
|
+
thinking_level: str | None,
|
|
102
|
+
list_models: bool,
|
|
103
|
+
pick_session: bool,
|
|
104
|
+
rpc: bool,
|
|
105
|
+
verbose: bool,
|
|
106
|
+
files: tuple[Path, ...],
|
|
107
|
+
) -> None:
|
|
108
|
+
"""pi - Interactive coding agent CLI.
|
|
109
|
+
|
|
110
|
+
Optionally pass FILES to include their contents in the initial context.
|
|
111
|
+
"""
|
|
112
|
+
# Early-exit commands
|
|
113
|
+
if list_models:
|
|
114
|
+
list_models_command()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if pick_session:
|
|
118
|
+
pick_session_command()
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Build overrides dict from CLI flags
|
|
122
|
+
overrides: dict[str, Any] = {}
|
|
123
|
+
if model is not None:
|
|
124
|
+
overrides["model"] = model
|
|
125
|
+
if thinking_level is not None:
|
|
126
|
+
overrides["thinking_level"] = thinking_level
|
|
127
|
+
|
|
128
|
+
# Defer to the main entry-point for actual execution
|
|
129
|
+
from xdog.coding.main import run_agent
|
|
130
|
+
|
|
131
|
+
run_agent(
|
|
132
|
+
overrides=overrides,
|
|
133
|
+
resume=resume,
|
|
134
|
+
resume_id=resume_id,
|
|
135
|
+
prompt=prompt,
|
|
136
|
+
print_mode=print_mode,
|
|
137
|
+
output_format=output_format,
|
|
138
|
+
working_dir=working_dir,
|
|
139
|
+
config_path=config_path,
|
|
140
|
+
rpc=rpc,
|
|
141
|
+
verbose=verbose,
|
|
142
|
+
files=files,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_args() -> None:
|
|
147
|
+
"""Parse CLI arguments and run the application."""
|
|
148
|
+
cli(standalone_mode=True)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Interactive config selector: set default model, thinking level, etc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from xdog.coding.config import GlobalConfig, get_global_settings_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _prompt_provider() -> str:
|
|
11
|
+
"""Ask the user which provider to configure."""
|
|
12
|
+
providers = ["anthropic", "openai", "google"]
|
|
13
|
+
print("\nAvailable providers:")
|
|
14
|
+
for idx, name in enumerate(providers, 1):
|
|
15
|
+
print(f" {idx}. {name}")
|
|
16
|
+
while True:
|
|
17
|
+
choice = input("\nSelect provider [1-3]: ").strip()
|
|
18
|
+
if choice.isdigit() and 1 <= int(choice) <= len(providers):
|
|
19
|
+
return providers[int(choice) - 1]
|
|
20
|
+
print("Invalid selection, try again.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _prompt_api_key(provider: str) -> str:
|
|
24
|
+
"""Show environment variable instructions for a provider.
|
|
25
|
+
|
|
26
|
+
API keys are managed by the ai package via environment variables,
|
|
27
|
+
not stored in the coding agent's config file.
|
|
28
|
+
"""
|
|
29
|
+
env_vars = {
|
|
30
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
31
|
+
"openai": "OPENAI_API_KEY",
|
|
32
|
+
"google": "GOOGLE_API_KEY",
|
|
33
|
+
}
|
|
34
|
+
var = env_vars.get(provider, f"{provider.upper()}_API_KEY")
|
|
35
|
+
print(f"\n Set the {var} environment variable to use {provider}.")
|
|
36
|
+
print(f" Example: export {var}=your-key-here")
|
|
37
|
+
return provider
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def config_selector(config_path: Path | None = None) -> GlobalConfig:
|
|
41
|
+
"""Interactive configuration wizard.
|
|
42
|
+
|
|
43
|
+
Lets the user set the default model and other preferences.
|
|
44
|
+
API keys are managed by the ai package via environment variables.
|
|
45
|
+
|
|
46
|
+
Returns the updated :class:`GlobalConfig`.
|
|
47
|
+
"""
|
|
48
|
+
cfg = GlobalConfig.load(config_path)
|
|
49
|
+
|
|
50
|
+
print("=== Pi Coding Agent - Configuration ===\n")
|
|
51
|
+
print("Note: API keys are set via environment variables")
|
|
52
|
+
print(" (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY)\n")
|
|
53
|
+
|
|
54
|
+
# Default model
|
|
55
|
+
new_model = input(f"Default model [{cfg.default_model}]: ").strip()
|
|
56
|
+
if new_model:
|
|
57
|
+
cfg = cfg.model_copy(update={"default_model": new_model})
|
|
58
|
+
|
|
59
|
+
# Thinking level
|
|
60
|
+
new_thinking = input(f"Thinking level [{cfg.thinking_level}] (off/normal/high): ").strip()
|
|
61
|
+
if new_thinking:
|
|
62
|
+
cfg = cfg.model_copy(update={"thinking_level": new_thinking})
|
|
63
|
+
|
|
64
|
+
# Custom instructions
|
|
65
|
+
if cfg.custom_instructions:
|
|
66
|
+
print(f"\nCurrent custom instructions: {cfg.custom_instructions[:80]}...")
|
|
67
|
+
new_instructions = input("Custom instructions (leave empty to keep): ").strip()
|
|
68
|
+
if new_instructions:
|
|
69
|
+
cfg = cfg.model_copy(update={"custom_instructions": new_instructions})
|
|
70
|
+
|
|
71
|
+
save_path = config_path or get_global_settings_path()
|
|
72
|
+
cfg.save(save_path)
|
|
73
|
+
print(f"\nConfiguration saved to {save_path}")
|
|
74
|
+
return cfg
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Process files passed as CLI arguments into context messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# Binary / image extensions that should be handled differently
|
|
9
|
+
_IMAGE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"})
|
|
10
|
+
_BINARY_EXTENSIONS = frozenset({
|
|
11
|
+
".zip", ".tar", ".gz", ".bz2", ".xz",
|
|
12
|
+
".pdf", ".doc", ".docx", ".xls", ".xlsx",
|
|
13
|
+
".exe", ".dll", ".so", ".dylib",
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
MAX_FILE_SIZE = 512 * 1024 # 512 KiB
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_image(path: Path) -> bool:
|
|
20
|
+
return path.suffix.lower() in _IMAGE_EXTENSIONS
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_binary(path: Path) -> bool:
|
|
24
|
+
return path.suffix.lower() in _BINARY_EXTENSIONS
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def process_file(path: Path) -> dict[str, Any] | None:
|
|
28
|
+
"""Read a single file and return a context dict for the system prompt.
|
|
29
|
+
|
|
30
|
+
Returns ``None`` when the file cannot be processed (binary, too large, etc.).
|
|
31
|
+
"""
|
|
32
|
+
resolved = path.resolve()
|
|
33
|
+
if not resolved.is_file():
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
if _is_binary(resolved):
|
|
37
|
+
return {
|
|
38
|
+
"type": "file_reference",
|
|
39
|
+
"path": str(resolved),
|
|
40
|
+
"note": "Binary file - contents not included.",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if _is_image(resolved):
|
|
44
|
+
return {
|
|
45
|
+
"type": "image",
|
|
46
|
+
"path": str(resolved),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
stat = resolved.stat()
|
|
51
|
+
except OSError:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
if stat.st_size > MAX_FILE_SIZE:
|
|
55
|
+
return {
|
|
56
|
+
"type": "file_reference",
|
|
57
|
+
"path": str(resolved),
|
|
58
|
+
"note": f"File too large ({stat.st_size:,} bytes) - contents not included.",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
content = resolved.read_text(encoding="utf-8", errors="replace")
|
|
63
|
+
except OSError:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"type": "file_content",
|
|
68
|
+
"path": str(resolved),
|
|
69
|
+
"content": content,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def process_files(paths: tuple[Path, ...] | list[Path]) -> list[dict[str, Any]]:
|
|
74
|
+
"""Process multiple files and return a list of context dicts."""
|
|
75
|
+
results: list[dict[str, Any]] = []
|
|
76
|
+
for p in paths:
|
|
77
|
+
entry = process_file(Path(p))
|
|
78
|
+
if entry is not None:
|
|
79
|
+
results.append(entry)
|
|
80
|
+
return results
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Initial message builder: construct the first message from CLI args and piped stdin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from xdog.coding.cli.file_processor import process_files
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_initial_message(
|
|
12
|
+
*,
|
|
13
|
+
prompt: str | None = None,
|
|
14
|
+
files: tuple[Path, ...] = (),
|
|
15
|
+
read_stdin: bool = False,
|
|
16
|
+
) -> str | None:
|
|
17
|
+
"""Build the initial user message from CLI arguments.
|
|
18
|
+
|
|
19
|
+
Combines prompt text, file contents, and piped stdin into
|
|
20
|
+
a single message string. Returns None if no input is provided.
|
|
21
|
+
"""
|
|
22
|
+
parts: list[str] = []
|
|
23
|
+
|
|
24
|
+
# Read piped stdin
|
|
25
|
+
if read_stdin and not sys.stdin.isatty():
|
|
26
|
+
try:
|
|
27
|
+
stdin_content = sys.stdin.read()
|
|
28
|
+
if stdin_content.strip():
|
|
29
|
+
parts.append(stdin_content.strip())
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
# Process file arguments
|
|
34
|
+
if files:
|
|
35
|
+
file_entries = process_files(files)
|
|
36
|
+
for entry in file_entries:
|
|
37
|
+
etype = entry.get("type", "")
|
|
38
|
+
path = entry.get("path", "")
|
|
39
|
+
if etype == "file_content":
|
|
40
|
+
content = entry.get("content", "")
|
|
41
|
+
parts.append(f"File: {path}\n```\n{content}\n```")
|
|
42
|
+
elif etype == "image":
|
|
43
|
+
parts.append(f"[Image: {path}]")
|
|
44
|
+
elif etype == "file_reference":
|
|
45
|
+
note = entry.get("note", "")
|
|
46
|
+
parts.append(f"File: {path} ({note})")
|
|
47
|
+
|
|
48
|
+
# Add the explicit prompt last
|
|
49
|
+
if prompt:
|
|
50
|
+
parts.append(prompt)
|
|
51
|
+
|
|
52
|
+
if not parts:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""List available models in a user-friendly table."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def list_models_command() -> None:
|
|
7
|
+
"""Print a table of all registered models and exit."""
|
|
8
|
+
import xdog.ai as ai
|
|
9
|
+
runtime = ai.load()
|
|
10
|
+
models = sorted(
|
|
11
|
+
(m for m in runtime.models() if m.model_type == "chat"),
|
|
12
|
+
key=lambda m: (m.provider, m.id),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if not models:
|
|
16
|
+
print("No models available. Run 'xdog-ai login copilot' first.")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
header = f" {'Model ID':<45s} {'Provider':<15s} {'Protocol':<20s}"
|
|
20
|
+
separator = " " + "-" * 84
|
|
21
|
+
|
|
22
|
+
print("\nAvailable models:\n")
|
|
23
|
+
print(header)
|
|
24
|
+
print(separator)
|
|
25
|
+
for m in models:
|
|
26
|
+
print(f" {m.id:<45s} {m.provider:<15s} {m.api:<20s}")
|
|
27
|
+
print()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Interactive session picker for resuming previous sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
from xdog.coding.core.session_manager import SessionManager, SessionMeta
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _format_session(meta: SessionMeta, idx: int) -> str:
|
|
11
|
+
"""Format one session line for display."""
|
|
12
|
+
ts = datetime.fromtimestamp(meta.updated_at, tz=timezone.utc)
|
|
13
|
+
time_str = ts.strftime("%Y-%m-%d %H:%M")
|
|
14
|
+
summary = meta.summary[:60] if meta.summary else "(no summary)"
|
|
15
|
+
return f" {idx:>3d}. [{time_str}] {meta.session_id[:8]} {summary}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def pick_session_command() -> str | None:
|
|
19
|
+
"""Show recent sessions and let the user pick one.
|
|
20
|
+
|
|
21
|
+
Returns the chosen session ID or ``None`` if the user cancels.
|
|
22
|
+
"""
|
|
23
|
+
manager = SessionManager()
|
|
24
|
+
sessions = manager.list_sessions(limit=20)
|
|
25
|
+
|
|
26
|
+
if not sessions:
|
|
27
|
+
print("No sessions found.")
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
print("\nRecent sessions:\n")
|
|
31
|
+
for idx, meta in enumerate(sessions, 1):
|
|
32
|
+
print(_format_session(meta, idx))
|
|
33
|
+
print()
|
|
34
|
+
|
|
35
|
+
while True:
|
|
36
|
+
choice = input("Pick a session number (or 'q' to cancel): ").strip()
|
|
37
|
+
if choice.lower() == "q":
|
|
38
|
+
return None
|
|
39
|
+
if choice.isdigit():
|
|
40
|
+
num = int(choice)
|
|
41
|
+
if 1 <= num <= len(sessions):
|
|
42
|
+
selected = sessions[num - 1]
|
|
43
|
+
print(f"\nResuming session {selected.session_id[:8]}...")
|
|
44
|
+
return selected.session_id
|
|
45
|
+
print("Invalid selection, try again.")
|