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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ Tokens are stripped from the in-memory history buffer and masked on every displa
| `theme` | Terminal colour theme | `glean` (default), `mono`, `neon` |
| `default_page_size` | Default result count for search and entities | Integer, default `10` |
| `mock_corpus_path` | JSON file backing mock mode | Path; unset uses the built-in corpus |
| `window_title` | Terminal window/tab title | `full` (default, includes the instance host), `plain` (mode only), `off` |

Config lives at `~/.gleancode/config.json`. Change any key with `/config set <key> <value>`. Use `/mode live|mock|auto` to force a mode without editing config.

Expand Down
55 changes: 36 additions & 19 deletions glean_code/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,42 @@ def main() -> None:

setup_readline()

while session.running:
try:
bar = ui.status_bar(
mode=config.effective_mode,
instance=config.instance,
has_token=config.is_live_ready,
act_as=config.act_as,
chat_id=session.current_chat_id,
)
if bar:
print(bar)
line = input(ui.prompt_str(config.effective_mode))
except (EOFError, KeyboardInterrupt):
print()
break
try:
dispatch(session, line)
except Exception as e: # defensive: never crash the REPL
ui.print_err(f"Unhandled error: {e}")
# Terminal title tracks mode and instance, so several open windows stay
# tellable apart. Only rewritten when the state actually changes.
last_title = None
try:
while session.running:
try:
title = ui.title_text(
mode=config.effective_mode,
instance=config.instance,
has_token=config.is_live_ready,
style_name=config.window_title,
)
if title and title != last_title:
ui.set_title(title)
last_title = title

bar = ui.status_bar(
mode=config.effective_mode,
instance=config.instance,
has_token=config.is_live_ready,
act_as=config.act_as,
chat_id=session.current_chat_id,
)
if bar:
print(bar)
line = input(ui.prompt_str(config.effective_mode))
except (EOFError, KeyboardInterrupt):
print()
break
try:
dispatch(session, line)
except Exception as e: # defensive: never crash the REPL
ui.print_err(f"Unhandled error: {e}")
finally:
if last_title:
ui.clear_title()


if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions glean_code/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Config:
theme: str = "glean" # glean | mono | neon
default_page_size: int = 10
mock_corpus_path: Optional[str] = None # JSON file backing mock mode; falls back to the built-in corpus
window_title: str = "full" # full | plain (no hostname) | off
history: list = field(default_factory=list)

# ---- Interactive OAuth (SSO) settings ----
Expand Down
45 changes: 45 additions & 0 deletions glean_code/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,51 @@ def hyperlink(url: str, text: str) -> str:
return f"\033]8;;{url}\033\\{text}\033]8;;\033\\"


def title_text(
mode: str,
instance: Optional[str] = None,
has_token: bool = False,
style_name: str = "full",
) -> str:
"""Build the terminal window/tab title for the current session state.

style_name:
full "Glean Code - acme-be.glean.com (live)"
plain "Glean Code (live)" - omits the tenant hostname
off "" - caller should not set a title
"""
if style_name == "off":
return ""

label = mode if mode in ("live", "mock") else "auto"
if mode == "auto":
label = "live" if has_token else "mock"

if style_name == "plain" or not instance:
return f"Glean Code ({label})"

host = instance
if "://" in host:
host = host.split("://", 1)[1].split("/")[0]
return f"Glean Code - {host} ({label})"


def set_title(text: str) -> None:
"""Set the terminal window and tab title via OSC 0.

No-op when stdout is not a TTY, so piped runs stay clean.
"""
if not sys.stdout.isatty():
return
sys.stdout.write(f"\033]0;{text}\007")
sys.stdout.flush()


def clear_title() -> None:
"""Hand the title back to the shell on exit."""
set_title("")


def term_width(default: int = 80) -> int:
try:
return shutil.get_terminal_size().columns
Expand Down
55 changes: 55 additions & 0 deletions tests/test_ui.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Tests for glean_code.ui"""
import io
import os
import sys
import unittest
from pathlib import Path
from unittest import mock
from unittest.mock import patch

sys.path.insert(0, str(Path(__file__).parent.parent))
Expand Down Expand Up @@ -295,3 +297,56 @@ def test_short_chat_id_not_truncated(self):

if __name__ == "__main__":
unittest.main()


class TestTitleText(unittest.TestCase):
def test_full_includes_host_and_mode(self):
self.assertEqual(
ui.title_text("live", "acme-be.glean.com", True),
"Glean Code - acme-be.glean.com (live)",
)

def test_plain_omits_the_hostname(self):
self.assertEqual(
ui.title_text("live", "acme-be.glean.com", True, "plain"),
"Glean Code (live)",
)

def test_off_returns_empty(self):
self.assertEqual(ui.title_text("live", "acme-be.glean.com", True, "off"), "")

def test_auto_resolves_to_live_or_mock(self):
self.assertIn("(live)", ui.title_text("auto", "h", has_token=True))
self.assertIn("(mock)", ui.title_text("auto", "h", has_token=False))

def test_no_instance_falls_back_to_short_form(self):
self.assertEqual(ui.title_text("mock", None, False), "Glean Code (mock)")

def test_scheme_and_path_are_stripped_from_host(self):
self.assertEqual(
ui.title_text("live", "https://acme-be.glean.com/rest/api/v1", True),
"Glean Code - acme-be.glean.com (live)",
)


class TestSetTitle(unittest.TestCase):
def test_writes_osc_sequence_when_tty(self):
buf = io.StringIO()
buf.isatty = lambda: True
with mock.patch.object(ui.sys, "stdout", buf):
ui.set_title("Hello")
self.assertEqual(buf.getvalue(), "\033]0;Hello\007")

def test_silent_when_not_a_tty(self):
buf = io.StringIO()
buf.isatty = lambda: False
with mock.patch.object(ui.sys, "stdout", buf):
ui.set_title("Hello")
self.assertEqual(buf.getvalue(), "")

def test_clear_title_emits_empty_title(self):
buf = io.StringIO()
buf.isatty = lambda: True
with mock.patch.object(ui.sys, "stdout", buf):
ui.clear_title()
self.assertEqual(buf.getvalue(), "\033]0;\007")
Loading