A chat app's shape: sidebar, a new chat as a chat page, the chat's settings as composer chips
- Sidebar: New chat, search, Scheduled, Files, the chats of both kinds by day (Today, Earlier), Settings with the account at the foot; folds away (Ctrl+B), a drawer on a narrow window. - A new chat is the chat page empty: a greeting, Agent or Chat, and the composer. - The composer is a card: the text, then attach, the chips (folder and mode for an agent chat, model with its picture, effort as the model offers it, the team's size), and a round Send that Stop replaces while a reply is written. An open chat's chips change the chat. - Messages: the person's in a bubble on the right with attached pictures as thumbnails, a reply full width; thinking and tool calls as compact rows that unfold. - Activity: a chip above the composer for every open chat, the panel on the right as before. - The agent: a chip in the title bar opening its state and the one thing to do about it; "This computer is not linked yet" with Link this computer right above the composer of an agent chat, and "refused" only with the server's reason. - Files: a page with the two trees as tabs. Settings: grouped, the groups listed beside them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
15 files changed
+1856
-916
No files matched your search
+53
-5
@@ -1,9 +1,12 @@
|
||||
// The title bar: the app's own, in the page (the window has no system decoration). Moving and
|
||||
// double-click maximising come from `data-tauri-drag-region`; the edges resize the window (Tauri
|
||||
// does that for an undecorated one); the menu and the agent pill open the app's native menu.
|
||||
// does that for an undecorated one). The menu opens the app's native menu; the agent's chip opens
|
||||
// a small panel with what the agent says and the one thing to do about it.
|
||||
|
||||
import { $, icon, ICONS } from "./dom.js"
|
||||
import { $, h, icon, ICONS } from "./dom.js"
|
||||
import { invoke, listen, win } from "./bridge.js"
|
||||
import { agentBlock, onAgentChange } from "./agent.js"
|
||||
import { popover, closePopover } from "./popup.js"
|
||||
|
||||
const WORDS = {
|
||||
off: () => t("agent off"),
|
||||
@@ -19,8 +22,11 @@ const WORDS = {
|
||||
no_folders: () => t("agent: no folders"),
|
||||
}
|
||||
|
||||
let ctx = null // { status(), openSettings() }
|
||||
let pop = null
|
||||
|
||||
/**
|
||||
* The pill's state: what the CLI says of its link, not whether a process runs. Linked but allowed
|
||||
* The chip's state: what the CLI says of its link, not whether a process runs. Linked but allowed
|
||||
* into no folder is said too -- a green light would say it works when it cannot.
|
||||
*/
|
||||
export function pillState(s) {
|
||||
@@ -53,6 +59,46 @@ export function renderBar(s) {
|
||||
$("update").hidden = !s.update
|
||||
$("update").textContent = s.update ? t("update %(version)s", { version: s.update }) : t("update")
|
||||
$("update").title = s.update ? t("LLeMbas %(version)s is available (this is %(current)s)", { version: s.update, current: s.version }) : ""
|
||||
pop?.redraw()
|
||||
}
|
||||
|
||||
/** The chip's panel: the agent's state and its button, then start or stop, and Settings. */
|
||||
function agentPanel() {
|
||||
const s = ctx.status()
|
||||
const box = h("div", { class: "pop__body" }, h("p", { class: "pop__title", text: t("Agent") }), agentBlock(s))
|
||||
const row = h("div", { class: "pop__actions" })
|
||||
if (s.linked) {
|
||||
row.append(h("button", { type: "button", class: "btn btn--sm", text: s.agent_wanted ? t("Stop the agent") : t("Start the agent"), onclick: () => invoke("set_agent", { on: !s.agent_wanted }) }))
|
||||
}
|
||||
row.append(
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "btn btn--sm",
|
||||
text: t("Settings"),
|
||||
onclick: () => {
|
||||
closePopover()
|
||||
ctx.openSettings("agent")
|
||||
},
|
||||
}),
|
||||
)
|
||||
box.append(row)
|
||||
return box
|
||||
}
|
||||
|
||||
function toggleAgent() {
|
||||
if (pop) {
|
||||
closePopover()
|
||||
return
|
||||
}
|
||||
$("agent").setAttribute("aria-expanded", "true")
|
||||
pop = popover($("agent"), agentPanel, {
|
||||
align: "end",
|
||||
label: t("Agent"),
|
||||
onClose: () => {
|
||||
pop = null
|
||||
$("agent").setAttribute("aria-expanded", "false")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function maximized(on) {
|
||||
@@ -68,7 +114,8 @@ function menuAt(button) {
|
||||
return invoke("titlebar_menu", { x: r.left, y: r.bottom })
|
||||
}
|
||||
|
||||
export function initBar() {
|
||||
export function initBar(context) {
|
||||
ctx = context
|
||||
$("minimize").replaceChildren(icon(ICONS.min))
|
||||
$("close").replaceChildren(icon(ICONS.close))
|
||||
$("menu").replaceChildren(icon(ICONS.menu))
|
||||
@@ -78,7 +125,8 @@ export function initBar() {
|
||||
$("close").addEventListener("click", () => win.close())
|
||||
$("update").addEventListener("click", () => invoke("update_app"))
|
||||
$("menu").addEventListener("click", (e) => menuAt(e.currentTarget))
|
||||
$("agent").addEventListener("click", (e) => menuAt(e.currentTarget))
|
||||
$("agent").addEventListener("click", toggleAgent)
|
||||
onAgentChange(() => pop?.redraw())
|
||||
listen("maximized", (e) => maximized(!!e.payload))
|
||||
win.isMaximized().then(maximized).catch(() => maximized(false))
|
||||
}
|
||||
Reference in new issue
Block a user