Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
f0de1f4
Port version control panel work
Quicksaver Jun 18, 2026
ee9b701
Detect PR/MR base branches in source control panel
Quicksaver Jun 18, 2026
6a9d7a3
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 19, 2026
aaef573
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 19, 2026
3acf74d
Refactor source control and subagent helpers
Quicksaver Jun 19, 2026
c766991
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 19, 2026
e47d853
Port source control workspace scope updates
Quicksaver Jun 19, 2026
48378d5
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 19, 2026
52d2ff9
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 19, 2026
82b1da8
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 19, 2026
4b645ae
Refresh version control panel after upstream merge
Quicksaver Jun 19, 2026
e7e89ed
Clarify branch sync upstream handling
Quicksaver Jun 20, 2026
bc628e2
Force refresh cached SWR queries in atom runners
Quicksaver Jun 20, 2026
0225340
Show refresh spinner in source control summary
Quicksaver Jun 20, 2026
919eb7d
Fix source control panel merge behavior
Quicksaver Jun 20, 2026
54966aa
Fix source control review cleanup
Quicksaver Jun 20, 2026
7d226ba
Fix source control review edge cases
Quicksaver Jun 20, 2026
f624f85
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 21, 2026
4c95466
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 25, 2026
67f4559
Port source control service fixes
Quicksaver Jun 25, 2026
b18f677
Port VCS panel runtime labels
Quicksaver Jun 25, 2026
3818a4e
Port source control metadata errors
Quicksaver Jun 25, 2026
9b7e1ac
Harden source control metadata updates
Quicksaver Jun 25, 2026
ae654dd
Port source control polling cleanup
Quicksaver Jun 25, 2026
98ec2e8
Restore source control periodic fetch
Quicksaver Jun 25, 2026
5b834f1
Render working tree source rows eagerly
Quicksaver Jun 25, 2026
ec19c79
Add source control error cause sanitizer
Quicksaver Jun 25, 2026
942122a
Fix standalone source control branch validation
Quicksaver Jun 25, 2026
dc29076
Remove VS Code source control gating
Quicksaver Jun 25, 2026
5f3a26b
Remove obsolete webview bridge and sidebar helpers
Quicksaver Jun 25, 2026
b704651
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 25, 2026
ea9bf9b
Document version control upstream merge
Quicksaver Jun 25, 2026
6c431c1
Merge upstream main into version control panel
Quicksaver Jun 26, 2026
429b799
fix(source-control): address panel review feedback
Quicksaver Jun 26, 2026
fa3dd00
Show dirty sibling worktrees in source control panel
Quicksaver Jun 26, 2026
35ae2c7
Refresh expanded diffs on explicit local status updates
Quicksaver Jun 26, 2026
819fb9a
Stabilize source control refresh diffs
Quicksaver Jun 26, 2026
f797413
fix(source-control): address branch detail review gaps
Quicksaver Jun 26, 2026
1fe01f5
Fix source control review follow-ups
Quicksaver Jun 26, 2026
b74eb18
Fix source control review edge cases
Quicksaver Jun 26, 2026
769ce3e
Fix sibling file preview tab state
Quicksaver Jun 26, 2026
be1525e
Fix source control worktree edge cases
Quicksaver Jun 26, 2026
4abafaf
Handle renamed file diffs in source control panel
Quicksaver Jun 26, 2026
997467f
Harden source control review follow-ups
Quicksaver Jun 27, 2026
d104016
Avoid untracked fallback for rename diffs
Quicksaver Jun 27, 2026
b53331f
Add provider-backed commit author avatars
Quicksaver Jun 27, 2026
28f7aea
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 27, 2026
212e625
Move source control routing out of ChatView
Quicksaver Jun 27, 2026
d3c1b79
Fix draft source-control metadata updates
Quicksaver Jun 27, 2026
5786c1c
Harden source control metadata routing
Quicksaver Jun 27, 2026
836a188
Ignore stale source control metadata errors
Quicksaver Jun 27, 2026
f28c6bd
Harden source control review follow-ups
Quicksaver Jun 27, 2026
1f3e006
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 27, 2026
59621bc
Update source control merge snapshot
Quicksaver Jun 27, 2026
93106c7
Fetch actionable branches on initial cwd change
Quicksaver Jun 29, 2026
8e6897f
Remount source control panel when thread context changes
Quicksaver Jun 29, 2026
e25707a
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jun 29, 2026
a22812f
Refactor source control surface gating
Quicksaver Jun 29, 2026
0e8ec28
Persist source control panel state across sessions
Quicksaver Jun 30, 2026
475d5fb
Document source control panel cache and refresh behavior
Quicksaver Jun 30, 2026
a6973ff
Keep right panel surface visible when VCS hides
Quicksaver Jun 30, 2026
fb12a32
Fix cached source control panel refresh
Quicksaver Jun 30, 2026
3319d5e
Key stash panel state by stable identity
Quicksaver Jun 30, 2026
a49db42
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jul 1, 2026
933c7bb
Consolidate right-panel add-surface actions
Quicksaver Jul 1, 2026
ef3cc00
Harden right panel add-surface actions
Quicksaver Jul 1, 2026
ff137ce
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jul 3, 2026
d3a15c4
Update source control merge snapshot
Quicksaver Jul 3, 2026
a619fa3
Remove version control branch pollution
Quicksaver Jul 3, 2026
7a26626
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jul 4, 2026
ca848a8
Update source control merge snapshot
Quicksaver Jul 4, 2026
0a85ac6
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jul 5, 2026
d6f86b1
Update source control merge snapshot
Quicksaver Jul 5, 2026
37aa959
Merge remote-tracking branch 'upstream/main' into split/version-contr…
Quicksaver Jul 10, 2026
7141001
Adapt source control panel to upstream metadata
Quicksaver Jul 10, 2026
2ce693c
Merge upstream Android support
Quicksaver Jul 13, 2026
41b4d6c
test(source-control): avoid remote update race
Quicksaver Jul 13, 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
260 changes: 260 additions & 0 deletions SOURCE_CONTROL.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions apps/desktop/src/electron/ElectronMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,35 @@ describe("ElectronMenu", () => {
}).pipe(Effect.provide(TestLayer)),
);

it.effect("inserts a destructive separator when the first destructive item starts the menu", () =>
Effect.gen(function* () {
buildFromTemplateMock.mockImplementation(() => ({
popup: (options: Electron.PopupOptions) => {
options.callback?.();
},
}));

const electronMenu = yield* ElectronMenu.ElectronMenu;
yield* electronMenu.showContextMenu({
window: {} as Electron.BrowserWindow,
items: [
{ id: "delete-draft", label: "Delete draft", destructive: true },
{ id: "copy", label: "Copy" },
{ id: "delete-file", label: "Delete file", destructive: true },
],
position: Option.none(),
});

const template = buildFromTemplateMock.mock.calls[0]?.[0] as
| Electron.MenuItemConstructorOptions[]
| undefined;
assert.deepEqual(
template?.map((item) => item.type ?? item.label),
["Delete draft", "Copy", "separator", "Delete file"],
);
}).pipe(Effect.provide(TestLayer)),
);

it.effect("defers popupTemplate side effects until the returned Effect runs", () =>
Effect.gen(function* () {
const popupMock = vi.fn();
Expand Down
41 changes: 37 additions & 4 deletions apps/desktop/src/electron/ElectronMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,27 @@ export class ElectronMenu extends Context.Service<

function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextMenuItem[] {
const normalizedItems: ContextMenuItem[] = [];
let lastWasSeparator = false;

for (const sourceItem of source) {
if (typeof sourceItem.id !== "string" || typeof sourceItem.label !== "string") {
continue;
}

// Header items are decorative section labels for the web fallback only —
if (sourceItem.separator === true) {
if (normalizedItems.length === 0 || lastWasSeparator) {
continue;
}
normalizedItems.push({
id: sourceItem.id,
label: "",
separator: true,
});
lastWasSeparator = true;
continue;
}

// Header items are decorative section labels for the web fallback only -
// Electron's native menu has no equivalent affordance, so we skip them.
if (sourceItem.header === true) {
continue;
Expand All @@ -78,6 +92,7 @@ function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextM
label: sourceItem.label,
destructive: sourceItem.destructive === true,
disabled: sourceItem.disabled === true,
separator: false,
};

if (sourceItem.children) {
Expand All @@ -89,6 +104,11 @@ function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextM
}

normalizedItems.push(normalizedItem);
lastWasSeparator = false;
}

if (normalizedItems.at(-1)?.separator === true) {
normalizedItems.pop();
}

return normalizedItems;
Expand Down Expand Up @@ -134,11 +154,23 @@ export const make = Effect.gen(function* () {
): Electron.MenuItemConstructorOptions[] => {
const template: Electron.MenuItemConstructorOptions[] = [];
let hasInsertedDestructiveSeparator = false;
let lastWasSeparator = false;

for (const item of entries) {
if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) {
template.push({ type: "separator" });
hasInsertedDestructiveSeparator = true;
if (item.separator === true) {
if (template.length > 0 && !lastWasSeparator) {
template.push({ type: "separator" });
lastWasSeparator = true;
}
continue;
}

if (item.destructive && !hasInsertedDestructiveSeparator) {
if (template.length > 0 && !lastWasSeparator) {
template.push({ type: "separator" });
lastWasSeparator = true;
hasInsertedDestructiveSeparator = true;
}
Comment thread
Quicksaver marked this conversation as resolved.
}

const itemOption: Electron.MenuItemConstructorOptions = {
Expand All @@ -158,6 +190,7 @@ export const make = Effect.gen(function* () {
}

template.push(itemOption);
lastWasSeparator = false;
}

return template;
Expand Down
82 changes: 82 additions & 0 deletions apps/server/src/diagnostics/ErrorCause.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const MAX_DIAGNOSTIC_TEXT_LENGTH = 2_000;

export interface SanitizedErrorCause {
readonly tag?: string;
readonly name?: string;
readonly message?: string;
readonly detail?: string;
readonly code?: string;
readonly exitCode?: number;
readonly timeoutMs?: number;
readonly truncated?: boolean;
}

type MutableSanitizedErrorCause = {
-readonly [Key in keyof SanitizedErrorCause]?: SanitizedErrorCause[Key];
};

function truncateDiagnosticText(value: string): {
readonly text: string;
readonly truncated: boolean;
} {
if (value.length <= MAX_DIAGNOSTIC_TEXT_LENGTH) {
return { text: value, truncated: false };
}
return {
text: `${value.slice(0, MAX_DIAGNOSTIC_TEXT_LENGTH)}\n\n[truncated]`,
truncated: true,
};
}

function stringField(record: Record<PropertyKey, unknown>, key: string): string | undefined {
const value = record[key];
return typeof value === "string" && value.length > 0 ? value : undefined;
}

function numberField(record: Record<PropertyKey, unknown>, key: string): number | undefined {
const value = record[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}

function addTextField(
output: MutableSanitizedErrorCause,
key: "tag" | "name" | "message" | "detail" | "code",
value: string | undefined,
) {
if (!value) return;
const truncated = truncateDiagnosticText(value);
output[key] = truncated.text;
if (truncated.truncated) output.truncated = true;
}

export function sanitizeErrorCause(cause: unknown): SanitizedErrorCause {
if (typeof cause === "string") {
const output: MutableSanitizedErrorCause = {};
addTextField(output, "message", cause);
return output;
}

if (typeof cause === "object" && cause !== null) {
const record = cause as Record<PropertyKey, unknown>;
const output: MutableSanitizedErrorCause = {};
addTextField(output, "tag", stringField(record, "_tag"));
addTextField(output, "name", cause instanceof Error ? cause.name : stringField(record, "name"));
addTextField(
output,
"message",
cause instanceof Error ? cause.message : stringField(record, "message"),
);
addTextField(output, "detail", stringField(record, "detail"));
addTextField(output, "code", stringField(record, "code"));

const exitCode = numberField(record, "exitCode");
if (exitCode !== undefined) output.exitCode = exitCode;

const timeoutMs = numberField(record, "timeoutMs");
if (timeoutMs !== undefined) output.timeoutMs = timeoutMs;

return Object.keys(output).length > 0 ? output : { tag: "Object" };
}

return { message: "Unknown error" };
}
1 change: 1 addition & 0 deletions apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
cwd: input.cwd,
args: ["repo", "view", input.repository, "--json", "nameWithOwner,url,sshUrl"],
}).pipe(Effect.map((result) => JSON.parse(result.stdout))),
getCommitAvatarUrl: () => Effect.succeed(null),
createRepository: (input) =>
Effect.fail(
new GitHubCli.GitHubCliCommandError({
Expand Down
49 changes: 49 additions & 0 deletions apps/server/src/sourceControl/AzureDevOpsCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const AzureDevOpsRepositoryDecodeOperation = Schema.Literals([
"getRepositoryCloneUrls",
"getDefaultBranch",
"createRepository",
"getCommitAvatarUrl",
]);

export class AzureDevOpsRepositoryDecodeError extends Schema.TaggedErrorClass<AzureDevOpsRepositoryDecodeError>()(
Expand Down Expand Up @@ -207,6 +208,8 @@ export class AzureDevOpsCli extends Context.Service<
readonly cwd: string;
readonly headSelector: string;
readonly source?: SourceControlProvider.SourceControlRefSelector;
readonly repository?: string;
readonly project?: string;
readonly state: "open" | "closed" | "merged" | "all";
readonly limit?: number;
}) => Effect.Effect<ReadonlyArray<NormalizedAzureDevOpsPullRequestRecord>, AzureDevOpsCliError>;
Expand All @@ -221,6 +224,13 @@ export class AzureDevOpsCli extends Context.Service<
readonly repository: string;
}) => Effect.Effect<AzureDevOpsRepositoryCloneUrls, AzureDevOpsCliError>;

readonly getCommitAvatarUrl: (input: {
readonly cwd: string;
readonly repository: string;
readonly sha: string;
readonly project?: string;
}) => Effect.Effect<string | null, AzureDevOpsCliError>;

readonly createRepository: (input: {
readonly cwd: string;
readonly repository: string;
Expand Down Expand Up @@ -281,6 +291,14 @@ const RawAzureDevOpsRepositorySchema = Schema.Struct({
defaultBranch: Schema.optional(Schema.NullOr(Schema.String)),
});

const RawAzureDevOpsCommitSchema = Schema.Struct({
author: Schema.optional(
Schema.Struct({
imageUrl: Schema.optional(Schema.NullOr(Schema.String)),
}),
),
});

function normalizeDefaultBranch(value: string | null | undefined): string | null {
const trimmed = value?.trim().replace(/^refs\/heads\//, "") ?? "";
return trimmed.length > 0 ? trimmed : null;
Expand Down Expand Up @@ -377,6 +395,8 @@ export const make = Effect.gen(function* () {
"list",
"--detect",
"true",
...(input.repository ? ["--repository", input.repository] : []),
...(input.project ? ["--project", input.project] : []),
"--source-branch",
SourceControlProvider.sourceBranch(input),
"--status",
Expand Down Expand Up @@ -458,6 +478,35 @@ export const make = Effect.gen(function* () {
),
Effect.map(normalizeRepositoryCloneUrls),
),
getCommitAvatarUrl: (input) =>
executeJson({
cwd: input.cwd,
args: [
"devops",
"invoke",
"--detect",
"true",
"--area",
"git",
"--resource",
"commits",
"--route-parameters",
...(input.project ? [`project=${input.project}`] : []),
`repositoryId=${input.repository}`,
`commitId=${input.sha}`,
"--api-version",
"7.1-preview",
],
}).pipe(
Effect.map((result) => result.stdout.trim()),
Effect.flatMap((raw) =>
decodeAzureDevOpsJson(raw, RawAzureDevOpsCommitSchema, "getCommitAvatarUrl", input.cwd),
),
Effect.map((commit) => {
const avatarUrl = commit.author?.imageUrl?.trim();
return avatarUrl && avatarUrl.length > 0 ? avatarUrl : null;
}),
),
createRepository: (input) => {
const repository = parseRepositorySpecifier(input.repository);
// Azure Repos access is governed by project/organization permissions.
Expand Down
Loading
Loading