diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e7825..85841b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ All notable changes to LLeMbas Desktop. The format follows ### Changed +- **The chat's settings moved to the top right of its header; the composer keeps the message.** + The model picker, how hard it thinks, the subagents and an agent chat's mode sat as chips inside + the composer, where with a long model name or every chip on they were cut off and some could not + be reached. They are now in the chat's header, beside the Terminal, the activity panel, the + chat's tools and skills (a button of its own now, still in the chat's menu too) and the menu; a + new agent chat's computer and folder sit beside the title, where an open one says where it + works. The composer holds only the message's own: the text, attach, Dictate, Draw a picture and + Send/Stop. Where the header has no room for the settings -- measured, so a long model name + counts -- they fold behind Chat settings, which opens them as a wrapped row under the header: + nothing is cut off or drawn over another at any width (checked at 1920, 1280, 1024, 800 and + 390 px with long model names and every chip on). + - **Files, in the web explorer's shape, using the window's width.** Both tabs (This computer and Server) are now where you are (crumbs, each part a way back), the folder tree, the list in columns -- name with an icon by kind, type, size, changed -- and, on a wide window, the chosen diff --git a/scripts/gen-i18n.py b/scripts/gen-i18n.py index 14c931e..ffb155e 100755 --- a/scripts/gen-i18n.py +++ b/scripts/gen-i18n.py @@ -564,6 +564,8 @@ KEYS = ( "Path", "Choose a file to see it here.", "Details", + # chat.js: the chat's settings in its header, folded behind one button where they do not fit + "Chat settings", ) # The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the diff --git a/tests/ui/run.mjs b/tests/ui/run.mjs index 6fdb537..d08731b 100644 --- a/tests/ui/run.mjs +++ b/tests/ui/run.mjs @@ -355,11 +355,11 @@ async function newChatOn(p, kind, modelName) { async function subagentsChip(p) { // The chip comes after the others (an open chat's waits for the server's word). await p.waitForTimeout(150) - return p.locator("#composer-chips .cchip--subagents") + return p.locator("#chat-settings .cchip--subagents") } async function openSlider(p) { - await p.click("#composer-chips .cchip--subagents") + await p.click("#chat-settings .cchip--subagents") await p.waitForSelector(".range2") return { min: await p.getAttribute(".range2__input--min", "max"), @@ -413,7 +413,7 @@ test("A5: subagents switched on while the app stays open: the next new chat offe await p.evaluate((m) => (window.__mockConfig.routes["GET /models"] = m), models()) await p.waitForTimeout(5200) await p.click("#new-chat-btn") - await p.waitForSelector("#composer-chips .cchip--subagents") + await p.waitForSelector("#chat-settings .cchip--subagents") assert.deepEqual(p.errors, []) }) @@ -470,9 +470,9 @@ test("B3: a new agent chat offers the Terminal once its folder is chosen, and op const p = await page(b3()) await p.click("#new-chat-btn") await p.click(".kind__tab >> text=Agent") - await p.waitForSelector("#composer-chips .cchip--terminal") + await p.waitForSelector("#chat-tools [data-terminal-toggle]") assert.equal(await p.evaluate(() => window.__mock.terminals().length), 0, "nothing is opened until asked") - await p.click("#composer-chips .cchip--terminal") + await p.click("#chat-tools [data-terminal-toggle]") await p.waitForSelector("#terminal:not([hidden]) .xterm") await p.waitForFunction(() => window.__mock.terminals().length === 1) const [shell] = await p.evaluate(() => window.__mock.terminals()) @@ -480,7 +480,7 @@ test("B3: a new agent chat offers the Terminal once its folder is chosen, and op await p.waitForFunction(() => document.querySelector("#terminal .xterm-rows")?.textContent.includes("dev@workbench")) assert.equal(await p.textContent("#terminal-message"), "Connected.") assert.equal(await p.textContent("#terminal-title"), "workbench") - assert.equal(await p.getAttribute("#composer-chips .cchip--terminal", "aria-pressed"), "true") + assert.equal(await p.getAttribute("#chat-tools [data-terminal-toggle]", "aria-pressed"), "true") // Keys go up through the bridge, never a socket of the page's own. await p.click("#terminal .xterm") await p.keyboard.type("ls") @@ -571,22 +571,22 @@ test("B3: no Terminal where the device opens none, for a chat on the server, or let p = await page(b3(off)) await p.click("#new-chat-btn") await p.click(".kind__tab >> text=Agent") - await p.waitForSelector("#composer-chips .cchip") - assert.equal(await p.locator("#composer-chips .cchip--terminal").count(), 0, "the device opens no terminal") + await p.waitForSelector("#chat-settings .cchip") + assert.equal(await p.locator("#chat-tools [data-terminal-toggle]").count(), 0, "the device opens no terminal") await openChatRow(p, "chat2") assert.equal(await p.locator("#chat-tools [data-terminal-toggle]").count(), 0) await p.context().close() p = await page(b3({})) await p.click("#new-chat-btn") await p.click(".kind__tab >> text=Chat") - await p.waitForSelector("#composer-chips .cchip") - assert.equal(await p.locator("#composer-chips .cchip--terminal").count(), 0, "a chat on the server has no shell") + await p.waitForSelector("#chat-settings .cchip") + assert.equal(await p.locator("#chat-tools [data-terminal-toggle]").count(), 0, "a chat on the server has no shell") await p.context().close() p = await page({ ...b3({}), status: status({ remote: { enabled: true, roots: [] } }) }) await p.click("#new-chat-btn") await p.click(".kind__tab >> text=Agent") - await p.waitForSelector("#composer-chips .cchip") - assert.equal(await p.locator("#composer-chips .cchip--terminal").count(), 0, "no folder yet") + await p.waitForSelector("#chat-settings .cchip") + assert.equal(await p.locator("#chat-tools [data-terminal-toggle]").count(), 0, "no folder yet") assert.deepEqual(p.errors, []) }) @@ -802,30 +802,30 @@ test("C4: Subagents for a new agent chat as /models?kind=agent says for this dev const p = await page(c4({ "GET /models?kind=agent&device=token1": models })) await p.click("#new-chat-btn") await p.click(".kind__tab >> text=Agent") - await p.waitForSelector("#composer-chips .cchip") + await p.waitForSelector("#chat-settings .cchip") await p.waitForTimeout(200) return p } let p = await newAgent(facts) - assert.equal(await p.locator("#composer-chips .cchip--subagents").count(), 1, "offered, and the device runs them") - await p.click("#composer-chips .cchip--subagents") + assert.equal(await p.locator("#chat-settings .cchip--subagents").count(), 1, "offered, and the device runs them") + await p.click("#chat-settings .cchip--subagents") await p.waitForSelector(".range2") assert.equal(await p.getAttribute(".range2__input--min", "max"), "32", "the server's cap") await p.context().close() p = await newAgent({ ...facts, agent_subagents: { offered: true, device_runs: false } }) - assert.equal(await p.locator("#composer-chips .cchip--subagents").count(), 0, "this computer's CLI cannot run them") + assert.equal(await p.locator("#chat-settings .cchip--subagents").count(), 0, "this computer's CLI cannot run them") await p.context().close() p = await newAgent({ ...facts, agent_subagents: { offered: false, device_runs: true } }) - assert.equal(await p.locator("#composer-chips .cchip--subagents").count(), 0, "not offered") + assert.equal(await p.locator("#chat-settings .cchip--subagents").count(), 0, "not offered") await p.context().close() p = await newAgent({ __error: { status: 400, detail: "older server" } }) - assert.equal(await p.locator("#composer-chips .cchip--subagents").count(), 0, "a server before 3.4.0: none, as before") + assert.equal(await p.locator("#chat-settings .cchip--subagents").count(), 0, "a server before 3.4.0: none, as before") await p.context().close() // An open agent chat: its own `subagents` (chat_agent.json offers them). const chat = fixture("chat_agent") p = await page(c4({ "GET /chats/chat2": { chat: { ...agentChat({ running: false }), subagents: chat.subagents, agents: chat.agents }, messages: [userMessage("m1", "Go")], more: false, live: null } })) await openChatRow(p, "chat2") - await p.waitForSelector("#composer-chips .cchip--subagents") + await p.waitForSelector("#chat-settings .cchip--subagents") assert.deepEqual(p.errors, []) }) @@ -928,7 +928,7 @@ function d4(over = {}) { } async function pickDevice(p, name) { - await p.click("#composer-chips .cchip--device") + await p.click("#chat-place .cchip--device") await p.locator(".menu .menu__item", { hasText: name }).first().click() } @@ -936,16 +936,16 @@ test("D4: a new agent chat defaults to this computer and can be sent to another const p = await page(d4()) await p.click("#new-chat-btn") await p.click(".kind__tab >> text=Agent") - await p.waitForSelector("#composer-chips .cchip--device") - assert.match(await p.textContent("#composer-chips .cchip--device"), /This computer/) + await p.waitForSelector("#chat-place .cchip--device") + assert.match(await p.textContent("#chat-place .cchip--device"), /This computer/) await pickDevice(p, "studio") - await p.waitForFunction(() => /studio/.test(document.querySelector("#composer-chips .cchip--device")?.textContent || "")) + await p.waitForFunction(() => /studio/.test(document.querySelector("#chat-place .cchip--device")?.textContent || "")) assert.match(await p.textContent("#new-chat .hello__sub"), /studio/, "the greeting names the computer") - assert.match(await p.textContent("#composer-chips"), /site/, "the folder its agent chats work in") + assert.match(await p.textContent("#chat-place"), /site/, "the folder its agent chats work in") assert.ok(await p.isHidden("#link-notice"), "this computer's agent is not in the way") await shot(p, "d4-device-1280") // The Terminal opens on that computer, in that folder, before the first message. - await p.click("#composer-chips .cchip--terminal") + await p.click("#chat-tools [data-terminal-toggle]") await p.waitForFunction(() => window.__mock.terminals().length === 1) const [shell] = await p.evaluate(() => window.__mock.terminals()) assert.deepEqual(shell.target, { chat: null, device: "token9", directory: "/srv/site" }) @@ -964,18 +964,18 @@ test("D4: on a computer with no known folder the path is typed; back to this com await p.click("#new-chat-btn") await p.click(".kind__tab >> text=Agent") await pickDevice(p, "studio") - await p.waitForSelector("#composer-chips .cchip.is-warn") + await p.waitForSelector("#chat-place .cchip.is-warn") await p.fill("#composer-text", "Hello") await p.click("#send") await p.waitForSelector("#thread-note:not([hidden])") assert.equal(await p.evaluate(() => window.__mock.api("POST", "/chats").length), 0, "nothing sent without a folder") - await p.click("#composer-chips .cchip.is-warn") + await p.click("#chat-place .cchip.is-warn") await p.waitForSelector("dialog[open] .dialog__input") await p.fill("dialog[open] .dialog__input", "/home/dev/other") await p.click("dialog[open] .btn--primary") - await p.waitForFunction(() => /other/.test(document.querySelector("#composer-chips").textContent)) + await p.waitForFunction(() => /other/.test(document.querySelector("#chat-place").textContent)) await pickDevice(p, "This computer") - await p.waitForFunction(() => /project/.test(document.querySelector("#composer-chips").textContent)) + await p.waitForFunction(() => /project/.test(document.querySelector("#chat-place").textContent)) assert.deepEqual(p.errors, []) }) @@ -983,14 +983,14 @@ test("D4: with no other computer linked there is no choice to make; at 390 px th let p = await page(b3()) await p.click("#new-chat-btn") await p.click(".kind__tab >> text=Agent") - await p.waitForSelector("#composer-chips .cchip") - assert.equal(await p.locator("#composer-chips .cchip--device").count(), 0) + await p.waitForSelector("#chat-settings .cchip") + assert.equal(await p.locator("#chat-place .cchip--device").count(), 0) await p.context().close() p = await page(d4(), { width: 390, height: 844 }) await p.evaluate(() => document.getElementById("new-chat-btn").click()) await p.click(".kind__tab >> text=Agent") await pickDevice(p, "studio") - await p.waitForFunction(() => /studio/.test(document.querySelector("#composer-chips .cchip--device")?.textContent || "")) + await p.waitForFunction(() => /studio/.test(document.querySelector("#chat-place .cchip--device")?.textContent || "")) assert.ok(await noSideways(p, 390), "nothing scrolls sideways") await shot(p, "d4-device-390") assert.deepEqual(p.errors, []) @@ -1963,6 +1963,138 @@ test("FX: at every width nothing scrolls sideways and nothing is cut off; narrow } }) +// CI2: the chat's input overhauled -- the chat's settings (mode, model, effort, subagents) in the +// header's top right beside Terminal, the activity panel, tools and skills and the menu; the +// composer keeps the message's own: text, attach, Dictate, Draw a picture, Send/Stop. Nothing is +// cut off or out of reach at any width: where the settings do not fit they fold behind Chat +// settings, which opens them as a wrapped row of their own. + +const LONG_MODEL = "Qwen3.8 Coder 480B A35B Instruct, long context, tools and vision" +function ci2(kind) { + const cfg = i8() + const m = models() + m.models[0] = { ...m.models[0], name: LONG_MODEL } + m.models[1] = { ...m.models[1], name: "Another model with a long name, served elsewhere" } + Object.assign(cfg.routes, { + "GET /models": m, + "GET /models?chat=chat1": m, + "GET /models?chat=chat2": m, + "GET /chats/chat2": { chat: agentChat({ subagents: { offered: true, named_only: false, cap: 8 } }), messages: [userMessage("m1", "What is in build?")], more: false, live: null }, + "GET /chats/chat2/tree": { tree: [], live: true }, + }) + return cfg +} + +/** Every visible button of the header and the composer inside the window, none of zero width. */ +async function reachable(p, width) { + return p.$$eval( + "#chat-head button, #composer button", + (l, w) => + l + .filter((b) => b.offsetParent !== null && getComputedStyle(b).visibility !== "hidden") + .filter((b) => { + const r = b.getBoundingClientRect() + // A button inside a container that clips it (overflow) is cut off too. + let clip = { left: 0, right: w } + for (let el = b.parentElement; el; el = el.parentElement) { + const cs = getComputedStyle(el) + if (cs.overflowX !== "visible") { + const c = el.getBoundingClientRect() + clip = { left: Math.max(clip.left, c.left), right: Math.min(clip.right, c.right) } + } + } + return r.width < 8 || r.left < clip.left - 1 || r.right > clip.right + 1 + }) + .map((b) => b.className || b.id), + width, + ) +} + +/** Pairs of the header's controls (and where the chat works) drawn over each other. */ +async function overlapping(p) { + return p.$$eval("#chat-head button, #chat-head .place", (l) => { + const shown = l.filter((b) => b.offsetParent !== null && b.getBoundingClientRect().width > 0) + const out = [] + for (let i = 0; i < shown.length; i++) + for (let j = i + 1; j < shown.length; j++) { + const a = shown[i].getBoundingClientRect() + const b = shown[j].getBoundingClientRect() + if (shown[i].contains(shown[j]) || shown[j].contains(shown[i])) continue + if (a.left < b.right - 1 && b.left < a.right - 1 && a.top < b.bottom - 1 && b.top < a.bottom - 1) out.push(`${shown[i].className} / ${shown[j].className}`) + } + return out + }) +} + +test("CI2: the composer holds the message's own controls; the chat's settings are in its header", async () => { + const p = await page(ci2(), { width: 1600, height: 900 }) + await openChatRow(p, "chat1") + await p.waitForSelector("#chat-settings .cchip--model") + await p.waitForSelector("#chat-settings .cchip--subagents") + assert.equal(await p.locator("#composer .cchip--model, #composer .cchip--subagents").count(), 0, "no settings in the composer") + assert.equal(await p.locator("#composer-chips .cchip--picture").count(), 1, "Draw a picture stays with the message") + assert.ok(await p.isVisible("#chat-tools #activity-open")) + assert.ok(await p.isVisible("#chat-tools #scope-open")) + assert.ok(await p.isHidden("#chat-fold"), "everything fits: nothing folded") + // Tools and skills opens from the header. + await p.click("#scope-open") + await p.waitForSelector(".scope") + await shot(p, "ci2-chat-1600") + assert.deepEqual(p.errors, []) +}) + +test("CI2: at 1920, 1280, 1024, 800 and 390 px every control is reachable, with long model names and every chip on", async () => { + for (const [width, height] of [[1920, 1080], [1280, 860], [1024, 768], [800, 700], [390, 844]]) { + for (const id of ["chat1", "chat2"]) { + const p = await page(ci2(), { width, height }) + await p.evaluate((x) => document.querySelector(`.chatrow[data-id="${x}"]`).click(), id) + await p.waitForSelector("#messages .msg") + await p.waitForSelector("#chat-settings .cchip--subagents", { state: "attached" }) + await p.waitForTimeout(100) + assert.ok(await noSideways(p, width), `${width} ${id}: nothing sideways`) + assert.deepEqual(await reachable(p, width), [], `${width} ${id}: every control inside`) + assert.deepEqual(await overlapping(p), [], `${width} ${id}: nothing drawn over another`) + const folded = await p.isVisible("#chat-fold") + if (folded) { + await p.click("#chat-fold") + await p.waitForSelector("#chat-settings .cchip--model", { state: "visible" }) + assert.deepEqual(await reachable(p, width), [], `${width} ${id}: unfolded, every setting inside`) + assert.deepEqual(await overlapping(p), [], `${width} ${id}: unfolded, nothing drawn over another`) + } + // Every setting is there and can be used: the model's menu opens. + const names = await p.$$eval("#chat-settings .cchip", (l) => l.map((x) => x.className)) + assert.ok(names.some((n) => n.includes("cchip--model")) && names.some((n) => n.includes("cchip--subagents")), `${width} ${id}: ${names}`) + if (id === "chat2") assert.ok(await p.isVisible("#chat-tools [data-terminal-toggle]"), `${width}: the Terminal`) + await p.click("#chat-settings .cchip--model") + await p.waitForSelector(".menu") + assert.match(await p.textContent(".menu"), /Another model/) + await p.keyboard.press("Escape") + await shot(p, `ci2-${id}-${width}${folded ? "-unfolded" : ""}`) + assert.deepEqual(p.errors, []) + await p.context().close() + } + } +}) + +test("CI2: a new agent chat says where it works beside the title and its settings at the right; the composer only the message", async () => { + for (const width of [1280, 390]) { + const p = await page(ci2(), { width, height: 844 }) + await p.waitForSelector("#chat-settings .cchip--model", { state: "attached" }) + await p.click(".kind__tab >> text=Agent") + await p.waitForSelector("#chat-place .cchip") + assert.equal(await p.locator("#composer .cchip").count(), 0, "nothing but the message in an agent chat's composer") + assert.ok(await noSideways(p, width)) + assert.deepEqual(await reachable(p, width), [], `${width}: every control inside`) + assert.deepEqual(await overlapping(p), [], `${width}: nothing drawn over another`) + if (await p.isVisible("#chat-fold")) await p.click("#chat-fold") + assert.deepEqual(await overlapping(p), [], `${width}: unfolded, nothing drawn over another`) + assert.deepEqual(await reachable(p, width), [], `${width}: unfolded, every control inside`) + await shot(p, `ci2-new-agent-${width}`) + assert.deepEqual(p.errors, []) + await p.context().close() + } +}) + // --- Run ---------------------------------------------------------------------------------------- const only = process.argv[2] || "" diff --git a/ui/app.css b/ui/app.css index 4adb287..1437104 100644 --- a/ui/app.css +++ b/ui/app.css @@ -497,7 +497,29 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-body); co .head__title { font: 600 var(--text-base) / var(--leading-tight) var(--font-body); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .head__place { display: flex; gap: var(--sp-2); align-items: center; min-width: 0; } .head__tools { display: flex; gap: var(--sp-1); align-items: center; justify-content: end; } -.main[data-new="true"] .head { display: none; } +/* The chat's header: the title, where it works, then its settings and tools at the right. The + settings take what room is left after the title's minimum; where they do not fit they fold + behind Chat settings (chat.js `fitHead`, measured), which opens them as a wrapped row of their + own under the header. */ +.head.chathead { + grid-template-columns: minmax(min(6rem, 30%), max-content) minmax(0, 1fr) minmax(0, max-content) auto auto; + grid-template-areas: "title place set fold tools"; + column-gap: var(--sp-2); +} +.chathead > .head__title { grid-area: title; } +.chathead > .head__place { grid-area: place; overflow: hidden; } +.chathead > .head__tools { grid-area: tools; } +.chathead__fold { grid-area: fold; } +.chathead__fold[hidden] { display: none; } +.chathead__settings { grid-area: set; display: flex; flex-wrap: nowrap; gap: var(--sp-1); align-items: center; justify-content: start; min-width: 0; overflow: hidden; } +.chathead__settings > * { flex: none; } +.chathead__settings:empty { display: none; } +.chathead[data-fold="true"] .chathead__settings { display: none; } +.head.chathead[data-fold="true"][data-unfold="true"] { grid-template-areas: "title place set fold tools" "row row row row row"; row-gap: var(--sp-2); } +.chathead[data-fold="true"][data-unfold="true"] .chathead__settings { grid-area: row; display: flex; flex-wrap: wrap; justify-content: start; overflow: visible; padding-bottom: var(--sp-1); } +.chathead__settings .cchip { max-width: min(var(--w-field), 100%); } +.head__place .cchip { min-width: 0; } +.head__place > .cchip:last-child { flex: 0 1 auto; } .main > .head { grid-row: 1; } .main > .thread { grid-row: 2; } .main > .dock { grid-row: 3; } @@ -530,7 +552,7 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-body); co /* A new chat is the same page, empty: the greeting low in the space above the composer, which sits in the middle of the window rather than at its foot. */ -.main[data-new="true"] { grid-template-rows: 0 minmax(0, 1fr) auto minmax(0, 0.8fr); } +.main[data-new="true"] { grid-template-rows: auto minmax(0, 1fr) auto minmax(0, 0.8fr); } .main[data-new="true"] .thread { display: grid; align-content: end; padding-bottom: var(--sp-6); } .hello { display: grid; justify-items: center; gap: var(--sp-3); text-align: center; } .hello__mark { width: var(--sp-10); height: var(--sp-10); } @@ -1928,6 +1950,8 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); } .head__place { grid-column: 1 / -1; grid-row: 2; } .head__place:empty { display: none; } .cchip { max-width: var(--w-field-xs); } + .head.chathead { grid-template-columns: minmax(min(6rem, 30%), 1fr) minmax(0, max-content) auto auto; grid-template-areas: "title set fold tools" "place place place place"; } + .head.chathead[data-fold="true"][data-unfold="true"] { grid-template-areas: "title set fold tools" "place place place place" "row row row row"; } } /* 48rem (--bp-narrow): the sidebar is a drawer over the page, opened from the title bar. */ diff --git a/ui/i18n.js b/ui/i18n.js index 4ed37bb..5bcd703 100644 --- a/ui/i18n.js +++ b/ui/i18n.js @@ -81,6 +81,7 @@ const LEMBAS_I18N = { "Carry it out": "Provést to", "Changed": "Změněno", "Chat": "Konverzace", + "Chat settings": "Nastavení konverzace", "Chats": "Konverzace", "Check for updates…": "Zkontrolovat aktualizace…", "Choose": "Vybrat", @@ -585,6 +586,7 @@ const LEMBAS_I18N = { "Carry it out": "Ausführen", "Changed": "Geändert", "Chat": "Chat", + "Chat settings": "Chat-Einstellungen", "Chats": "Chats", "Check for updates…": "Nach Updates suchen…", "Choose": "Auswählen", @@ -1089,6 +1091,7 @@ const LEMBAS_I18N = { "Carry it out": "Llevarlo a cabo", "Changed": "Cambiado", "Chat": "Chat", + "Chat settings": "Ajustes del chat", "Chats": "Chats", "Check for updates…": "Buscar actualizaciones…", "Choose": "Elegir", @@ -1593,6 +1596,7 @@ const LEMBAS_I18N = { "Carry it out": "Exécuter", "Changed": "Modifié", "Chat": "Conversation", + "Chat settings": "Paramètres de la conversation", "Chats": "Conversations", "Check for updates…": "Rechercher des mises à jour…", "Choose": "Choisir", @@ -2097,6 +2101,7 @@ const LEMBAS_I18N = { "Carry it out": "Végrehajtás", "Changed": "Módosítva", "Chat": "Csevegés", + "Chat settings": "Csevegés beállításai", "Chats": "Csevegések", "Check for updates…": "Frissítések keresése…", "Choose": "Kiválasztás", @@ -2601,6 +2606,7 @@ const LEMBAS_I18N = { "Carry it out": "Eseguilo", "Changed": "Modificato", "Chat": "Chat", + "Chat settings": "Impostazioni della chat", "Chats": "Chat", "Check for updates…": "Cerca aggiornamenti…", "Choose": "Scegli", @@ -3105,6 +3111,7 @@ const LEMBAS_I18N = { "Carry it out": "Wykonaj", "Changed": "Zmieniono", "Chat": "Czat", + "Chat settings": "Ustawienia czatu", "Chats": "Czaty", "Check for updates…": "Sprawdź aktualizacje…", "Choose": "Wybierz", @@ -3609,6 +3616,7 @@ const LEMBAS_I18N = { "Carry it out": "Vykonať to", "Changed": "Zmenené", "Chat": "Konverzácia", + "Chat settings": "Nastavenia konverzácie", "Chats": "Konverzácie", "Check for updates…": "Skontrolovať aktualizácie…", "Choose": "Vybrať", diff --git a/ui/index.html b/ui/index.html index c8eafac..19412dc 100644 --- a/ui/index.html +++ b/ui/index.html @@ -64,9 +64,11 @@