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
2 changes: 1 addition & 1 deletion Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (1136) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (1144) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (95: 45 daemon_client + 20 conn-manager + 8 integration + 6 build-config + 7 gui-state + 3 preload-api + 4 boot-contract + 2 theme-guard) — syntax: `node --check main.js preload.js daemon_client.js`
Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (448: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 15 transcript + 7 TranscriptView + 15 history + 22 composer + 14 Composer + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 10 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 15 daemonBridge + 7 DaemonBridgeProvider + 25 Shell + 15 DialogHost + 20 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown) + `npm run build` → `renderer/dist/`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Expand Down
34 changes: 33 additions & 1 deletion emrg/client/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@

from __future__ import annotations

import asyncio, json, logging, os, platform, signal, subprocess, sys, threading, time
import asyncio, json, logging, os, platform, re, signal, subprocess, sys, threading, time
try:
import fcntl # POSIX-only(TUI 非阻塞 stdin);Windows 无此模块
except ImportError: # pragma: no cover - Windows
Expand DownExpand Up@@ -83,6 +83,29 @@ def _format_status_left(title: str, sid: str, model: str = "") -> str:
return " ".join(parts)


def _auto_title_from_prompt(text: str, max_len: int = 30) -> str | None:
"""Derive a short session title from the first user message.

Codex #40492 borrow: unnamed tasks/sessions get a descriptive title
automatically. Deterministic text-derived (no LLM call, zero latency,
no token cost) — unlike the on-demand LLM auto-title via ``/rename``
with an empty title.

Returns ``None`` when the message is a slash command or has no usable
text (session stays untitled in that case). Module-level so it is
unit-testable.
"""
if not text or text.lstrip().startswith("/"):
return None
first = next((ln.strip() for ln in text.splitlines() if ln.strip()), "")
if not first:
return None
first = re.sub(r"\s+", " ", first)
if len(first) <= max_len:
return first
return first[: max_len - 1] + "…"


# ── Clipboard image support (platform-adaptive) ─────────────

def _detect_clipboard_image() -> tuple[bool, str | None]:
Expand DownExpand Up@@ -2007,6 +2030,15 @@ def _is_image_token(s, i):
_pending_images[:] = [img for img in _pending_images if img.get("label") in inp.text]
images = _pending_images or None
_pending_images = []
# Auto-title a fresh, untitled session from its first user
# message (Codex #40492 borrow: unnamed sessions/tasks get a
# descriptive title automatically). Deterministic text-derived
# title — free, no LLM call. Skips slash commands.
if msg_count == 1 and not session_title:
auto_title = _auto_title_from_prompt(text)
if auto_title:
await conn.send_command("rename_session", session_id=session_id,
cwd=cwd, title=auto_title)
rid = await conn.send_task(session_id=session_id, cwd=cwd, prompt=text,
images=images)
if was_busy:
Expand Down
49 changes: 49 additions & 0 deletions tests/test_app_auto_title.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
"""Tests for _auto_title_from_prompt (Codex #40492 borrow).

New sessions previously displayed the raw session id until the host ran
`/rename` manually. The auto-title helper derives a short descriptive
title from the first user message — deterministic, no LLM call.
"""

from emrg.client.app import _auto_title_from_prompt


def test_plain_prompt_kept_verbatim():
assert _auto_title_from_prompt("fix the billing bug") == "fix the billing bug"


def test_long_prompt_truncated_with_ellipsis():
title = _auto_title_from_prompt("x" * 100)
assert len(title) == 30
assert title.endswith("…")


def test_slash_command_skipped():
assert _auto_title_from_prompt("/sessions") is None
assert _auto_title_from_prompt(" /rename foo bar") is None


def test_multiline_uses_first_line():
assert _auto_title_from_prompt("first line\nsecond line") == "first line"


def test_whitespace_only_returns_none():
assert _auto_title_from_prompt("") is None
assert _auto_title_from_prompt(" \n ") is None


def test_cjk_prompt_truncates_safely():
text = "请修复这个 bug,它发生在登录流程中,需要检查 token 刷新逻辑"
title = _auto_title_from_prompt(text)
assert isinstance(title, str)
assert len(title) == 30
assert title.endswith("…")


def test_collapses_internal_whitespace():
assert _auto_title_from_prompt(" hello world ") == "hello world"


def test_max_len_custom():
assert len(_auto_title_from_prompt("a" * 50, max_len=12)) == 12
assert _auto_title_from_prompt("short", max_len=12) == "short"
Loading