Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A native companion to [be-concise](https://github.com/yannelli/be-concise) for C
- Call counts, interventions, sessions, average hook duration, and a 30-minute activity chart.
- User and project configuration with threshold controls, check switches, writing presets, an advanced JSON editor, and test filter settings.
- An isolated playground for file writes, Codex patches, shell commands, and final replies. Previews inspect hook responses without executing the pasted command or writing the target file.
- A Plugin tab that installs, updates, downgrades, and removes be-concise for Claude Code and Codex from the be-concise GitHub releases.

## Screenshots

Expand Down Expand Up @@ -83,13 +84,17 @@ Plugins must be enabled in Paseo Settings → Plugins. Installation runs trusted

The plugin detects installed be-concise versions in the Claude and Codex caches, respecting `CLAUDE_CONFIG_DIR` and `CODEX_HOME`. For a source checkout or custom installation, set `PASEO_CONCISE_ROOT` in the daemon environment to the repository or `plugins/concise` directory, then reload this plugin.

### Install be-concise from Paseo

Open **Be concise → Plugin**, or use the panel that appears when be-concise is missing. Choose a release and press **Install**, **Update**, or **Downgrade** for Claude Code or Codex. The daemon host needs Git and the `claude` or `codex` CLI on `PATH` or in `~/.local/bin`. The plugin downloads the release from [yannelli/be-concise](https://github.com/yannelli/be-concise/releases) into `~/.local/share/paseo-be-concise/releases` and registers it as the `be-concise` marketplace. Claude Code changes use user scope. Restart Claude Code sessions after a change. For Codex, start a new session and review the hooks with `/hooks`. If the marketplace already uses another source, **Switch to GitHub releases** asks for confirmation before it points the marketplace at the release folder. Changes run in the background, and the tab shows the result when the host commands finish. See [be-concise installer](docs/plugin-installer.md) for the exact commands.

## Activity and settings

The view refreshes every two seconds. It follows the project registry and saved records under the daemon’s HOME/XDG directories, alongside any running `concise-web` console. It retains up to 500 events per project within a shared 16 MiB budget. Stats describe that retained window; they are not lifetime totals. New activity requires `monitor.persist` to be enabled and `BEC_MONITOR_DISABLED` to be unset in the agent’s environment.

Configuration controls edit the selected file and preserve its other keys. **Save changes** validates through be-concise and checks the file revision. An intervening edit preserves your draft and requires an explicit reload. The effective view uses the daemon’s environment; agent-specific environment overrides apply separately. The interface labels active and inactive layers because creating a higher-priority file changes which configuration is selected.

Each preview starts with separate retry state. Preview results do not enter live activity. The plugin does not modify the Claude or Codex installation or start another web console.
Each preview starts with separate retry state. Preview results do not enter live activity. Only the Plugin tab changes the Claude or Codex installation, and only when you press one of its buttons. The plugin does not start another web console.

## Development

Expand All @@ -103,7 +108,7 @@ Tests use Node.js 24 and temporary home/project directories. They cover activity

Paseo API reference: [v0.9 plugins](https://paseo.sh/docs/plugins/v0.9/reference).

See the [documentation index](docs/INDEX.md) for the current composer button contract and screenshot capture details. Consult it when changing pill labels or updating screenshots.
See the [documentation index](docs/INDEX.md) for the current composer button contract, screenshot capture details, and the be-concise installer commands. Consult it when you change pill labels, update screenshots, or change install behavior.

## Releases

Expand Down
23 changes: 23 additions & 0 deletions backend.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ test("discovery respects configured cache homes and rejects older installations"
assert.equal(found!.version, "0.7.1");
});

test("the newest managed release wins discovery and reset reloads the runtime", async (t) => {
const { root, home } = await fixture(t);
const data = join(root, "data");
const env = { HOME: home, XDG_DATA_HOME: data, CLAUDE_CONFIG_DIR: join(root, "claude") };
await fakeInstallation(join(root, "claude/plugins/cache/be-concise/concise/0.8.2"), "0.8.2");
const releases = join(data, "paseo-be-concise/releases");
await fakeInstallation(join(releases, "v0.8.1/plugins/concise"), "0.8.1");
await fakeInstallation(join(releases, "v0.8.2/plugins/concise"), "0.8.2");
const select = (claude: string, codex: string) => writeFile(join(data, "paseo-be-concise/state.json"), JSON.stringify({ hosts: {
claude: { tag: claude, updatedAt: "2026-10-05T02:00:00.000Z" }, codex: { tag: codex, updatedAt: "2026-10-05T01:00:00.000Z" },
} }));
await select("v0.8.2", "v0.8.1");
const backend = createBackend(env);
t.after(backend.close);
assert.equal((await backend.getSnapshot()).version, "0.8.2");
await select("v0.8.1", "v0.8.2");
assert.equal((await backend.getSnapshot()).version, "0.8.2");
await backend.reset();
const reloaded = await backend.getSnapshot();
assert.equal(reloaded.version, "0.8.1");
assert.equal(reloaded.root, await realpath(join(releases, "v0.8.1/plugins/concise")));
});

test("project filtering resolves directory aliases", async (t) => {
const { root, cwd } = await fixture(t);
const alias = join(root, "alias");
Expand Down
93 changes: 93 additions & 0 deletions client/installer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { PluginTheme } from "@getpaseo/plugin";
import { useRpc } from "@getpaseo/plugin/client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { type HostId, type HostStatus, installerApply, installerStatus } from "../shared/contracts";
import { compareVersions } from "../shared/versions";
import { Button, Card, Chips, Label, Row } from "./ui";

const queryKey = ["concise", "installer"];

function actionLabel(host: HostStatus, version: string) {
if (host.source && !host.managed) return `Switch to GitHub releases ${version}`;
if (!host.version) return `Install ${version}`;
const order = compareVersions(version, host.version);
return order > 0 ? `Update to ${version}` : order < 0 ? `Downgrade to ${version}` : `Reinstall ${version}`;
}

function hostSummary(host: HostStatus) {
if (!host.available || host.error) return host.error ?? "Unavailable";
if (!host.version) return "Not installed";
return `Installed ${host.version}${host.enabled ? "" : " · Disabled"}`;
}

export function Installer({ theme, compact }: { theme: PluginTheme; compact: boolean }) {
const read = useRpc(installerStatus);
const apply = useRpc(installerApply);
const cache = useQueryClient();
const query = useQuery({ queryKey, queryFn: () => read({}), staleTime: 60_000, retry: 1,
refetchInterval: (current) => current.state.data?.job?.running ? 1500 : false });
const [chosen, setChosen] = useState("");
const [confirming, setConfirming] = useState<{ host: HostId; action: "switch" | "remove" } | null>(null);
const refresh = useMutation({ mutationFn: () => read({ refresh: true }), onSuccess: (data) => cache.setQueryData(queryKey, data) });
const mutation = useMutation({ mutationFn: apply, onSuccess: (data) => { cache.setQueryData(queryKey, data); setConfirming(null); } });
const job = query.data?.job ?? null;
const running = useRef(false);
useEffect(() => {
if (running.current && !job?.running) {
void cache.invalidateQueries({ queryKey: ["concise", "snapshot"] });
void cache.invalidateQueries({ queryKey: ["concise", "configuration"] });
}
running.current = Boolean(job?.running);
}, [cache, job?.running]);
if (query.isPending) return <Label theme={theme} muted>Reading be-concise releases and hosts…</Label>;
if (!query.data) return <Card theme={theme}><Label theme={theme}>{query.error?.message ?? "Release status is unavailable."}</Label><Button theme={theme} label="Retry" onPress={() => { void query.refetch(); }} /></Card>;
const { releases, hosts } = query.data;
const latest = releases[0];
const version = releases.some((release) => release.version === chosen) ? chosen : latest?.version ?? "";
const busy = mutation.isPending || refresh.isPending || Boolean(job?.running);
return <View style={{ gap: compact ? 12 : 16 }}>
<View style={{ gap: 5 }}>
<Label theme={theme} size={18}>be-concise plugin</Label>
<Label theme={theme} muted>Install, update, or remove the Claude Code and Codex plugin from {query.data.repository} GitHub releases.</Label>
</View>
<Card theme={theme}>
<Row>
<View style={{ flex: 1, minWidth: 0 }}>
<Label theme={theme}>{latest ? `Latest release ${latest.version}` : "No compatible release found"}</Label>
{latest?.publishedAt && <Label theme={theme} muted size={12}>Published {new Date(latest.publishedAt).toLocaleDateString()}</Label>}
</View>
<Button theme={theme} label={refresh.isPending ? "Checking…" : "Check for updates"} disabled={busy} onPress={() => refresh.mutate()} />
</Row>
{(refresh.error?.message ?? query.data.releasesError) && <Label theme={theme}>{refresh.error?.message ?? query.data.releasesError}</Label>}
{releases.length > 1 && <Chips theme={theme} items={releases.slice(0, 6).map((release) => ({ value: release.version, label: release.version }))} value={version} onChange={setChosen} />}
</Card>
{hosts.map((host) => {
const external = Boolean(host.source && !host.managed);
const confirm = confirming?.host === host.id ? confirming.action : null;
const install = () => mutation.mutate({ host: host.id, action: "install", version, switchSource: external });
return <Card key={host.id} theme={theme}>
<View style={{ minWidth: 0 }}>
<Label theme={theme}>{host.label}</Label>
<Label theme={theme} muted size={12}>{hostSummary(host)}</Label>
</View>
{host.available && !host.error && <Label theme={theme} muted size={12}>Marketplace: {host.source ? `${host.source}${host.managed ? " · GitHub release" : ""}` : "not added"}</Label>}
{confirm === "switch" && <Label theme={theme}>This replaces the be-concise marketplace source {host.source} with a downloaded GitHub release. The result lists the replaced source.</Label>}
{host.available && !host.error && <Row>
{!!version && !confirm && <Button theme={theme} primary={!host.version || external || compareVersions(version, host.version) > 0} disabled={busy}
label={actionLabel(host, version)} onPress={() => external ? setConfirming({ host: host.id, action: "switch" }) : install()} />}
{confirm === "switch" && <Button theme={theme} primary disabled={busy} label={`Confirm switch to ${version}`} onPress={install} />}
{(host.version || host.managed) && confirm !== "switch" && <Button theme={theme} disabled={busy} label={confirm === "remove" ? "Confirm remove" : "Remove"}
onPress={() => confirm === "remove" ? mutation.mutate({ host: host.id, action: "remove" }) : setConfirming({ host: host.id, action: "remove" })} />}
{confirm && <Button theme={theme} label="Cancel" disabled={busy} onPress={() => setConfirming(null)} />}
</Row>}
{mutation.isError && mutation.variables?.host === host.id && <Label theme={theme}>Failed: {mutation.error.message}</Label>}
{job?.host === host.id && job.running && <Label theme={theme} muted>Working… Downloads and host commands can take a minute.</Label>}
{job?.host === host.id && job.error && !mutation.isError && <Label theme={theme}>Failed: {job.error}</Label>}
{job?.host === host.id && job.message && !mutation.isError && <Label theme={theme}>{job.message}</Label>}
</Card>;
})}
<Label theme={theme} muted size={11}>Releases are stored in {query.data.directory}. Claude Code changes use user scope.</Label>
</View>;
}
7 changes: 5 additions & 2 deletions client/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { snapshot } from "../shared/contracts";
import { Activity } from "./activity";
import { BrandIcon } from "./brand";
import { ConfigurationEditor } from "./configuration";
import { Installer } from "./installer";
import { Playground } from "./playground";
import { Button, Card, Chips, Label, Row } from "./ui";

Expand Down Expand Up @@ -51,7 +52,7 @@ function Dashboard({ theme, layout, cwd }: Props) {
</View>
</Row>
<View style={{ flexDirection: compact ? "column" : "row", flexWrap: "wrap", gap: 8, justifyContent: "space-between", minWidth: 0 }}>
<Chips theme={theme} value={tab} onChange={setTab} items={[{ value: "activity", label: "Activity" }, { value: "configuration", label: compact ? "Config" : "Configuration" }, { value: "playground", label: compact ? "Preview" : "Playground" }]} />
<Chips theme={theme} value={tab} onChange={setTab} items={[{ value: "activity", label: "Activity" }, { value: "configuration", label: compact ? "Config" : "Configuration" }, { value: "playground", label: compact ? "Preview" : "Playground" }, { value: "plugin", label: "Plugin" }]} />
{!cwd && <Button theme={theme} label={`${name} ${projectsOpen ? "−" : "+"}`} onPress={() => setProjectsOpen(!projectsOpen)} />}
{cwd && <Text numberOfLines={1} style={{ color: theme.colors.foregroundMuted, fontSize: 11, minWidth: 0, flexShrink: 1, alignSelf: compact ? "flex-start" : "center" }}>{name}</Text>}
</View>
Expand All @@ -71,15 +72,17 @@ function Dashboard({ theme, layout, cwd }: Props) {
<Label theme={theme} muted>{data.message}</Label>
<Button theme={theme} label="Check again" onPress={() => void query.refetch()} />
</Card>}
{data && !data.connected && <Installer theme={theme} compact={compact} />}
{data?.connected && <>
{tab === "activity" && <Activity key={selected} theme={theme} compact={compact} events={data.events} stats={data.stats}
paused={Boolean(paused)} onPause={() => setPaused(paused ? null : data)} />}
{tab !== "activity" && !selected && <Card theme={theme}>
{(tab === "configuration" || tab === "playground") && !selected && <Card theme={theme}>
<Label theme={theme} size={18}>Choose a project</Label><Label theme={theme} muted>Configuration and previews use the selected project’s settings.</Label>
<Button theme={theme} label="Choose project" onPress={() => setProjectsOpen(true)} />
</Card>}
{tab === "configuration" && selected && <ConfigurationEditor key={selected} cwd={selected} theme={theme} compact={compact} />}
{tab === "playground" && selected && <Playground key={selected} cwd={selected} theme={theme} compact={compact} />}
{tab === "plugin" && <Installer theme={theme} compact={compact} />}
<View style={{ borderTopWidth: 1, borderColor: theme.colors.border, paddingTop: 10, gap: 3 }}>
<Label theme={theme} muted size={11}>be-concise {data.version} · Updated {new Date(data.updatedAt).toLocaleTimeString()}</Label>
<Label theme={theme} muted size={11}>Last {data.retainedLimit} records per project · Requires monitor.persist in each agent.</Label>
Expand Down
1 change: 1 addition & 0 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
Created: 2026-10-04. Last updated: 2026-10-05.

- [Composer pill and screenshots](composer-pill.md): compact status labels, the Paseo button contract, and screenshot capture details. Read this before changing labels or refreshing images.
- [be-concise installer](plugin-installer.md): GitHub release handling, the Claude Code and Codex CLI commands, storage, and adapter discovery order. Read this before you change the Plugin tab or install commands.
Loading
Loading