@junctionpanel/server 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/server/client/daemon-client.d.ts +39 -4
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +83 -3
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
  6. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  7. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  8. package/dist/server/server/agent/providers/codex-app-server-agent.js +1 -39
  9. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  10. package/dist/server/server/agent/providers/gemini-agent.d.ts +4 -1
  11. package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
  12. package/dist/server/server/agent/providers/gemini-agent.js +36 -8
  13. package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
  14. package/dist/server/server/agent/providers/image-attachments.d.ts +8 -0
  15. package/dist/server/server/agent/providers/image-attachments.d.ts.map +1 -0
  16. package/dist/server/server/agent/providers/image-attachments.js +47 -0
  17. package/dist/server/server/agent/providers/image-attachments.js.map +1 -0
  18. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +3 -0
  19. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  20. package/dist/server/server/daemon-doctor.d.ts +39 -0
  21. package/dist/server/server/daemon-doctor.d.ts.map +1 -0
  22. package/dist/server/server/daemon-doctor.js +260 -0
  23. package/dist/server/server/daemon-doctor.js.map +1 -0
  24. package/dist/server/server/daemon-provider-settings.d.ts +42 -0
  25. package/dist/server/server/daemon-provider-settings.d.ts.map +1 -0
  26. package/dist/server/server/daemon-provider-settings.js +207 -0
  27. package/dist/server/server/daemon-provider-settings.js.map +1 -0
  28. package/dist/server/server/file-explorer/service.d.ts +4 -2
  29. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  30. package/dist/server/server/file-explorer/service.js +104 -2
  31. package/dist/server/server/file-explorer/service.js.map +1 -1
  32. package/dist/server/server/persisted-config.d.ts +24 -24
  33. package/dist/server/server/session.d.ts +10 -1
  34. package/dist/server/server/session.d.ts.map +1 -1
  35. package/dist/server/server/session.js +421 -60
  36. package/dist/server/server/session.js.map +1 -1
  37. package/dist/server/server/worktree-bootstrap.d.ts +1 -0
  38. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  39. package/dist/server/server/worktree-bootstrap.js +4 -0
  40. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  41. package/dist/server/shared/messages.d.ts +3673 -22
  42. package/dist/server/shared/messages.d.ts.map +1 -1
  43. package/dist/server/shared/messages.js +151 -0
  44. package/dist/server/shared/messages.js.map +1 -1
  45. package/dist/server/utils/checkout-git.d.ts +23 -4
  46. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  47. package/dist/server/utils/checkout-git.js +298 -79
  48. package/dist/server/utils/checkout-git.js.map +1 -1
  49. package/dist/server/utils/directory-suggestions.d.ts +4 -0
  50. package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
  51. package/dist/server/utils/directory-suggestions.js +83 -5
  52. package/dist/server/utils/directory-suggestions.js.map +1 -1
  53. package/dist/server/utils/workspace-ref-files.d.ts +31 -0
  54. package/dist/server/utils/workspace-ref-files.d.ts.map +1 -0
  55. package/dist/server/utils/workspace-ref-files.js +207 -0
  56. package/dist/server/utils/workspace-ref-files.js.map +1 -0
  57. package/dist/server/utils/worktree.d.ts +6 -3
  58. package/dist/server/utils/worktree.d.ts.map +1 -1
  59. package/dist/server/utils/worktree.js +46 -45
  60. package/dist/server/utils/worktree.js.map +1 -1
  61. package/package.json +2 -2
@@ -1,15 +1,16 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
- import { watch } from 'node:fs';
2
+ import { existsSync, watch } from 'node:fs';
3
3
  import { exec } from 'child_process';
4
4
  import { promisify } from 'util';
5
- import { resolve, sep, basename, dirname } from 'path';
6
- import { homedir } from 'node:os';
5
+ import { resolve, sep, basename, dirname, parse as parsePath } from 'path';
6
+ import { homedir, hostname } from 'node:os';
7
7
  import { z } from 'zod';
8
8
  import { serializeAgentStreamEvent, } from './messages.js';
9
9
  import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from '../shared/binary-mux.js';
10
10
  import { buildConfigOverrides, buildSessionConfig, extractTimestamps, extractTimelineSnapshot, } from './persistence-hooks.js';
11
11
  import { experimental_createMCPClient } from 'ai';
12
12
  import { buildProviderRegistry } from './agent/provider-registry.js';
13
+ import { applyProviderEnv, } from './agent/provider-launch-config.js';
13
14
  import { scheduleAgentMetadataGeneration } from './agent/agent-metadata-generator.js';
14
15
  import { resolveEffectiveThinkingOptionId, toAgentPayload } from './agent/agent-projections.js';
15
16
  import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from './agent/timeline-append.js';
@@ -20,14 +21,18 @@ import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from
20
21
  import { slugify, validateBranchSlug, listJunctionWorktrees, deleteJunctionWorktree, isJunctionOwnedWorktreeCwd, resolveJunctionWorktreeRootForCwd, createInRepoWorktree, restoreInRepoWorktree, } from '../utils/worktree.js';
21
22
  import { readJunctionWorktreeMetadata } from '../utils/worktree-metadata.js';
22
23
  import { runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
23
- import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestFailureLogs, getPullRequestStatus, mergePullRequest, resolveBaseRef, } from '../utils/checkout-git.js';
24
+ import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestFailureLogs, getPullRequestStatus, listGitRemotes, mergePullRequest, resolveBaseRef, } from '../utils/checkout-git.js';
24
25
  import { getProjectIcon } from '../utils/project-icon.js';
25
26
  import { expandTilde } from '../utils/path.js';
26
- import { searchHomeDirectories, searchWorkspaceEntries, searchGitRepositories, checkIsGitRepo } from '../utils/directory-suggestions.js';
27
+ import { searchHomeDirectories, searchWorkspaceEntries, searchWorkspaceEntriesAtGitRef, searchGitRepositories, checkIsGitRepo, } from '../utils/directory-suggestions.js';
27
28
  import { cloneRepository } from '../utils/git-clone.js';
28
29
  import { initRepository } from '../utils/git-init.js';
29
30
  import { resolveClientMessageId } from './client-message-id.js';
30
31
  import { deriveProjectGroupingKey, deriveProjectGroupingName } from '../shared/project-grouping.js';
32
+ import { resolveDaemonVersion } from './daemon-version.js';
33
+ import { runDaemonDoctor } from './daemon-doctor.js';
34
+ import { MANAGED_DAEMON_PROVIDERS, autoRouteProviderExecutable, loadDaemonProviderSettings, saveDaemonProviderExecutablePath, } from './daemon-provider-settings.js';
35
+ import { loadPersistedConfig } from './persisted-config.js';
31
36
  const execAsync = promisify(exec);
32
37
  const READ_ONLY_GIT_ENV = {
33
38
  ...process.env,
@@ -50,6 +55,7 @@ class SessionRequestError extends Error {
50
55
  }
51
56
  }
52
57
  const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
58
+ const SAFE_GIT_REMOTE_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
53
59
  function coerceAgentProvider(logger, value, agentId) {
54
60
  if (isValidAgentProvider(value)) {
55
61
  return value;
@@ -800,9 +806,15 @@ export class Session {
800
806
  case 'branch_suggestions_request':
801
807
  await this.handleBranchSuggestionsRequest(msg);
802
808
  break;
809
+ case 'git_remotes_request':
810
+ await this.handleGitRemotesRequest(msg);
811
+ break;
803
812
  case 'directory_suggestions_request':
804
813
  await this.handleDirectorySuggestionsRequest(msg);
805
814
  break;
815
+ case 'workspace_file_suggestions_request':
816
+ await this.handleWorkspaceFileSuggestionsRequest(msg);
817
+ break;
806
818
  case 'git_clone_request':
807
819
  await this.handleGitCloneRequest(msg);
808
820
  break;
@@ -863,6 +875,18 @@ export class Session {
863
875
  case 'list_available_providers_request':
864
876
  await this.handleListAvailableProvidersRequest(msg);
865
877
  break;
878
+ case 'run_daemon_doctor_request':
879
+ await this.handleRunDaemonDoctorRequest(msg);
880
+ break;
881
+ case 'get_daemon_provider_settings_request':
882
+ await this.handleGetDaemonProviderSettingsRequest(msg);
883
+ break;
884
+ case 'update_daemon_provider_settings_request':
885
+ await this.handleUpdateDaemonProviderSettingsRequest(msg);
886
+ break;
887
+ case 'auto_route_provider_request':
888
+ await this.handleAutoRouteProviderRequest(msg);
889
+ break;
866
890
  case 'clear_agent_attention':
867
891
  await this.handleClearAgentAttention(msg.agentId);
868
892
  break;
@@ -1150,19 +1174,43 @@ export class Session {
1150
1174
  if (!record) {
1151
1175
  throw new Error(`Agent not found: ${agentId}`);
1152
1176
  }
1177
+ const allRecords = await this.agentStorage.list();
1178
+ const siblingRecords = record.archivedWorktree?.cleanupState === 'deleted' && record.archivedWorktree
1179
+ ? allRecords.filter((candidate) => {
1180
+ if (candidate.id === record.id || !candidate.archivedAt) {
1181
+ return false;
1182
+ }
1183
+ return candidate.cwd === record.cwd;
1184
+ })
1185
+ : [];
1153
1186
  let nextRecord = {
1154
1187
  ...record,
1155
1188
  archivedAt: null,
1156
1189
  };
1157
1190
  let restoredWorktree = null;
1158
1191
  if (record.archivedWorktree?.cleanupState === 'deleted') {
1159
- restoredWorktree = await restoreInRepoWorktree({
1160
- repoRoot: record.archivedWorktree.repoRoot,
1161
- baseBranch: record.archivedWorktree.baseBranch,
1162
- branchName: record.archivedWorktree.branchName,
1163
- worktreeSlug: record.archivedWorktree.worktreeSlug,
1164
- runSetup: false,
1165
- });
1192
+ try {
1193
+ restoredWorktree = await restoreInRepoWorktree({
1194
+ repoRoot: record.archivedWorktree.repoRoot,
1195
+ baseBranch: record.archivedWorktree.baseBranch,
1196
+ branchName: record.archivedWorktree.branchName,
1197
+ worktreeSlug: record.archivedWorktree.worktreeSlug,
1198
+ runSetup: false,
1199
+ });
1200
+ }
1201
+ catch (error) {
1202
+ const message = error instanceof Error ? error.message : String(error);
1203
+ const restoredPath = record.archivedWorktree.originalCwd;
1204
+ if (!message.includes('Worktree path already exists') || !existsSync(restoredPath)) {
1205
+ throw error;
1206
+ }
1207
+ restoredWorktree = {
1208
+ branchName: record.archivedWorktree.branchName,
1209
+ worktreePath: restoredPath,
1210
+ baseBranch: record.archivedWorktree.baseBranch,
1211
+ workspaceName: basename(restoredPath),
1212
+ };
1213
+ }
1166
1214
  nextRecord = {
1167
1215
  ...nextRecord,
1168
1216
  cwd: restoredWorktree.worktreePath,
@@ -1174,13 +1222,34 @@ export class Session {
1174
1222
  },
1175
1223
  };
1176
1224
  }
1177
- await this.agentStorage.upsert(nextRecord);
1178
- const liveAgent = this.agentManager.getAgent(agentId);
1179
- if (liveAgent) {
1180
- this.agentManager.notifyAgentState(agentId);
1181
- }
1182
- else {
1183
- await this.forwardStoredAgentRecordUpdate(nextRecord);
1225
+ const recordsToRestore = [record, ...siblingRecords].map((candidate) => {
1226
+ const nextArchivedWorktree = restoredWorktree && candidate.archivedWorktree
1227
+ ? {
1228
+ ...candidate.archivedWorktree,
1229
+ originalCwd: restoredWorktree.worktreePath,
1230
+ cleanupState: 'active',
1231
+ cleanedUpAt: null,
1232
+ }
1233
+ : candidate.archivedWorktree;
1234
+ return {
1235
+ ...candidate,
1236
+ archivedAt: null,
1237
+ cwd: restoredWorktree ? restoredWorktree.worktreePath : candidate.cwd,
1238
+ archivedWorktree: nextArchivedWorktree,
1239
+ };
1240
+ });
1241
+ for (const restoredRecord of recordsToRestore) {
1242
+ await this.agentStorage.upsert(restoredRecord);
1243
+ const liveAgent = this.agentManager.getAgent(restoredRecord.id);
1244
+ if (liveAgent) {
1245
+ this.agentManager.notifyAgentState(restoredRecord.id);
1246
+ }
1247
+ else {
1248
+ await this.forwardStoredAgentRecordUpdate(restoredRecord);
1249
+ }
1250
+ if (restoredRecord.id === agentId) {
1251
+ nextRecord = restoredRecord;
1252
+ }
1184
1253
  }
1185
1254
  this.emit({
1186
1255
  type: 'agent_unarchived',
@@ -1363,7 +1432,7 @@ export class Session {
1363
1432
  * Handle create agent request
1364
1433
  */
1365
1434
  async handleCreateAgentRequest(msg) {
1366
- const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, bootstrapSetupOverride, images, labels, } = msg;
1435
+ const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, bootstrapSetupOverride, generalPreferencesApplied, images, labels, } = msg;
1367
1436
  this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ''}`);
1368
1437
  try {
1369
1438
  const { sessionConfig, worktreeConfig, autoWorkspaceName } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
@@ -1387,20 +1456,23 @@ export class Session {
1387
1456
  },
1388
1457
  });
1389
1458
  }
1390
- const trimmedPrompt = initialPrompt?.trim();
1459
+ const trimmedPrompt = initialPrompt?.trim() ?? '';
1460
+ const hasInitialMessage = trimmedPrompt.length > 0 || Boolean(images?.length);
1391
1461
  const runInitialPrompt = async () => {
1392
- if (!trimmedPrompt) {
1462
+ if (!hasInitialMessage) {
1393
1463
  return;
1394
1464
  }
1395
- scheduleAgentMetadataGeneration({
1396
- agentManager: this.agentManager,
1397
- agentId: snapshot.id,
1398
- cwd: snapshot.cwd,
1399
- initialPrompt: trimmedPrompt,
1400
- explicitTitle: snapshot.config.title,
1401
- junctionHome: this.junctionHome,
1402
- logger: this.sessionLogger,
1403
- });
1465
+ if (trimmedPrompt.length > 0) {
1466
+ scheduleAgentMetadataGeneration({
1467
+ agentManager: this.agentManager,
1468
+ agentId: snapshot.id,
1469
+ cwd: snapshot.cwd,
1470
+ initialPrompt: trimmedPrompt,
1471
+ explicitTitle: snapshot.config.title,
1472
+ junctionHome: this.junctionHome,
1473
+ logger: this.sessionLogger,
1474
+ });
1475
+ }
1404
1476
  await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, outputSchema ? { outputSchema } : undefined);
1405
1477
  };
1406
1478
  const handleInitialPromptError = (promptError) => {
@@ -1415,7 +1487,7 @@ export class Session {
1415
1487
  },
1416
1488
  });
1417
1489
  };
1418
- if (trimmedPrompt && !worktreeConfig) {
1490
+ if (hasInitialMessage && !worktreeConfig) {
1419
1491
  void runInitialPrompt().catch(handleInitialPromptError);
1420
1492
  }
1421
1493
  if (worktreeConfig) {
@@ -1423,6 +1495,7 @@ export class Session {
1423
1495
  agentId: snapshot.id,
1424
1496
  worktree: worktreeConfig,
1425
1497
  setupOverride: bootstrapSetupOverride,
1498
+ generalPreferencesApplied,
1426
1499
  terminalManager: this.terminalManager,
1427
1500
  appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1428
1501
  agentManager: this.agentManager,
@@ -1435,7 +1508,7 @@ export class Session {
1435
1508
  item,
1436
1509
  }),
1437
1510
  onSetupSettled: async (result) => {
1438
- if (!trimmedPrompt || result.setupStatus === 'failed') {
1511
+ if (!hasInitialMessage || result.setupStatus === 'failed') {
1439
1512
  return;
1440
1513
  }
1441
1514
  await runInitialPrompt().catch(handleInitialPromptError);
@@ -1587,7 +1660,7 @@ export class Session {
1587
1660
  this.handleAgentRunError(agentId, error, 'Failed to cancel running agent on request');
1588
1661
  }
1589
1662
  }
1590
- async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
1663
+ async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, labels) {
1591
1664
  const cwd = expandTilde(config.cwd);
1592
1665
  const normalized = this.normalizeGitOptions(gitOptions, legacyWorktreeName);
1593
1666
  let repoRoot;
@@ -1608,7 +1681,26 @@ export class Session {
1608
1681
  catch {
1609
1682
  throw new Error('Selected project must be a git repository. Junction always creates a new worktree in .junction/.');
1610
1683
  }
1611
- const baseBranch = normalized?.baseBranch ?? (await resolveBaseRef(repoRoot));
1684
+ if (!normalized) {
1685
+ const ownership = await isJunctionOwnedWorktreeCwd(cwd, {
1686
+ junctionHome: this.junctionHome,
1687
+ });
1688
+ if (ownership.allowed) {
1689
+ const resolvedWorktree = await resolveJunctionWorktreeRootForCwd(cwd, {
1690
+ junctionHome: this.junctionHome,
1691
+ });
1692
+ const workspaceName = labels?.['junction:workspace']
1693
+ ?? (resolvedWorktree ? basename(resolvedWorktree.worktreePath) : basename(cwd));
1694
+ return {
1695
+ sessionConfig: {
1696
+ ...config,
1697
+ cwd,
1698
+ },
1699
+ autoWorkspaceName: workspaceName,
1700
+ };
1701
+ }
1702
+ }
1703
+ const baseBranch = normalized?.baseBranch ?? (await resolveBaseRef(repoRoot, { remoteName: normalized?.remoteName }));
1612
1704
  if (!baseBranch) {
1613
1705
  throw new Error('Unable to determine a base branch for worktree creation');
1614
1706
  }
@@ -1616,6 +1708,7 @@ export class Session {
1616
1708
  const createdWorktree = await createInRepoWorktree({
1617
1709
  repoRoot,
1618
1710
  baseBranch,
1711
+ remoteName: normalized?.remoteName,
1619
1712
  runSetup: false,
1620
1713
  });
1621
1714
  return {
@@ -1684,6 +1777,153 @@ export class Session {
1684
1777
  });
1685
1778
  }
1686
1779
  }
1780
+ resolveDaemonVersionSafe() {
1781
+ try {
1782
+ return resolveDaemonVersion(import.meta.url);
1783
+ }
1784
+ catch {
1785
+ return null;
1786
+ }
1787
+ }
1788
+ toDaemonMetadata(input) {
1789
+ const homeDir = input?.homeDir ?? homedir();
1790
+ const rootDir = input?.rootDir ?? parsePath(homeDir).root;
1791
+ return {
1792
+ hostname: hostname(),
1793
+ version: this.resolveDaemonVersionSafe(),
1794
+ platform: process.platform,
1795
+ homeDir,
1796
+ rootDir,
1797
+ };
1798
+ }
1799
+ async handleRunDaemonDoctorRequest(msg) {
1800
+ try {
1801
+ const result = await runDaemonDoctor(this.junctionHome, this.sessionLogger);
1802
+ this.emit({
1803
+ type: 'run_daemon_doctor_response',
1804
+ payload: {
1805
+ daemon: result.daemon,
1806
+ summary: result.summary,
1807
+ checks: result.checks,
1808
+ ranAt: result.ranAt,
1809
+ error: null,
1810
+ requestId: msg.requestId,
1811
+ },
1812
+ });
1813
+ }
1814
+ catch (error) {
1815
+ this.sessionLogger.error({ err: error }, 'Failed to run daemon doctor');
1816
+ const snapshot = loadDaemonProviderSettings(this.junctionHome);
1817
+ this.emit({
1818
+ type: 'run_daemon_doctor_response',
1819
+ payload: {
1820
+ daemon: this.toDaemonMetadata(snapshot),
1821
+ summary: 'fail',
1822
+ checks: [],
1823
+ ranAt: new Date().toISOString(),
1824
+ error: error instanceof Error ? error.message : String(error),
1825
+ requestId: msg.requestId,
1826
+ },
1827
+ });
1828
+ }
1829
+ }
1830
+ async handleGetDaemonProviderSettingsRequest(msg) {
1831
+ try {
1832
+ const snapshot = loadDaemonProviderSettings(this.junctionHome);
1833
+ this.emit({
1834
+ type: 'get_daemon_provider_settings_response',
1835
+ payload: {
1836
+ daemon: this.toDaemonMetadata(snapshot),
1837
+ providers: MANAGED_DAEMON_PROVIDERS.map((provider) => snapshot.providers[provider]),
1838
+ error: null,
1839
+ requestId: msg.requestId,
1840
+ },
1841
+ });
1842
+ }
1843
+ catch (error) {
1844
+ this.sessionLogger.error({ err: error }, 'Failed to load daemon provider settings');
1845
+ this.emit({
1846
+ type: 'get_daemon_provider_settings_response',
1847
+ payload: {
1848
+ daemon: this.toDaemonMetadata(),
1849
+ providers: [],
1850
+ error: error instanceof Error ? error.message : String(error),
1851
+ requestId: msg.requestId,
1852
+ },
1853
+ });
1854
+ }
1855
+ }
1856
+ async handleUpdateDaemonProviderSettingsRequest(msg) {
1857
+ try {
1858
+ const snapshot = saveDaemonProviderExecutablePath({
1859
+ junctionHome: this.junctionHome,
1860
+ provider: msg.provider,
1861
+ executablePath: msg.executablePath,
1862
+ });
1863
+ this.emit({
1864
+ type: 'update_daemon_provider_settings_response',
1865
+ payload: {
1866
+ daemon: this.toDaemonMetadata(snapshot),
1867
+ provider: snapshot.providers[msg.provider],
1868
+ error: null,
1869
+ requestId: msg.requestId,
1870
+ },
1871
+ });
1872
+ await this.handleRestartServerRequest(msg.requestId, 'settings_update');
1873
+ }
1874
+ catch (error) {
1875
+ this.sessionLogger.error({ err: error, provider: msg.provider }, 'Failed to update daemon provider settings');
1876
+ this.emit({
1877
+ type: 'update_daemon_provider_settings_response',
1878
+ payload: {
1879
+ daemon: this.toDaemonMetadata(),
1880
+ provider: null,
1881
+ error: error instanceof Error ? error.message : String(error),
1882
+ requestId: msg.requestId,
1883
+ },
1884
+ });
1885
+ }
1886
+ }
1887
+ async handleAutoRouteProviderRequest(msg) {
1888
+ try {
1889
+ const config = loadPersistedConfig(this.junctionHome);
1890
+ const env = applyProviderEnv(process.env, config.agents?.providers?.[msg.provider]);
1891
+ const executablePath = autoRouteProviderExecutable(msg.provider, {
1892
+ env,
1893
+ platform: process.platform,
1894
+ });
1895
+ if (!executablePath) {
1896
+ throw new SessionRequestError('provider_not_found', `Could not automatically locate ${msg.provider} on this daemon.`);
1897
+ }
1898
+ const snapshot = saveDaemonProviderExecutablePath({
1899
+ junctionHome: this.junctionHome,
1900
+ provider: msg.provider,
1901
+ executablePath,
1902
+ });
1903
+ this.emit({
1904
+ type: 'auto_route_provider_response',
1905
+ payload: {
1906
+ daemon: this.toDaemonMetadata(snapshot),
1907
+ provider: snapshot.providers[msg.provider],
1908
+ error: null,
1909
+ requestId: msg.requestId,
1910
+ },
1911
+ });
1912
+ await this.handleRestartServerRequest(msg.requestId, 'settings_update');
1913
+ }
1914
+ catch (error) {
1915
+ this.sessionLogger.error({ err: error, provider: msg.provider }, 'Failed to auto-route provider executable');
1916
+ this.emit({
1917
+ type: 'auto_route_provider_response',
1918
+ payload: {
1919
+ daemon: this.toDaemonMetadata(),
1920
+ provider: null,
1921
+ error: error instanceof Error ? error.message : String(error),
1922
+ requestId: msg.requestId,
1923
+ },
1924
+ });
1925
+ }
1926
+ }
1687
1927
  normalizeGitOptions(gitOptions, legacyWorktreeName) {
1688
1928
  const fallbackOptions = legacyWorktreeName
1689
1929
  ? {
@@ -1698,6 +1938,7 @@ export class Session {
1698
1938
  return null;
1699
1939
  }
1700
1940
  const baseBranch = merged.baseBranch?.trim() || undefined;
1941
+ const remoteName = merged.remoteName?.trim() || undefined;
1701
1942
  const createWorktree = Boolean(merged.createWorktree);
1702
1943
  const createNewBranch = Boolean(merged.createNewBranch);
1703
1944
  const normalizedBranchName = merged.newBranchName ? slugify(merged.newBranchName) : undefined;
@@ -1710,6 +1951,9 @@ export class Session {
1710
1951
  if (baseBranch) {
1711
1952
  this.assertSafeGitRef(baseBranch, 'base branch');
1712
1953
  }
1954
+ if (remoteName) {
1955
+ this.assertSafeRemoteName(remoteName);
1956
+ }
1713
1957
  if (createWorktree && !baseBranch) {
1714
1958
  throw new Error('Base branch is required when creating a worktree');
1715
1959
  }
@@ -1733,6 +1977,7 @@ export class Session {
1733
1977
  }
1734
1978
  return {
1735
1979
  baseBranch,
1980
+ remoteName,
1736
1981
  createNewBranch,
1737
1982
  newBranchName: normalizedBranchName,
1738
1983
  createWorktree,
@@ -1744,6 +1989,11 @@ export class Session {
1744
1989
  throw new Error(`Invalid ${label}: ${ref}`);
1745
1990
  }
1746
1991
  }
1992
+ assertSafeRemoteName(remoteName) {
1993
+ if (!SAFE_GIT_REMOTE_NAME_PATTERN.test(remoteName)) {
1994
+ throw new Error(`Invalid remote name: ${remoteName}`);
1995
+ }
1996
+ }
1747
1997
  toCheckoutError(error) {
1748
1998
  if (error instanceof NotGitRepoError) {
1749
1999
  return { code: 'NOT_GIT_REPO', message: error.message };
@@ -2201,6 +2451,10 @@ export class Session {
2201
2451
  const { cwd, branchName, requestId } = msg;
2202
2452
  try {
2203
2453
  const resolvedCwd = expandTilde(cwd);
2454
+ const remoteName = msg.remoteName?.trim() || undefined;
2455
+ if (remoteName) {
2456
+ this.assertSafeRemoteName(remoteName);
2457
+ }
2204
2458
  // Try local branch first
2205
2459
  try {
2206
2460
  await execAsync(`git rev-parse --verify ${branchName}`, {
@@ -2222,26 +2476,45 @@ export class Session {
2222
2476
  catch {
2223
2477
  // Local branch doesn't exist, try remote
2224
2478
  }
2225
- // Try remote branch (origin/{branchName})
2226
- try {
2227
- await execAsync(`git rev-parse --verify origin/${branchName}`, {
2228
- cwd: resolvedCwd,
2229
- env: READ_ONLY_GIT_ENV,
2230
- });
2231
- this.emit({
2232
- type: 'validate_branch_response',
2233
- payload: {
2234
- exists: true,
2235
- resolvedRef: `origin/${branchName}`,
2236
- isRemote: true,
2237
- error: null,
2238
- requestId,
2239
- },
2240
- });
2241
- return;
2242
- }
2243
- catch {
2244
- // Remote branch doesn't exist either
2479
+ const { stdout: remoteStdout } = await execAsync('git remote', {
2480
+ cwd: resolvedCwd,
2481
+ env: READ_ONLY_GIT_ENV,
2482
+ });
2483
+ const configuredRemotes = remoteStdout
2484
+ .split('\n')
2485
+ .map((line) => line.trim())
2486
+ .filter((line) => line.length > 0)
2487
+ .sort((left, right) => left.localeCompare(right));
2488
+ const remoteCandidates = remoteName
2489
+ ? [remoteName, 'origin', ...configuredRemotes]
2490
+ : ['origin', ...configuredRemotes];
2491
+ const seen = new Set();
2492
+ for (const candidateRemote of remoteCandidates) {
2493
+ const trimmedRemote = candidateRemote.trim();
2494
+ if (!trimmedRemote || seen.has(trimmedRemote)) {
2495
+ continue;
2496
+ }
2497
+ seen.add(trimmedRemote);
2498
+ try {
2499
+ await execAsync(`git rev-parse --verify ${trimmedRemote}/${branchName}`, {
2500
+ cwd: resolvedCwd,
2501
+ env: READ_ONLY_GIT_ENV,
2502
+ });
2503
+ this.emit({
2504
+ type: 'validate_branch_response',
2505
+ payload: {
2506
+ exists: true,
2507
+ resolvedRef: `${trimmedRemote}/${branchName}`,
2508
+ isRemote: true,
2509
+ error: null,
2510
+ requestId,
2511
+ },
2512
+ });
2513
+ return;
2514
+ }
2515
+ catch {
2516
+ // try next remote
2517
+ }
2245
2518
  }
2246
2519
  // Branch not found anywhere
2247
2520
  this.emit({
@@ -2272,7 +2545,11 @@ export class Session {
2272
2545
  const { cwd, query, limit, requestId } = msg;
2273
2546
  try {
2274
2547
  const resolvedCwd = expandTilde(cwd);
2275
- const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2548
+ const branches = await listBranchSuggestions(resolvedCwd, {
2549
+ query,
2550
+ limit,
2551
+ remoteName: msg.remoteName,
2552
+ });
2276
2553
  this.emit({
2277
2554
  type: 'branch_suggestions_response',
2278
2555
  payload: {
@@ -2293,6 +2570,30 @@ export class Session {
2293
2570
  });
2294
2571
  }
2295
2572
  }
2573
+ async handleGitRemotesRequest(msg) {
2574
+ const { cwd, requestId } = msg;
2575
+ try {
2576
+ const remotes = await listGitRemotes(expandTilde(cwd));
2577
+ this.emit({
2578
+ type: 'git_remotes_response',
2579
+ payload: {
2580
+ remotes,
2581
+ error: null,
2582
+ requestId,
2583
+ },
2584
+ });
2585
+ }
2586
+ catch (error) {
2587
+ this.emit({
2588
+ type: 'git_remotes_response',
2589
+ payload: {
2590
+ remotes: [],
2591
+ error: error instanceof Error ? error.message : String(error),
2592
+ requestId,
2593
+ },
2594
+ });
2595
+ }
2596
+ }
2296
2597
  async handleDirectorySuggestionsRequest(msg) {
2297
2598
  const { query, limit, requestId, cwd, includeFiles, includeDirectories, onlyGitRepos } = msg;
2298
2599
  try {
@@ -2351,6 +2652,55 @@ export class Session {
2351
2652
  });
2352
2653
  }
2353
2654
  }
2655
+ async handleWorkspaceFileSuggestionsRequest(msg) {
2656
+ const { cwd, query, limit, requestId, includeDirectories, includeFiles, ref } = msg;
2657
+ try {
2658
+ const workspaceCwd = expandTilde(cwd);
2659
+ if (ref) {
2660
+ this.assertSafeGitRef(ref, 'workspace file ref');
2661
+ }
2662
+ const entries = ref
2663
+ ? await searchWorkspaceEntriesAtGitRef({
2664
+ cwd: workspaceCwd,
2665
+ ref,
2666
+ query,
2667
+ limit,
2668
+ includeDirectories,
2669
+ includeFiles,
2670
+ })
2671
+ : await searchWorkspaceEntries({
2672
+ cwd: workspaceCwd,
2673
+ query,
2674
+ limit,
2675
+ includeDirectories,
2676
+ includeFiles,
2677
+ });
2678
+ this.emit({
2679
+ type: 'workspace_file_suggestions_response',
2680
+ payload: {
2681
+ cwd,
2682
+ query,
2683
+ ref: ref ?? null,
2684
+ entries,
2685
+ error: null,
2686
+ requestId,
2687
+ },
2688
+ });
2689
+ }
2690
+ catch (error) {
2691
+ this.emit({
2692
+ type: 'workspace_file_suggestions_response',
2693
+ payload: {
2694
+ cwd,
2695
+ query,
2696
+ ref: ref ?? null,
2697
+ entries: [],
2698
+ error: error instanceof Error ? error.message : String(error),
2699
+ requestId,
2700
+ },
2701
+ });
2702
+ }
2703
+ }
2354
2704
  async handleGitCloneRequest(msg) {
2355
2705
  const { url, targetDirectory, requestId } = msg;
2356
2706
  try {
@@ -2792,6 +3142,7 @@ export class Session {
2792
3142
  }
2793
3143
  await mergeFromBase(cwd, {
2794
3144
  baseRef: msg.baseRef,
3145
+ remoteName: msg.remoteName,
2795
3146
  requireCleanTarget: msg.requireCleanTarget ?? true,
2796
3147
  });
2797
3148
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
@@ -2820,7 +3171,7 @@ export class Session {
2820
3171
  async handleCheckoutPushRequest(msg) {
2821
3172
  const { cwd, requestId } = msg;
2822
3173
  try {
2823
- await pushCurrentBranch(cwd);
3174
+ await pushCurrentBranch(cwd, { remoteName: msg.remoteName });
2824
3175
  this.emit({
2825
3176
  type: 'checkout_push_response',
2826
3177
  payload: {
@@ -2859,6 +3210,7 @@ export class Session {
2859
3210
  title,
2860
3211
  body,
2861
3212
  base: msg.baseRef,
3213
+ remoteName: msg.remoteName,
2862
3214
  });
2863
3215
  this.emit({
2864
3216
  type: 'checkout_pr_create_response',
@@ -2887,7 +3239,7 @@ export class Session {
2887
3239
  async handleCheckoutPrStatusRequest(msg) {
2888
3240
  const { cwd, requestId } = msg;
2889
3241
  try {
2890
- const prStatus = await getPullRequestStatus(cwd);
3242
+ const prStatus = await getPullRequestStatus(cwd, { remoteName: msg.remoteName });
2891
3243
  this.emit({
2892
3244
  type: 'checkout_pr_status_response',
2893
3245
  payload: {
@@ -2915,7 +3267,7 @@ export class Session {
2915
3267
  async handleCheckoutPrFailureLogsRequest(msg) {
2916
3268
  const { cwd, requestId } = msg;
2917
3269
  try {
2918
- const result = await getPullRequestFailureLogs(cwd);
3270
+ const result = await getPullRequestFailureLogs(cwd, { remoteName: msg.remoteName });
2919
3271
  this.emit({
2920
3272
  type: 'checkout_pr_failure_logs_response',
2921
3273
  payload: {
@@ -2945,6 +3297,7 @@ export class Session {
2945
3297
  try {
2946
3298
  await mergePullRequest(cwd, {
2947
3299
  method: msg.method ?? 'squash',
3300
+ remoteName: msg.remoteName,
2948
3301
  });
2949
3302
  this.emit({
2950
3303
  type: 'checkout_pr_merge_response',
@@ -3295,19 +3648,24 @@ export class Session {
3295
3648
  * Handle read-only file explorer requests scoped to a workspace cwd
3296
3649
  */
3297
3650
  async handleWorkspaceFileExplorerRequest(request) {
3298
- const { cwd, path: requestedPath = '.', mode, requestId } = request;
3651
+ const { cwd, path: requestedPath = '.', mode, requestId, ref } = request;
3299
3652
  try {
3300
3653
  const root = expandTilde(cwd);
3654
+ if (ref) {
3655
+ this.assertSafeGitRef(ref, 'workspace file ref');
3656
+ }
3301
3657
  if (mode === 'list') {
3302
3658
  const directory = await listDirectoryEntries({
3303
3659
  root,
3304
3660
  relativePath: requestedPath,
3661
+ ref,
3305
3662
  });
3306
3663
  this.emit({
3307
3664
  type: 'workspace_file_explorer_response',
3308
3665
  payload: {
3309
3666
  cwd,
3310
3667
  path: directory.path,
3668
+ ref: ref ?? null,
3311
3669
  mode,
3312
3670
  directory,
3313
3671
  file: null,
@@ -3320,12 +3678,14 @@ export class Session {
3320
3678
  const file = await readExplorerFile({
3321
3679
  root,
3322
3680
  relativePath: requestedPath,
3681
+ ref,
3323
3682
  });
3324
3683
  this.emit({
3325
3684
  type: 'workspace_file_explorer_response',
3326
3685
  payload: {
3327
3686
  cwd,
3328
3687
  path: file.path,
3688
+ ref: ref ?? null,
3329
3689
  mode,
3330
3690
  directory: null,
3331
3691
  file,
@@ -3342,6 +3702,7 @@ export class Session {
3342
3702
  payload: {
3343
3703
  cwd,
3344
3704
  path: requestedPath,
3705
+ ref: ref ?? null,
3345
3706
  mode,
3346
3707
  directory: null,
3347
3708
  file: null,