Skip to content

Commit c935204

Browse files
authored
docs: Add readme demo (#265)
1 parent b5cdcf0 commit c935204

10 files changed

Lines changed: 163 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424

2525
</div>
2626

27-
Semble is a code search library built for agents. It returns the exact code snippets they need instantly, using ~99% fewer tokens than grep+read. Indexing and searching a full codebase end-to-end takes under a second, matching the retrieval quality of a code-specialized transformer while indexing ~340x faster and querying ~17x faster (see [benchmarks](#benchmarks)). Everything runs on CPU with no API keys, GPU, or external services. Use it as an MCP server, a CLI tool via AGENTS.md, or a dedicated sub-agent, and any coding agent (Claude Code, Cursor, Codex, OpenCode, etc.) gets instant access to any repo.
27+
Semble is a code search library built for agents. It returns the exact code snippets they need instantly, using ~99% fewer tokens than grep+read. Indexing and searching a full codebase end-to-end takes under a second for most repos, matching the retrieval quality of a code-specialized transformer while indexing ~340x faster and querying ~17x faster (see [benchmarks](#benchmarks)). Everything runs on CPU with no API keys, GPU, or external services. Use it as an MCP server, a CLI tool via AGENTS.md, or a dedicated sub-agent, and any coding agent (Claude Code, Cursor, Codex, OpenCode, etc.) gets instant access to any repo.
28+
29+
<picture>
30+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/MinishLab/semble/main/assets/images/demo-dark.gif">
31+
<img src="https://raw.githubusercontent.com/MinishLab/semble/main/assets/images/demo-light.gif" width="1000" alt="semble install detecting Claude Code, Cursor, and Codex, then semble search returning ranked code snippets from pydantic" />
32+
</picture>
2833

2934
## Quickstart
3035

‎assets/images/demo-dark.gif‎

519 KB
Loading

‎assets/images/demo-light.gif‎

527 KB
Loading

‎scripts/demo/dark.tape‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Output "assets/images/demo-dark.gif"
2+
3+
Set Shell "bash"
4+
Set FontSize 18
5+
Set Width 1200
6+
Set Height 672
7+
Set Theme "Catppuccin Mocha"
8+
Set Padding 20
9+
Set Margin 30
10+
Set MarginFill "#11111b"
11+
Set BorderRadius 12
12+
Set WindowBar Colorful
13+
Set WindowBarSize 36
14+
Set TypingSpeed 45ms
15+
16+
Hide
17+
Type "exec env -i HOME=$DEMO_HOME PATH=$SEMBLE_BIN_DIR:/usr/bin:/bin TERM=xterm-256color /bin/bash --norc --noprofile"
18+
Enter
19+
Sleep 800ms
20+
Type "cd $HOME; PS1='$ '; clear"
21+
Enter
22+
Sleep 300ms
23+
Show
24+
25+
Type "semble install"
26+
Enter
27+
Sleep 1200ms
28+
Enter
29+
Sleep 500ms
30+
Enter
31+
Sleep 500ms
32+
Enter
33+
Sleep 2200ms
34+
35+
Hide
36+
Type "clear"
37+
Enter
38+
Sleep 300ms
39+
Show
40+
41+
Type "cd pydantic"
42+
Enter
43+
Sleep 600ms
44+
45+
Type@25ms `semble search "Where is snake_case converted to camelCase?" -k 1 --format text`
46+
Enter
47+
Wait@60s /\$\s*$/
48+
Sleep 3500ms

‎scripts/demo/light.tape‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Output "assets/images/demo-light.gif"
2+
3+
Set Shell "bash"
4+
Set FontSize 18
5+
Set Width 1200
6+
Set Height 672
7+
Set Theme "Catppuccin Latte"
8+
Set Padding 20
9+
Set Margin 30
10+
Set MarginFill "#eff1f5"
11+
Set BorderRadius 12
12+
Set WindowBar Colorful
13+
Set WindowBarSize 36
14+
Set TypingSpeed 45ms
15+
16+
Hide
17+
Type "exec env -i HOME=$DEMO_HOME PATH=$SEMBLE_BIN_DIR:/usr/bin:/bin TERM=xterm-256color /bin/bash --norc --noprofile"
18+
Enter
19+
Sleep 800ms
20+
Type "cd $HOME; PS1='$ '; clear"
21+
Enter
22+
Sleep 300ms
23+
Show
24+
25+
Type "semble install"
26+
Enter
27+
Sleep 1200ms
28+
Enter
29+
Sleep 500ms
30+
Enter
31+
Sleep 500ms
32+
Enter
33+
Sleep 2200ms
34+
35+
Hide
36+
Type "clear"
37+
Enter
38+
Sleep 300ms
39+
Show
40+
41+
Type "cd pydantic"
42+
Enter
43+
Sleep 600ms
44+
45+
Type@25ms `semble search "Where is snake_case converted to camelCase?" -k 1 --format text`
46+
Enter
47+
Wait@60s /\$\s*$/
48+
Sleep 3500ms

‎scripts/demo/record.sh‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
# Records the README demo GIFs (assets/images/demo-{dark,light}.gif) from dark.tape and light.tape.
3+
set -euo pipefail
4+
5+
cd "$(dirname "$0")/../.."
6+
command -v vhs >/dev/null || { echo "vhs not found, install it with: brew install vhs" >&2; exit 1; }
7+
[ -x .venv/bin/semble ] || { echo ".venv/bin/semble not found, run: make install" >&2; exit 1; }
8+
export SEMBLE_BIN_DIR="$PWD/.venv/bin"
9+
10+
seed=$(mktemp -d)
11+
dark=$(mktemp -d)
12+
light=$(mktemp -d)
13+
trap 'rm -rf "$seed" "$dark" "$light"' EXIT
14+
15+
# Create an isolated $HOME with agent config dirs so `semble install` detects some agents.
16+
mkdir -p "$seed"/.claude "$seed"/.cursor "$seed"/.codex
17+
git -c advice.detachedHead=false clone -q --depth 1 --branch v2.13.5 https://github.com/pydantic/pydantic "$seed/pydantic"
18+
# Warm the model cache so the recording doesn't hit the network or print an HF Hub warning.
19+
env -i HOME="$seed" PATH="$SEMBLE_BIN_DIR:/usr/bin:/bin" "$SEMBLE_BIN_DIR/semble" search x "$seed/pydantic" -k 1 >/dev/null
20+
21+
# Record the demo for both dark and light themes. Each home gets its own agent dirs and repo copy, but shares the
22+
# seed's .cache: grammar dylibs load slowly the first time from a new path, which stalls the start of indexing.
23+
for home in "$dark" "$light"; do
24+
mkdir -p "$home"/.claude "$home"/.cursor "$home"/.codex
25+
cp -R "$seed/pydantic" "$home/pydantic"
26+
ln -s "$seed/.cache" "$home/.cache"
27+
done
28+
DEMO_HOME="$dark" vhs scripts/demo/dark.tape >/dev/null &
29+
dark_pid=$!
30+
DEMO_HOME="$light" vhs scripts/demo/light.tape >/dev/null &
31+
light_pid=$!
32+
wait "$dark_pid"
33+
wait "$light_pid"
34+
echo "Recorded assets/images/demo-dark.gif and assets/images/demo-light.gif"

‎src/semble/index/create.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ def create_index_from_path(
104104

105105
files = list(walk_files(path, resolved_extensions))
106106
for file_path in tqdm(
107-
files, desc="Indexing", unit="file", file=sys.stderr, leave=False, colour="green", disable=not show_progress_bar
107+
files,
108+
desc="Indexing",
109+
unit="file",
110+
file=sys.stderr,
111+
leave=False,
112+
colour="green",
113+
miniters=1, # tqdm's adaptive miniters stalls the bar when fast files are followed by slow ones
114+
disable=not show_progress_bar,
108115
):
109116
language = detect_language(file_path)
110117
with contextlib.suppress(OSError):

‎src/semble/installer/installer.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,15 +155,21 @@ def _checkbox(prompt: str, items: Sequence[tuple[str, _T, bool]]) -> list[_T] |
155155
return questionary.checkbox(prompt, choices=choices, style=style, instruction=instruction).ask()
156156

157157

158+
def _display_path(path: Path) -> str:
159+
"""Return path with the home directory shortened to ~, keeping installer output readable."""
160+
home = Path.home()
161+
return str(Path("~") / path.relative_to(home)) if path.is_relative_to(home) else str(path)
162+
163+
158164
def _print_plan(agents: list[AgentTarget], integrations: list[_Integration]) -> None:
159165
"""Print what will be written or removed for each selected agent and integration."""
160166
print(f"\n {_BOLD}Plan:{_RESET}\n")
161167
for agent in agents:
162168
print(f" {_BOLD}{agent.display_name}{_RESET}")
163169
for integ in integrations:
164170
path = integ.plan_path(agent)
165-
ok = path is not None
166-
print(f" {integ.label:<13} {_tick(ok)} {path if ok else '(not supported)'}")
171+
shown = _display_path(path) if path is not None else "(not supported)"
172+
print(f" {integ.label:<13} {_tick(path is not None)} {shown}")
167173
print()
168174

169175

@@ -180,7 +186,7 @@ def _apply(mode: Mode, agents: list[AgentTarget], integrations: list[_Integratio
180186
ok = result.action in ("created", "updated", "removed", "unchanged")
181187
detail = _ACTION_DETAIL.get(result.action, "")
182188
suffix = f" — {detail}" if detail else ""
183-
print(f" {_tick(ok)} {integ.id.value} ({result.action}){suffix} → {result.path}")
189+
print(f" {_tick(ok)} {integ.id.value} ({result.action}){suffix} → {_display_path(result.path)}")
184190
print()
185191

186192

‎tests/test_installer.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,13 +520,17 @@ def ask(self):
520520
assert _checkbox("Pick:", [("Option A", "a", False)]) == ["a"]
521521

522522

523-
def test_print_plan(capsys, claude_agent):
524-
"""_print_plan prints each agent, integration, and resolved path (or 'not supported')."""
523+
@pytest.mark.parametrize("under_home", [True, False])
524+
def test_print_plan(capsys, claude_agent, monkeypatch, tmp_path, under_home):
525+
"""_print_plan prints each agent, integration, and path (shortened to ~ under home, or 'not supported')."""
526+
monkeypatch.setattr(Path, "home", lambda: tmp_path if under_home else tmp_path / "elsewhere")
525527
no_mcp = replace(claude_agent, display_name="No MCP Agent", mcp=None)
526528
_print_plan([claude_agent, no_mcp], _INTEGRATIONS)
527529
out = capsys.readouterr().out
528530
assert "Claude Code" in out
529531
assert "not supported" in out # no_mcp has no MCP
532+
expected = Path("~") / ".claude" / "CLAUDE.md" if under_home else claude_agent.instructions_path
533+
assert str(expected) in out
530534

531535

532536
def test_run_completes(run_setup, monkeypatch, capsys):

‎uv.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)