LLeMbas CLI 1.0.0
ci / check (push) Canceled after 0s

The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM
API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a
link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64
and arm64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-09 21:59:03 +00:00
commit f9bad01ed7
355 files changed
+47028

No files matched your search

+209
View File
@@ -0,0 +1,209 @@
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
import { useKeyboard } from "@opentui/solid"
import { createSignal, For, Show } from "solid-js"
import type { AskReply } from "../../bus/index.ts"
import { useTui } from "../context.ts"
import type { Pending } from "../state.ts"
import { diffStat } from "../../tool/write.ts"
import { filetypeOf } from "./messages.tsx"
interface Choice {
key: string
label: string
reply: () => AskReply | "reason" | "edit"
}
/** A command as the rows it takes at `width` columns (it is drawn wrapped by character). */
export function rowsOf(text: string, width: number): string[] {
const w = Math.max(1, width)
return text.split("\n").flatMap((l) => (l.length <= w ? [l] : Array.from({ length: Math.ceil(l.length / w) }, (_, i) => l.slice(i * w, (i + 1) * w))))
}
/** Rows of a long command shown before ctrl+o. */
const COLLAPSED = 6
const clipText = (t: string, n: number) => (t.length > n ? `${t.slice(0, n - 1)}…` : t)
/** Asked above the prompt: y once · e edit the command, then once · s for the session · p for the
* project · n no · r no, and why.
* First what the model says the call is for; then the command, or the change to files as a diff
* (pgup/pgdn scroll it), so the answer is about the thing itself. The card never grows past the
* screen: a long command is cut to a few rows (ctrl+o: all of it, scrolling), and the body gets
* what the terminal's height leaves after the choices, the prompt and the status bar. */
export function PermissionPrompt(p: { pending: Pending }) {
const { theme, app, dims } = useTui()
const c = () => theme().colors
const always = () => p.pending.decision.always.join(", ")
const choices = (): Choice[] => [
{ key: "y", label: "Allow once", reply: () => ({ kind: "once" }) },
// A command can be corrected before it runs (after LLeMbas): what is written is judged again.
...(p.pending.request.command !== undefined ? [{ key: "e", label: "Edit, then allow", reply: () => "edit" as const }] : []),
...(always() ? [{ key: "s", label: "Always this session", reply: () => ({ kind: "session" }) as AskReply }] : []),
...(always() && app.trusted ? [{ key: "p", label: "Always in this project", reply: () => ({ kind: "project" }) as AskReply }] : []),
{ key: "n", label: "Deny", reply: () => ({ kind: "deny" }) },
{ key: "r", label: "Deny, and say why", reply: () => "reason" as const },
]
const [sel, setSel] = createSignal(0)
const [reasoning, setReasoning] = createSignal(false)
const [editing, setEditing] = createSignal(false)
const [whole, setWhole] = createSignal(false)
let input: { value: string } | undefined
let box: ScrollBoxRenderable | undefined
const diff = () => p.pending.preview?.diff
const stat = () => {
const d = diff()
if (!d) return ""
const { added, removed } = diffStat(d)
return `+${added} −${removed}`
}
const choose = (ch: Choice) => {
const r = ch.reply()
if (r === "reason") return setReasoning(true)
if (r === "edit") return setEditing(true)
p.pending.resolve(r)
}
useKeyboard((key) => {
if (reasoning() || editing()) {
if (key.name === "escape") {
setReasoning(false)
setEditing(false)
key.preventDefault()
}
return
}
const list = choices()
if (key.name === "left" || key.name === "h" || (key.name === "tab" && key.shift)) setSel((i) => (i + list.length - 1) % list.length)
else if (key.name === "right" || key.name === "l" || key.name === "tab") setSel((i) => (i + 1) % list.length)
else if (key.name === "return") choose(list[sel()]!)
else if (key.name === "escape") p.pending.resolve({ kind: "deny" })
else if (key.ctrl && key.name === "o") {
setWhole((v) => !v)
box?.scrollTo(0)
} else if (key.name === "pageup") box?.scrollBy(-Math.max(1, Math.floor(box.height / 2)))
else if (key.name === "pagedown") box?.scrollBy(Math.max(1, Math.floor(box.height / 2)))
else {
const hit = list.find((ch) => ch.key === key.name)
if (!hit) return
choose(hit)
}
key.preventDefault()
})
const what = () => p.pending.request.command ?? p.pending.request.patterns.join(", ")
// Inside the border, the padding and the scrollbar.
const width = () => Math.max(10, dims().width - 5)
const purpose = () => (p.pending.purpose ? rowsOf(p.pending.purpose, width()).slice(0, 3) : [])
const failRows = () => (p.pending.preview?.error ? rowsOf(`this will fail: ${p.pending.preview.error}`, width()).length : 0)
// Rows the body may take: the screen, less the prompt and status bar (4), the border (2), the
// reason and the choices (2 each at most), two rows of the conversation, purpose and failure.
const budget = () => Math.max(3, dims().height - 12 - purpose().length - failRows())
const rows = () => rowsOf(what(), width())
const cut = () => !whole() && rows().length > Math.min(COLLAPSED, budget())
const shown = () => (cut() ? rows().slice(0, Math.min(COLLAPSED, budget()) - 1).join("\n") : what())
// A scrollbox takes the height it is given, so it is given what its content needs, up to the
// budget: the command's rows, the "more lines" row, and the diff's (wrapped beside its gutter; file headers and
// @@ lines are not drawn).
const bodyHeight = () => {
const d = diff()
const diffRows = d ? rowsOf(d.split("\n").filter((l) => !/^(Index:|={5,}|--- |\+\+\+ |@@)/.test(l)).join("\n").replace(/\n$/, ""), width() - 8).length : 0
const commandRows = cut() ? Math.min(COLLAPSED, budget()) : rows().length
return Math.max(1, Math.min(budget(), commandRows + diffRows))
}
const hint = () =>
[cut() ? `… ${rows().length - Math.min(COLLAPSED, budget()) + 1} more lines — ctrl+o shows all` : whole() && rows().length > COLLAPSED ? "ctrl+o shortens it" : "", diff() || whole() ? "pgup/pgdn scroll" : ""]
.filter(Boolean)
.join(" · ")
return (
<box border borderStyle="rounded" borderColor={c().warn} paddingLeft={1} paddingRight={1} flexDirection="column" flexShrink={0} title={` ${p.pending.tool} `} titleColor={c().warn}>
<Show when={purpose().length}>
<text fg={c().accent} wrapMode="none">
{purpose().join("\n")}
</text>
</Show>
<scrollbox ref={(r: ScrollBoxRenderable) => (box = r)} height={bodyHeight()} scrollX={false}>
<text fg={c().text} wrapMode="char">
{shown()}
<Show when={stat()}>
<span style={{ fg: c().muted }}> {stat()}</span>
</Show>
</text>
<Show when={cut()}>
<text fg={c().muted}>{hint().split(" · ")[0]}</text>
</Show>
<Show when={diff()}>
<diff
diff={diff()!}
view="unified"
filetype={filetypeOf(diff()!)}
showLineNumbers
wrapMode="char"
fg={c().text}
addedBg={c().diffAddedBg}
removedBg={c().diffRemovedBg}
contextBg={c().diffContextBg}
addedSignColor={c().diffAddedSign}
removedSignColor={c().diffRemovedSign}
lineNumberFg={c().lineNumber}
lineNumberBg={c().diffContextBg}
/>
</Show>
</scrollbox>
<Show when={p.pending.preview?.error}>
<text fg={c().error} wrapMode="word">
{`this will fail: ${p.pending.preview!.error}`}
</text>
</Show>
<text fg={c().muted} wrapMode="word">
{clipText(p.pending.decision.reason, 160)}
{always() ? ` · always would allow: ${clipText(always(), 80)}` : ""}
{!cut() && hint() ? ` · ${hint()}` : ""}
</text>
<Show when={editing()}>
<box flexDirection="row">
<text fg={c().label}>run › </text>
<input
focused
flexGrow={1}
value={p.pending.request.command ?? ""}
textColor={c().text}
placeholder="the command as it should run (enter to allow it, esc to go back)"
onSubmit={(v: unknown) => {
const line = String(typeof v === "string" ? v : "").trim()
if (line) p.pending.resolve({ kind: "once", command: line })
}}
/>
</box>
</Show>
<Show when={reasoning()}>
<box flexDirection="row">
<text fg={c().label}>why? </text>
<input
focused
flexGrow={1}
ref={(r) => (input = r as never)}
textColor={c().text}
placeholder="tell the model what to do instead (enter to send, esc to go back)"
onSubmit={(v: unknown) => p.pending.resolve({ kind: "deny", feedback: String(typeof v === "string" ? v : (input?.value ?? "")).trim() || undefined })}
/>
</box>
</Show>
<Show when={!reasoning() && !editing()}>
<box flexDirection="row" flexWrap="wrap" gap={1}>
<For each={choices()}>
{(ch, i) => (
<text
fg={i() === sel() ? c().panel : c().text}
bg={i() === sel() ? c().accent : undefined}
attributes={i() === sel() ? TextAttributes.BOLD : 0}
>
{` ${ch.key} ${ch.label} `}
</text>
)}
</For>
</box>
</Show>
</box>
)
}