loom-agent 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +216 -10
- package/README.md +10 -8
- package/package.json +2 -2
- package/src/core/agents.js +14 -1
- package/src/core/cli.js +30 -2
- package/src/core/hooks.js +28 -14
- package/src/core/memory.js +54 -6
- package/src/core/permissions.js +17 -4
- package/src/core/plugin-cmd.js +88 -1
- package/src/core/sandbox.js +311 -75
- package/src/core/session.js +262 -35
- package/src/core/transcript-undo.js +74 -0
- package/src/core/update.js +116 -6
- package/src/mcp/connector-presets.js +105 -0
- package/src/providers/anthropic.js +122 -25
- package/src/providers/google.js +19 -11
- package/src/providers/nvidia.js +45 -33
- package/src/providers/openai-compat.js +59 -27
- package/src/providers/openai.js +42 -18
- package/src/providers/openrouter.js +56 -26
- package/src/tools/index.js +206 -35
- package/src/tui/App.tsx +250 -36
- package/src/tui/components/Modals.tsx +130 -26
- package/src/tui/mcp-presets.ts +4 -65
- package/src/tui/store.ts +6 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Modals -- provider picker, model picker, key input, base URL, settings, palette.
|
|
2
|
-
import { createSignal, createMemo, onMount } from "solid-js";
|
|
2
|
+
import { createSignal, createMemo, onMount, onCleanup } from "solid-js";
|
|
3
3
|
import { useKeyboard, usePaste } from "@opentui/solid";
|
|
4
4
|
import { palette } from "../theme.ts";
|
|
5
5
|
import * as kbs from "../keybinds.ts";
|
|
@@ -441,7 +441,8 @@ export function showAgentsText() {
|
|
|
441
441
|
|
|
442
442
|
// ── MCP server / connector browser popup ──
|
|
443
443
|
// Lists every configured server (seeded defaults + user-added) with on/off
|
|
444
|
-
// state; Enter toggles
|
|
444
|
+
// state; Enter toggles a server (or opens the add flow on the pinned "+ Add"
|
|
445
|
+
// row at the end of the list), A opens the add flow, Esc closes.
|
|
445
446
|
// `kind` decides which preset list the "A" flow offers: "mcp" (dev tools) or
|
|
446
447
|
// "connector" (hosting/cloud services). Both share the same underlying
|
|
447
448
|
// mcp-manager — a connector IS an MCP server, just surfaced separately.
|
|
@@ -470,11 +471,15 @@ function presetAddLine(p: any, envValues?: Record<string, string>): string {
|
|
|
470
471
|
function openPresetPicker(presets: any[], kind: "mcp" | "connector") {
|
|
471
472
|
const addLabel = kind === "connector" ? "Add connector" : "Add MCP server";
|
|
472
473
|
const backType = kind === "connector" ? "connectors" : "mcp";
|
|
474
|
+
// Connector adds run through the preset-aware engine: a bare preset name
|
|
475
|
+
// plus -e KEY=V flags expands to the full one-liner; full one-liners (with
|
|
476
|
+
// "--") pass through unchanged.
|
|
477
|
+
const addLine = kind === "connector" ? plugin.connectorAddLineCmd : plugin.mcpAddLineCmd;
|
|
473
478
|
|
|
474
479
|
// Build the server from collected env + optional args and land it; reopen
|
|
475
480
|
// the browser on success, toast the error otherwise.
|
|
476
481
|
const submitLine = (line: string, reopenOnError: boolean) => {
|
|
477
|
-
const msg =
|
|
482
|
+
const msg = addLine(line);
|
|
478
483
|
if (msg.startsWith("Added")) {
|
|
479
484
|
showToast(msg, "ok");
|
|
480
485
|
closeModal();
|
|
@@ -547,7 +552,7 @@ const openLine = function(line: string) {
|
|
|
547
552
|
caretStart: line ? line.lastIndexOf("=") + 1 : 0,
|
|
548
553
|
onCancel: function() { closeModal(); setTimeout(function() { openModal({ type: backType as any }); }, 10); },
|
|
549
554
|
onPick: function(line2: string) {
|
|
550
|
-
const msg =
|
|
555
|
+
const msg = addLine(line2);
|
|
551
556
|
if (msg.startsWith("Added")) {
|
|
552
557
|
showToast(msg, "ok");
|
|
553
558
|
closeModal();
|
|
@@ -592,9 +597,11 @@ function ServerBrowser(props: { kind: "mcp" | "connector" }) {
|
|
|
592
597
|
const ks = kbs.keyString(key);
|
|
593
598
|
if (kbs.is("modal_cancel", ks)) { closeModal(); return; }
|
|
594
599
|
if (kbs.dialogIs("dialog_select_prev", ks)) { setSel(i => Math.max(0, i - 1)); return; }
|
|
595
|
-
|
|
600
|
+
// One past the last server is the pinned "+ Add" row.
|
|
601
|
+
if (kbs.dialogIs("dialog_select_next", ks)) { setSel(i => Math.min(servers().length, i + 1)); return; }
|
|
596
602
|
if (key.name === "a" || key.name === "A") { openPresetPicker(presets, kind); return; }
|
|
597
603
|
if (kbs.dialogIs("dialog_select_submit", ks)) {
|
|
604
|
+
if (sel() >= servers().length) { openPresetPicker(presets, kind); return; }
|
|
598
605
|
const s = servers()[sel()];
|
|
599
606
|
if (!s) return;
|
|
600
607
|
const res = toggleServer(s.name);
|
|
@@ -605,7 +612,7 @@ function ServerBrowser(props: { kind: "mcp" | "connector" }) {
|
|
|
605
612
|
}
|
|
606
613
|
});
|
|
607
614
|
|
|
608
|
-
const scrollBy = (e: any) => { setSel(i => Math.max(0, Math.min(servers().length
|
|
615
|
+
const scrollBy = (e: any) => { setSel(i => Math.max(0, Math.min(servers().length, i + wheelStep(e, 1)))); };
|
|
609
616
|
let winStart = 0;
|
|
610
617
|
const win = () => {
|
|
611
618
|
const total = servers().length;
|
|
@@ -626,7 +633,7 @@ function ServerBrowser(props: { kind: "mcp" | "connector" }) {
|
|
|
626
633
|
const cmdW = () => Math.max(22, 60 - nameW());
|
|
627
634
|
|
|
628
635
|
return (
|
|
629
|
-
<ModalFrame title={title} subtitle={"Enter toggles
|
|
636
|
+
<ModalFrame title={title} subtitle={"Enter toggles, + Add row adds" + rangeSub()} footer={kbNav().submit + " toggle/add · A add · " + kbNav().cancel + " close"}>
|
|
630
637
|
<box onMouseScroll={scrollBy} flexDirection="column" flexShrink={0}>
|
|
631
638
|
{win().items.map((s, i) => {
|
|
632
639
|
const abs = win().start + i;
|
|
@@ -648,6 +655,25 @@ function ServerBrowser(props: { kind: "mcp" | "connector" }) {
|
|
|
648
655
|
</box>
|
|
649
656
|
);
|
|
650
657
|
})}
|
|
658
|
+
{/* Pinned add row — always visible under the list; Enter (or a click)
|
|
659
|
+
opens the preset picker, same as the A shortcut. */}
|
|
660
|
+
{(() => {
|
|
661
|
+
const addActive = sel() >= servers().length;
|
|
662
|
+
const addLabel2 = "+ Add " + (kind === "connector" ? "connector" : "MCP server") + "…";
|
|
663
|
+
return (
|
|
664
|
+
<box
|
|
665
|
+
flexDirection="row" paddingY={0} height={1} flexShrink={0}
|
|
666
|
+
paddingLeft={1}
|
|
667
|
+
backgroundColor={addActive ? ui.bgPanelAlt : undefined}
|
|
668
|
+
onMouseDown={() => setSel(servers().length)}
|
|
669
|
+
onMouseUp={() => { if (sel() >= servers().length) openPresetPicker(presets, kind); }}
|
|
670
|
+
>
|
|
671
|
+
<text fg={addActive ? ui.primary : ui.fgMuted} height={1}>
|
|
672
|
+
{(addActive ? "▸ " : " ") + addLabel2}
|
|
673
|
+
</text>
|
|
674
|
+
</box>
|
|
675
|
+
);
|
|
676
|
+
})()}
|
|
651
677
|
</box>
|
|
652
678
|
</ModalFrame>
|
|
653
679
|
);
|
|
@@ -663,29 +689,66 @@ export function ConnectorsModal() {
|
|
|
663
689
|
|
|
664
690
|
// ── Sandbox setup wizard ──
|
|
665
691
|
// Three steps: docker check (with install link when missing) → OS pick →
|
|
666
|
-
// live setup
|
|
667
|
-
//
|
|
692
|
+
// live setup with a real progress bar (setup streams from the core layer —
|
|
693
|
+
// the TUI never freezes). The container is global and idles in the
|
|
694
|
+
// background; the agent's sandbox tool uses it on demand.
|
|
668
695
|
export function SandboxModal() {
|
|
669
696
|
const sb = require("../../core/sandbox.js");
|
|
670
697
|
const [step, setStep] = createSignal<"docker" | "os" | "setup">("docker");
|
|
671
|
-
|
|
698
|
+
// null = detection running (spawn-based, non-blocking — the old sync
|
|
699
|
+
// detectDocker() froze the whole TUI for up to 10s while docker answered).
|
|
700
|
+
const [docker, setDocker] = createSignal<{ state: string; version: string } | null>(null);
|
|
672
701
|
const [osSel, setOsSel] = createSignal(0);
|
|
673
702
|
const [logs, setLogs] = createSignal<string[]>([]);
|
|
674
703
|
const [busy, setBusy] = createSignal(false);
|
|
675
704
|
const [err, setErr] = createSignal<string | null>(null);
|
|
705
|
+
const [prog, setProg] = createSignal<{ stage: string; pct: number | null; step: number; total: number } | null>(null);
|
|
706
|
+
const [spin, setSpin] = createSignal(0);
|
|
707
|
+
let spinTimer: ReturnType<typeof setInterval> | null = null;
|
|
708
|
+
onCleanup(() => { if (spinTimer) clearInterval(spinTimer); });
|
|
709
|
+
|
|
710
|
+
// Spinner frames while setup is busy (drives the "working…" indicator).
|
|
711
|
+
function startSpinner() {
|
|
712
|
+
if (spinTimer) clearInterval(spinTimer);
|
|
713
|
+
spinTimer = setInterval(() => setSpin((s) => (s + 1) % 10), 120);
|
|
714
|
+
}
|
|
715
|
+
function stopSpinner() {
|
|
716
|
+
if (spinTimer) clearInterval(spinTimer);
|
|
717
|
+
spinTimer = null;
|
|
718
|
+
}
|
|
719
|
+
const SPIN_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
|
|
720
|
+
|
|
721
|
+
const STAGE_LABELS: Record<string, string> = {
|
|
722
|
+
pull: "Pulling image",
|
|
723
|
+
create: "Creating container",
|
|
724
|
+
start: "Starting container",
|
|
725
|
+
essentials: "Installing essentials",
|
|
726
|
+
browser: "Installing browser",
|
|
727
|
+
done: "Ready",
|
|
728
|
+
};
|
|
676
729
|
|
|
677
|
-
// Docker
|
|
678
|
-
// brief "installed" flash so the user sees the check happened).
|
|
730
|
+
// Async Docker check on open — never blocks the render loop.
|
|
679
731
|
onMount(() => {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
732
|
+
startSpinner();
|
|
733
|
+
sb.detectDockerAsync().then((/** @type {{ state: string; version: string }} */ d) => {
|
|
734
|
+
setDocker(d);
|
|
735
|
+
if (d.state === "ready") {
|
|
736
|
+
// brief "installed" flash, then straight to OS selection.
|
|
737
|
+
setTimeout(() => { if (step() === "docker") setStep("os"); }, 600);
|
|
738
|
+
} else {
|
|
739
|
+
stopSpinner();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
683
742
|
});
|
|
684
743
|
|
|
685
744
|
function recheck() {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
745
|
+
setDocker(null);
|
|
746
|
+
startSpinner();
|
|
747
|
+
sb.detectDockerAsync().then((/** @type {{ state: string; version: string }} */ d) => {
|
|
748
|
+
setDocker(d);
|
|
749
|
+
stopSpinner();
|
|
750
|
+
if (d.state === "ready") setStep("os");
|
|
751
|
+
});
|
|
689
752
|
}
|
|
690
753
|
|
|
691
754
|
function startSetup() {
|
|
@@ -693,14 +756,18 @@ export function SandboxModal() {
|
|
|
693
756
|
setStep("setup");
|
|
694
757
|
setBusy(true);
|
|
695
758
|
setErr(null);
|
|
759
|
+
setProg(null);
|
|
760
|
+
startSpinner();
|
|
696
761
|
setLogs(["Preparing " + os.label + " (" + os.image + ")…"]);
|
|
697
762
|
sb.setupSandbox({
|
|
698
763
|
osId: os.id,
|
|
699
764
|
onLog: (/** @type {string} */ l) => setLogs((xs) => [...xs, l]),
|
|
765
|
+
onProgress: (/** @type {{ stage: string; pct: number | null; step: number; total: number }} */ p) => setProg(p),
|
|
700
766
|
}).then((/** @type {{ ok: boolean, error?: string }} */ res) => {
|
|
701
767
|
setBusy(false);
|
|
768
|
+
stopSpinner();
|
|
702
769
|
if (res.ok) {
|
|
703
|
-
appendMessage({ role: "system", content: "Sandbox is live (" + os.label + ").
|
|
770
|
+
appendMessage({ role: "system", content: "Sandbox is live (" + os.label + "). The container idles in the background; the agent will use it via the sandbox tool whenever isolation is needed. Your project is mounted at /workspace." });
|
|
704
771
|
showToast("Sandbox ready — " + os.label, "ok");
|
|
705
772
|
closeModal();
|
|
706
773
|
} else {
|
|
@@ -722,12 +789,14 @@ export function SandboxModal() {
|
|
|
722
789
|
if (kbs.dialogIs("dialog_select_next", ks)) { if (s === "os") setOsSel((i) => Math.min(sb.OS_CATALOG.length - 1, i + 1)); return; }
|
|
723
790
|
if (kbs.dialogIs("dialog_select_submit", ks)) {
|
|
724
791
|
if (s === "docker") {
|
|
725
|
-
|
|
792
|
+
const d = docker();
|
|
793
|
+
if (!d) return; // still detecting — enter does nothing yet
|
|
794
|
+
if (d.state === "not_installed") {
|
|
726
795
|
sb.markInstallPending();
|
|
727
796
|
sb.openInstallPage();
|
|
728
797
|
appendMessage({ role: "system", content: "Opened the Docker download page. Install Docker (restart if asked), then run /sandbox again — or just say \"docker is ready\" in chat." });
|
|
729
798
|
closeModal();
|
|
730
|
-
} else if (
|
|
799
|
+
} else if (d.state === "no_daemon") {
|
|
731
800
|
recheck();
|
|
732
801
|
} else {
|
|
733
802
|
setStep("os");
|
|
@@ -740,7 +809,7 @@ export function SandboxModal() {
|
|
|
740
809
|
});
|
|
741
810
|
|
|
742
811
|
const link = sb.installLink();
|
|
743
|
-
const st = () => docker()
|
|
812
|
+
const st = () => (docker() ? docker()!.state : "checking");
|
|
744
813
|
|
|
745
814
|
return (
|
|
746
815
|
<ModalFrame
|
|
@@ -760,6 +829,11 @@ export function SandboxModal() {
|
|
|
760
829
|
: "enter retry · esc close"
|
|
761
830
|
}
|
|
762
831
|
>
|
|
832
|
+
{step() === "docker" && st() === "checking" ? (
|
|
833
|
+
<box flexDirection="column">
|
|
834
|
+
<text fg={ui.fgMuted}>{SPIN_FRAMES[spin()] + " Checking Docker…"}</text>
|
|
835
|
+
</box>
|
|
836
|
+
) : null}
|
|
763
837
|
{step() === "docker" && st() === "not_installed" ? (
|
|
764
838
|
<box flexDirection="column">
|
|
765
839
|
<text fg={ui.warning}>Docker is not installed</text>
|
|
@@ -780,7 +854,7 @@ export function SandboxModal() {
|
|
|
780
854
|
) : null}
|
|
781
855
|
{step() === "docker" && st() === "ready" ? (
|
|
782
856
|
<box flexDirection="column">
|
|
783
|
-
<text fg={ui.success}>Docker installed (v{docker().version})</text>
|
|
857
|
+
<text fg={ui.success}>Docker installed (v{(docker() || { version: "?" }).version})</text>
|
|
784
858
|
<text fg={ui.fgMuted} marginTop={1}>Opening OS selection…</text>
|
|
785
859
|
</box>
|
|
786
860
|
) : null}
|
|
@@ -805,9 +879,39 @@ export function SandboxModal() {
|
|
|
805
879
|
</box>
|
|
806
880
|
) : null}
|
|
807
881
|
{step() === "setup" ? (
|
|
808
|
-
<box flexDirection="column" maxWidth={
|
|
809
|
-
{
|
|
810
|
-
{
|
|
882
|
+
<box flexDirection="column" maxWidth={72}>
|
|
883
|
+
{/* Stage checklist with a live pull-progress bar (pct from core). */}
|
|
884
|
+
{prog() ? (() => {
|
|
885
|
+
const p = prog()!;
|
|
886
|
+
const os = sb.OS_CATALOG[osSel()];
|
|
887
|
+
// Same stage order the core reports (browser only when offered).
|
|
888
|
+
const keys = ["pull", "create", "start", "essentials"]
|
|
889
|
+
.concat(os.browser ? ["browser"] : [], ["done"])
|
|
890
|
+
.slice(0, p.total);
|
|
891
|
+
return (
|
|
892
|
+
<box flexDirection="column" marginBottom={1}>
|
|
893
|
+
{keys.map((key, i) => {
|
|
894
|
+
const done = i < p.step || p.stage === "done";
|
|
895
|
+
const active = i === p.step && p.stage !== "done";
|
|
896
|
+
const mark = done ? "✓" : active ? SPIN_FRAMES[spin()] : "○";
|
|
897
|
+
const color = done ? ui.success : active ? ui.primary : ui.fgDim;
|
|
898
|
+
let bar = "";
|
|
899
|
+
if (active && p.pct != null) {
|
|
900
|
+
const width = 20;
|
|
901
|
+
const filled = Math.round((p.pct / 100) * width);
|
|
902
|
+
bar = " [" + "█".repeat(filled) + "░".repeat(width - filled) + "] " + p.pct + "%";
|
|
903
|
+
} else if (active) {
|
|
904
|
+
bar = " …";
|
|
905
|
+
}
|
|
906
|
+
return (
|
|
907
|
+
<text fg={color}>{mark + " " + STAGE_LABELS[key] + bar}</text>
|
|
908
|
+
);
|
|
909
|
+
})}
|
|
910
|
+
</box>
|
|
911
|
+
);
|
|
912
|
+
})() : <text fg={ui.fgMuted}>Starting…</text>}
|
|
913
|
+
{logs().slice(-6).map((l) => <text fg={ui.fgMuted}>{"▸ " + l.slice(0, 68)}</text>)}
|
|
914
|
+
{busy() ? <text fg={ui.primary}>{SPIN_FRAMES[spin()] + " working — you can esc; setup continues in the background"}</text> : null}
|
|
811
915
|
{err() ? (
|
|
812
916
|
<box flexDirection="column" marginTop={1}>
|
|
813
917
|
<text fg={ui.error}>{err()}</text>
|
package/src/tui/mcp-presets.ts
CHANGED
|
@@ -114,71 +114,10 @@ export const MCP_PRESETS = [
|
|
|
114
114
|
];
|
|
115
115
|
|
|
116
116
|
// Hosting / cloud-service connectors — platforms you "connect" your project to
|
|
117
|
-
// rather than dev-tool servers. Surfaced under /connectors, not /mcp.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
label: "Supabase",
|
|
122
|
-
package: "@supabase/mcp-server-supabase",
|
|
123
|
-
description: "Query schema, run SQL, manage Supabase projects",
|
|
124
|
-
args: ["--access-token", "$SUPABASE_ACCESS_TOKEN"],
|
|
125
|
-
env: { SUPABASE_ACCESS_TOKEN: "" },
|
|
126
|
-
prompts: [
|
|
127
|
-
{ key: "SUPABASE_ACCESS_TOKEN", label: "Personal access token (supabase.com/dashboard/account/tokens)", mask: true },
|
|
128
|
-
],
|
|
129
|
-
optionalArgsPrompt: { flag: "--project-ref", label: "Project ref (optional, e.g. abcdefghijklmnop) — Enter to skip" },
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
id: "nextjs",
|
|
133
|
-
label: "Next.js",
|
|
134
|
-
package: "nextjs-mcp-server",
|
|
135
|
-
description: "Next.js project introspection (routes, pages, config)",
|
|
136
|
-
args: [],
|
|
137
|
-
env: {},
|
|
138
|
-
prompts: [],
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
id: "railway",
|
|
142
|
-
label: "Railway",
|
|
143
|
-
package: "@railway/mcp-server",
|
|
144
|
-
description: "Railway deploys, services, variables",
|
|
145
|
-
args: [],
|
|
146
|
-
env: { RAILWAY_API_TOKEN: "" },
|
|
147
|
-
prompts: [
|
|
148
|
-
{ key: "RAILWAY_API_TOKEN", label: "API token (railway.app/account/tokens)", mask: true },
|
|
149
|
-
],
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
id: "vercel",
|
|
153
|
-
label: "Vercel",
|
|
154
|
-
command: "npx",
|
|
155
|
-
args: ["-y", "mcp-remote", "https://mcp.vercel.com"],
|
|
156
|
-
description: "Vercel deployments and projects (remote MCP endpoint, OAuth)",
|
|
157
|
-
env: {},
|
|
158
|
-
prompts: [],
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
id: "netlify",
|
|
162
|
-
label: "Netlify",
|
|
163
|
-
command: "npx",
|
|
164
|
-
args: ["-y", "mcp-remote", "https://api.netlify.com/mcp"],
|
|
165
|
-
description: "Netlify sites, deploys, and environment vars (remote MCP endpoint, OAuth)",
|
|
166
|
-
env: {},
|
|
167
|
-
prompts: [],
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
id: "cloudflare",
|
|
171
|
-
label: "Cloudflare",
|
|
172
|
-
package: "@cloudflare/mcp-server-cloudflare",
|
|
173
|
-
description: "Workers, DNS, and account management",
|
|
174
|
-
args: [],
|
|
175
|
-
env: { CLOUDFLARE_API_TOKEN: "", CLOUDFLARE_ACCOUNT_ID: "" },
|
|
176
|
-
prompts: [
|
|
177
|
-
{ key: "CLOUDFLARE_API_TOKEN", label: "API token (dash.cloudflare.com/profile/api-tokens)", mask: true },
|
|
178
|
-
{ key: "CLOUDFLARE_ACCOUNT_ID", label: "Account ID (dash.cloudflare.com — right sidebar)", mask: false },
|
|
179
|
-
],
|
|
180
|
-
},
|
|
181
|
-
];
|
|
117
|
+
// rather than dev-tool servers. Surfaced under /connectors, not /mcp. The
|
|
118
|
+
// catalog lives in the core layer (src/mcp/connector-presets.js) so the CLI
|
|
119
|
+
// (`loom add connector <preset> -e KEY=V`) and this picker share one source.
|
|
120
|
+
export const CONNECTOR_PRESETS: any[] = require("../mcp/connector-presets.js").CONNECTOR_PRESETS;
|
|
182
121
|
|
|
183
122
|
export function presetSpawn(preset, resolvedEnv) {
|
|
184
123
|
if (preset.command) {
|
package/src/tui/store.ts
CHANGED
|
@@ -523,7 +523,7 @@ export const SLASH_LIST: SlashCmd[] = [
|
|
|
523
523
|
{ cmd: "graph", desc: "Memory graph" },
|
|
524
524
|
{ cmd: "skills", desc: "Manage skills", args: "install <dir|git> | remove <name>" },
|
|
525
525
|
{ cmd: "mcp", desc: "Manage MCP servers", args: "add <name> <cmd> | remove | toggle" },
|
|
526
|
-
{ cmd: "connectors", desc: "Manage connectors", args: "add <
|
|
526
|
+
{ cmd: "connectors", desc: "Manage connectors", args: "add <preset|spec> | remove | toggle" },
|
|
527
527
|
{ cmd: "permissions", desc: "Permission rules", args: "reset | auto" },
|
|
528
528
|
{ cmd: "exit", desc: "Quit Loom Code" },
|
|
529
529
|
// Newer commands sit at the END so early rows (help/connect/…) keep their
|
|
@@ -539,6 +539,11 @@ export const SLASH_LIST: SlashCmd[] = [
|
|
|
539
539
|
{ cmd: "style", desc: "Output style preset" },
|
|
540
540
|
{ cmd: "vim", desc: "Toggle vim editing" },
|
|
541
541
|
{ cmd: "remember", desc: "Save a fact to LOOM.md", args: "<fact>" },
|
|
542
|
+
{ cmd: "compact", desc: "Summarize older messages to free context" },
|
|
543
|
+
{ cmd: "undo", desc: "Undo the last exchange" },
|
|
544
|
+
{ cmd: "redo", desc: "Redo the last undone exchange" },
|
|
545
|
+
{ cmd: "undo-files", desc: "Rewind working tree to last undo point" },
|
|
546
|
+
{ cmd: "export", desc: "Export chat as markdown" },
|
|
542
547
|
];
|
|
543
548
|
|
|
544
549
|
// ─── TUI prefs ───
|