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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Changelog

All notable changes to taOS are documented in this file.

Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotion.

## [Unreleased]

## [1.0.0-beta.2] - 2026-06-16

### Added
- Mail app with IMAP/SMTP account setup, message list, read, and send.
- Reddit, YouTube, GitHub, and X apps available as optional Store installs.
- Agent-callable screenshot endpoint for desktop-control workflows.

### Changed
- Browser app redesigned with the Store/Images design bar and taos.my set as the default homepage, with automatic dark/light scheme applied to proxied sites.
- Projects app shell redesigned with a Workspace hero tab.
- Notification bell wired to the backend feed with actionable click routing to the originating app or agent.
- Updates panel now shows version numbers (e.g. 1.0.0-beta.2) as the primary display, with commit SHAs as a secondary detail.

### Fixed
- Controller restart time reduced from ~46 s to ~7 s by eliminating the graceful-stop hang.
- Projects canvas crash caused by malformed element payloads written by agents.
- Window move and resize jitter under rapid pointer events.

## [1.0.0-beta.1] - 2026-06-09

Initial source-available public beta release under the taOS Sustainable Use License v0.1.
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tinyagentos-desktop",
"private": true,
"version": "1.0.0-beta",
"version": "1.0.0-beta.2",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
7 changes: 6 additions & 1 deletion desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,12 @@ export function App() {
Return to fullscreen
</button>
)}
<div className={`transition-all duration-500 ${launched ? "opacity-100 scale-100" : "opacity-0 scale-95"}`}>
{/* Keep the entrance zoom (scale-95 -> none animates), but do NOT
retain a transform once launched: a `transform` on this ancestor
(even scale(1)) makes a containing block for every position:fixed
descendant, which pins the dock to the top and renders context
menus in the wrong corner. Steady state must be transform-free. */}
<div className={`transition-all duration-500 ${launched ? "opacity-100" : "opacity-0 scale-95"}`}>
<div className="h-screen w-screen flex flex-col overflow-hidden bg-shell-bg text-shell-text">
<EffectsLayer />
<TopBar onSearchOpen={toggleSearch} onAssistantOpen={toggleAssistant} />
Expand Down
29 changes: 28 additions & 1 deletion desktop/src/apps/SettingsApp/UpdatesPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,41 @@ const jResp = (b: any) => Promise.resolve({ ok: true, json: async () => b } as a
beforeEach(() => {
global.fetch = vi.fn(async (url: string) => {
if (url === "/api/preferences/auto-update") return jResp({ check_enabled: true });
if (url === "/api/settings/update-check") return jResp({ has_updates: false, current_commit: "abc x" });
if (url === "/api/settings/update-check")
return jResp({ has_updates: false, current_version: "1.0.0-beta.2", current_commit: "abc x" });
if (url === "/api/settings/update-status") return jResp({ current_sha: "abc", pending_restart_sha: null });
if (url === "/api/settings/branches") return jResp({ branches: ["master", "dev"], current: "dev" });
if (url === "/api/settings/update-channel") return jResp({ status: "switching", branch: "master" });
return jResp({});
}) as any;
});

describe("UpdatesPanel — version display", () => {
it("shows current version prominently when up to date", async () => {
render(<UpdatesPanel />);
await waitFor(() => expect(screen.getByText("1.0.0-beta.2")).toBeInTheDocument());
});

it("shows available version when an update is present", async () => {
(global.fetch as any) = vi.fn(async (url: string) => {
if (url === "/api/preferences/auto-update") return jResp({ check_enabled: true });
if (url === "/api/settings/update-check")
return jResp({
has_updates: true,
current_version: "1.0.0-beta.2",
new_version: "1.0.0-beta.3",
current_commit: "abc defg",
new_commit: "xyz uvwx",
});
if (url === "/api/settings/update-status") return jResp({ current_sha: "abc", pending_restart_sha: null });
return jResp({});
});
render(<UpdatesPanel />);
await waitFor(() => expect(screen.getByText("1.0.0-beta.2")).toBeInTheDocument());
await waitFor(() => expect(screen.getByText("1.0.0-beta.3")).toBeInTheDocument());
});
});

describe("UpdatesPanel — branch selector", () => {
it("hides the branch selector until Advanced is expanded", async () => {
render(<UpdatesPanel />);
Expand Down
29 changes: 22 additions & 7 deletions desktop/src/apps/SettingsApp/UpdatesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RestartProgressModal } from "@/apps/SettingsApp/_shared";
interface UpdateInfo {
has_updates: boolean;
current_version: string;
new_version?: string | null;
current_commit: string;
new_commit?: string | null;
}
Expand Down Expand Up @@ -234,17 +235,31 @@ export function UpdatesPanel() {
<p className="text-sm font-medium">taOS</p>
{info?.has_updates && info.new_commit ? (
<div className="flex flex-col gap-0.5">
<p className="text-xs text-shell-text-tertiary tabular-nums">
<span className="text-white/40">installed </span>{info.current_commit}
<p className="text-xs text-shell-text-secondary">
<span className="text-white/40">installed </span>
<span className="font-mono">{info.current_version}</span>
{info.new_version ? (
<>
<span className="text-white/40"> &rarr; </span>
<span className="font-mono text-amber-300/90">{info.new_version}</span>
</>
) : null}
</p>
<p className="text-xs text-amber-300/90 tabular-nums">
<span className="text-amber-300/50">available </span>{info.new_commit}
<p className="text-[10px] text-shell-text-tertiary tabular-nums font-mono opacity-60">
{info.current_commit} &rarr; {info.new_commit}
</p>
</div>
) : (
<p className="text-xs text-shell-text-tertiary tabular-nums">
{info?.current_commit ?? "v0.1.0-dev"}
</p>
<div className="flex flex-col gap-0.5">
<p className="text-xs text-shell-text-secondary font-mono">
{info?.current_version ?? "unknown"}
</p>
{info?.current_commit ? (
<p className="text-[10px] text-shell-text-tertiary tabular-nums font-mono opacity-60">
{info.current_commit}
</p>
) : null}
</div>
)}
</div>
{info?.has_updates && (
Expand Down
10 changes: 8 additions & 2 deletions desktop/src/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useIsMobile } from "@/hooks/use-is-mobile";

export interface MenuItem {
Expand Down Expand Up @@ -103,7 +104,11 @@ export function ContextMenu({ x, y, items, onClose }: Props) {
// Roving tabindex: track which navigable (non-separator, non-disabled) item is active
let navigableCounter = -1;

return (
// Portal to <body> so the menu is never trapped inside a transformed ancestor
// (e.g. the dock's -translate-x-1/2). A transform on any ancestor makes a
// containing block for position:fixed, which would offset the menu away from
// the cursor. Rendered on body, its fixed coordinates are viewport-relative.
return createPortal(
<div
ref={menuRef}
role="menu"
Expand Down Expand Up @@ -158,6 +163,7 @@ export function ContextMenu({ x, y, items, onClose }: Props) {
</button>
);
})}
</div>
</div>,
document.body,
);
}
6 changes: 6 additions & 0 deletions desktop/src/components/DockIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function DockIcon({ appId, isRunning, onClick }: Props) {
const restoreWindow = useProcessStore((s) => s.restoreWindow);
const minimizeWindow = useProcessStore((s) => s.minimizeWindow);
const maximizeWindow = useProcessStore((s) => s.maximizeWindow);
const recenterWindow = useProcessStore((s) => s.recenterWindow);
const closeWindow = useProcessStore((s) => s.closeWindow);
const pinned = useDockStore((s) => s.pinned);
const pin = useDockStore((s) => s.pin);
Expand Down Expand Up @@ -56,6 +57,11 @@ export function DockIcon({ appId, isRunning, onClick }: Props) {
icon: <icons.Maximize2 size={14} />,
action: () => maximizeWindow(win.id),
},
{
label: "Center Window",
icon: <icons.LocateFixed size={14} />,
action: () => recenterWindow(win.id),
},
{ label: "", separator: true },
isPinned
? { label: "Remove from Dock", icon: <icons.PinOff size={14} />, action: () => unpin(appId) }
Expand Down
57 changes: 50 additions & 7 deletions desktop/src/components/Window.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from "react";
import { memo, useCallback, useRef, useState } from "react";
import { Rnd } from "react-rnd";
import { motion, useReducedMotion } from "motion/react";
import { useProcessStore, type WindowState, type SnapPosition } from "@/stores/process-store";
Expand All @@ -13,9 +13,20 @@ interface Props {
onDragStop: () => SnapPosition;
}

export function Window({ win, onDrag, onDragStop }: Props) {
const { focusWindow, closeWindow, removeWindow, minimizeWindow, maximizeWindow, updatePosition, updateSize, snapWindow } =
useProcessStore();
function WindowImpl({ win, onDrag, onDragStop }: Props) {
// Select each action individually. Destructuring useProcessStore() with no
// selector subscribes the window to EVERY store change, so any unrelated
// store write would re-render it mid-drag and react-rnd would reset its
// controlled position. Action references are stable, so these never trigger
// a re-render on their own.
const focusWindow = useProcessStore((s) => s.focusWindow);
const closeWindow = useProcessStore((s) => s.closeWindow);
const removeWindow = useProcessStore((s) => s.removeWindow);
const minimizeWindow = useProcessStore((s) => s.minimizeWindow);
const maximizeWindow = useProcessStore((s) => s.maximizeWindow);
const updatePosition = useProcessStore((s) => s.updatePosition);
const updateBounds = useProcessStore((s) => s.updateBounds);
const snapWindow = useProcessStore((s) => s.snapWindow);
const app = getApp(win.appId);
const preSnapRef = useRef<{ x: number; y: number; w: number; h: number } | null>(null);
const isMobile = useIsMobile();
Expand Down Expand Up @@ -95,12 +106,37 @@ export function Window({ win, onDrag, onDragStop }: Props) {
[onDragStop, snapWindow, updatePosition, win.id, win.size],
);

// Feed react-rnd's live position+size back every resize tick. react-rnd's
// position prop is controlled, and resizing from a top/left edge changes the
// position; without live feedback react-rnd's own internal re-render re-reads
// the stale stored position and the window jumps sideways mid-resize. Keeping
// the controlled props in lockstep with react-rnd's reported bounds keeps the
// resize smooth from every edge.
const handleResize = useCallback(
(
_e: unknown,
_dir: unknown,
ref: HTMLElement,
_delta: unknown,
position: { x: number; y: number },
) => {
updateBounds(win.id, position.x, position.y, ref.offsetWidth, ref.offsetHeight);
},
[updateBounds, win.id],
);

const handleResizeStop = useCallback(
(_e: unknown, _dir: unknown, ref: HTMLElement) => {
(
_e: unknown,
_dir: unknown,
ref: HTMLElement,
_delta: unknown,
position: { x: number; y: number },
) => {
setDragging(false);
updateSize(win.id, ref.offsetWidth, ref.offsetHeight);
updateBounds(win.id, position.x, position.y, ref.offsetWidth, ref.offsetHeight);
},
[updateSize, win.id],
[updateBounds, win.id],
);

const minSize = app?.minSize ?? { w: 300, h: 200 };
Expand Down Expand Up @@ -142,6 +178,7 @@ export function Window({ win, onDrag, onDragStop }: Props) {
onDrag={handleDrag}
onDragStop={handleDragStop}
onResizeStart={() => setDragging(true)}
onResize={handleResize}
onResizeStop={handleResizeStop}
onMouseDown={() => focusWindow(win.id)}
bounds="parent"
Expand Down Expand Up @@ -248,3 +285,9 @@ export function Window({ win, onDrag, onDragStop }: Props) {
</Rnd>
);
}

// Memoized so unrelated desktop re-renders (snap-zone preview, the live
// wallpaper, the agent command stream) do not re-render every window during a
// drag. Props are stable: `win` only changes when this window's own state
// changes, and onDrag/onDragStop are stabilized in useSnapZones.
export const Window = memo(WindowImpl);
Loading
Loading