Telegram bot as a chat interface (per user, paired, CLI-like commands)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 16:50:51 +01:00
1 parent b7c8741a7d
commit 011756ca3d
12 files changed
+1361 -17

No files matched your search

+2
View File
@@ -6,6 +6,7 @@ import { GitAccount } from "./GitAccount";
import { ApiTokens } from "./ApiTokens";
import { CliInstall } from "./CliInstall";
import { Memory } from "./Memory";
import { Telegram } from "./Telegram";
export function Account() {
const t = useT();
@@ -51,6 +52,7 @@ export function Account() {
<Memory />
<GitAccount />
<CliInstall />
<Telegram />
<ApiTokens />
</>
);
+94
View File
@@ -0,0 +1,94 @@
import { useState } from "react";
import { del, put } from "../../api";
import { confirmDialog, toast } from "../../dialogs";
import { useFetch } from "../../hooks";
import { useT } from "../../i18n";
import { Icon } from "../Icon";
interface Status {
configured: boolean;
bot: string;
paired: boolean;
pair_code: string;
running: boolean;
error?: string | null;
}
/** Connect a personal Telegram bot as a chat interface. */
export function Telegram() {
const t = useT();
const { data, reload } = useFetch<Status>("/account/telegram");
const [token, setToken] = useState("");
const [busy, setBusy] = useState(false);
const connect = async () => {
setBusy(true);
try {
await put("/account/telegram", { token: token.trim() });
setToken("");
reload();
} catch (e) {
toast(e);
} finally {
setBusy(false);
}
};
const disconnect = async () => {
if (!(await confirmDialog({ message: t("telegram.disconnectConfirm"), danger: true, confirmLabel: t("telegram.disconnect") }))) return;
await del("/account/telegram").catch((e) => toast(e));
reload();
};
return (
<div className="panel">
<h3>
<Icon name="share" /> {t("telegram.title")}
<span className="spacer" />
{data?.configured && (
<button className="small ghost danger" onClick={disconnect}>
{t("telegram.disconnect")}
</button>
)}
</h3>
<div className="hint" style={{ marginBottom: 10 }}>
{t("telegram.intro")}
</div>
{data && !data.configured && (
<>
<ol className="cli-steps">
<li>{t("telegram.step1")}</li>
<li>{t("telegram.step2")}</li>
</ol>
<div className="row" style={{ gap: 6 }}>
<input type="password" value={token} placeholder="123456789:AA…" autoComplete="off" onChange={(e) => setToken(e.target.value)} style={{ flex: 1 }} />
<button className="primary" onClick={() => void connect()} disabled={busy || !token.trim()}>
{busy ? t("telegram.checking") : t("telegram.connect")}
</button>
</div>
</>
)}
{data?.configured && (
<>
<div className="row" style={{ gap: 8, alignItems: "center", flexWrap: "wrap" }}>
<span className="chip">@{data.bot}</span>
<span className={`chip ${data.running && !data.error ? "ok" : "warn"}`}>{data.running && !data.error ? t("telegram.running") : t("telegram.stopped")}</span>
{data.paired && <span className="chip ok">{t("telegram.paired")}</span>}
</div>
{!data.paired && data.pair_code && (
<div className="notice" style={{ marginTop: 10 }}>
{t("telegram.pairHint", { bot: data.bot })}{" "}
<a href={`https://t.me/${data.bot}?start=${data.pair_code}`} target="_blank" rel="noreferrer">
t.me/{data.bot}
</a>
<div style={{ marginTop: 6 }}>
<code className="token-value">/start {data.pair_code}</code>
</div>
</div>
)}
{data.error && <div className="notice err" style={{ marginTop: 10 }}>{data.error}</div>}
{data.paired && <div className="hint" style={{ marginTop: 10 }}>{t("telegram.commands")}</div>}
</>
)}
</div>
);
}
+13
View File
@@ -405,6 +405,19 @@ export const de: Messages = {
"tokens.title": "API-Tokens",
"tokens.hint": "Für den Kommandozeilen-Client (codecraft) und Skripte: als „Authorization: Bearer <token>“ mitschicken. Nur-Lesen-Tokens können nur lesen (keine Prompts, keine Änderungen).",
"tokens.copyNow": "Token jetzt kopieren – er wird nur einmal angezeigt:",
"telegram.title": "Telegram",
"telegram.intro": "CodeCraft über Telegram mit deinem eigenen Bot nutzen: Nachrichten gehen an deine aktuelle Sitzung, Befehle wechseln Sitzungen, Modi und Modelle, Freigaben und Rückfragen kommen mit Knöpfen. Nur dein Telegram-Konto kann den Bot nutzen.",
"telegram.step1": "In Telegram @BotFather öffnen, /newbot senden und den Schritten folgen.",
"telegram.step2": "Das Token von BotFather hier einfügen.",
"telegram.connect": "Verbinden",
"telegram.checking": "Prüfe…",
"telegram.running": "läuft",
"telegram.stopped": "läuft nicht",
"telegram.paired": "gekoppelt",
"telegram.pairHint": "Letzter Schritt: deinen Bot öffnen und den Code senden (oder den Link antippen) – danach bedient er nur noch dein Telegram-Konto:",
"telegram.commands": "In Telegram: /help zeigt die Befehle – /sessions, /new, /mode, /model, /stop, /status, /costs …",
"telegram.disconnect": "Trennen",
"telegram.disconnectConfirm": "Telegram-Bot trennen? Er antwortet dann nicht mehr; mit seinem Token kannst du ihn wieder verbinden.",
"tokens.copy": "Kopieren",
"tokens.copied": "Kopiert",
"tokens.name": "Name",
+13
View File
@@ -404,6 +404,19 @@ export const en = {
"tokens.title": "API tokens",
"tokens.hint": "For the command line client (codecraft) and scripts: send as “Authorization: Bearer <token>”. Read-only tokens can only read (no prompts, no changes).",
"tokens.copyNow": "Copy the token now – it is shown only once:",
"telegram.title": "Telegram",
"telegram.intro": "Use CodeCraft from Telegram with your own bot: messages go to your current session, commands switch sessions, modes and models, approvals and questions come with buttons. Only your Telegram account can use the bot.",
"telegram.step1": "In Telegram, open @BotFather, send /newbot and follow the steps.",
"telegram.step2": "Paste the token BotFather gives you here.",
"telegram.connect": "Connect",
"telegram.checking": "Checking…",
"telegram.running": "running",
"telegram.stopped": "not running",
"telegram.paired": "paired",
"telegram.pairHint": "Last step: open your bot and send the code (or tap the link) – from then on only your Telegram account is served:",
"telegram.commands": "In Telegram: /help lists the commands – /sessions, /new, /mode, /model, /stop, /status, /costs …",
"telegram.disconnect": "Disconnect",
"telegram.disconnectConfirm": "Disconnect the Telegram bot? It stops answering; you can connect it again with its token.",
"tokens.copy": "Copy",
"tokens.copied": "Copied",
"tokens.name": "Name",
+4
View File
@@ -3,6 +3,10 @@
* under "unreleased"; scripts/release.sh renames the key to the release version.
*/
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
unreleased: {
en: ["Telegram: connect your own bot under Settings → Account → Telegram and chat with CodeCraft from your phone"],
de: ["Telegram: eigenen Bot unter Einstellungen → Konto → Telegram verbinden und von unterwegs mit CodeCraft chatten"],
},
"202611-00006": {
en: ["Docker CLI in the image – with the host's Docker socket mounted, admins' agents and terminals can use Docker"],
de: ["Docker-CLI im Image – mit eingehängtem Docker-Socket können Admin-Agents und -Terminals Docker nutzen"],