Skip to content

feat: Add slash commands to chat REPL#20

Open
KylinMountain wants to merge 14 commits intomainfrom
feat/chat-slash-commands
Open

feat: Add slash commands to chat REPL#20
KylinMountain wants to merge 14 commits intomainfrom
feat/chat-slash-commands

Conversation

@KylinMountain
Copy link
Copy Markdown
Collaborator

@KylinMountain KylinMountain commented Apr 12, 2026

Summary

  • Add /status, /list, /lint, /add <path> slash commands to the interactive chat REPL
  • Extract print_status(), print_list(), run_lint() from CLI command callbacks into reusable functions so both CLI and chat REPL share the same logic
  • /add uses asyncio.to_thread() to wrap add_single_file (which internally calls asyncio.run() and can't be nested in the REPL's event loop)
  • Strip surrounding quotes from slash command arguments (supports paths with spaces)
  • Tab completion for slash commands (/st/status) and file paths after /add
  • Persist input history across sessions via FileHistory (up/down arrows recall previous inputs)

Test plan

  • python -m pytest tests/ — 213 passed
  • openkb chat/help shows new commands
  • /status displays KB status
  • /list lists documents
  • /lint runs structural + knowledge lint
  • /add <path> adds a document from within chat
  • /add '/path/with spaces' works with quoted paths
  • Tab completes slash commands and file paths
  • Up/down arrows recall previous inputs across sessions
  • Verify CLI commands (openkb status, openkb list, openkb lint) still work unchanged

@KylinMountain
Copy link
Copy Markdown
Collaborator Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@KylinMountain KylinMountain changed the title Add slash commands to chat REPL feat: Add slash commands to chat REPL Apr 12, 2026
@KylinMountain KylinMountain requested a review from rejojer April 12, 2026 01:47
Extract core logic from CLI commands into reusable functions
(print_status, print_list, run_lint) so the chat REPL can call
them directly instead of duplicating code. /add uses
asyncio.to_thread since _add_single_file internally calls
asyncio.run which cannot be nested.
- Rename _add_single_file → add_single_file (public API, consistent
  with print_status/print_list/run_lint already being public)
- Add progress counter [i/total] for /add when processing directories
- Move `import asyncio` to module-level imports in chat.py
- Add 12 tests covering all new slash commands in _handle_slash
When users type /add '/path/to file' the quotes were included as
part of the path, causing "Path does not exist" errors. Now single
and double quotes wrapping the argument are stripped.
- Tab on / prefix completes slash commands (/status, /list, etc.)
- Tab after /add completes file paths with expanduser support
- Handles quoted paths (strips leading quote before path completion)
Store prompt history in .openkb/chat_history so users can press
up/down arrows to recall previous inputs across chat sessions.
@KylinMountain KylinMountain force-pushed the feat/chat-slash-commands branch from a3446d1 to 09743c2 Compare April 12, 2026 01:59
@KylinMountain
Copy link
Copy Markdown
Collaborator Author

Code review

Found 1 issue:

  1. Test renamed to testadd_single_file_calls_helper (missing underscore) — pytest requires the test_ prefix to discover tests, so this test is now silently skipped. The rename was done while updating the mock target from _add_single_file to add_single_file, but accidentally dropped the underscore separator between test and add.

def testadd_single_file_calls_helper(self, tmp_path):
kb_dir = self._setup_kb(tmp_path)
doc = tmp_path / "test.md"
doc.write_text("# Hello")
runner = CliRunner()
with patch("openkb.cli.add_single_file") as mock_add, \
patch("openkb.cli._find_kb_dir", return_value=kb_dir):
result = runner.invoke(cli, ["add", str(doc)])
mock_add.assert_called_once_with(doc, kb_dir)

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@KylinMountain
Copy link
Copy Markdown
Collaborator Author

Code review

Found 2 issues:

  1. Test method testadd_single_file_calls_helper is silently skipped by pytest — missing underscore after test

The rename from _add_single_fileadd_single_file correctly updated the patch target in this test, but accidentally dropped the _ between test and add in the method name (was test_add_single_file_calls_helper). pytest only discovers methods starting with test_, so this test is never collected or run.

def testadd_single_file_calls_helper(self, tmp_path):
kb_dir = self._setup_kb(tmp_path)
doc = tmp_path / "test.md"
doc.write_text("# Hello")
runner = CliRunner()
with patch("openkb.cli.add_single_file") as mock_add, \
patch("openkb.cli._find_kb_dir", return_value=kb_dir):
result = runner.invoke(cli, ["add", str(doc)])
mock_add.assert_called_once_with(doc, kb_dir)

  1. run_lint declares return type -> Path but returns None on the early-exit path when the KB has no indexed documents

The return on line 547 (bare, no value) contradicts the -> Path annotation added in this PR. Current callers discard the return value so there is no runtime crash, but the function contract as documented is broken, and future callers relying on the return value will get None.

OpenKB/openkb/cli.py

Lines 528 to 547 in 09743c2

async def run_lint(kb_dir: Path) -> Path:
"""Run structural + knowledge lint, write report, return report path.
Async because knowledge lint uses an LLM agent. Usable from CLI
(via ``asyncio.run``) and directly from the chat REPL.
"""
from openkb.lint import run_structural_lint
from openkb.agent.linter import run_knowledge_lint
openkb_dir = kb_dir / ".openkb"
# Skip lint entirely when the KB has no indexed documents
hashes_file = openkb_dir / "hashes.json"
if hashes_file.exists():
hashes = json.loads(hashes_file.read_text(encoding="utf-8"))
else:
hashes = {}
if not hashes:
click.echo("Nothing to lint — no documents indexed yet. Run `openkb add` first.")
return

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@KylinMountain
Copy link
Copy Markdown
Collaborator Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- Show command descriptions next to completions (e.g. /status → "Show
  knowledge base status")
- Switch completion display to MULTI_COLUMN (zsh-like, listed below
  the prompt instead of a dropdown menu)
- Filter out dotfiles from /add path completion unless the user
  explicitly types a dot prefix (e.g. /add ./. will show dotfiles)
- Restyle completion menu: transparent background, subtle text colors,
  no heavy inverted bar
Disable complete_while_typing so completions only appear on Tab
press. Subsequent Tabs cycle through candidates, like zsh.
Custom key bindings so that:
- Tab with no menu open: trigger completion
- Arrow keys: navigate candidates in the menu
- Tab with menu open: accept current selection and insert
Guard against current_completion being None when the menu is open
but no item is highlighted. Falls back to selecting the first item.
When only one candidate matches, Tab inserts it directly without
requiring arrow key selection first.
@KylinMountain
Copy link
Copy Markdown
Collaborator Author

Code review

Found 2 issues:

  1. run_lint returns None when the KB is empty, violating its -> Path return type annotation. The early-exit branch at line 547 hits a bare return (no value), but the function signature declares -> Path. This means callers that use the return value (e.g. any future code) will get None instead of a Path and may fail at runtime.

OpenKB/openkb/cli.py

Lines 528 to 547 in adbb200

async def run_lint(kb_dir: Path) -> Path:
"""Run structural + knowledge lint, write report, return report path.
Async because knowledge lint uses an LLM agent. Usable from CLI
(via ``asyncio.run``) and directly from the chat REPL.
"""
from openkb.lint import run_structural_lint
from openkb.agent.linter import run_knowledge_lint
openkb_dir = kb_dir / ".openkb"
# Skip lint entirely when the KB has no indexed documents
hashes_file = openkb_dir / "hashes.json"
if hashes_file.exists():
hashes = json.loads(hashes_file.read_text(encoding="utf-8"))
else:
hashes = {}
if not hashes:
click.echo("Nothing to lint — no documents indexed yet. Run `openkb add` first.")
return

  1. Test method was renamed from test_add_single_file_calls_helper to testadd_single_file_calls_helper (underscore dropped between test and add). pytest requires the test_ prefix to discover tests, so this test is now silently skipped and no longer runs.

def testadd_single_file_calls_helper(self, tmp_path):
kb_dir = self._setup_kb(tmp_path)
doc = tmp_path / "test.md"
doc.write_text("# Hello")
runner = CliRunner()
with patch("openkb.cli.add_single_file") as mock_add, \
patch("openkb.cli._find_kb_dir", return_value=kb_dir):
result = runner.invoke(cli, ["add", str(doc)])
mock_add.assert_called_once_with(doc, kb_dir)

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@KylinMountain
Copy link
Copy Markdown
Collaborator Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

KylinMountain and others added 3 commits April 12, 2026 10:41
- Restore underscore in test_add_single_file_calls_helper (accidental
  removal during _add_single_file rename)
- Catch KeyboardInterrupt around _handle_slash so Ctrl-C during /add
  or /lint aborts the command instead of triggering the exit prompt
- Add test for Ctrl-C behavior during slash command execution
- Append closing quote for file completions so quotes stay paired
- Skip closing quote for directory completions to allow continued navigation
- Fallback strip unmatched leading quote in _handle_slash
@rejojer
Copy link
Copy Markdown
Member

rejojer commented Apr 12, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants