Compare commits

...

1 Commits

Author SHA1 Message Date
Tomu Hirata bfbd9b9bc1 fix(sessions): guard terminal snapshot against null runner_client
Sessions without a bound runner raised AttributeError in _resource_snapshot
because the terminals fetch called runner_client.get(...) unconditionally.
Wrap the block with `if runner_client is not None:` so runner-less sessions
snapshot cleanly.

Also refactors pi_native_credentials: merges _databricks_completions_provider
into _databricks_openai_provider (via api_type param), splits _fetch_pi_model_lists
into responses/completions buckets for GPT models that require the Responses API,
and updates NewChatDialog to fetch directorySessions lazily (only when a host is
selected) using runner_online instead of the /health poll for occupancy counts.
2026-07-15 22:08:05 +09:00
4 changed files with 190 additions and 115 deletions
+103 -44
View File
@@ -63,10 +63,14 @@ _PI_PROVIDER_ID = "omnigent"
# carries no explicit model override.
_DATABRICKS_PI_DEFAULT_MODEL = "databricks-claude-sonnet-4-6"
# Provider id for the secondary OpenAI Completions provider registered alongside
# the primary Anthropic provider in Databricks gateway configs.
# Provider id for the secondary OpenAI Responses provider (GPT models that only
# support tools via the Responses API, e.g. gpt-5.5, gpt-5.6-*).
_PI_OPENAI_PROVIDER_ID = "omnigent-openai"
# Provider id for the tertiary OpenAI Completions provider (non-GPT models that
# work via /chat/completions: Kimi, Llama, GLM, Gemini, older GPT models).
_PI_COMPLETIONS_PROVIDER_ID = "omnigent-completions"
# Databricks AI Gateway Anthropic Messages surface. Pi speaks this protocol
# natively (``api: anthropic-messages``); the gateway authenticates with a
# workspace bearer token, so we set ``authHeader`` (Authorization: Bearer).
@@ -224,13 +228,25 @@ def _databricks_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiPro
from omnigent.runtime.credentials.databricks import resolve_databricks_workspace
creds = resolve_databricks_workspace(entry.profile)
claude_models, openai_models = _fetch_pi_model_lists(creds.host, creds.token)
claude_models, gpt_models, completions_models = _fetch_pi_model_lists(
creds.host, creds.token
)
except Exception: # noqa: BLE001 — credential/network failure must not break launch
_LOGGER.info(
"pi-native: falling back to single-model display (could not resolve credentials)"
)
claude_models = []
openai_models = []
gpt_models = []
completions_models = []
additional: dict[str, Any] = {}
if gpt_models:
additional[_PI_OPENAI_PROVIDER_ID] = _databricks_openai_provider(
api_key, f"{host}/ai-gateway/codex/v1", gpt_models
)
if completions_models:
additional[_PI_COMPLETIONS_PROVIDER_ID] = _databricks_openai_provider(
api_key, f"{host}/serving-endpoints", completions_models, api_type="openai-completions"
)
return PiProviderConfig(
provider_id=_PI_PROVIDER_ID,
base_url=f"{host}{_DATABRICKS_ANTHROPIC_GATEWAY_PATH}",
@@ -242,33 +258,34 @@ def _databricks_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiPro
api_key=api_key,
auth_header=True,
extra_models=claude_models,
additional_providers=(
{
_PI_OPENAI_PROVIDER_ID: _databricks_openai_provider(
api_key, f"{host}/serving-endpoints", openai_models
)
}
if openai_models
else {}
),
additional_providers=additional,
)
def _databricks_openai_provider(
api_key: str,
serving_endpoints_url: str,
base_url: str,
models: list[dict[str, Any]],
api_type: str = "openai-responses",
) -> dict[str, Any]:
"""Build a Pi OpenAI Completions provider config for the Databricks gateway.
"""Build a Pi OpenAI provider config for Databricks models.
GPT models on the Databricks workspace are served via the OpenAI
Completions API at ``/serving-endpoints``. The ``compat`` block disables
OpenAI-specific features the Databricks endpoint doesn't support.
``api_type`` selects the wire protocol:
* ``"openai-responses"`` — AI Gateway codex surface
(``/ai-gateway/codex/v1``). Required for newer GPT models (gpt-5.5,
gpt-5.6-*) that reject function tool calls via ``/chat/completions``.
* ``"openai-completions"`` — workspace serving-endpoints surface. Works
for Kimi, Llama, GLM, Gemini, and older GPT models.
``authHeader`` sends ``Authorization: Bearer {token}`` (Databricks requires
this; without it the OpenAI SDK uses ``api-key`` which is rejected).
"""
return {
"baseUrl": serving_endpoints_url,
"baseUrl": base_url,
"apiKey": api_key,
"api": "openai-completions",
"api": api_type,
"authHeader": True,
"compat": {
"supportsDeveloperRole": False,
"supportsStore": False,
@@ -309,19 +326,38 @@ def _run_auth_command(auth_command: str, *, timeout: float = 15.0) -> str | None
return None
def _needs_responses_api(model_id_lower: str) -> bool:
"""Return True when a Databricks model requires the Responses API for tools.
Newer GPT models (gpt-5.5, gpt-5.6-*, gpt-5.3-codex) reject function tool
calls via ``/chat/completions`` with 400; they work via the Responses API at
the AI Gateway (``/ai-gateway/codex/v1/responses``). Detected by name: these
models have ``gpt-5.5``, ``gpt-5.6``, or ``gpt-5.3-codex`` in their id.
Non-GPT models (Kimi, Llama, GLM, Gemini) and older GPT (5.4, 5.2, …) work
fine with ``/chat/completions`` + tools.
Expects a pre-lowercased model id (the caller typically has ``name_lower``
already computed).
"""
return any(token in model_id_lower for token in ("gpt-5-5", "gpt-5-6", "gpt-5-3-codex"))
def _fetch_pi_model_lists(
workspace_url: str,
token: str,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
"""Fetch live model lists from the Databricks serving-endpoints API.
Calls ``GET <workspace>/api/2.0/serving-endpoints``, filters for READY LLM
endpoints, and splits them into two Pi model entry dict lists:
* Claude models → ``anthropic-messages`` provider.
* All other LLMs (GPT, GLM, Llama, Qwen, Kimi, …) → ``openai-completions``
provider. All non-Claude Databricks LLMs share the same serving-endpoints
URL and wire protocol, so a single provider covers them all.
* Newer GPT models (gpt-5.5, gpt-5.6-*, gpt-5.3-codex, …) that reject
function tools via ``/chat/completions`` → ``openai-responses`` provider
at the AI Gateway codex surface.
* Other LLMs (Kimi, Llama, GLM, Gemini, older GPT …) that work with
function tools via ``/chat/completions`` → ``openai-completions`` provider
at the serving-endpoints surface.
Falls back to empty lists on any HTTP or auth failure so a network blip
never breaks Pi session launch.
@@ -329,8 +365,8 @@ def _fetch_pi_model_lists(
:param workspace_url: Databricks workspace base URL, e.g.
``"https://wkspc.example.com"`` — **no** trailing slash or path.
:param token: Bearer token for the workspace API.
:returns: ``(claude_models, openai_models)`` — Pi model entry dicts ready
to write into ``models.json``.
:returns: ``(claude_models, gpt_responses_models, completions_models)`` —
Pi model entry dicts ready to write into ``models.json``.
"""
import httpx
@@ -348,14 +384,16 @@ def _fetch_pi_model_lists(
"Pi will show only the selected model",
exc_info=True,
)
return [], []
return [], [], []
endpoints = payload.get("endpoints") if isinstance(payload, dict) else None
claude: list[dict[str, Any]] = []
# All non-Claude LLM models (GPT, GLM, Llama, Qwen, Kimi, …) route to the
# same OpenAI Completions provider and serving-endpoints URL, so they share
# one list regardless of model family.
openai: list[dict[str, Any]] = []
# Newer GPT models (gpt-5.5, gpt-5.6-*, gpt-5.3-codex) reject function tools
# via /chat/completions; they need the Responses API at the AI Gateway.
gpt_responses: list[dict[str, Any]] = []
# Non-GPT models (Kimi, Llama, GLM, Gemini) and older GPT models work fine
# with function tools via /chat/completions at serving-endpoints.
completions: list[dict[str, Any]] = []
for endpoint in endpoints if isinstance(endpoints, list) else []:
if not isinstance(endpoint, dict):
@@ -383,16 +421,18 @@ def _fetch_pi_model_lists(
entry: dict[str, Any] = {"id": name, "input": ["text", "image"]}
if "claude" in name_lower:
claude.append(entry)
elif _needs_responses_api(name_lower):
gpt_responses.append(entry)
else:
openai.append(entry)
completions.append(entry)
if not claude and not openai:
if not claude and not gpt_responses and not completions:
_LOGGER.info(
"pi-native: Databricks serving-endpoints returned no LLM models; "
"Pi will show only the selected model"
)
return claude, openai
return claude, gpt_responses, completions
def _gateway_anthropic_base_url(codex_base_url: str) -> str:
@@ -567,7 +607,8 @@ def _cli_config_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiPro
# the API call. The SDK's minted token may not have serving-endpoints
# access on workspaces where access is controlled via the auth command.
claude_models: list[dict[str, Any]] = []
openai_models: list[dict[str, Any]] = []
gpt_models: list[dict[str, Any]] = []
completions_models: list[dict[str, Any]] = []
# Derive the workspace URL for the serving-endpoints API call.
# For dedicated-subdomain URLs (ai-gateway.cloud.databricks.com), the
# real workspace hostname must come from ~/.databrickscfg. For
@@ -593,20 +634,38 @@ def _cli_config_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiPro
if real_workspace_url and transport.auth_command:
token = _run_auth_command(transport.auth_command)
if token:
claude_models, openai_models = _fetch_pi_model_lists(real_workspace_url, token)
claude_models, gpt_models, completions_models = _fetch_pi_model_lists(
real_workspace_url, token
)
else:
_LOGGER.info(
"pi-native: auth command produced no token; Pi will show only the selected model"
)
additional: dict[str, Any] = (
{
_PI_OPENAI_PROVIDER_ID: _databricks_openai_provider(
api_key, f"{real_workspace_url}/serving-endpoints", openai_models
)
}
if real_workspace_url and openai_models
else {}
# Derive the AI Gateway codex URL for the openai-responses provider. For
# workspace-hosted URLs the transport base is already the codex path;
# for dedicated-subdomain URLs we build it from the workspace URL.
if _DATABRICKS_AI_GATEWAY_LABEL in gateway_labels:
# Dedicated subdomain: transport.base_url is the codex gateway URL.
# Strip trailing path suffixes to get the codex base, not /anthropic.
codex_gateway_url = transport.base_url.rstrip("/")
if codex_gateway_url.endswith(_DATABRICKS_GATEWAY_CODEX_SUFFIX):
codex_gateway_url = codex_gateway_url[: -len(_DATABRICKS_GATEWAY_CODEX_SUFFIX)]
codex_gateway_url = f"{codex_gateway_url}{_DATABRICKS_GATEWAY_CODEX_SUFFIX}"
else:
# Workspace-hosted gateway: build from workspace hostname.
codex_gateway_url = f"https://{parsed_gateway.hostname}/ai-gateway/codex/v1"
workspace_completions_url = (
real_workspace_url + "/serving-endpoints" if real_workspace_url else None
)
additional: dict[str, Any] = {}
if gpt_models:
additional[_PI_OPENAI_PROVIDER_ID] = _databricks_openai_provider(
api_key, codex_gateway_url, gpt_models
)
if completions_models and workspace_completions_url:
additional[_PI_COMPLETIONS_PROVIDER_ID] = _databricks_openai_provider(
api_key, workspace_completions_url, completions_models, api_type="openai-completions"
)
return PiProviderConfig(
provider_id=_PI_PROVIDER_ID,
base_url=_gateway_anthropic_base_url(transport.base_url),
+21 -20
View File
@@ -20515,26 +20515,27 @@ def create_sessions_router(
)
except Exception: # noqa: BLE001 -- best-effort snapshot; never block live tail
_logger.debug("snapshot: child sessions failed for %s", session_id, exc_info=True)
try:
resp = await asyncio.wait_for(
# order=asc: the web cache appends each replayed
# ``created`` event, so the replay must arrive in
# creation order or the session's own terminal (always
# created first) lands behind later agent-launched
# ones. limit=1000 (the runner endpoint max) keeps the
# oldest-first window from dropping the newest
# terminals past the default page of 20.
runner_client.get(
f"/v1/sessions/{session_id}/resources/terminals",
params={"order": "asc", "limit": "1000"},
),
timeout=_SNAPSHOT_RUNNER_TIMEOUT_S,
)
if resp.status_code == 200:
for item in resp.json().get("data", []):
events.append({"type": "session.resource.created", "resource": item})
except Exception: # noqa: BLE001 -- best-effort snapshot; never block live tail
_logger.debug("snapshot: terminals failed for %s", session_id, exc_info=True)
if runner_client is not None:
try:
resp = await asyncio.wait_for(
# order=asc: the web cache appends each replayed
# ``created`` event, so the replay must arrive in
# creation order or the session's own terminal (always
# created first) lands behind later agent-launched
# ones. limit=1000 (the runner endpoint max) keeps the
# oldest-first window from dropping the newest
# terminals past the default page of 20.
runner_client.get(
f"/v1/sessions/{session_id}/resources/terminals",
params={"order": "asc", "limit": "1000"},
),
timeout=_SNAPSHOT_RUNNER_TIMEOUT_S,
)
if resp.status_code == 200:
for item in resp.json().get("data", []):
events.append({"type": "session.resource.created", "resource": item})
except Exception: # noqa: BLE001 -- best-effort snapshot; never block live tail
_logger.debug("snapshot: terminals failed for %s", session_id, exc_info=True)
# Tell the client to (re)fetch the changed-files list rather
# than fetching it here (avoids a second runner round-trip).
events.append(
+48 -24
View File
@@ -845,19 +845,31 @@ def test_databricks_profile_registers_gpt_provider(monkeypatch: pytest.MonkeyPat
"resolve_databricks_workspace",
lambda profile: db_creds_mod.WorkspaceCreds(host="https://wkspc.example.com", token="tok"),
)
live_gpt = [{"id": "databricks-gpt-5-4", "input": ["text", "image"]}]
# gpt-5-5 needs the Responses API; gpt-5-4 uses Completions
live_gpt_responses = [{"id": "databricks-gpt-5-5", "input": ["text", "image"]}]
live_gpt_completions = [{"id": "databricks-gpt-5-4", "input": ["text", "image"]}]
live_claude = [{"id": "databricks-claude-sonnet-4-6", "input": ["text", "image"]}]
monkeypatch.setattr(creds, "_fetch_pi_model_lists", lambda *_: (live_claude, live_gpt))
monkeypatch.setattr(
creds,
"_fetch_pi_model_lists",
lambda *_: (live_claude, live_gpt_responses, live_gpt_completions),
)
provider = creds.resolve_pi_native_provider(config_loader=_databricks_config)
assert provider is not None
cfg = provider.to_models_config()
openai_entry = cfg["providers"].get("omnigent-openai")
assert openai_entry is not None, "omnigent-openai provider missing from models.json"
assert openai_entry["baseUrl"] == "https://wkspc.example.com/serving-endpoints"
assert openai_entry["api"] == "openai-completions"
assert any(m["id"] == "databricks-gpt-5-4" for m in openai_entry["models"])
assert openai_entry is not None, (
"omnigent-openai (responses) provider missing from models.json"
)
assert openai_entry["baseUrl"] == "https://wkspc.example.com/ai-gateway/codex/v1"
assert openai_entry["api"] == "openai-responses"
assert any(m["id"] == "databricks-gpt-5-5" for m in openai_entry["models"])
completions_entry = cfg["providers"].get("omnigent-completions")
assert completions_entry is not None, "omnigent-completions provider missing from models.json"
assert completions_entry["api"] == "openai-completions"
assert any(m["id"] == "databricks-gpt-5-4" for m in completions_entry["models"])
def test_cli_config_databricks_registers_gpt_provider(
@@ -893,7 +905,7 @@ def test_cli_config_databricks_registers_gpt_provider(
# Assert the auth_command token is used, not the SDK token
assert token == "cmd-tok", f"expected auth_command token, got {token!r}"
assert "dbc-a5d4177a" in workspace_url
return live_claude, live_gpt
return live_claude, live_gpt, []
monkeypatch.setattr(creds, "_fetch_pi_model_lists", _mock_fetch)
@@ -903,13 +915,13 @@ def test_cli_config_databricks_registers_gpt_provider(
cfg = provider.to_models_config()
openai_entry = cfg["providers"].get("omnigent-openai")
assert openai_entry is not None, "omnigent-openai provider missing from models.json"
# The serving-endpoints URL uses the REAL workspace hostname from databrickscfg,
# not a derived gateway hostname (which would be NXDOMAIN).
# Uses the AI Gateway codex URL (supports tools); the REAL workspace hostname
# from databrickscfg fixes the NXDOMAIN issue for dedicated-subdomain gateways.
assert (
openai_entry["baseUrl"]
== "https://dbc-a5d4177a-49dc.cloud.databricks.com/serving-endpoints"
== "https://1965859176160743.ai-gateway.cloud.databricks.com/codex/v1"
)
assert openai_entry["api"] == "openai-completions"
assert openai_entry["api"] == "openai-responses"
assert any(m["id"] == "databricks-gpt-5-4" for m in openai_entry["models"])
@@ -943,7 +955,13 @@ def test_fetch_pi_model_lists_parses_serving_endpoints() -> None:
# GLM without task field — falls back to name token "glm"
{"name": "databricks-glm-4-7", "state": {"ready": "READY"}},
{"name": "my-embedding-model", "task": "llm/v1/embeddings"},
{"name": "databricks-gpt-5-5", "task": "llm/v1/chat", "state": {"ready": "NOT_READY"}},
{"name": "databricks-gpt-5-5", "task": "llm/v1/chat", "state": {"ready": "READY"}},
# NOT_READY — excluded regardless of API type
{
"name": "databricks-gpt-5-5-pro",
"task": "llm/v1/chat",
"state": {"ready": "NOT_READY"},
},
]
}
@@ -958,22 +976,25 @@ def test_fetch_pi_model_lists_parses_serving_endpoints() -> None:
"httpx.Client",
lambda **kw: _real_client(transport=_MockTransport()),
):
claude, openai = creds._fetch_pi_model_lists("https://wkspc.example.com", "tok")
claude, gpt, completions = creds._fetch_pi_model_lists("https://wkspc.example.com", "tok")
assert [m["id"] for m in claude] == [
"databricks-claude-sonnet-4-6",
"databricks-claude-opus-4-8",
]
# GPT, Llama, and GLM (both task-detected and name-detected) all go to openai
openai_ids = [m["id"] for m in openai]
assert "databricks-gpt-5-4" in openai_ids
assert "databricks-llama-3-70b" in openai_ids
assert "databricks-zai-org-glm-4-7" in openai_ids
assert "databricks-glm-4-7" in openai_ids
# Newer GPT needing Responses API
gpt_ids = [m["id"] for m in gpt]
assert "databricks-gpt-5-5" in gpt_ids # needs responses API
assert "databricks-gpt-5-4" not in gpt_ids # works with completions
# Llama and GLM go to completions (work with /chat/completions + tools)
completions_ids = [m["id"] for m in completions]
assert "databricks-gpt-5-4" in completions_ids
assert "databricks-llama-3-70b" in completions_ids
assert "databricks-zai-org-glm-4-7" in completions_ids
assert "databricks-glm-4-7" in completions_ids
# Embeddings and not-ready endpoints excluded
assert "my-embedding-model" not in openai_ids
assert "databricks-gpt-5-5" not in openai_ids
assert all(m.get("input") == ["text", "image"] for m in claude + openai)
assert "my-embedding-model" not in gpt_ids + completions_ids
assert all(m.get("input") == ["text", "image"] for m in claude + gpt + completions)
def test_fetch_pi_model_lists_falls_back_on_http_error() -> None:
@@ -995,7 +1016,10 @@ def test_fetch_pi_model_lists_falls_back_on_http_error() -> None:
"httpx.Client",
lambda **kw: _real_client(transport=_ErrorTransport()),
):
claude, openai = creds._fetch_pi_model_lists("https://wkspc.example.com", "bad-tok")
claude, gpt, completions = creds._fetch_pi_model_lists(
"https://wkspc.example.com", "bad-tok"
)
assert claude == []
assert openai == []
assert gpt == []
assert completions == []
+18 -27
View File
@@ -96,7 +96,6 @@ import {
import { useAutoGrowTextarea } from "@/hooks/useAutoGrowTextarea";
import { useRecentWorkspaces } from "@/hooks/useRecentWorkspaces";
import { useDirectorySessions } from "@/hooks/useDirectorySessions";
import { useRunnerHealthRegistration } from "@/hooks/RunnerHealthProvider";
import { useHostFilesystem, type HostFilesystemEntry } from "@/hooks/useHostFilesystem";
import { useHostWorktrees } from "@/hooks/useHostWorktrees";
import { useNativeServerSwitcherForMainSurface } from "@/hooks/useNativeServerSwitcher";
@@ -1730,9 +1729,6 @@ export function NewChatLandingScreen() {
const { data: agents } = useAvailableAgents();
const brainHarnessLabels = useBrainHarnessLabels();
const { data: hosts, isLoading: hostsLoading } = useHosts();
// Sessions the caller can access, to warn when a new session would share a
// working directory with a live one (see the conflict tooltip below).
const { data: directorySessions } = useDirectorySessions(true);
const agentList = useMemo(
() =>
@@ -1844,6 +1840,9 @@ export function NewChatLandingScreen() {
const [selectedHostId, setSelectedHostId] = useState<string | null>(
() => landingDraft?.selectedHostId ?? null,
);
// Sessions on the selected host — fetched only when a host is selected,
// to avoid registering hundreds of sessions into the health poll at idle.
const { data: directorySessions } = useDirectorySessions(selectedHostId !== null);
// True when the user picked the sandbox option instead of a connected
// host — the server provisions a sandbox host at create time
// (host_type: "managed"), so no host_id or workspace is sent.
@@ -2216,27 +2215,21 @@ export function NewChatLandingScreen() {
const isCloudHost =
sandboxSelected || (selectedHost?.name?.toLowerCase().includes("cloud") ?? false);
// Sessions on the selected host that have a workspace — candidates for a
// directory conflict, fed to the runner-health poll so only *connected*
// agents count (same /health signal as the sidebar dots).
const conflictCandidates = useMemo(
() =>
(directorySessions ?? []).filter((s) => s.host_id === selectedHostId && s.workspace != null),
[directorySessions, selectedHostId],
);
const runnerHealth = useRunnerHealthRegistration(conflictCandidates);
// Count of live agents per normalized directory on this host. The file
// browser uses this to warn when you navigate into an occupied directory.
// Uses runner_online from the session list response directly — good enough
// for a conflict hint without registering hundreds of sessions into the
// /health poll.
const occupancyByDir = useMemo(() => {
const counts = new Map<string, number>();
for (const s of conflictCandidates) {
if (s.workspace == null || runnerHealth.get(s.id) !== true) continue;
for (const s of directorySessions ?? []) {
if (s.host_id !== selectedHostId || s.workspace == null || s.runner_online !== true) continue;
const dir = normalizeWorkspacePath(s.workspace);
if (dir === null) continue;
counts.set(dir, (counts.get(dir) ?? 0) + 1);
}
return counts;
}, [conflictCandidates, runnerHealth]);
}, [directorySessions, selectedHostId]);
// Existing git worktrees of the picked directory's repo, for the
// worktree picker. Skipped for sandbox sessions (server-managed) and
@@ -2426,17 +2419,15 @@ export function NewChatLandingScreen() {
if (mentionFsQuery.isPlaceholderData) return [];
const rows = (mentionFsQuery.data?.entries ?? [])
.filter((e) => e.type === "directory" || e.type === "file")
.map(
(e): WorkspaceFile => ({
path: e.path.startsWith(workspaceRoot)
? e.path.slice(workspaceRoot.length).replace(/^\/+/, "")
: e.name,
name: e.name,
type: e.type === "directory" ? "directory" : "file",
bytes: e.bytes,
modified_at: e.modified_at,
}),
);
.map((e): WorkspaceFile => ({
path: e.path.startsWith(workspaceRoot)
? e.path.slice(workspaceRoot.length).replace(/^\/+/, "")
: e.name,
name: e.name,
type: e.type === "directory" ? "directory" : "file",
bytes: e.bytes,
modified_at: e.modified_at,
}));
return rankMentionEntries(rows, mentionFilter);
}, [
mentionEnabled,