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:
355 files changed
+47028
No files matched your search
@@ -0,0 +1,248 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { createEffect, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { duration } from "../../duration.ts"
|
||||
import { estimateTokens } from "../../provider/tokens.ts"
|
||||
import { useTui } from "../context.ts"
|
||||
import type { Item } from "../state.ts"
|
||||
import { clipDiff } from "../diffclip.ts"
|
||||
|
||||
const FILETYPES: Record<string, string> = { ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", mjs: "javascript", md: "markdown", zig: "zig" }
|
||||
export const filetypeOf = (diff: string) => FILETYPES[/^\+\+\+ \S*\.(\w+)/m.exec(diff)?.[1] ?? ""]
|
||||
|
||||
/** The one-line summary of a call before its result says more, e.g. the command or the path. */
|
||||
function argSummary(args: unknown): string {
|
||||
if (!args || typeof args !== "object") return ""
|
||||
const a = args as Record<string, unknown>
|
||||
if (Array.isArray(a.questions)) return (a.questions as { header?: string }[]).map((q) => q.header).filter(Boolean).join(" · ") || "asking"
|
||||
if (Array.isArray(a.todos)) return `${a.todos.length} item${a.todos.length === 1 ? "" : "s"}`
|
||||
if (typeof a.patch === "string") return [...a.patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)].map((m) => m[1]).join(", ")
|
||||
const v = a.description ?? a.command ?? a.path ?? a.pattern ?? a.id ?? ""
|
||||
return String(v).split("\n")[0]!.slice(0, 120)
|
||||
}
|
||||
|
||||
function UserMessage(p: { item: Extract<Item, { kind: "user" }> }) {
|
||||
const { theme } = useTui()
|
||||
return (
|
||||
<box border={["left"]} borderColor={theme().colors.userBar} customBorderChars={{ vertical: "┃", topLeft: "", topRight: "", bottomLeft: "", bottomRight: "", horizontal: "", topT: "", bottomT: "", leftT: "", rightT: "", cross: "" } as never} paddingLeft={1} marginTop={1}>
|
||||
<box flexDirection="column">
|
||||
<Show when={p.item.mid}>
|
||||
<text fg={theme().colors.dim} wrapMode="none" truncate>
|
||||
↳ sent while it worked
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme().colors.text} wrapMode="word">
|
||||
{p.item.text}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function AssistantMessage(p: { item: Extract<Item, { kind: "assistant" }> }) {
|
||||
const { theme, md } = useTui()
|
||||
return (
|
||||
<box paddingLeft={2} marginTop={1}>
|
||||
<markdown content={p.item.text.trim()} syntaxStyle={md()} streaming={p.item.streaming} fg={theme().colors.text} conceal />
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
/** Thinking is one line, never the text as it streams (a growing block jumped about as it
|
||||
* re-laid itself out; what matters is the tools and the answer): "Thinking." "Thinking.."
|
||||
* "Thinking..." while it thinks, then "Thought for 12.3s · 1,234 tokens" — this thought's time
|
||||
* and tokens, nothing else (the task's are on the line at the bottom). The tokens are the
|
||||
* provider's count when the step had this one thought, else an estimate (~). Ctrl+R opens a
|
||||
* finished thought. */
|
||||
function Reasoning(p: { item: Extract<Item, { kind: "reasoning" }> }) {
|
||||
const { theme, thought, view } = useTui()
|
||||
const [now, setNow] = createSignal(Date.now())
|
||||
createEffect(() => {
|
||||
if (!p.item.streaming) return
|
||||
const t = setInterval(() => setNow(Date.now()), 200)
|
||||
onCleanup(() => clearInterval(t))
|
||||
})
|
||||
const tokens = () => {
|
||||
const n = p.item.tokens ?? estimateTokens(p.item.text)
|
||||
return `${p.item.tokens === undefined ? "~" : ""}${n.toLocaleString("en")} token${n === 1 ? "" : "s"}`
|
||||
}
|
||||
const dots = () => ".".repeat(1 + (Math.floor((now() - p.item.started) / 400) % 3))
|
||||
const line = () => (p.item.streaming ? `Thinking${dots()}` : p.item.ms !== undefined ? `Thought for ${duration(p.item.ms)} · ${tokens()}` : `Thought · ${tokens()}`)
|
||||
return (
|
||||
<box paddingLeft={2} marginTop={1} flexDirection="column">
|
||||
<text fg={theme().colors.reasoning} attributes={TextAttributes.ITALIC} wrapMode="none" truncate>
|
||||
{line()}
|
||||
</text>
|
||||
<Show when={view.state.showReasoning && !p.item.streaming}>
|
||||
<box paddingLeft={2}>
|
||||
<code filetype="markdown" content={p.item.text.trim()} syntaxStyle={thought()} drawUnstyledText conceal />
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolLine(p: { item: Extract<Item, { kind: "tool" }> }) {
|
||||
const { theme, view } = useTui()
|
||||
const c = () => theme().colors
|
||||
const emoji = () => (p.item.status === "denied" ? theme().icons.denied : (theme().toolEmoji[p.item.name] ?? (p.item.name.startsWith("mcp__") ? theme().icons.mcp : theme().icons.other)))
|
||||
const title = () => p.item.title ?? argSummary(p.item.args)
|
||||
const time = () => (p.item.status === "running" ? "…" : p.item.ms !== undefined ? duration(p.item.ms) : "")
|
||||
const tail = () => (p.item.output ?? "").trimEnd().split("\n").slice(-12).join("\n")
|
||||
// Short in the transcript, whole with ctrl+o; always a diff the renderer can parse.
|
||||
const shown = () => (view.state.expandTools ? { diff: p.item.diff ?? "", hidden: 0 } : clipDiff(p.item.diff ?? "", 24))
|
||||
return (
|
||||
<box flexDirection="column" paddingLeft={1}>
|
||||
<text wrapMode="none" truncate>
|
||||
<span style={{ fg: c().dim }}>{theme().branding.toolPrefix} </span>
|
||||
{emoji()}{" "}
|
||||
<span style={{ fg: p.item.status === "denied" ? c().warn : c().label, attributes: p.item.status === "denied" ? TextAttributes.STRIKETHROUGH : 0 }}>{p.item.name.padEnd(6)}</span>{" "}
|
||||
<span style={{ fg: p.item.status === "error" ? c().error : p.item.status === "denied" ? c().warn : c().text }}>{title()}</span>{" "}
|
||||
<span style={{ fg: c().dim }}>{time()}</span>
|
||||
<Show when={p.item.status === "error"}>
|
||||
<span style={{ fg: c().error }}> ✗</span>
|
||||
</Show>
|
||||
</text>
|
||||
<For each={p.item.children ?? []}>
|
||||
{(ch) => (
|
||||
<text wrapMode="none" truncate>
|
||||
<span style={{ fg: c().dim }}>{" "}{theme().branding.toolPrefix} </span>
|
||||
{theme().toolEmoji[ch.name] ?? (ch.name.startsWith("mcp__") ? theme().icons.mcp : theme().icons.other)}{" "}
|
||||
<span style={{ fg: c().label }}>{ch.name.padEnd(6)}</span>{" "}
|
||||
<span style={{ fg: ch.isError ? c().error : c().muted }}>{ch.title}</span>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
<Show when={p.item.diff}>
|
||||
<box paddingLeft={4} paddingRight={1} flexDirection="column">
|
||||
<WrappedDiff patch={shown().diff} filetype={filetypeOf(p.item.diff!)} />
|
||||
<Show when={shown().hidden > 0}>
|
||||
<text fg={c().muted}>{`… ${shown().hidden} more line${shown().hidden === 1 ? "" : "s"} (ctrl+o)`}</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={view.state.expandTools && !p.item.diff && tail()}>
|
||||
<box paddingLeft={4}>
|
||||
<text fg={c().muted} wrapMode="char">
|
||||
{tail()}
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
/** A unified diff in the transcript, given its width outright. Left to fill its box, OpenTUI's
|
||||
* diff wrapped long lines at a width measured before the transcript's scrollbar took its column:
|
||||
* with the scrollbar showing, a long line ran one column past its own background. The box's laid-out width is right, so the diff gets that number. */
|
||||
function WrappedDiff(p: { patch: string; filetype?: string }) {
|
||||
const { theme } = useTui()
|
||||
const c = () => theme().colors
|
||||
const [width, setWidth] = createSignal<number>()
|
||||
return (
|
||||
<box
|
||||
flexDirection="column"
|
||||
onSizeChange={function (this: { width: number }) {
|
||||
const w = this.width
|
||||
// After this layout pass, not inside it.
|
||||
if (w > 0 && w !== width()) setTimeout(() => setWidth(w), 0)
|
||||
}}
|
||||
>
|
||||
<diff
|
||||
width={width()}
|
||||
diff={p.patch}
|
||||
view="unified"
|
||||
filetype={p.filetype}
|
||||
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}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
/** A multi-file diff (/diff), one block per file. */
|
||||
function DiffBlock(p: { item: Extract<Item, { kind: "diff" }> }) {
|
||||
const { theme } = useTui()
|
||||
const c = () => theme().colors
|
||||
const files = () =>
|
||||
p.item.patch
|
||||
.split(/^(?=diff --git )/m)
|
||||
.filter((f) => f.startsWith("diff --git"))
|
||||
.map((f) => ({ name: /^diff --git a\/(.+?) b\//.exec(f)?.[1] ?? "?", patch: f }))
|
||||
return (
|
||||
<box flexDirection="column" paddingLeft={1} marginTop={1}>
|
||||
<text fg={c().title}>{p.item.title}</text>
|
||||
<For each={files()}>
|
||||
{(f) => (
|
||||
<box flexDirection="column" paddingLeft={2}>
|
||||
<text fg={c().label}>{f.name}</text>
|
||||
<WrappedDiff patch={f.patch} filetype={filetypeOf(f.patch)} />
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function Notice(p: { item: Extract<Item, { kind: "notice" }> }) {
|
||||
const { theme } = useTui()
|
||||
return (
|
||||
<box paddingLeft={1}>
|
||||
<text fg={p.item.level === "warn" ? theme().colors.warn : theme().colors.muted} wrapMode="word">
|
||||
{p.item.level === "warn" ? "⚠ " : "· "}
|
||||
{p.item.text}
|
||||
</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function ErrorLine(p: { item: Extract<Item, { kind: "error" }> }) {
|
||||
const { theme } = useTui()
|
||||
return (
|
||||
<box paddingLeft={1} marginTop={1}>
|
||||
<text fg={theme().colors.error} wrapMode="word">
|
||||
✗ {p.item.text}
|
||||
</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
export function Messages() {
|
||||
const { view } = useTui()
|
||||
return (
|
||||
<For each={view.state.items}>
|
||||
{(item) => (
|
||||
<Switch>
|
||||
<Match when={item.kind === "user"}>
|
||||
<UserMessage item={item as Extract<Item, { kind: "user" }>} />
|
||||
</Match>
|
||||
<Match when={item.kind === "assistant"}>
|
||||
<AssistantMessage item={item as Extract<Item, { kind: "assistant" }>} />
|
||||
</Match>
|
||||
<Match when={item.kind === "reasoning"}>
|
||||
<Reasoning item={item as Extract<Item, { kind: "reasoning" }>} />
|
||||
</Match>
|
||||
<Match when={item.kind === "tool"}>
|
||||
<ToolLine item={item as Extract<Item, { kind: "tool" }>} />
|
||||
</Match>
|
||||
<Match when={item.kind === "notice"}>
|
||||
<Notice item={item as Extract<Item, { kind: "notice" }>} />
|
||||
</Match>
|
||||
<Match when={item.kind === "diff"}>
|
||||
<DiffBlock item={item as Extract<Item, { kind: "diff" }>} />
|
||||
</Match>
|
||||
<Match when={item.kind === "error"}>
|
||||
<ErrorLine item={item as Extract<Item, { kind: "error" }>} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</For>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user