Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5cd3a48
docs(status): release #938 to master; DeepSeek + popularity merged; P…
jaylfc Jun 15, 2026
5c513bf
feat(notifications): wire the desktop bell to the backend notificatio…
jaylfc Jun 15, 2026
57e779d
docs(status): notification system (#939 live, #940 click-routing) + n…
jaylfc Jun 15, 2026
5002dcd
feat(notifications): route notification clicks to the relevant app (#…
jaylfc Jun 15, 2026
af8a236
feat(desktop): arrange desktop icons from the top-right (content-end)
jaylfc Jun 15, 2026
b3d7bfe
fix(files): open a desktop folder at its path (FilesApp accepts a pat…
jaylfc Jun 15, 2026
d12bcf9
feat(projects): redesign app shell + Workspace hero tab (#59) (#941)
jaylfc Jun 15, 2026
566060b
fix(cluster): do not notify when the local worker self-registers
jaylfc Jun 15, 2026
5b3958b
feat(projects): collapsible project-list sidebar (narrow rail keeps s…
jaylfc Jun 15, 2026
86f5c6b
docs(status): #940/#941 merged + Projects live, worker-notif + sideba…
jaylfc Jun 15, 2026
9ffab81
fix(messages): tokenize Projects + Archived sidebar text (invisible i…
jaylfc Jun 15, 2026
4506c3a
fix(chat): opaque thread panel + theme-aware text (fixes overlapping …
jaylfc Jun 15, 2026
961d799
fix(controller): stop the 45s restart hang (bounded shutdown + loopba…
jaylfc Jun 15, 2026
f82399d
docs(status): #942 restart fix merged (#64 done); screenshot fixes; t…
jaylfc Jun 15, 2026
4b3ffa7
feat(projects): make the Workspace divider draggable to resize the panes
jaylfc Jun 15, 2026
931b7a8
fix(controller): bound uvicorn graceful-shutdown so restarts do not h…
jaylfc Jun 15, 2026
ba94584
fix(controller): unified shutdown for the dual uvicorn servers (real …
jaylfc Jun 15, 2026
ec72462
fix(controller): close github_identities + sweep all stores on shutdo…
jaylfc Jun 15, 2026
00a38c6
docs(status): #64 restart hang truly fixed + verified (46s->7s); divi…
jaylfc Jun 15, 2026
6d269f8
fix(chat): mark-read 500 on empty body
jaylfc Jun 15, 2026
22ae5cf
fix(canvas): stabilize Projects canvas (#68) (#943)
jaylfc Jun 15, 2026
4842eeb
fix(canvas): register custom shapes in the store schema + allow data:…
jaylfc Jun 15, 2026
d742bed
fix(canvas): upsert canvas elements + allow blob: images in CSP
jaylfc Jun 15, 2026
0ebe95b
fix(mobile): indigo window-bg flash + Projects sidebar full-width on …
jaylfc Jun 15, 2026
b7192cf
feat(mail): Mail app phase 1 (IMAP/SMTP accounts, list/read/send) (#6…
jaylfc Jun 15, 2026
623c26d
docs: list Mail in README platform apps + STATUS dev tip to b7192cfd
jaylfc Jun 15, 2026
337cace
feat(browser): redesign chrome to the Store/Images bar (#66) (#944)
jaylfc Jun 15, 2026
114a1e3
feat(apps): make Reddit/YouTube/GitHub/X optional Store installs (#53…
jaylfc Jun 15, 2026
93d87fb
docs: README marks Reddit/YouTube/GitHub/X optional + STATUS to dev 1…
jaylfc Jun 15, 2026
e3421b2
docs: remove em/en dashes from README, STATUS, AGENT_HANDOFF
jaylfc Jun 15, 2026
2eb05bf
feat(browser): taos.my homepage + dark/light scheme for proxied sites…
jaylfc Jun 15, 2026
e576b9a
feat(desktop): agent-callable screenshot + capture flash effect (#948)
jaylfc Jun 15, 2026
04c914b
fix(agent-panel): use shell-bg-glass token instead of hardcoded indigo
jaylfc Jun 15, 2026
4e4d865
fix(notifications): stop re-toasting the unread backlog on reload
jaylfc Jun 15, 2026
f5a9c65
docs(status): LATEST-6 -- agent-driven OS demo live, Tailscale HTTPS,…
jaylfc Jun 15, 2026
f31a2eb
fix(canvas): contain a single bad element + crashes, do not take down…
jaylfc Jun 16, 2026
f9bd39c
docs(status): canvas crash contained via #949 (root cause still pendi…
jaylfc Jun 16, 2026
71fdf9b
fix(browser): scope the prefers-color-scheme matchMedia override to e…
jaylfc Jun 16, 2026
79af4b0
fix(canvas): coerce element payloads so malformed agent writes still …
jaylfc Jun 16, 2026
6566566
docs(status): LATEST-7 -- canvas crash root cause found+fixed (#951),…
jaylfc Jun 16, 2026
d1c68dc
Merge master into dev (back-merge promotion merge commit #938)
jaylfc Jun 16, 2026
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
60 changes: 31 additions & 29 deletions README.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@radix-ui/react-switch": "^1.3.0",
"@radix-ui/react-tabs": "^1.1.14",
"@radix-ui/react-tooltip": "^1.2.9",
"@tldraw/assets": "4.5.12",
"@tldraw/tldraw": "^4.5.10",
"@tsparticles/engine": "^3.9.1",
"@tsparticles/react": "^3.0.0",
Expand All @@ -42,6 +43,7 @@
"highlight.js": "^11.11.1",
"lucide-react": "^0.500.0",
"mathjs": "^15.2.0",
"modern-screenshot": "^4.7.0",
"motion": "^12.40.0",
"plyr": "^3.8.4",
"react": "^19.2.7",
Expand Down
5 changes: 5 additions & 0 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { LoginScreen } from "@/components/LoginScreen";
import { NotificationToasts } from "@/components/NotificationToast";
import { NotificationCentre } from "@/components/NotificationCentre";
import { useNotificationStore } from "@/stores/notification-store";
import { useServerNotifications } from "@/hooks/use-server-notifications";
import { TaosAssistantPanel } from "@/components/TaosAssistantPanel";
import { useTaosAgentStore } from "@/stores/taos-agent-store";
import { InstallPromptBanner } from "@/shell/InstallPromptBanner";
Expand Down Expand Up @@ -189,6 +190,10 @@ export function App() {

useSessionPersistence();

// Sync the persistent backend notification feed into the bell (desktop and
// mobile both render NotificationCentre under this component).
useServerNotifications();

// Re-apply the persisted active theme on app boot so a reload keeps the
// user's chosen theme app-wide (not only when Settings is opened).
useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/apps/BrowserApp/AddressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export function AddressBar({ windowId }: AddressBarProps) {
setSelectedIndex((i) => Math.max(i - 1, -1));
}
}}
className={`w-full bg-shell-bg-deep text-shell-text px-2 py-0.5 rounded text-xs border border-shell-border-subtle focus:border-accent focus:outline-none ${
className={`w-full bg-transparent text-[13px] text-shell-text placeholder:text-shell-text-tertiary focus:outline-none ${
activeTab?.readerAvailable && activeTab.url !== "about:blank"
? "pr-14"
: activeTab?.readerAvailable || activeTab?.url !== "about:blank"
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/apps/BrowserApp/AgentPresencePill.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ describe("AgentPresencePill", () => {
);
await waitFor(() => {
const btn = screen.getByRole("button");
expect(btn.className).toContain("bg-shell-hover");
expect(btn.className).toContain("bg-accent-glow");
});
});

Expand Down
4 changes: 2 additions & 2 deletions desktop/src/apps/BrowserApp/AgentPresencePill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ export function AgentPresencePill({
aria-expanded={panelIsOpen}
onClick={() => togglePanel(windowId, tabId, firstAgentId)}
className={[
"flex items-center relative p-0.5 rounded-full border border-shell-border-subtle",
panelIsOpen ? "bg-shell-hover" : "bg-shell-bg-deep hover:bg-shell-hover",
"flex items-center relative h-[32px] px-1.5 rounded-full border border-accent-line transition-colors",
panelIsOpen ? "bg-accent-glow" : "bg-accent-soft hover:bg-accent-glow",
].join(" ")}
>
{/* Stacked avatars */}
Expand Down
29 changes: 15 additions & 14 deletions desktop/src/apps/BrowserApp/BrowserApp.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
/**
* BrowserApp v2 — top-level container.
*
* Mounted by WindowContent for each browser window. Composes:
* - Chrome (browser-specific nav row + profile chip)
* - TabStrip (compact tab strip with embedded AddressBar in active tab)
* - AddressBar (URL input + suggest popover) — for now rendered ABOVE
* TabStrip; PR 5 may move it inside the active tab per
* the Q8 layout A "compact unified bar" mockup.
* Mounted by WindowContent for each browser window. Composes (top to bottom):
* - TabStrip (tab strip + Proxy/Streamed engine toggle)
* - Chrome (toolbar: nav buttons, pill omnibox with AddressBar, agent
* presence pill, settings, profile chip)
* - BookmarksBar
* - TabRenderer (iframe pool + discard scheduler)
*
* On mobile, a single bottom bar hosts the window switcher, the AddressBar
* omnibox, and the tab overview.
*
* On mount, auto-creates the window entry in browser-store with the
* default profile if it doesn't exist. Idempotent — preserves any
* existing entry (e.g. restored by useSessionPersistence on app boot).
Expand Down Expand Up @@ -119,21 +121,23 @@ export function BrowserApp({ windowId }: BrowserAppProps) {
)}
</div>

<div className="flex items-center gap-1 px-2 py-1 bg-shell-surface border-t border-shell-border-subtle">
<div className="flex items-center gap-1 px-2 py-1 bg-shell-surface border-t border-shell-border">
<button
type="button"
aria-label="Browser windows"
onClick={() => setWindowChooserOpen(true)}
className="p-1.5 rounded hover:bg-shell-hover"
className="p-1.5 rounded hover:bg-white/[0.06]"
>
<Layers size={14} />
</button>
<AddressBar windowId={windowId} />
<div className="flex flex-1 min-w-0 items-center h-9 px-3 rounded-full bg-shell-bg-deep border border-shell-border focus-within:border-accent/40">
<AddressBar windowId={windowId} />
</div>
<button
type="button"
aria-label="Tab overview"
onClick={() => setTabOverviewOpen(true)}
className="p-1.5 rounded hover:bg-shell-hover"
className="p-1.5 rounded hover:bg-white/[0.06]"
>
<ListChecks size={14} />
</button>
Expand All @@ -145,12 +149,9 @@ export function BrowserApp({ windowId }: BrowserAppProps) {

return (
<div className="relative flex flex-col h-full bg-shell-bg overflow-hidden">
<TabStrip windowId={windowId} />
<Chrome windowId={windowId} />
<div className="flex items-center gap-1 px-2 py-1 bg-shell-surface border-b border-shell-border-subtle">
<AddressBar windowId={windowId} />
</div>
<BookmarksBar windowId={windowId} profileId={win.profileId} />
<TabStrip windowId={windowId} />
<TabRenderer windowId={windowId} />
{findOpen && (
<FindInPage
Expand Down
107 changes: 107 additions & 0 deletions desktop/src/apps/BrowserApp/BrowserModeToggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
import { BrowserModeToggle } from "./BrowserModeToggle";
import { useBrowserStore } from "@/stores/browser-store";

const originalFetch = global.fetch;
const WIN_ID = "win-1";

function activeTabId() {
return useBrowserStore.getState().getWindow(WIN_ID)!.activeTabId;
}

beforeEach(() => {
useBrowserStore.setState({ windows: {} });
useBrowserStore.getState().createWindow(WIN_ID, "personal");
const tabId = activeTabId();
useBrowserStore.getState().navigateTab(WIN_ID, tabId, "https://example.com/");
});

afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
cleanup();
});

describe("BrowserModeToggle - segmented engine control", () => {
it("renders Proxy and Streamed radios with Proxy selected by default", () => {
render(<BrowserModeToggle windowId={WIN_ID} />);
const proxy = screen.getByRole("radio", { name: /proxy browser/i });
const streamed = screen.getByRole("radio", { name: /streamed browser/i });
expect(proxy.getAttribute("aria-checked")).toBe("true");
expect(streamed.getAttribute("aria-checked")).toBe("false");
});

it("marks Streamed selected when the active tab has a liveSession", () => {
useBrowserStore.getState().setTabLiveSession(WIN_ID, activeTabId(), {
nekoUrl: "http://neko.local:8080/room",
streamToken: "tok-1",
});
render(<BrowserModeToggle windowId={WIN_ID} />);
expect(
screen.getByRole("radio", { name: /streamed browser/i }).getAttribute("aria-checked"),
).toBe("true");
});

it("clicking Streamed posts a session and sets liveSession on a running response", async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 201,
json: async () => ({
session: {
id: "sess-1",
status: "running",
neko_url: "http://neko.local:8080/room",
stream_token: "tok-xyz",
},
}),
} as Response);

const tabId = activeTabId();
render(<BrowserModeToggle windowId={WIN_ID} />);
fireEvent.click(screen.getByRole("radio", { name: /streamed browser/i }));

await waitFor(() => {
const tab = useBrowserStore
.getState()
.getWindow(WIN_ID)!
.tabs.find((t) => t.id === tabId);
expect(tab?.liveSession).toEqual({
nekoUrl: "http://neko.local:8080/room",
streamToken: "tok-xyz",
});
});
});

it("clicking Proxy clears an existing liveSession", () => {
const tabId = activeTabId();
useBrowserStore.getState().setTabLiveSession(WIN_ID, tabId, {
nekoUrl: "http://neko.local:8080/room",
streamToken: "tok-1",
});
render(<BrowserModeToggle windowId={WIN_ID} />);
fireEvent.click(screen.getByRole("radio", { name: /proxy browser/i }));
const tab = useBrowserStore
.getState()
.getWindow(WIN_ID)!
.tabs.find((t) => t.id === tabId);
expect(tab?.liveSession).toBeUndefined();
});

it("shows a gate hint when the host has no capable node (409)", async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: false,
status: 409,
json: async () => ({ error: "no_capable_node" }),
} as Response);

render(<BrowserModeToggle windowId={WIN_ID} />);
fireEvent.click(screen.getByRole("radio", { name: /streamed browser/i }));

await waitFor(() => {
expect(screen.getByRole("alert").textContent).toMatch(
/streamed browser needs a more capable device/i,
);
});
});
});
Loading
Loading