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

+7 -3
View File
@@ -39,12 +39,15 @@ export interface BotAddressSettings {
* @default false
*/
businessAddress?: boolean
chatLayout?: T.ChatLayout
}
export const defaultBotAddressSettings: BotAddressSettings = {
autoAccept: true,
welcomeMessage: undefined,
businessAddress: false
businessAddress: false,
chatLayout: undefined
}
export type EventSubscriberFunc<K extends CEvt.Tag> = (event: ChatEvent & {type: K}, chat: ChatApi) => void | Promise<void>
@@ -413,12 +416,13 @@ export class ChatApi {
* Set bot address settings.
* Network usage: interactive.
*/
async apiSetAddressSettings(userId: number, {autoAccept, welcomeMessage, businessAddress}: BotAddressSettings): Promise<void> {
async apiSetAddressSettings(userId: number, {autoAccept, welcomeMessage, businessAddress, chatLayout}: BotAddressSettings): Promise<void> {
const autoReply = welcomeMessage || defaultBotAddressSettings.welcomeMessage
const settings: T.AddressSettings = {
autoAccept: (autoAccept === undefined ? defaultBotAddressSettings.autoAccept : autoAccept) ? {acceptIncognito: false} : undefined,
autoReply: typeof autoReply === "string" ? {type: "text", text: autoReply} : autoReply,
businessAddress: businessAddress || defaultBotAddressSettings.businessAddress || false
businessAddress: businessAddress || defaultBotAddressSettings.businessAddress || false,
chatLayout
}
const r = await this.sendChatCmd(CC.APISetAddressSettings.cmdString({userId, settings}))
if (r.type !== "userContactLinkUpdated") {
+2 -1
View File
@@ -82,7 +82,8 @@ function fullOptions(options: BotOptions): Required<BotOptions> {
opts.addressSettings = {
autoAccept: opts.addressSettings.autoAccept ?? defaultOpts.addressSettings.autoAccept,
welcomeMessage: typeof welcomeMessage === "string" ? {type: "text", text: welcomeMessage} : welcomeMessage,
businessAddress: opts.addressSettings.businessAddress ?? defaultOpts.addressSettings.businessAddress
businessAddress: opts.addressSettings.businessAddress ?? defaultOpts.addressSettings.businessAddress,
chatLayout: opts.addressSettings.chatLayout
}
return opts
}
+2 -1
View File
@@ -49,7 +49,8 @@ export function botAddressSettings({addressSettings}: T.UserContactLink): BotAdd
return {
autoAccept: addressSettings.autoAccept ? true : false,
welcomeMessage: addressSettings.autoReply,
businessAddress: addressSettings.businessAddress
businessAddress: addressSettings.businessAddress,
chatLayout: addressSettings.chatLayout
}
}
@@ -127,4 +127,32 @@ describe("run", () => {
await runBot("calc.simplex", {updateAddress: false})
expect(chat.apiSetUserDomain).not.toHaveBeenCalled()
})
it("updates the address settings when they changed", async () => {
const chat = fakeChat()
await runBot(undefined, {addressSettings: {welcomeMessage: "hello"}})
expect(chat.apiSetAddressSettings).toHaveBeenCalledWith(1, {autoAccept: true, welcomeMessage: {type: "text", text: "hello"}, businessAddress: false})
})
it("updates the address settings when the chat layout changed", async () => {
const chat = fakeChat()
await runBot(undefined, {addressSettings: {chatLayout: T.ChatLayout.Terminal}})
expect(chat.apiSetAddressSettings).toHaveBeenCalledWith(1, {autoAccept: true, businessAddress: false, chatLayout: T.ChatLayout.Terminal})
})
it("does not update the address settings when they are the same", async () => {
const chat = fakeChat()
chat.apiGetUserAddress.mockResolvedValue({...address, addressSettings: {...address.addressSettings, chatLayout: T.ChatLayout.Terminal}})
await runBot(undefined, {addressSettings: {chatLayout: T.ChatLayout.Terminal}})
expect(chat.apiSetAddressSettings).not.toHaveBeenCalled()
})
})
describe("apiSetAddressSettings", () => {
it("writes the chat layout of the bot address settings", async () => {
const sendChatCmd = jest.fn().mockResolvedValue({type: "userContactLinkUpdated"})
await api.ChatApi.prototype.apiSetAddressSettings.call({sendChatCmd} as unknown as api.ChatApi, 1, {autoAccept: true, chatLayout: T.ChatLayout.Terminal})
const cmd: string = sendChatCmd.mock.calls[0][0]
expect(JSON.parse(cmd.slice(cmd.indexOf("{")))).toEqual({businessAddress: false, autoAccept: {acceptIncognito: false}, chatLayout: "terminal"})
})
})