Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
80fa2cc
Route telemetry through ScientFactory gateway (#43)
yaacovcorcos Jul 20, 2026
e55716c
Fix unsigned macOS release signatures (#44)
yaacovcorcos Jul 20, 2026
1d72ef9
Add consent-aware desktop analytics (#47)
yaacovcorcos Jul 20, 2026
4e4019c
Fix Claude terminal auth and connection recovery (#52)
yaacovcorcos Jul 20, 2026
c88a239
Use direct OAuth for Grok sign-in (#53)
yaacovcorcos Jul 21, 2026
8058028
Secure Scient state initialization (#48)
yaacovcorcos Jul 21, 2026
a577f50
Supervise the desktop backend lifecycle (#49)
yaacovcorcos Jul 21, 2026
e1a4d4d
Supervise desktop connection recovery (#50)
yaacovcorcos Jul 21, 2026
40dfa6c
Define safe RPC recovery policies (#51)
yaacovcorcos Jul 21, 2026
211ed85
Surface connection recovery diagnostics (#55)
yaacovcorcos Jul 21, 2026
f658bbc
Recover Codex sessions after authentication loss (#56)
yaacovcorcos Jul 21, 2026
88ac61d
Support Linux desktop development launch (#57)
yaacovcorcos Jul 21, 2026
3c04060
Fix Antigravity browser authentication (#59)
yaacovcorcos Jul 21, 2026
71da4e4
Scope desktop signing credentials by platform (#60)
yaacovcorcos Jul 21, 2026
4813299
Fix managed Antigravity updates, authentication, and dialog UX (#62)
yaacovcorcos Jul 21, 2026
b80b249
Stabilize macOS release identity (#63)
yaacovcorcos Jul 21, 2026
f55be4e
Reduce unintended macOS permission prompts (#64)
yaacovcorcos Jul 21, 2026
1eafedb
Control macOS notarization lifecycle (#65)
yaacovcorcos Jul 21, 2026
95cf946
Add Evidence to Note as a latent built-in (#70)
yaacovcorcos Jul 22, 2026
d05d4e1
Harden bidirectional chat rendering (#71)
yaacovcorcos Jul 22, 2026
5982a79
Add Medical Exam Study built-in (#72)
yaacovcorcos Jul 22, 2026
abb7a05
Fix YAML frontmatter in Markdown previews (#69)
yaacovcorcos Jul 22, 2026
cbf4c16
Add T3-inspired project source dialog (#76)
yaacovcorcos Jul 22, 2026
4ad0351
Add T3-style right dock surfaces (#77)
yaacovcorcos Jul 22, 2026
490c907
fix: remove AppSnap startup announcement (#80)
yaacovcorcos Jul 22, 2026
8071f14
Add artifact preview cards to chat (#81)
yaacovcorcos Jul 22, 2026
2257adf
fix: select providers after connection (#82)
yaacovcorcos Jul 22, 2026
df36dda
Add message-level conversation forks (#78)
yaacovcorcos Jul 22, 2026
4c678de
Improve desktop contribution workflow (#75)
yaacovcorcos Jul 22, 2026
a14aa27
Set default base font to 15px and fix numeric editing (#79)
yaacovcorcos Jul 23, 2026
66c93fc
Use recommended provider model defaults (#83)
yaacovcorcos Jul 23, 2026
a757575
Align desktop approval policy (#85)
yaacovcorcos Jul 23, 2026
8d421bb
Fix release-candidate regressions (#86)
yaacovcorcos Jul 23, 2026
640473f
fix(release): preserve Linux sandbox (#87)
yaacovcorcos Jul 23, 2026
554ca86
fix: preserve whitespace in folder browsing (#90)
yaacovcorcos Jul 23, 2026
bee5a9d
Collapse long assistant artifact shelves (#91)
yaacovcorcos Jul 23, 2026
482a948
Remove project setup confirmation card (#93)
yaacovcorcos Jul 23, 2026
67818a1
Build unified notification system (#96)
yaacovcorcos Jul 23, 2026
d711e05
Hide provider setup banner on empty chats (#94)
yaacovcorcos Jul 23, 2026
9ec7017
Fix Droid model discovery lifecycle (#95)
yaacovcorcos Jul 23, 2026
1ff9a26
Add ChatGPT-first voice transcription with local Whisper fallback (#97)
yaacovcorcos Jul 23, 2026
f6488bc
Default Git writing to GPT-5.6 Luna (#99)
yaacovcorcos Jul 23, 2026
5201456
Apply updated settings defaults once (#101)
yaacovcorcos Jul 23, 2026
dd5f543
Improve voice dictation and active-turn composer controls (#102)
yaacovcorcos Jul 23, 2026
b3b0e7a
Fix folder picker navigation and project opening (#98)
yaacovcorcos Jul 23, 2026
1d2ef85
Fix Whisper runtime packaging on Windows (#104)
yaacovcorcos Jul 23, 2026
db689d6
Secure HTML artifact previews with isolated execution (#100)
yaacovcorcos Jul 23, 2026
888c12e
Use trash icon for voice cancel action (#106)
yaacovcorcos Jul 24, 2026
4764fbd
Fix browser pane close and runtime recovery (#113)
yaacovcorcos Jul 24, 2026
be82b20
Isolate temporary Claude discovery from MCP servers (#116)
yaacovcorcos Jul 24, 2026
077fb96
Respect reduced motion in thread spinner (#84)
yaacovcorcos Jul 24, 2026
de03926
Harden upstream-derived release tooling (#109)
yaacovcorcos Jul 24, 2026
4042ff4
Salvage safe PR 61 interaction improvements (#107)
yaacovcorcos Jul 24, 2026
dc5e334
Adapt Studio Git gating and folder access (#108)
yaacovcorcos Jul 24, 2026
606f348
Improve changed-file accessibility and density (#110)
yaacovcorcos Jul 24, 2026
e6af16d
Add compact previews for bulky changed-file cards (#117)
yaacovcorcos Jul 24, 2026
fffd9e7
feat: add curated in-app release notes (#112)
yaacovcorcos Jul 24, 2026
4144022
Fix browser multi-tab controls and menu occlusion (#119)
yaacovcorcos Jul 24, 2026
d399509
Refine branch and automation workflow affordances (#111)
yaacovcorcos Jul 24, 2026
0428e69
Add universal local file viewer (#114)
yaacovcorcos Jul 24, 2026
65a6961
Harden provider installation and sign-in (#103)
yaacovcorcos Jul 24, 2026
ca615ee
Harden OpenCode and Kilo turn completion (#120)
yaacovcorcos Jul 24, 2026
cfc702a
Add native folder drop to Add Project (#115)
yaacovcorcos Jul 24, 2026
341055d
Make new thread workspace intent explicit (#118)
yaacovcorcos Jul 24, 2026
ac0b826
docs: add Scient 0.5.13 release notes
yaacovcorcos Jul 24, 2026
e659d41
Merge remote-tracking branch 'origin/release/stable' into release/pro…
yaacovcorcos Jul 24, 2026
dec4960
chore: keep promotion tree aligned with main
yaacovcorcos Jul 24, 2026
46d5857
chore: restore test cleanup in promotion tree
yaacovcorcos Jul 24, 2026
6d713ed
chore: align promotion test tree
yaacovcorcos Jul 24, 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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun.lock text eol=lf
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ jobs:
cd apps/web
./node_modules/.bin/playwright install --with-deps chromium

- name: Electron browser overlay lifecycle
timeout-minutes: 2
run: |
ELECTRON_BINARY="$(cd apps/desktop && bun -e 'console.log(require("electron"))')"
ELECTRON_SANDBOX="$(dirname "$ELECTRON_BINARY")/chrome-sandbox"
test -f "$ELECTRON_SANDBOX"
sudo chown root:root "$ELECTRON_SANDBOX"
sudo chmod 4755 "$ELECTRON_SANDBOX"
xvfb-run -a bun run test:desktop-browser-overlay-lifecycle

# Blocking product-behavior coverage. Linux-only pixel/font/layout
# comparisons run separately below while their rendering is quarantined.
- name: Browser test (stable)
Expand Down Expand Up @@ -176,6 +186,9 @@ jobs:
- name: Test Effect Windows process spawn
run: bun run --cwd apps/server test src/windowsProcessEffect.test.ts

- name: Test Windows provider runtime discovery
run: bun run --cwd apps/server test src/provider/Layers/ProviderRuntimeManager.test.ts

- name: Test Windows private state initialization
run: |
bun run --cwd apps/server test src/serverPrivateDirectories.test.ts
Expand Down
19 changes: 11 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,23 @@ jobs:

echo "Publication source verified at ${RELEASE_BRANCH} (${release_branch_sha})."

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version-file: package.json

- name: Install dependencies
run: bun install --frozen-lockfile

- id: release_meta
name: Resolve release policy
env:
RELEASE_VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }}
run: node scripts/resolve-release-update-policy.ts "$RELEASE_VERSION"

- name: Verify release-note catalog structure
run: node scripts/verify-release-notes.ts "${{ steps.release_meta.outputs.version }}"

- name: Verify public updater repository
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -177,14 +188,6 @@ jobs:
fi
done

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version-file: package.json

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Verify Scient identity
run: bun run brand:check

Expand Down
4 changes: 2 additions & 2 deletions KEYBINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged
- `terminal.split`: split terminal (in focused terminal context by default)
- `terminal.new`: create new terminal (in focused terminal context by default)
- `terminal.close`: close/kill the focused terminal (in focused terminal context by default)
- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state
- `chat.new`: create a new chat thread using the target project's configured workspace default
- `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`))
- `chat.newTerminal`: create a new terminal-first thread preserving the active thread's branch/worktree state
- `chat.newTerminal`: create a new terminal-first thread using the target project's configured workspace default; if a configured worktree has no concrete path, safely fall back to the project's local checkout
- `composer.focus.toggle`: focus or blur the chat prompt composer
- `editor.openFavorite`: open current project/worktree in the last-used editor
- `script.{id}.run`: run a project script by id (for example `script.test.run`)
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"start": "bun run scripts/start-electron.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"test:browser-overlay-lifecycle": "bun scripts/run-browser-overlay-lifecycle.mjs",
"smoke-test": "node scripts/smoke-test.mjs"
},
"dependencies": {
Expand Down
195 changes: 195 additions & 0 deletions apps/desktop/scripts/browser-overlay-lifecycle.electron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// FILE: browser-overlay-lifecycle.electron.ts
// Purpose: Real-Electron regression for adopted renderer webview overlay lifetime.
// Layer: Desktop integration harness (bundled and launched by the sibling runner).

import { join } from "node:path";

import type { ThreadId } from "@synara/contracts";
import { app, BrowserWindow, ipcMain, type WebContents } from "electron";

import { DesktopBrowserManager } from "../src/browserManager";

const THREAD_ID = "electron-overlay-lifecycle" as ThreadId;
const OVERLAY_HOLD_MS = 31_250;
const TEST_TIMEOUT_MS = 10_000;
const BOUNDS_CHANNEL = "scient:test:browser-overlay:set-bounds";

function invariant(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}

function withTimeout<T>(promise: Promise<T>, label: string): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_resolve, reject) => {
setTimeout(() => reject(new Error(`Timed out waiting for ${label}.`)), TEST_TIMEOUT_MS);
}),
]);
}

async function waitForRendererHarness(hostWindow: BrowserWindow): Promise<void> {
await withTimeout(
(async () => {
while (!hostWindow.isDestroyed()) {
const ready = await hostWindow.webContents.executeJavaScript(
`Boolean(window.scientBrowserOverlayLifecycle?.ready)`,
);
if (ready) return;
await new Promise((resolve) => setTimeout(resolve, 25));
}
throw new Error("The host window was destroyed before its renderer harness loaded.");
})(),
"the renderer coordination harness",
);
}

async function main(): Promise<void> {
const profileDir = process.env.SCIENT_BROWSER_OVERLAY_TEST_PROFILE;
const fixturePath = process.env.SCIENT_BROWSER_OVERLAY_TEST_FIXTURE;
const preloadPath = process.env.SCIENT_BROWSER_OVERLAY_TEST_PRELOAD;
invariant(profileDir, "Missing isolated Electron profile path.");
invariant(fixturePath, "Missing renderer fixture path.");
invariant(preloadPath, "Missing preload fixture path.");
app.setPath("userData", profileDir);
app.setPath("sessionData", join(profileDir, "session-data"));

await app.whenReady();

let resolveGuest: ((webContents: WebContents) => void) | null = null;
const guestPromise = new Promise<WebContents>((resolve) => {
resolveGuest = resolve;
});
app.on("web-contents-created", (_event, webContents) => {
if (webContents.getType() === "webview") {
resolveGuest?.(webContents);
resolveGuest = null;
}
});

const hostWindow = new BrowserWindow({
show: false,
width: 900,
height: 700,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: preloadPath,
sandbox: true,
webviewTag: true,
},
});
const manager = new DesktopBrowserManager();
manager.setWindow(hostWindow);
const boundsEvents: Array<{ width: number; height: number } | null> = [];
ipcMain.handle(BOUNDS_CHANNEL, (_event, bounds: { width: number; height: number } | null) => {
boundsEvents.push(bounds);
manager.setPanelBounds({
threadId: THREAD_ID,
bounds: bounds ? { x: 20, y: 30, width: bounds.width, height: bounds.height } : null,
surface: "renderer",
});
});

try {
await hostWindow.loadFile(fixturePath);
await waitForRendererHarness(hostWindow);
const guest = await withTimeout(guestPromise, "the renderer webview");
const guestId = guest.id;

const opened = manager.open({ threadId: THREAD_ID });
const tabId = opened.activeTabId;
invariant(tabId, "The browser session did not create an active tab.");
manager.attachWebview({ threadId: THREAD_ID, tabId, webContentsId: guestId });
const initialMode = await hostWindow.webContents.executeJavaScript(
`window.scientBrowserOverlayLifecycle.syncBounds()`,
);
invariant(initialMode === "send", `Initial renderer bounds mode was ${String(initialMode)}.`);

const beforeOverlay = manager.getState({ threadId: THREAD_ID });
invariant(beforeOverlay.activeTabId === tabId, "The adopted tab was not active.");
invariant(beforeOverlay.tabs[0]?.status === "live", "The adopted tab was not live.");
const boundsEventsBeforeOverlay = boundsEvents.length;

const openMode = await hostWindow.webContents.executeJavaScript(
`window.scientBrowserOverlayLifecycle.openOverlay()`,
);
invariant(openMode === "suppress", `Overlay renderer bounds mode was ${String(openMode)}.`);
invariant(
boundsEvents.length === boundsEventsBeforeOverlay,
"Opening the overlay sent a lifecycle-changing bounds event.",
);

await new Promise((resolve) => setTimeout(resolve, OVERLAY_HOLD_MS));

const whileOccluded = manager.getState({ threadId: THREAD_ID });
invariant(!guest.isDestroyed(), "The adopted renderer webview was destroyed while occluded.");
invariant(whileOccluded.activeTabId === tabId, "The active tab changed while occluded.");
invariant(
whileOccluded.tabs.some((tab) => tab.id === tabId && tab.status === "live"),
"The adopted tab session was suspended while occluded.",
);
invariant(
!boundsEvents.slice(boundsEventsBeforeOverlay).includes(null),
"Overlay occlusion sent null bounds and started the hide lifecycle.",
);

const closeMode = await hostWindow.webContents.executeJavaScript(
`window.scientBrowserOverlayLifecycle.closeOverlay()`,
);
invariant(closeMode === "send", `Recovered renderer bounds mode was ${String(closeMode)}.`);

const recoveredWidth = await hostWindow.webContents.executeJavaScript(
`document.querySelector('#browser-host').getBoundingClientRect().width`,
);
const afterOverlay = manager.getState({ threadId: THREAD_ID });
const internals = manager as unknown as {
runtimes: Map<string, { webContents: WebContents; ownsWebContents: boolean }>;
suspendTimers: Map<ThreadId, ReturnType<typeof setTimeout>>;
};
const runtime = internals.runtimes.get(`${THREAD_ID}:${tabId}`);

invariant(recoveredWidth === 700, `Renderer geometry recovered to ${recoveredWidth}, not 700.`);
invariant(
boundsEvents.at(-1)?.width === 700,
"The renderer did not send recovered geometry after the overlay closed.",
);
invariant(
runtime?.webContents.id === guestId,
"A different runtime replaced the adopted webview.",
);
invariant(runtime.ownsWebContents === false, "The adopted renderer runtime changed ownership.");
invariant(internals.suspendTimers.size === 0, "Overlay occlusion scheduled a thread suspend.");
invariant(
afterOverlay.activeTabId === tabId,
"The tab session did not recover after occlusion.",
);

console.log(
JSON.stringify({
result: "passed",
heldOccludedMs: OVERLAY_HOLD_MS,
adoptedWebContentsId: guestId,
activeTabId: tabId,
openMode,
closeMode,
recoveredWidth,
}),
);
} finally {
ipcMain.removeHandler(BOUNDS_CHANNEL);
manager.dispose();
if (!hostWindow.isDestroyed()) {
hostWindow.destroy();
}
}
}

void main().then(
() => app.quit(),
(error) => {
console.error(error instanceof Error ? error.stack : error);
app.exit(1);
},
);
11 changes: 11 additions & 0 deletions apps/desktop/scripts/browser-overlay-lifecycle.preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// FILE: browser-overlay-lifecycle.preload.ts
// Purpose: Minimal isolated bridge for the Electron overlay lifecycle regression.

import { contextBridge, ipcRenderer } from "electron";

const BOUNDS_CHANNEL = "scient:test:browser-overlay:set-bounds";

contextBridge.exposeInMainWorld("scientBrowserOverlayTestApi", {
setPanelBounds: (bounds: { width: number; height: number } | null) =>
ipcRenderer.invoke(BOUNDS_CHANNEL, bounds),
});
63 changes: 63 additions & 0 deletions apps/desktop/scripts/browser-overlay-lifecycle.renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// FILE: browser-overlay-lifecycle.renderer.ts
// Purpose: Exercise the production overlay classifier and bounds decision in a real renderer.

import {
hasNativeBrowserObscuringOverlay,
resolveNativeBrowserBoundsSyncMode,
setBrowserWebviewOverlayOcclusion,
type BrowserWebviewElement,
} from "../../web/src/components/BrowserPanel.overlay";

declare global {
interface Window {
scientBrowserOverlayTestApi: {
setPanelBounds(bounds: { width: number; height: number } | null): Promise<void>;
};
scientBrowserOverlayLifecycle: {
ready: true;
syncBounds(): Promise<string>;
openOverlay(): Promise<string>;
closeOverlay(): Promise<string>;
};
}
}

const host = document.querySelector<HTMLElement>("#browser-host");
const webview = document.querySelector<BrowserWebviewElement>("#browser");
if (!host || !webview) {
throw new Error("Missing browser overlay lifecycle fixture elements.");
}

async function syncBounds(): Promise<string> {
const obscuredByOverlay = hasNativeBrowserObscuringOverlay(host);
setBrowserWebviewOverlayOcclusion(webview, obscuredByOverlay);
const rect = host.getBoundingClientRect();
const mode = resolveNativeBrowserBoundsSyncMode({
obscuredByOverlay,
paneIsActuallyHidden: rect.width <= 0 || rect.height <= 0,
});
if (mode === "suppress") {
return mode;
}
await window.scientBrowserOverlayTestApi.setPanelBounds(
mode === "hide" ? null : { width: rect.width, height: rect.height },
);
return mode;
}

window.scientBrowserOverlayLifecycle = {
ready: true,
syncBounds,
async openOverlay() {
const overlay = document.createElement("div");
overlay.id = "test-overlay";
overlay.dataset.nativeBrowserOverlay = "true";
document.body.append(overlay);
host.style.width = "700px";
return syncBounds();
},
async closeOverlay() {
document.querySelector("#test-overlay")?.remove();
return syncBounds();
},
};
8 changes: 6 additions & 2 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,13 @@ function buildMacLauncher(electronBinaryPath) {
return targetBinaryPath;
}

export function resolveElectronPath() {
export function resolveElectronPackagePath() {
const require = createRequire(import.meta.url);
const electronBinaryPath = require("electron");
return require("electron");
}

export function resolveElectronPath() {
const electronBinaryPath = resolveElectronPackagePath();

if (process.platform !== "darwin") {
return electronBinaryPath;
Expand Down
Loading
Loading