agentram 0.1.20 → 0.1.29
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.
- package/README.md +159 -146
- package/agentram/cli.py +426 -77
- package/agentram/orchestration.py +159 -28
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/agentram/cli.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
5
5
|
from concurrent.futures import ThreadPoolExecutor
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import shlex
|
|
9
10
|
import shutil
|
|
10
11
|
import textwrap
|
|
@@ -16,6 +17,51 @@ from .config import default_ram_root
|
|
|
16
17
|
from .mcp_server import McpContext, call_tool
|
|
17
18
|
from .orchestration import AgentModelEndpoint, MultiModelCodingOrchestrator
|
|
18
19
|
|
|
20
|
+
|
|
21
|
+
ENV_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
22
|
+
RAW_KEY_PREFIXES = ("sk-", "sk_", "sk-proj-", "gsk_", "xai-", "AIza")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def looks_like_raw_api_key(value: str) -> bool:
|
|
26
|
+
stripped = value.strip()
|
|
27
|
+
if not stripped:
|
|
28
|
+
return False
|
|
29
|
+
if stripped.startswith(RAW_KEY_PREFIXES):
|
|
30
|
+
return True
|
|
31
|
+
return not bool(ENV_NAME_PATTERN.fullmatch(stripped))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def update_env_file_value(path: Path, key: str, value: str) -> None:
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
|
|
37
|
+
assignment = f"{key}={value}"
|
|
38
|
+
updated = False
|
|
39
|
+
output: list[str] = []
|
|
40
|
+
for line in lines:
|
|
41
|
+
if line.strip().startswith(f"{key}="):
|
|
42
|
+
output.append(assignment)
|
|
43
|
+
updated = True
|
|
44
|
+
else:
|
|
45
|
+
output.append(line)
|
|
46
|
+
if not updated:
|
|
47
|
+
if output and output[-1].strip():
|
|
48
|
+
output.append("")
|
|
49
|
+
output.append("# Added by AgentRAM TUI setup")
|
|
50
|
+
output.append(assignment)
|
|
51
|
+
path.write_text("\n".join(output) + "\n", encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_api_key_env(slot: str, value: str) -> tuple[str, str | None]:
|
|
55
|
+
stripped = value.strip()
|
|
56
|
+
if not stripped:
|
|
57
|
+
return "", None
|
|
58
|
+
if looks_like_raw_api_key(stripped):
|
|
59
|
+
env_name = f"AGENTRAM_{slot.upper()}_API_KEY"
|
|
60
|
+
update_env_file_value(Path(".env"), env_name, stripped)
|
|
61
|
+
os.environ[env_name] = stripped
|
|
62
|
+
return env_name, env_name
|
|
63
|
+
return stripped, None
|
|
64
|
+
|
|
19
65
|
SUBAGENTS: dict[str, str] = {
|
|
20
66
|
"memory": "Summarize important facts, decisions, files, risks, and next tasks into AgentRAM events. Do not edit code.",
|
|
21
67
|
"planner": "Break the coding request into small ordered steps. Check AgentRAM goal drift before scope changes.",
|
|
@@ -55,6 +101,135 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
55
101
|
]
|
|
56
102
|
|
|
57
103
|
|
|
104
|
+
|
|
105
|
+
CLAUDE_COMPAT_COMMANDS: list[tuple[str, str]] = [
|
|
106
|
+
("/add-dir <path>", "Claude: add a working directory for file access"),
|
|
107
|
+
("/advisor [model|off]", "Claude: enable or disable advisor model"),
|
|
108
|
+
("/autofix-pr [prompt]", "Claude: cloud session fixes PR CI/review comments"),
|
|
109
|
+
("/background [prompt]", "Claude: detach session as background agent"),
|
|
110
|
+
("/bg [prompt]", "Claude alias: /background"),
|
|
111
|
+
("/batch <instruction>", "Claude workflow: parallel codebase change"),
|
|
112
|
+
("/branch [name]", "Claude: fork conversation branch"),
|
|
113
|
+
("/btw <question>", "Claude: side question without history"),
|
|
114
|
+
("/cd <path>", "Claude: move session to another directory"),
|
|
115
|
+
("/chrome", "Claude: configure Claude in Chrome"),
|
|
116
|
+
("/claude-api [migrate|managed-agents-onboard]", "Claude skill: API reference and migration"),
|
|
117
|
+
("/reset [name]", "Claude alias: /clear"),
|
|
118
|
+
("/new [name]", "Claude alias: /clear"),
|
|
119
|
+
("/code-review [level] [--fix] [--comment] [target]", "Claude skill: review current diff"),
|
|
120
|
+
("/color [color|default]", "Claude: prompt bar color"),
|
|
121
|
+
("/compact [instructions]", "Claude: summarize context to free space"),
|
|
122
|
+
("/config [key=value ...]", "Claude: settings interface"),
|
|
123
|
+
("/settings [key=value ...]", "Claude alias: /config"),
|
|
124
|
+
("/context [all]", "Claude: visualize context usage"),
|
|
125
|
+
("/copy [N]", "Claude: copy assistant response"),
|
|
126
|
+
("/cost", "Claude alias: /usage"),
|
|
127
|
+
("/dataviz [request]", "Claude skill: chart/dashboard design guidance"),
|
|
128
|
+
("/debug [description]", "Claude skill: enable and inspect debug logs"),
|
|
129
|
+
("/deep-research <question>", "Claude workflow: web research report"),
|
|
130
|
+
("/design-login", "Claude: authorize design-system access"),
|
|
131
|
+
("/design-sync [hint]", "Claude skill: sync React design system"),
|
|
132
|
+
("/desktop", "Claude: continue in desktop app"),
|
|
133
|
+
("/app", "Claude alias: /desktop"),
|
|
134
|
+
("/diff", "Claude: interactive diff viewer"),
|
|
135
|
+
("/doctor", "Claude: diagnose install/runtime"),
|
|
136
|
+
("/effort [level|auto]", "Claude: set reasoning effort"),
|
|
137
|
+
("/export [filename]", "Claude: export conversation"),
|
|
138
|
+
("/fast [on|off]", "Claude: toggle fast mode"),
|
|
139
|
+
("/feedback [report]", "Claude: send feedback"),
|
|
140
|
+
("/bug [report]", "Claude alias: /feedback"),
|
|
141
|
+
("/share [report]", "Claude alias: /feedback"),
|
|
142
|
+
("/fewer-permission-prompts", "Claude skill: reduce permission prompts"),
|
|
143
|
+
("/focus", "Claude: toggle focus view"),
|
|
144
|
+
("/fork <directive>", "Claude: forked background subagent"),
|
|
145
|
+
("/heapdump", "Claude: write heap snapshot"),
|
|
146
|
+
("/hooks", "Claude: view hook config"),
|
|
147
|
+
("/ide", "Claude: manage IDE integration"),
|
|
148
|
+
("/insights", "Claude: analyze Claude Code sessions"),
|
|
149
|
+
("/install-github-app", "Claude: install GitHub app"),
|
|
150
|
+
("/install-slack-app", "Claude: install Slack app"),
|
|
151
|
+
("/keybindings", "Claude: open keyboard shortcut file"),
|
|
152
|
+
("/login", "Claude: sign in"),
|
|
153
|
+
("/logout", "Claude: sign out"),
|
|
154
|
+
("/loop [interval] [prompt]", "Claude skill: repeated prompt loop"),
|
|
155
|
+
("/proactive [interval] [prompt]", "Claude alias: /loop"),
|
|
156
|
+
("/mcp [reconnect|enable|disable]", "Claude: manage MCP servers"),
|
|
157
|
+
("/memory", "Claude: edit CLAUDE.md memory"),
|
|
158
|
+
("/mobile", "Claude: show mobile app QR"),
|
|
159
|
+
("/ios", "Claude alias: /mobile"),
|
|
160
|
+
("/android", "Claude alias: /mobile"),
|
|
161
|
+
("/model [model]", "Claude: switch model"),
|
|
162
|
+
("/passes", "Claude: share Claude Code pass"),
|
|
163
|
+
("/permissions", "Claude: manage tool permissions"),
|
|
164
|
+
("/allowed-tools", "Claude alias: /permissions"),
|
|
165
|
+
("/plan [description]", "Claude: enter plan mode"),
|
|
166
|
+
("/plugin [subcommand]", "Claude: manage plugins"),
|
|
167
|
+
("/powerup", "Claude: feature lessons"),
|
|
168
|
+
("/pr-comments [PR]", "Claude legacy: PR comments"),
|
|
169
|
+
("/privacy-settings", "Claude: privacy settings"),
|
|
170
|
+
("/radio", "Claude: Claude FM radio"),
|
|
171
|
+
("/recap", "Claude: one-line session summary"),
|
|
172
|
+
("/release-notes", "Claude: changelog picker"),
|
|
173
|
+
("/reload-plugins [--force]", "Claude: reload plugins"),
|
|
174
|
+
("/reload-skills", "Claude: reload skills and commands"),
|
|
175
|
+
("/remote-control", "Claude: remote control from claude.ai"),
|
|
176
|
+
("/rc", "Claude alias: /remote-control"),
|
|
177
|
+
("/remote-env", "Claude: cloud agent environment"),
|
|
178
|
+
("/rename [name]", "Claude: rename session"),
|
|
179
|
+
("/resume [session]", "Claude: resume conversation"),
|
|
180
|
+
("/continue [session]", "Claude alias: /resume"),
|
|
181
|
+
("/review [PR]", "Claude: review GitHub PR"),
|
|
182
|
+
("/rewind", "Claude: rewind conversation/code"),
|
|
183
|
+
("/checkpoint", "Claude alias: /rewind"),
|
|
184
|
+
("/undo", "Claude alias: /rewind"),
|
|
185
|
+
("/run", "Claude skill: run app and verify change"),
|
|
186
|
+
("/run-skill-generator", "Claude skill: generate run/verify skill"),
|
|
187
|
+
("/sandbox", "Claude: toggle sandbox mode"),
|
|
188
|
+
("/schedule [description]", "Claude: cloud routine"),
|
|
189
|
+
("/routines [description]", "Claude alias: /schedule"),
|
|
190
|
+
("/scroll-speed", "Claude: adjust mouse wheel speed"),
|
|
191
|
+
("/security-review", "Claude: security review current diff"),
|
|
192
|
+
("/setup-bedrock", "Claude: Bedrock setup wizard"),
|
|
193
|
+
("/setup-vertex", "Claude: Vertex setup wizard"),
|
|
194
|
+
("/simplify [target]", "Claude skill: cleanup review and fixes"),
|
|
195
|
+
("/skills", "Claude: list skills"),
|
|
196
|
+
("/stats", "Claude alias: /usage"),
|
|
197
|
+
("/statusline", "Claude: configure status line"),
|
|
198
|
+
("/stickers", "Claude: order stickers"),
|
|
199
|
+
("/stop", "Claude: stop background session"),
|
|
200
|
+
("/tasks", "Claude: view background work"),
|
|
201
|
+
("/bashes", "Claude alias: /tasks"),
|
|
202
|
+
("/team-onboarding", "Claude: generate team onboarding"),
|
|
203
|
+
("/teleport", "Claude: pull web session into terminal"),
|
|
204
|
+
("/tp", "Claude alias: /teleport"),
|
|
205
|
+
("/terminal-setup", "Claude: terminal keybindings setup"),
|
|
206
|
+
("/theme", "Claude: change color theme"),
|
|
207
|
+
("/tui [default|fullscreen]", "Claude: switch renderer"),
|
|
208
|
+
("/ultraplan <prompt>", "Claude: draft cloud plan"),
|
|
209
|
+
("/ultrareview [PR]", "Claude: deep cloud review"),
|
|
210
|
+
("/upgrade", "Claude: open upgrade page"),
|
|
211
|
+
("/usage", "Claude: show usage and cost"),
|
|
212
|
+
("/usage-credits", "Claude: configure usage credits"),
|
|
213
|
+
("/verify", "Claude skill: verify app behavior"),
|
|
214
|
+
("/vim", "Claude legacy: use /config for editor mode"),
|
|
215
|
+
("/voice [hold|tap|off]", "Claude: voice dictation"),
|
|
216
|
+
("/web-setup", "Claude: connect GitHub for web"),
|
|
217
|
+
("/workflows", "Claude: workflow progress view"),
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
CLAUDE_COMPAT_NAMES = {command.split()[0] for command, _ in CLAUDE_COMPAT_COMMANDS}
|
|
221
|
+
|
|
222
|
+
def slash_entries() -> list[tuple[str, str]]:
|
|
223
|
+
seen: set[str] = set()
|
|
224
|
+
entries: list[tuple[str, str]] = []
|
|
225
|
+
for command, description in [*SLASH_COMMANDS, *CLAUDE_COMPAT_COMMANDS]:
|
|
226
|
+
name = command.split()[0]
|
|
227
|
+
if name in seen:
|
|
228
|
+
continue
|
|
229
|
+
seen.add(name)
|
|
230
|
+
entries.append((command, description))
|
|
231
|
+
return entries
|
|
232
|
+
|
|
58
233
|
SLASH_HELP = """Slash commands:
|
|
59
234
|
/help Show commands
|
|
60
235
|
/init Create .agentram files
|
|
@@ -307,6 +482,8 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
307
482
|
self.prompt_history: list[str] = []
|
|
308
483
|
self.history_index: int | None = None
|
|
309
484
|
self.last_ctrl_c_at = 0.0
|
|
485
|
+
self.setup_slots = ("supervisor", "main", "small")
|
|
486
|
+
self.setup_index = 0
|
|
310
487
|
|
|
311
488
|
def compose(self) -> ComposeResult:
|
|
312
489
|
yield Header(show_clock=True)
|
|
@@ -314,18 +491,20 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
314
491
|
yield Static(id="sidebar")
|
|
315
492
|
with Vertical(id="main"):
|
|
316
493
|
with Vertical(id="setup"):
|
|
317
|
-
yield Static("Model setup -
|
|
494
|
+
yield Static("Model setup - 1/3 Supervisor", id="setup-title")
|
|
318
495
|
for slot, label, default_model in [
|
|
319
|
-
("supervisor", "Supervisor", ""),
|
|
320
|
-
("main", "Main coding", ""),
|
|
321
|
-
("small", "Small memory", ""),
|
|
496
|
+
("supervisor", "1. Supervisor", ""),
|
|
497
|
+
("main", "2. Main coding", ""),
|
|
498
|
+
("small", "3. Small memory", ""),
|
|
322
499
|
]:
|
|
323
|
-
with Vertical(classes="agent-setup"):
|
|
500
|
+
with Vertical(id=f"setup-{slot}-panel", classes="agent-setup"):
|
|
324
501
|
yield Static(f"{label} agent")
|
|
325
502
|
yield Select(
|
|
326
503
|
[
|
|
327
504
|
("OpenAI Compatible", "openai-compatible"),
|
|
328
505
|
("Anthropic Compatible", "anthropic"),
|
|
506
|
+
("Claude Code CLI", "claude-code"),
|
|
507
|
+
("Codex CLI", "codex-cli"),
|
|
329
508
|
("Custom", "custom"),
|
|
330
509
|
],
|
|
331
510
|
value="openai-compatible",
|
|
@@ -334,14 +513,16 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
334
513
|
)
|
|
335
514
|
yield Input(placeholder="Custom provider if preset=Custom", id=f"setup-{slot}-custom-provider", classes="setup-input")
|
|
336
515
|
yield Input(value=default_model, placeholder="Model name", id=f"setup-{slot}-model", classes="setup-input")
|
|
337
|
-
yield Input(placeholder="Base URL, e.g. http://localhost:20128/v1", id=f"setup-{slot}-base-url", classes="setup-input")
|
|
338
|
-
yield Input(value="OPENAI_API_KEY", placeholder="API key env name
|
|
516
|
+
yield Input(placeholder="Base URL or CLI command, e.g. http://localhost:20128/v1 / claude / codex exec", id=f"setup-{slot}-base-url", classes="setup-input")
|
|
517
|
+
yield Input(value="OPENAI_API_KEY", placeholder="API key or env name", id=f"setup-{slot}-api-key-env", classes="setup-input")
|
|
339
518
|
with Horizontal(id="setup-actions"):
|
|
340
|
-
yield Button("
|
|
519
|
+
yield Button("Back", id="setup-prev")
|
|
520
|
+
yield Button("Next", id="setup-next", variant="primary")
|
|
521
|
+
yield Button("Save setup", id="setup-save", variant="success")
|
|
341
522
|
yield Button("Skip", id="setup-skip")
|
|
342
523
|
yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
|
|
343
524
|
yield Static(id="palette")
|
|
344
|
-
yield Input(placeholder="Prompt
|
|
525
|
+
yield Input(placeholder="Prompt or /command. Mouse wheel scroll in chat.", id="input")
|
|
345
526
|
yield Footer()
|
|
346
527
|
|
|
347
528
|
def on_mount(self) -> None:
|
|
@@ -349,10 +530,11 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
349
530
|
self.chat.write("system > AgentRAM Textual TUI ready")
|
|
350
531
|
self.chat.write(f"system > RAM root: {self.context.ram_root}")
|
|
351
532
|
self.prefill_setup_from_profile()
|
|
533
|
+
self.refresh_setup_step()
|
|
352
534
|
self.refresh_sidebar()
|
|
353
535
|
self.set_interval(1.0, self.refresh_sidebar)
|
|
354
536
|
try:
|
|
355
|
-
self.
|
|
537
|
+
self.focus_current_setup_model()
|
|
356
538
|
except Exception: # noqa: BLE001 - fallback if setup is hidden/unmounted
|
|
357
539
|
self.query_one("#input", Input).focus()
|
|
358
540
|
|
|
@@ -372,7 +554,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
372
554
|
if not endpoint:
|
|
373
555
|
continue
|
|
374
556
|
provider = endpoint.provider
|
|
375
|
-
preset = provider if provider in {"openai-compatible", "anthropic"} else "custom"
|
|
557
|
+
preset = provider if provider in {"openai-compatible", "anthropic", "claude-code", "codex-cli"} else "custom"
|
|
376
558
|
self.query_one(f"#setup-{slot}-provider", Select).value = preset
|
|
377
559
|
self.query_one(f"#setup-{slot}-custom-provider", Input).value = "" if preset != "custom" else provider
|
|
378
560
|
self.query_one(f"#setup-{slot}-model", Input).value = endpoint.model
|
|
@@ -380,6 +562,37 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
380
562
|
self.query_one(f"#setup-{slot}-api-key-env", Input).value = endpoint.api_key_env
|
|
381
563
|
self.chat.write("system > Existing workflow models loaded. Save to update, or Skip to keep.")
|
|
382
564
|
|
|
565
|
+
def current_setup_slot(self) -> str:
|
|
566
|
+
return self.setup_slots[self.setup_index]
|
|
567
|
+
|
|
568
|
+
def current_setup_label(self) -> str:
|
|
569
|
+
labels = {"supervisor": "Supervisor", "main": "Main coding", "small": "Small memory"}
|
|
570
|
+
return labels[self.current_setup_slot()]
|
|
571
|
+
|
|
572
|
+
def refresh_setup_step(self) -> None:
|
|
573
|
+
total = len(self.setup_slots)
|
|
574
|
+
slot = self.current_setup_slot()
|
|
575
|
+
self.query_one("#setup-title", Static).update(f"Model setup - {self.setup_index + 1}/{total} {self.current_setup_label()}")
|
|
576
|
+
for item in self.setup_slots:
|
|
577
|
+
self.query_one(f"#setup-{item}-panel", Vertical).styles.display = "block" if item == slot else "none"
|
|
578
|
+
|
|
579
|
+
def focus_current_setup_model(self) -> None:
|
|
580
|
+
self.query_one(f"#setup-{self.current_setup_slot()}-model", Input).focus()
|
|
581
|
+
|
|
582
|
+
def action_next_setup(self) -> None:
|
|
583
|
+
if self.setup_index < len(self.setup_slots) - 1:
|
|
584
|
+
self.setup_index += 1
|
|
585
|
+
self.refresh_setup_step()
|
|
586
|
+
self.focus_current_setup_model()
|
|
587
|
+
else:
|
|
588
|
+
self.action_save_setup()
|
|
589
|
+
|
|
590
|
+
def action_prev_setup(self) -> None:
|
|
591
|
+
if self.setup_index > 0:
|
|
592
|
+
self.setup_index -= 1
|
|
593
|
+
self.refresh_setup_step()
|
|
594
|
+
self.focus_current_setup_model()
|
|
595
|
+
|
|
383
596
|
def setup_value(self, widget_id: str) -> str:
|
|
384
597
|
return self.query_one(f"#{widget_id}", Input).value.strip()
|
|
385
598
|
|
|
@@ -396,10 +609,8 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
396
609
|
provider = self.setup_provider(slot)
|
|
397
610
|
model = self.setup_value(f"setup-{slot}-model")
|
|
398
611
|
base_url = self.setup_value(f"setup-{slot}-base-url")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
self.chat.write(f"agentram > setup error: {slot} api key env must be variable name, not raw key")
|
|
402
|
-
return
|
|
612
|
+
api_key_input = self.setup_value(f"setup-{slot}-api-key-env")
|
|
613
|
+
api_key_env, saved_env_name = resolve_api_key_env(slot, api_key_input)
|
|
403
614
|
if not model:
|
|
404
615
|
continue
|
|
405
616
|
endpoint = AgentModelEndpoint(
|
|
@@ -411,7 +622,10 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
411
622
|
modalities=["text"],
|
|
412
623
|
)
|
|
413
624
|
self.context.profile_store.set_agent(slot, endpoint)
|
|
414
|
-
|
|
625
|
+
if saved_env_name:
|
|
626
|
+
bound.append(f"{slot}={provider}:{model} key=.env:{saved_env_name}")
|
|
627
|
+
else:
|
|
628
|
+
bound.append(f"{slot}={provider}:{model}")
|
|
415
629
|
if not bound:
|
|
416
630
|
self.chat.write("agentram > setup skipped: no model entered")
|
|
417
631
|
else:
|
|
@@ -426,8 +640,10 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
426
640
|
|
|
427
641
|
def show_setup(self) -> None:
|
|
428
642
|
self.setup_done = False
|
|
643
|
+
self.setup_index = 0
|
|
429
644
|
self.query_one("#setup", Vertical).styles.display = "block"
|
|
430
|
-
self.
|
|
645
|
+
self.refresh_setup_step()
|
|
646
|
+
self.focus_current_setup_model()
|
|
431
647
|
|
|
432
648
|
def action_history_prev(self) -> None:
|
|
433
649
|
input_widget = self.query_one("#input", Input)
|
|
@@ -468,6 +684,10 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
468
684
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
469
685
|
if event.button.id == "setup-save":
|
|
470
686
|
self.action_save_setup()
|
|
687
|
+
elif event.button.id == "setup-next":
|
|
688
|
+
self.action_next_setup()
|
|
689
|
+
elif event.button.id == "setup-prev":
|
|
690
|
+
self.action_prev_setup()
|
|
471
691
|
elif event.button.id == "setup-skip":
|
|
472
692
|
self.chat.write("system > Model setup skipped. Use /bind-agent later.")
|
|
473
693
|
self.hide_setup()
|
|
@@ -488,7 +708,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
488
708
|
return
|
|
489
709
|
query = value.strip().lower().lstrip("/")
|
|
490
710
|
matches = []
|
|
491
|
-
for command, description in
|
|
711
|
+
for command, description in slash_entries():
|
|
492
712
|
if not command.startswith("/"):
|
|
493
713
|
continue
|
|
494
714
|
command_name = command.split()[0]
|
|
@@ -500,7 +720,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
500
720
|
if not matches:
|
|
501
721
|
palette.update("No matching slash command")
|
|
502
722
|
else:
|
|
503
|
-
lines = ["Slash commands
|
|
723
|
+
lines = ["Slash commands - Tab completes first match"]
|
|
504
724
|
lines.extend(f" {command.ljust(18)} {description}" for command, description in matches[:8])
|
|
505
725
|
palette.update("\n".join(lines))
|
|
506
726
|
palette.styles.display = "block"
|
|
@@ -524,7 +744,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
524
744
|
|
|
525
745
|
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
526
746
|
if event.input.id and str(event.input.id).startswith("setup-"):
|
|
527
|
-
self.
|
|
747
|
+
self.action_next_setup()
|
|
528
748
|
return
|
|
529
749
|
text = event.value.strip()
|
|
530
750
|
event.input.value = ""
|
|
@@ -584,11 +804,11 @@ def run_with_activity(context: McpContext, messages: list[str], user_input: str)
|
|
|
584
804
|
|
|
585
805
|
|
|
586
806
|
def slash_command_names() -> list[str]:
|
|
587
|
-
return [command.split()[0] for command, _ in
|
|
807
|
+
return [command.split()[0] for command, _ in slash_entries() if command.startswith("/")]
|
|
588
808
|
|
|
589
809
|
|
|
590
810
|
def slash_command_meta() -> dict[str, str]:
|
|
591
|
-
return {command.split()[0]: description for command, description in
|
|
811
|
+
return {command.split()[0]: description for command, description in slash_entries() if command.startswith("/")}
|
|
592
812
|
|
|
593
813
|
|
|
594
814
|
def create_prompt_session() -> Any | None:
|
|
@@ -608,7 +828,7 @@ def create_prompt_session() -> Any | None:
|
|
|
608
828
|
completer=completer,
|
|
609
829
|
complete_while_typing=True,
|
|
610
830
|
reserve_space_for_menu=12,
|
|
611
|
-
bottom_toolbar="Type / for slash commands
|
|
831
|
+
bottom_toolbar="Type / for slash commands | /agents for subagents | /ask to call bound models",
|
|
612
832
|
)
|
|
613
833
|
|
|
614
834
|
|
|
@@ -708,9 +928,25 @@ def handle_slash_command(context: McpContext, text: str, messages: list[str] | N
|
|
|
708
928
|
return note_text(context, raw_rest)
|
|
709
929
|
if command == "/clear":
|
|
710
930
|
return "__clear__"
|
|
931
|
+
if command in CLAUDE_COMPAT_NAMES:
|
|
932
|
+
return claude_compat_command_text(command, raw_rest)
|
|
711
933
|
return f"Unknown slash command: {command}\n\n{SLASH_HELP}"
|
|
712
934
|
|
|
713
935
|
|
|
936
|
+
def claude_compat_command_text(command: str, arguments: str) -> str:
|
|
937
|
+
descriptions = slash_command_meta()
|
|
938
|
+
description = descriptions.get(command, "Claude Code command")
|
|
939
|
+
return "\n".join(
|
|
940
|
+
[
|
|
941
|
+
f"Claude-compatible command: {command}",
|
|
942
|
+
f"Purpose: {description}",
|
|
943
|
+
f"Args: {arguments or '-'}",
|
|
944
|
+
"Status: listed for Claude parity in AgentRAM TUI.",
|
|
945
|
+
"Native execution is not implemented here unless AgentRAM has its own handler for this command.",
|
|
946
|
+
"Run the same command inside Claude Code for Claude-native behavior.",
|
|
947
|
+
]
|
|
948
|
+
)
|
|
949
|
+
|
|
714
950
|
def init_text(context: McpContext) -> str:
|
|
715
951
|
data = payload(call_tool(context, "agentram_init", {}))
|
|
716
952
|
created = data.get("created", [])
|
|
@@ -805,7 +1041,7 @@ def agents_text() -> str:
|
|
|
805
1041
|
def slash_palette_text(filter_text: str = "") -> str:
|
|
806
1042
|
normalized = filter_text.strip().lower().lstrip("/")
|
|
807
1043
|
rows = []
|
|
808
|
-
for command, description in
|
|
1044
|
+
for command, description in slash_entries():
|
|
809
1045
|
command_key = command.lower().lstrip("/")
|
|
810
1046
|
if normalized and normalized not in command_key:
|
|
811
1047
|
continue
|
|
@@ -946,45 +1182,154 @@ def workflow_text(context: McpContext) -> str:
|
|
|
946
1182
|
lines.append(f"- {slot}: not bound")
|
|
947
1183
|
return "\n".join(lines)
|
|
948
1184
|
|
|
949
|
-
def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
|
|
950
|
-
agents = context.profile_store.list_agents()
|
|
951
|
-
supervisor = agents.get("supervisor")
|
|
952
|
-
main_agent = agents.get("main")
|
|
953
|
-
small_agent = agents.get("small")
|
|
954
|
-
memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1185
|
+
def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
|
|
1186
|
+
agents = context.profile_store.list_agents()
|
|
1187
|
+
supervisor = agents.get("supervisor")
|
|
1188
|
+
main_agent = agents.get("main")
|
|
1189
|
+
small_agent = agents.get("small")
|
|
1190
|
+
memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
|
|
1191
|
+
route = classify_agent_runtime_route(context, prompt)
|
|
1192
|
+
intent = str(route.get("intent", "chat"))
|
|
1193
|
+
outputs: dict[str, str] = {}
|
|
1194
|
+
|
|
1195
|
+
if intent == "planning":
|
|
1196
|
+
plan = supervisor_plan(supervisor, prompt, memory_context)
|
|
1197
|
+
outputs["supervisor"] = plan
|
|
1198
|
+
outputs["small"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
|
|
1199
|
+
sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
|
|
1200
|
+
persist_workflow_memory(context, prompt, plan, outputs)
|
|
1201
|
+
return "\n".join(
|
|
1202
|
+
[
|
|
1203
|
+
f"Workflow: route=planning confidence={float(route.get('confidence', 0.0)):.2f} -> supervisor -> small memory note -> merge",
|
|
1204
|
+
"Supervisor plan:",
|
|
1205
|
+
plan,
|
|
1206
|
+
"",
|
|
1207
|
+
"Small memory agent notes:",
|
|
1208
|
+
outputs.get("small", "No small agent bound."),
|
|
1209
|
+
]
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
if intent == "memory":
|
|
1213
|
+
plan = "Memory-only event. Small agent writes RAM note directly."
|
|
1214
|
+
outputs["small"] = small_agent_observe(context, small_agent, prompt, "memory_event", prompt, plan, memory_context)
|
|
1215
|
+
sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
|
|
1216
|
+
persist_workflow_memory(context, prompt, plan, outputs)
|
|
1217
|
+
return "\n".join(
|
|
1218
|
+
[
|
|
1219
|
+
f"Workflow: route=memory confidence={float(route.get('confidence', 0.0)):.2f} -> small memory note -> merge",
|
|
1220
|
+
"Small memory agent notes:",
|
|
1221
|
+
outputs.get("small", "No small agent bound."),
|
|
1222
|
+
]
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
|
|
1226
|
+
outputs["supervisor"] = plan
|
|
1227
|
+
outputs["small_after_plan"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
|
|
1228
|
+
if main_agent:
|
|
1229
|
+
try:
|
|
1230
|
+
outputs["main"] = call_agent_endpoint(main_agent, build_main_agent_prompt(prompt, plan, memory_context))
|
|
1231
|
+
except Exception as error: # noqa: BLE001 - agent boundary
|
|
1232
|
+
outputs["main"] = f"error: {model_error_hint(str(error))}"
|
|
1233
|
+
else:
|
|
1234
|
+
outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
|
|
1235
|
+
outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs.get("main", ""), plan, memory_context)
|
|
1236
|
+
sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
|
|
1237
|
+
persist_workflow_memory(context, prompt, plan, outputs)
|
|
1238
|
+
return "\n".join(
|
|
1239
|
+
[
|
|
1240
|
+
f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main -> small memory note -> merge",
|
|
1241
|
+
"Supervisor plan:",
|
|
1242
|
+
plan,
|
|
1243
|
+
"",
|
|
1244
|
+
"Main agent result:",
|
|
1245
|
+
outputs.get("main", "-"),
|
|
1246
|
+
"",
|
|
1247
|
+
"Small memory agent after plan:",
|
|
1248
|
+
outputs.get("small_after_plan", "No small agent bound."),
|
|
1249
|
+
"",
|
|
1250
|
+
"Small memory agent notes:",
|
|
1251
|
+
outputs.get("small", "No small agent bound."),
|
|
1252
|
+
]
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
def classify_agent_runtime_route(context: McpContext, prompt: str) -> dict[str, object]:
|
|
1256
|
+
lower = prompt.lower()
|
|
1257
|
+
execute_words = [
|
|
1258
|
+
"bắt đầu làm", "bat dau lam", "làm đi", "lam di", "tiến hành", "tien hanh", "thực hiện", "thuc hien",
|
|
1259
|
+
"chốt kế hoạch", "chot ke hoach", "bắt tay", "start", "execute", "implement", "do it", "go ahead",
|
|
1260
|
+
]
|
|
1261
|
+
plan_words = ["lên kế hoạch", "len ke hoach", "kế hoạch", "ke hoach", "plan", "roadmap", "milestone", "thiết kế", "thiet ke"]
|
|
1262
|
+
if any(word in lower for word in execute_words):
|
|
1263
|
+
return {"intent": "execution", "confidence": 0.9, "source": "heuristic"}
|
|
1264
|
+
if any(word in lower for word in plan_words):
|
|
1265
|
+
return {"intent": "planning", "confidence": 0.85, "source": "heuristic"}
|
|
1266
|
+
decision = classify_prompt_intent(context, prompt)
|
|
1267
|
+
intent = str(decision.get("intent", "chat"))
|
|
1268
|
+
if intent == "coding":
|
|
1269
|
+
decision["intent"] = "execution"
|
|
1270
|
+
return decision
|
|
1271
|
+
|
|
1272
|
+
def runtime_execution_plan(context: McpContext, supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str, route: dict[str, object]) -> str:
|
|
1273
|
+
state = context.goal_store.load().context_block()
|
|
1274
|
+
if state:
|
|
1275
|
+
return "\n".join(["Use current accepted Goal Stack as execution plan.", state])
|
|
1276
|
+
if str(route.get("intent")) == "execution" and any(word in prompt.lower() for word in ["bắt đầu", "bat dau", "làm đi", "lam di", "execute", "do it"]):
|
|
1277
|
+
return "User approved previous plan. Use AgentRAM memory context and current prompt to execute next concrete task."
|
|
1278
|
+
return supervisor_plan(supervisor, prompt, memory_context)
|
|
1279
|
+
|
|
1280
|
+
def small_agent_observe(context: McpContext, small_agent: AgentModelEndpoint | None, prompt: str, event_type: str, event_content: str, plan: str, memory_context: str) -> str:
|
|
1281
|
+
if not small_agent:
|
|
1282
|
+
return "No small agent bound."
|
|
1283
|
+
try:
|
|
1284
|
+
return call_agent_endpoint(
|
|
1285
|
+
small_agent,
|
|
1286
|
+
build_small_agent_prompt(prompt, plan, memory_context, event_content, event_type=event_type),
|
|
1287
|
+
)
|
|
1288
|
+
except Exception as error: # noqa: BLE001 - agent boundary
|
|
1289
|
+
return f"error: {model_error_hint(str(error))}"
|
|
1290
|
+
|
|
1291
|
+
def sync_goal_state_from_small_agent(context: McpContext, prompt: str, plan: str, small_notes: str) -> None:
|
|
1292
|
+
if not small_notes.strip() or small_notes.strip().startswith("error:"):
|
|
1293
|
+
return
|
|
1294
|
+
state = context.goal_store.load()
|
|
1295
|
+
goal = extract_labeled_value(small_notes, ("current_goal", "goal", "mục tiêu", "muc tieu")) or first_nonempty_line(plan)
|
|
1296
|
+
task = extract_labeled_value(small_notes, ("current_task", "task", "nhiệm vụ", "nhiem vu")) or first_nonempty_line(small_notes) or prompt
|
|
1297
|
+
patch: dict[str, object] = {
|
|
1298
|
+
"current_task": trim(task.strip(), 180),
|
|
1299
|
+
"goal_history": [trim(goal.strip() or prompt.strip(), 180)],
|
|
1300
|
+
}
|
|
1301
|
+
if not state.mission:
|
|
1302
|
+
patch["mission"] = "Xây dựng và vận hành AgentRAM cho coding agents."
|
|
1303
|
+
if not state.project_goal:
|
|
1304
|
+
patch["project_goal"] = "AgentRAM workflow: supervisor lập kế hoạch, main coding agent thực thi, small memory agent ghi RAM."
|
|
1305
|
+
if goal:
|
|
1306
|
+
patch["current_goal"] = trim(goal.strip(), 180)
|
|
1307
|
+
context.goal_store.update(patch)
|
|
1308
|
+
|
|
1309
|
+
def extract_labeled_value(text: str, labels: tuple[str, ...]) -> str:
|
|
1310
|
+
for line in text.splitlines():
|
|
1311
|
+
cleaned = line.strip().lstrip("-*").strip()
|
|
1312
|
+
for label in labels:
|
|
1313
|
+
pattern = rf"^{re.escape(label)}\s*[:=]\s*(.+)$"
|
|
1314
|
+
match = re.match(pattern, cleaned, flags=re.IGNORECASE)
|
|
1315
|
+
if match:
|
|
1316
|
+
return match.group(1).strip().strip("`*_-")
|
|
1317
|
+
return ""
|
|
1318
|
+
|
|
1319
|
+
def first_nonempty_line(value: str) -> str:
|
|
1320
|
+
for line in value.splitlines():
|
|
1321
|
+
cleaned = line.strip().lstrip("-0123456789. )")
|
|
1322
|
+
if cleaned and not cleaned.lower().startswith(("supervisor error", "fallback plan")):
|
|
1323
|
+
return cleaned
|
|
1324
|
+
return ""
|
|
1325
|
+
|
|
1326
|
+
def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
|
|
1327
|
+
small_notes = "\n".join(
|
|
1328
|
+
value.strip()
|
|
1329
|
+
for key, value in outputs.items()
|
|
1330
|
+
if key.startswith("small") and value.strip() and not value.strip().startswith("error:")
|
|
1331
|
+
)
|
|
1332
|
+
main_result = outputs.get("main", "").strip()
|
|
988
1333
|
note_parts = [
|
|
989
1334
|
f"User request: {prompt}",
|
|
990
1335
|
f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
|
|
@@ -1015,14 +1360,14 @@ def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_c
|
|
|
1015
1360
|
def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
1016
1361
|
return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
|
|
1017
1362
|
|
|
1018
|
-
def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
|
|
1019
|
-
return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Create concise plan
|
|
1020
|
-
|
|
1021
|
-
def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
1022
|
-
return "\n".join(["You are AgentRAM main coding agent.", "Follow supervisor plan. Focus on planning, orchestration, coding, refactor, bug fix, and final result.", "Do not invent repo facts. Mention files to inspect/change.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
|
|
1023
|
-
|
|
1024
|
-
def build_small_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
1025
|
-
return "\n".join(["You are AgentRAM small memory agent.", "Do not edit code.
|
|
1363
|
+
def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
|
|
1364
|
+
return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Create a concise plan only. Do not code. Main agent runs later only after user approves/starts execution.", "Small memory agent observes this plan and writes RAM notes.", "Return only actionable plan. No markdown table.", "User request:", prompt, "AgentRAM context:", memory_context])
|
|
1365
|
+
|
|
1366
|
+
def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
1367
|
+
return "\n".join(["You are AgentRAM main coding agent.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Follow supervisor plan. Focus on planning, orchestration, coding, refactor, bug fix, and final result.", "Do not invent repo facts. Mention files to inspect/change.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
|
|
1368
|
+
|
|
1369
|
+
def build_small_agent_prompt(prompt: str, plan: str, memory_context: str, event_content: str = "", event_type: str = "main_result") -> str:
|
|
1370
|
+
return "\n".join(["You are AgentRAM small memory agent.", "Nếu user prompt bằng tiếng Việt, ghi note RAM bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "You participate in every AgentRAM event directly: user request, supervisor plan, main result, compact/merge.", "Do not edit code. Observe event content, then note decisions, risks, files, next tasks, and coding facts for RAM.", "You are responsible for setting the visible RAM goal/task from the latest event.", "Return concise memory notes only. Không dùng tiếng Anh nếu user đang dùng tiếng Việt.", "Bắt buộc gồm 2 dòng đầu theo format: current_goal: ... và current_task: ...", "User request:", prompt, "Supervisor plan:", plan, "Observed event type:", event_type, "Observed event content:", event_content or "-", "AgentRAM context:", memory_context])
|
|
1026
1371
|
|
|
1027
1372
|
def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
|
|
1028
1373
|
api_key_env = str(options.get("api_key_env", ""))
|
|
@@ -1107,9 +1452,9 @@ def extract_json_object(text: str) -> str:
|
|
|
1107
1452
|
|
|
1108
1453
|
def heuristic_intent(prompt: str) -> dict[str, object]:
|
|
1109
1454
|
lower = prompt.lower()
|
|
1110
|
-
memory_words = ["ghi
|
|
1111
|
-
planning_words = ["goal", "
|
|
1112
|
-
coding_words = ["
|
|
1455
|
+
memory_words = ["ghi nhớ", "remember", "note", "decision", "lưu lại", "save this"]
|
|
1456
|
+
planning_words = ["goal", "kế hoạch", "ke hoach", "milestone", "mission", "drift", "next task", "roadmap", "mục tiêu", "muc tieu"]
|
|
1457
|
+
coding_words = ["sá»a", "sua", "fix", "bug", "code", "test", "refactor", "review", "file", ".py", ".js", "endpoint", "class", "function"]
|
|
1113
1458
|
if any(word in lower for word in memory_words):
|
|
1114
1459
|
return {"intent": "memory", "confidence": 0.75}
|
|
1115
1460
|
if any(word in lower for word in planning_words):
|
|
@@ -1144,12 +1489,16 @@ def model_error_hint(message: str) -> str:
|
|
|
1144
1489
|
return message + " | Fix: /remove-model <id> or re-bind with --base-url http://localhost:8000/v1"
|
|
1145
1490
|
if "actively refused" in lower or "connection refused" in lower or "winerror 10061" in lower:
|
|
1146
1491
|
return message + " | Fix: start your OpenAI-compatible server at base_url, or change --base-url."
|
|
1492
|
+
if "http 403" in lower or "403" in lower and "groq" in lower:
|
|
1493
|
+
return message + " | Fix: Groq rejected auth/model. Rotate/check API key, ensure provider=openai-compatible, base_url=https://api.groq.com/openai/v1, and use a Groq-supported model name."
|
|
1147
1494
|
if "http 404" in lower or "404: not found" in lower:
|
|
1148
1495
|
return message + " | Fix: verify base_url includes /v1, provider supports /chat/completions, and model name exists."
|
|
1149
1496
|
if "http 400" in lower and "not supported" in lower:
|
|
1150
1497
|
return message + " | Fix: this account/provider rejects that model. Bind a supported model name from your local endpoint."
|
|
1151
1498
|
if "returned non-json" in lower or "expecting value" in lower:
|
|
1152
1499
|
return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
|
|
1500
|
+
if "command not found" in lower or "winerror 2" in lower or "the system cannot find the file" in lower:
|
|
1501
|
+
return message + r" | Fix: install the CLI or set Main base_url to full command path. For Codex use base_url='C:\Users\admin\AppData\Roaming\npm\codex.cmd exec' or paste codex.cmd path; AgentRAM auto-adds exec when missing."
|
|
1153
1502
|
if "missing api key env" in lower:
|
|
1154
1503
|
return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
|
|
1155
1504
|
return message
|
|
@@ -1226,7 +1575,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
|
|
|
1226
1575
|
left_width = 30
|
|
1227
1576
|
right_width = width - left_width - 3
|
|
1228
1577
|
print("+" + "-" * (width - 2) + "+")
|
|
1229
|
-
print(box_text("
|
|
1578
|
+
print(box_text("AgentRAM Code | RAM + Claude subagents + slash commands", width))
|
|
1230
1579
|
print("+" + "-" * (width - 2) + "+")
|
|
1231
1580
|
|
|
1232
1581
|
sidebar = sidebar_lines(context)
|
|
@@ -1281,7 +1630,7 @@ def sidebar_lines(context: McpContext) -> list[str]:
|
|
|
1281
1630
|
|
|
1282
1631
|
|
|
1283
1632
|
def trim(text: str, limit: int) -> str:
|
|
1284
|
-
return text if len(text) <= limit else text[: limit -
|
|
1633
|
+
return text if len(text) <= limit else text[: limit - 3] + "..."
|
|
1285
1634
|
|
|
1286
1635
|
|
|
1287
1636
|
def menu_lines() -> list[str]:
|