Skip to content
Closed
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
12 changes: 1 addition & 11 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3620,7 +3620,7 @@ const server = createServer(async (req, res) => {
} else return json(res, 400, { error: "pinnedMessageId must be a message id" });
}
if (section !== undefined) patch.section = section ?? undefined;
if (body.chiefOfStaff === false) patch.chiefOfStaff = false;
if (body.chiefOfStaff !== undefined) patch.chiefOfStaff = body.chiefOfStaff;
// per-bot gate on the workspace's connected apps (Composio)
if (body.composio !== undefined) {
if (typeof body.composio !== "boolean") return json(res, 400, { error: "composio must be true or false" });
Expand Down Expand Up @@ -3689,18 +3689,8 @@ const server = createServer(async (req, res) => {
?.adapter.interruptTurn(existingBot.threadId)
.catch(() => {});
}
const chiefMovedSections =
Boolean(existingBot?.chiefOfStaff) &&
body.chiefOfStaff !== false &&
section !== undefined &&
sectionKey(existingBot?.section) !== sectionKey(section);
const bot = store.patchBot(m[1], patch);
if (!bot) return json(res, 404, { error: "no such bot" });
const chiefChanges =
body.chiefOfStaff === true || chiefMovedSections
? store.setChiefOfStaff(bot.id)
: [];
if (chiefChanges === null) return json(res, 404, { error: "no such bot" });
return json(res, 200, { bot: wireBot(store.bot(bot.id)!) });
}

Expand Down
16 changes: 16 additions & 0 deletions server/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,22 @@ describe("Store", () => {
expect(reloaded.bot(second.id)?.chiefOfStaff).toBe(false);
});

it("patches a Chief section change and handoff in one store operation", () => {
const store = new Store(selection);
const work = store.createBot({ section: "Work" });
const personal = store.createBot({ section: "Personal" });
store.setChiefOfStaff(work.id);
store.setChiefOfStaff(personal.id);

store.patchBot(work.id, { section: "Personal" });

expect(store.bot(work.id)?.chiefOfStaff).toBe(true);
expect(store.bot(personal.id)?.chiefOfStaff).toBe(false);
const reloaded = new Store(selection);
expect(reloaded.bot(work.id)?.chiefOfStaff).toBe(true);
expect(reloaded.bot(personal.id)?.chiefOfStaff).toBe(false);
});

it("patchMessage merges card patches and returns null for unknown ids", () => {
const store = new Store(selection);
const bot = store.createBot();
Expand Down
11 changes: 10 additions & 1 deletion server/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -861,8 +861,17 @@ export class Store {
const bot = this.bot(id);
if (!bot) return null;
Object.assign(bot, patch);
const changed = [bot];
if (bot.chiefOfStaff) {
const section = sectionKey(bot.section);
for (const candidate of this.bots) {
if (candidate.id === bot.id || !candidate.chiefOfStaff || sectionKey(candidate.section) !== section) continue;
candidate.chiefOfStaff = false;
changed.push(candidate);
}
}
this.saveBots();
this.emit({ type: "bot", botId: id });
for (const candidate of changed) this.emit({ type: "bot", botId: candidate.id });
return bot;
}

Expand Down
8 changes: 5 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import { DesktopCapabilitiesProvider } from "@/components/DesktopCapabilities";
import { RoutinesPage } from "@/components/RoutinesPage";
import { NoEngines } from "@/components/NoEngines";
import { CommandPalette } from "@/components/CommandPalette";
import { useI18n } from "@/lib/i18n-context";
import { SkillRecorderPage } from "@/components/SkillRecorderPage";

function Shell() {
const { state, dispatch } = useStore();
const { t } = useI18n();
// Mobile-only drawer state. Above md, none of these properties are emitted
// at all — Sidebar scopes every mobile class with max-md: rather than
// cancelling them with md:, which would still emit a translate value and
Expand Down Expand Up @@ -86,7 +88,7 @@ function Shell() {
<button
type="button"
ref={menuButtonRef}
aria-label="Open bot list"
aria-label={t("Open bot list")}
aria-expanded={drawerOpen}
onClick={() => setDrawerOpen(true)}
className="absolute left-3 top-3 z-30 rounded-md p-1.5 text-ink-secondary hover:bg-raised hover:text-ink md:hidden"
Expand Down Expand Up @@ -121,11 +123,11 @@ function Shell() {
<main className="flex h-full min-w-0 flex-1 flex-col items-center justify-center gap-3 bg-app text-ink-secondary">
<Loader2 size={20} className="animate-spin" />
<div className="text-[14px]">
{state.connected ? "No bots yet" : "Connecting to the bot server…"}
{state.connected ? t("No bots yet") : t("Connecting to the bot server…")}
</div>
{!state.connected && (
<div className="text-[12px]">
Start it with <code className="rounded bg-raised px-1.5 py-0.5">pnpm dev:server</code>
{t("Start it with")} <code className="rounded bg-raised px-1.5 py-0.5">pnpm dev:server</code>
</div>
)}
</main>
Expand Down
38 changes: 18 additions & 20 deletions src/components/AndroidDevicePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { ArrowLeft, Circle, Loader2, RotateCcw, ShieldCheck, Smartphone, Usb } from "lucide-react";
import type { AndroidDeviceInput, AndroidDeviceStatus, AndroidUsbDevice } from "@/types/ogb";
import { useI18n } from "@/lib/i18n-context";

type UnitPoint = { x: number; y: number };

Expand Down Expand Up @@ -44,6 +45,7 @@ function deviceLabel(device: AndroidUsbDevice) {
}

export function AndroidDevicePanel({ status }: { status: AndroidDeviceStatus }) {
const { t } = useI18n();
const bridge = window.ogb?.androidDevice;
const authorized = status.devices.filter((device) => device.state === "device");
const [serial, setSerial] = useState(authorized[0]?.serial ?? status.devices[0]?.serial ?? "");
Expand Down Expand Up @@ -196,7 +198,7 @@ export function AndroidDevicePanel({ status }: { status: AndroidDeviceStatus })
<Smartphone size={16} className="text-success" /> {deviceLabel(selected)}
</div>
<div className="mt-1 text-[12px] leading-relaxed text-ink-secondary">
Connected directly over USB. The screen and controls stay on this computer.
{t("Connected directly over USB. The screen and controls stay on this computer.")}
</div>
</div>
<span className="flex shrink-0 items-center gap-1 text-[11px] text-success">
Expand All @@ -211,7 +213,7 @@ export function AndroidDevicePanel({ status }: { status: AndroidDeviceStatus })
>
{status.devices.map((device) => (
<option key={device.serial} value={device.serial}>
{deviceLabel(device)} · {device.state}
{deviceLabel(device)} · {t(device.state)}
</option>
))}
</select>
Expand All @@ -221,14 +223,14 @@ export function AndroidDevicePanel({ status }: { status: AndroidDeviceStatus })
{!ready ? (
<div className="rounded-xl border border-warning/25 bg-warning/10 p-4 text-[12px] leading-relaxed text-warning">
{selected.state === "unauthorized"
? "Unlock the Android phone, accept the “Allow USB debugging” prompt, and optionally choose Always allow from this computer."
: `The phone is ${selected.state}. Reconnect the USB cable and keep USB debugging enabled.`}
? t("Unlock the Android phone, accept the “Allow USB debugging” prompt, and optionally choose Always allow from this computer.")
: t("The phone is {state}. Reconnect the USB cable and keep USB debugging enabled.", { state: selected.state })}
</div>
) : (
<>
<div
role="application"
aria-label={`Interactive Android screen for ${deviceLabel(selected)}`}
aria-label={t("Interactive Android screen for {name}", { name: deviceLabel(selected) })}
tabIndex={0}
onKeyDown={keyDown}
onWheel={wheel}
Expand Down Expand Up @@ -268,7 +270,7 @@ export function AndroidDevicePanel({ status }: { status: AndroidDeviceStatus })
<img
ref={imageRef}
src={frame}
alt={`${deviceLabel(selected)} screen`}
alt={t("{name} screen", { name: deviceLabel(selected) })}
draggable={false}
onLoad={(event) => {
setDimensions({
Expand All @@ -287,23 +289,23 @@ export function AndroidDevicePanel({ status }: { status: AndroidDeviceStatus })
onClick={() => send({ type: "key", key: "back" })}
className="flex items-center justify-center gap-1.5 rounded-lg bg-raised py-2 text-[12px] text-ink hover:bg-raised-hover"
>
<ArrowLeft size={13} /> Back
<ArrowLeft size={13} /> {t("Back")}
</button>
<button
onClick={() => send({ type: "key", key: "home" })}
className="flex items-center justify-center gap-1.5 rounded-lg bg-raised py-2 text-[12px] text-ink hover:bg-raised-hover"
>
<Circle size={12} /> Home
<Circle size={12} /> {t("Home")}
</button>
<button
onClick={() => send({ type: "key", key: "recent" })}
className="flex items-center justify-center gap-1.5 rounded-lg bg-raised py-2 text-[12px] text-ink hover:bg-raised-hover"
>
<RotateCcw size={13} /> Recent
<RotateCcw size={13} /> {t("Recent")}
</button>
</div>
<div className="text-center text-[11px] leading-relaxed text-ink-secondary">
Click to tap, drag or use a trackpad to scroll, and type after selecting a field.
{t("Click to tap, drag or use a trackpad to scroll, and type after selecting a field.")}
</div>
</>
)}
Expand All @@ -316,26 +318,22 @@ export function AndroidDevicePanel({ status }: { status: AndroidDeviceStatus })

<div className="rounded-xl bg-card p-4">
<div className="flex items-center gap-2 text-[13px] font-medium text-ink">
<Usb size={15} className="text-accent" /> First-time USB setup
<Usb size={15} className="text-accent" /> {t("First-time USB setup")}
</div>
<ol className="mt-2 list-decimal space-y-1.5 pl-4 text-[11.5px] leading-relaxed text-ink-secondary">
<li>Connect the phone with a data-capable USB cable and keep it unlocked.</li>
<li>{t("Connect the phone with a data-capable USB cable and keep it unlocked.")}</li>
<li>
Enable Developer options by tapping <span className="text-ink">Build number</span> seven times in
About phone, then turn on <span className="text-ink">USB debugging</span>.
{t("Enable Developer options by tapping Build number seven times in About phone, then turn on USB debugging.")}
</li>
<li>
Accept <span className="text-ink">Allow USB debugging</span> on the phone. You can choose Always allow
for this trusted computer.
{t("Accept Allow USB debugging on the phone. You can choose Always allow for this trusted computer.")}
</li>
</ol>
<div className="mt-2 text-[11px] leading-relaxed text-ink-secondary">
Agent control uses this same authorized USB connection. No phone companion app, account, Tailscale, or
wireless pairing is needed.
{t("Agent control uses this same authorized USB connection. No phone companion app, account, Tailscale, or wireless pairing is needed.")}
</div>
<div className="mt-2 rounded-lg bg-inset px-3 py-2 text-[11px] leading-relaxed text-ink-secondary">
Once connected, ask any compatible Maus to open an Android app or complete a task on your phone. The
bundled Phone Harness skill loads automatically for phone requests.
{t("Once connected, ask any compatible Maus to open an Android app or complete a task on your phone. The bundled Phone Harness skill loads automatically for phone requests.")}
</div>
</div>
</div>
Expand Down
Loading
Loading