core, ui: terminal chat layout for LLM and service bot chats (#7811)

* plan: UI improvements to improve LLM usage via SimpleX

* core, ui: types for chat layout

* core, ui: settings for alternative terminal-like chat layout for managing LLMs or other service bots

* plan: message priority

* fixes

* query plan

* update plans

* remove silent messages plan

* rename migrations

* implement layout

* amend

* fix

* fix

* fix

* improve

* time on the right, sender name in blue

* no avatar in direct chats

* better previews

* update, fixes

* update

* time gap

* changes

* ui: terminal layout line heights, gaps and time placement

* ui: simplify terminal meta layout, correct its docs and tests

* ui: right-to-left emoji edge in terminal rows, time placement tests

* android, desktop: raise Kotlin compiler memory to 4 GB

* ui: terminal captions as follow-up text, classic event separators

* ui: terminal media edge and overlay time, plan decisions

* ui: simplify terminal row separation, correct doc line references

* ui: keep the media time inside terminal rows, restore caption conditions

* ui: terminal heading capitals at large font scales, idle live message time

* ui: rename the classic voice-with-text flag, correct the meta copy docs

* ui: smaller terminal layout diff, sender gap and meta names

* ui: terminal row as tall as the media time

* docs: remove the chat layout section from the bots readme

* docs: shorten the terminal layout section of the chat view

* core: address settings API as on master, /auto_accept reads the stored layout

* ui: smaller terminal headings

* ui: classic sender rows and avatar rule as on master

* ui: classic reply quote text colour as on master

* ios: keep the chat layout setting with other UI settings

* ui: go-to button at the end of terminal rows, no gutter in direct chats

* ui: forwarded arrow on the header line of terminal rows, above the time

* ui: forwarded arrow centred above the time line of terminal rows

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Evgeny @ SimpleX ChatandEvgeny Poberezkin authored and GitHub committed 2026-10-25 18:03:11 +00:00
1 parent 4f81c6f632
commit 7810fc4752
103 files changed
+5098 -901

No files matched your search

@@ -729,6 +729,7 @@ class _StubApi:
def __init__(self) -> None:
self.profiles: list[dict] = []
self.address_settings: list[dict] = []
self.user: dict = {"userId": 1, "profile": {"displayName": "x", "fullName": ""}}
self.address: dict | None = None
@@ -758,8 +759,8 @@ class _StubApi:
async def api_get_user_address(self, _user_id):
return self.address
async def api_set_address_settings(self, _user_id, _settings):
pass
async def api_set_address_settings(self, _user_id, settings):
self.address_settings.append(settings)
async def send_chat_cmd(self, _cmd):
return {"type": "cmdOk"}
@@ -942,6 +943,55 @@ def test_sync_profile_keeps_the_bot_address_in_the_profile(monkeypatch):
assert api.profiles[0]["contactLink"] == "https://l"
def test_bot_address_settings_include_the_chat_layout(monkeypatch):
import simplex_chat.client as client_mod
api = _StubApi()
api.address = {
"connLinkContact": {"connFullLink": "https://l"},
"addressSettings": {"businessAddress": False, "autoAccept": {"acceptIncognito": False}},
}
monkeypatch.setattr(client_mod, "ChatApi", _init_returning(api))
bot = Bot(
profile=BotProfile(display_name="x"),
db=SqliteDb(file_prefix="/tmp/test"),
update_profile=False,
chat_layout="terminal",
)
async def go():
async with bot:
pass
asyncio.run(go())
assert api.address_settings == [
{"businessAddress": False, "autoAccept": {"acceptIncognito": False}, "chatLayout": "terminal"}
]
def test_bot_address_settings_remove_the_chat_layout_not_in_bot_settings(monkeypatch):
import simplex_chat.client as client_mod
api = _StubApi()
api.address = {
"connLinkContact": {"connFullLink": "https://l"},
"addressSettings": {"businessAddress": False, "chatLayout": "terminal"},
}
monkeypatch.setattr(client_mod, "ChatApi", _init_returning(api))
bot = Bot(
profile=BotProfile(display_name="x"),
db=SqliteDb(file_prefix="/tmp/test"),
update_profile=False,
)
async def go():
async with bot:
pass
asyncio.run(go())
assert api.address_settings == [{"businessAddress": False, "autoAccept": {"acceptIncognito": False}}]
def test_profile_can_be_replaced(monkeypatch):
c, _ = _client_with_stub_api(monkeypatch)
c.profile = Profile(display_name="other", full_name="Other")