Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
832af09
Add canonical onboarding provider resolution
sambitcreate Aug 10, 2026
2f0aae9
Recognize canonical local provider aliases
sambitcreate Aug 10, 2026
1be27c7
Migrate released local provider identities
sambitcreate Aug 10, 2026
e09c3a3
Cover local provider identity migration
sambitcreate Aug 10, 2026
feb674e
Verify provider migration persistence
sambitcreate Aug 10, 2026
0426ca5
Harden local model discovery
sambitcreate Aug 10, 2026
bca7dfc
Cover local model discovery edge cases
sambitcreate Aug 10, 2026
0c15827
Discover local models during onboarding
sambitcreate Aug 10, 2026
26cf8cd
Cover local provider onboarding
sambitcreate Aug 10, 2026
9357f84
Add provider editor focus targeting
sambitcreate Aug 10, 2026
6739d8a
Restore provider editor focus
sambitcreate Aug 10, 2026
6aed86f
Bind provider editor focus triggers
sambitcreate Aug 10, 2026
bfb6e05
Cover provider dialog focus restoration
sambitcreate Aug 10, 2026
83e8a7b
Cover local provider preference aliases
sambitcreate Aug 10, 2026
51fb4e3
Preserve local provider product icons
sambitcreate Aug 10, 2026
f29a855
Cover collision-safe local provider icons
sambitcreate Aug 10, 2026
18985a7
Align scheduled task tab labels
sambitcreate Aug 10, 2026
0436756
Allow fixed attachment preload channels
sambitcreate Aug 10, 2026
75422e7
Add fixed-purpose attachment preload bridge
sambitcreate Aug 10, 2026
b1fe35a
Expose trusted attachment ingestion methods
sambitcreate Aug 10, 2026
4585073
Add renderer attachment bridge contracts
sambitcreate Aug 10, 2026
5b56b12
Enforce authoritative attachment contracts
sambitcreate Aug 10, 2026
4ee823b
Cover strict raster attachment parsing
sambitcreate Aug 10, 2026
ace71fa
Harden attachment ingestion and admission
sambitcreate Aug 10, 2026
300ea89
Cover bounded attachment ingestion
sambitcreate Aug 10, 2026
14eda8e
Register trusted attachment ingestion handlers
sambitcreate Aug 10, 2026
74dcbc1
Cover main-owned attachment picker contract
sambitcreate Aug 10, 2026
9e944ae
Reject malformed appended attachments
sambitcreate Aug 10, 2026
8e8ba36
Cover fixed attachment IPC surface
sambitcreate Aug 10, 2026
3e85d5c
Support safe drop and paste attachments
sambitcreate Aug 10, 2026
fb96b97
Cover composer attachment interactions
sambitcreate Aug 10, 2026
14872e4
Add deterministic Electron Playwright config
sambitcreate Aug 10, 2026
1305ead
Add strict E2E TypeScript project
sambitcreate Aug 10, 2026
7b63e85
Isolate Electron E2E runtime paths
sambitcreate Aug 10, 2026
2c2579b
Add hermetic Electron E2E fixture
sambitcreate Aug 10, 2026
5edd7e3
Test LM Studio onboarding persistence
sambitcreate Aug 10, 2026
f7ce6aa
Test exact multimodal chat payload
sambitcreate Aug 10, 2026
754ff7f
Add opt-in live LM Studio vision test
sambitcreate Aug 10, 2026
09d946d
Test chat shell interactions
sambitcreate Aug 10, 2026
e5a443b
Test secondary application surfaces
sambitcreate Aug 10, 2026
c3b545c
Test settings and local model selection
sambitcreate Aug 10, 2026
afca165
Document Electron E2E workflows
sambitcreate Aug 10, 2026
69572a0
Ignore Playwright artifacts
sambitcreate Aug 10, 2026
c1ea2f8
Register Electron E2E commands
sambitcreate Aug 10, 2026
ef7e491
Lock E2E test dependencies
sambitcreate Aug 10, 2026
ee49213
Gate CI with deterministic Electron E2E
sambitcreate Aug 10, 2026
c0dbca9
Gate releases with deterministic Electron E2E
sambitcreate Aug 10, 2026
d0a8c65
fix(terminal): chmod+verify node-pty spawn-helper and verify shell be…
sambitcreate Aug 10, 2026
474af5f
fix(build): chmod+verify node-pty spawn-helper in afterPack on macOS
sambitcreate Aug 10, 2026
f48747e
test(terminal): cover shell fallback, spawn-helper chmod/verify, no-o…
sambitcreate Aug 10, 2026
350dea4
test(build): cover makeSpawnHelpersExecutable chmod, missing-helper t…
sambitcreate Aug 10, 2026
80753e7
chore: remove unused writeFile import (fix CI lint)
sambitcreate Aug 10, 2026
dac9d3a
feat(terminal): per-workspace history store with device-query sanitiz…
sambitcreate Aug 11, 2026
b26f879
feat(terminal): shell-candidate retry loop + persisted history wiring
sambitcreate Aug 11, 2026
f5380f7
test(terminal): cover history store round-trip, sanitization, debounc…
sambitcreate Aug 11, 2026
5f517e1
test(terminal): cover shell retry/fallback/non-retryable and history …
sambitcreate Aug 11, 2026
6f58706
feat(ipc): add resolvedShell and preferredShellSkipped to TerminalSes…
sambitcreate Aug 11, 2026
da5dbd3
feat(terminal-drawer): toast when a fallback shell launched the terminal
sambitcreate Aug 11, 2026
15ad2d2
chore(test): register terminal + terminal-history tests in test/test:…
sambitcreate Aug 11, 2026
432ea21
test(subagents): update terminal spawn source assertion for trySpawnS…
sambitcreate Aug 11, 2026
5718840
Merge pull request #21 from sambitcreate/fix/terminal-shell-fallback-…
sambitcreate Aug 11, 2026
bb914c2
test(terminal): make shell retries portable
sambitcreate Aug 11, 2026
3a66cc9
Merge pull request #20 from sambitcreate/fix/terminal-posix-spawnp-mac
sambitcreate Aug 11, 2026
8a91093
fix(terminal): activate persisted sanitized history
sambitcreate Aug 11, 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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,34 @@ jobs:

- name: Build production bundles
run: npm run build

e2e:
name: Deterministic Electron E2E
runs-on: macos-26
timeout-minutes: 30
steps:
- name: Check out source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803

- name: Use Node.js 22
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with:
node-version: 22.22.3
cache: npm

- name: Install locked dependencies
run: npm ci

- name: Run deterministic Electron E2E gate
run: npm run test:e2e

- name: Upload Playwright failure artifacts
if: ${{ failure() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: playwright-e2e-ci-${{ github.run_id }}-${{ github.run_attempt }}
path: |
test-results/e2e
playwright-report/e2e
if-no-files-found: warn
retention-days: 14
14 changes: 14 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ jobs:
- name: Run Apple Foundation Models tests
run: npm run test:native

- name: Run deterministic Electron E2E gate
run: npm run test:e2e

- name: Upload Playwright failure artifacts
if: ${{ failure() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: playwright-e2e-release-${{ github.run_id }}-${{ github.run_attempt }}
path: |
test-results/e2e
playwright-report/e2e
if-no-files-found: warn
retention-days: 14

- name: Prepare App Store Connect key
shell: bash
env:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ Thumbs.db
# Logs
*.log

# Playwright artifacts
/test-results/
/playwright-report/
/blob-report/

# Temporary files
tmp/*

Expand Down
16 changes: 16 additions & 0 deletions main/handlers/attachments.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,19 @@ test("attachment IPC combines native selection and bounded reading in main", ()
assert.doesNotMatch(rendererIpcSource, /attachments:read/);
assert.doesNotMatch(rendererIpcSource, /function pickFiles/);
});

test("every attachment ingestion channel uses process-owned owner-bound admission", () => {
assert.equal(
handlerSource.match(/\brunOwnedAttachmentIngestion\(/gu)?.length,
3,
"picker, drop, and clipboard admission calls must remain present",
);
assert.match(handlerSource, /async function runOwnedAttachmentIngestion<T>\(/u);
assert.match(handlerSource, /owner\.onInvalidated\(lease\.cancel\)/u);
assert.match(
handlerSource,
/finally \{\s+removeOwnerInvalidation\(\);\s+lease\.release\(\);\s+\}/u,
);
assert.match(handlerSource, /validateClipboardAttachmentPayload\(/u);
assert.match(handlerSource, /materializeClipboardAttachments\(payload, isActive\)/u);
});
156 changes: 133 additions & 23 deletions main/handlers/attachments.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
// Reading user-attached files + bundled model capability lookups.

import { BrowserWindow, dialog, ipcMain } from "../platform.js";
import { isImageAttachmentPath, readPickedAttachments } from "../services/attachments.js";
import {
attachmentIngestionRepresentationBytes,
AttachmentIngestionAdmission,
isImageAttachmentPath,
materializeClipboardAttachments,
readPickedAttachments,
validateClipboardAttachmentPayload,
} from "../services/attachments.js";
import { providerModelInfo } from "../services/provider-model-info.js";
import {
MAX_ATTACHMENT_INLINE_BYTES,
MAX_ATTACHMENTS_PER_MESSAGE,
} from "../../renderer/shared/attachment-contract.js";
import { rendererDocumentOwner } from "../services/renderer-document-owner.js";
import {
rendererDocumentOwner,
type RendererDocumentOwner,
} from "../services/renderer-document-owner.js";

const attachmentIngestionAdmission = new AttachmentIngestionAdmission();

async function runOwnedAttachmentIngestion<T>(
owner: RendererDocumentOwner,
attachmentCount: number,
representationBytes: number,
operation: (isActive: () => boolean) => T | Promise<T>,
): Promise<T> {
const lease = attachmentIngestionAdmission.acquire(
owner.documentId,
attachmentCount,
representationBytes,
);
const isActive = (): boolean => lease.isActive() && !owner.isDestroyed();
let removeOwnerInvalidation = (): void => undefined;
try {
removeOwnerInvalidation = owner.onInvalidated(lease.cancel);
if (!isActive()) throw new Error("The renderer document is no longer active.");
const result = await operation(isActive);
// Keep accounting through one main-loop turn so simultaneous invoke bursts
// cannot each materialize an unaccounted Base64 result before delivery.
await new Promise<void>((resolve) => setImmediate(resolve));
if (!isActive()) throw new Error("The renderer document is no longer active.");
return result;
} finally {
removeOwnerInvalidation();
lease.release();
}
}

export function registerAttachmentHandlers(): void {
let pickerActive = false;
Expand Down Expand Up @@ -42,33 +82,103 @@ export function registerAttachmentHandlers(): void {

pickerActive = true;
try {
const result = await dialog.showOpenDialog(parent, {
properties: ["openFile", "multiSelections"],
});
if (owner.isDestroyed()) throw new Error("The renderer document is no longer active.");
if (result.canceled || result.filePaths.length === 0) {
return { attachments: [], skipped: 0 };
}
const selectedPaths: string[] = [];
for (const filePath of result.filePaths.slice(0, 200)) {
if (!includeImages && isImageAttachmentPath(filePath)) continue;
selectedPaths.push(filePath);
if (selectedPaths.length === remainingSlots) break;
}
const attachments = await readPickedAttachments(selectedPaths, {
isActive: () => !owner.isDestroyed(),
maxBatchBytes: remainingInlineBytes,
});
return {
attachments,
skipped: Math.max(0, result.filePaths.length - selectedPaths.length),
};
return await runOwnedAttachmentIngestion(
owner,
remainingSlots,
attachmentIngestionRepresentationBytes(remainingInlineBytes, remainingSlots),
async (isActive) => {
const result = await dialog.showOpenDialog(parent, {
properties: ["openFile", "multiSelections"],
});
if (!isActive()) throw new Error("The renderer document is no longer active.");
if (result.canceled || result.filePaths.length === 0) {
return { attachments: [], skipped: 0 };
}
const selectedPaths: string[] = [];
for (const filePath of result.filePaths.slice(0, 200)) {
if (!includeImages && isImageAttachmentPath(filePath)) continue;
selectedPaths.push(filePath);
if (selectedPaths.length === remainingSlots) break;
}
const attachments = await readPickedAttachments(selectedPaths, {
isActive,
maxBatchBytes: remainingInlineBytes,
});
return {
attachments,
skipped: Math.max(0, result.filePaths.length - selectedPaths.length),
};
},
);
} finally {
pickerActive = false;
}
},
);

ipcMain.handle(
"aiden:attachments:dropped-read",
async (
event,
value: unknown,
remainingSlots: unknown,
includeImages: unknown,
remainingInlineBytes: unknown,
) => {
if (
!Array.isArray(value) ||
value.length === 0 ||
value.length > MAX_ATTACHMENTS_PER_MESSAGE ||
!Number.isSafeInteger(remainingSlots) ||
(remainingSlots as number) < 1 ||
(remainingSlots as number) > MAX_ATTACHMENTS_PER_MESSAGE ||
typeof includeImages !== "boolean" ||
!Number.isSafeInteger(remainingInlineBytes) ||
(remainingInlineBytes as number) < 1 ||
(remainingInlineBytes as number) > MAX_ATTACHMENT_INLINE_BYTES
) {
throw new Error("Invalid dropped attachment request.");
}
const paths = [...new Set(value)].filter(
(entry): entry is string => typeof entry === "string" && entry.length > 0,
);
if (paths.length !== value.length) throw new Error("Invalid dropped attachment request.");
const owner = rendererDocumentOwner(event, () => new Error("Untrusted attachment drop."));
const selected = paths
.filter((filePath) => includeImages || !isImageAttachmentPath(filePath))
.slice(0, remainingSlots as number);
if (selected.length === 0) return [];
return runOwnedAttachmentIngestion(
owner,
selected.length,
attachmentIngestionRepresentationBytes(remainingInlineBytes as number, selected.length),
(isActive) =>
readPickedAttachments(selected, {
isActive,
maxBatchBytes: remainingInlineBytes as number,
}),
);
},
);

ipcMain.handle(
"aiden:attachments:clipboard-read",
async (event, value: unknown, remainingSlots: unknown, remainingInlineBytes: unknown) => {
const owner = rendererDocumentOwner(event, () => new Error("Untrusted clipboard image."));
const payload = validateClipboardAttachmentPayload(
value,
remainingSlots,
remainingInlineBytes,
);
return runOwnedAttachmentIngestion(
owner,
payload.images.length,
payload.representationBytes,
(isActive) => materializeClipboardAttachments(payload, isActive),
);
},
);

ipcMain.handle("models:info", async (_event, providerId: unknown, modelIds: unknown) => {
const pid = typeof providerId === "string" ? providerId : "";
const ids = Array.isArray(modelIds)
Expand Down
11 changes: 5 additions & 6 deletions main/handlers/chats.append.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ test("append envelopes reject many extra properties without materializing Object
});

test("append admission charges encoded image representation and metadata", () => {
const data = Buffer.alloc(3).toString("base64");
const data =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFgAI/ScL2aQAAAABJRU5ErkJggg==";
const size = Buffer.byteLength(data, "base64");
const parsed = parseChatAppend(
"chat-1",
{
Expand All @@ -175,15 +177,12 @@ test("append admission charges encoded image representation and metadata", () =>
name: "a.png",
mimeType: "image/png",
kind: "image",
size: 3,
size,
data,
},
],
},
{ turnId: "turn-1", providerId: "provider", model: "model" },
);
assert.ok(
parsed.retainedBytes >=
data.length + Buffer.byteLength("providermodel", "utf8"),
);
assert.ok(parsed.retainedBytes >= data.length + Buffer.byteLength("providermodel", "utf8"));
});
Loading