@spotpatch/runtime 1.1.1 → 1.2.0

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.
package/dist/index.js CHANGED
@@ -86,6 +86,7 @@ import {
86
86
  AGENT_CHECK_STATUSES,
87
87
  AGENT_FILE_CHANGE_KINDS,
88
88
  AGENT_JOB_STATUSES,
89
+ AGENT_WORKSPACE_STATES,
89
90
  ERROR_CODES
90
91
  } from "@spotpatch/shared";
91
92
  var ERROR_CODE_VALUES = new Set(Object.values(ERROR_CODES));
@@ -93,6 +94,7 @@ var CAPABILITY_STATE_VALUES = new Set(AGENT_CAPABILITY_STATES);
93
94
  var CHECK_STATUS_VALUES = new Set(AGENT_CHECK_STATUSES);
94
95
  var FILE_CHANGE_KIND_VALUES = new Set(AGENT_FILE_CHANGE_KINDS);
95
96
  var JOB_STATUS_VALUES = new Set(AGENT_JOB_STATUSES);
97
+ var WORKSPACE_STATE_VALUES = new Set(AGENT_WORKSPACE_STATES);
96
98
  var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
97
99
  var JOB_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
98
100
  var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
@@ -146,6 +148,35 @@ function isAgentCapabilitySnapshot(value) {
146
148
  }
147
149
  return isProfileId(value.providerProfileId) && isBoundedString(value.providerLabel, 100) && isProfileId(value.modelProfileId) && isBoundedString(value.modelLabel, 100) && (value.protocol === "responses" || value.protocol === "chat-completions") && typeof value.state === "string" && CAPABILITY_STATE_VALUES.has(value.state) && typeof value.authenticated === "boolean" && typeof value.modelAvailable === "boolean" && typeof value.toolCalling === "boolean" && typeof value.toolResultContinuation === "boolean" && typeof value.streaming === "boolean" && (value.checkedAt === void 0 || isIsoTimestamp(value.checkedAt)) && (value.errorCode === void 0 || isAgentErrorCode(value.errorCode));
148
150
  }
151
+ function isAgentWorkspaceHealthSnapshot(value) {
152
+ if (!isRecord(value) || !hasOnlyKeys(value, [
153
+ "state",
154
+ "checkedAt",
155
+ "changes",
156
+ "canIncludeLocalChanges",
157
+ "errorCode"
158
+ ]) || typeof value.state !== "string" || !WORKSPACE_STATE_VALUES.has(value.state) || !isIsoTimestamp(value.checkedAt) || typeof value.canIncludeLocalChanges !== "boolean" || value.errorCode !== void 0 && !isAgentErrorCode(value.errorCode) || !isRecord(value.changes) || !hasOnlyKeys(value.changes, [
159
+ "staged",
160
+ "unstaged",
161
+ "untracked",
162
+ "conflicted",
163
+ "total"
164
+ ])) {
165
+ return false;
166
+ }
167
+ const changes = value.changes;
168
+ const { staged, unstaged, untracked, conflicted, total } = changes;
169
+ if (!isNonnegativeInteger(staged) || !isNonnegativeInteger(unstaged) || !isNonnegativeInteger(untracked) || !isNonnegativeInteger(conflicted) || !isNonnegativeInteger(total) || total < staged || total < unstaged || total < untracked || total < conflicted || total > staged + unstaged + untracked) {
170
+ return false;
171
+ }
172
+ if (value.state === "ready") {
173
+ return total === 0 && !value.canIncludeLocalChanges && value.errorCode === void 0;
174
+ }
175
+ if (value.state === "consent-required") {
176
+ return total > 0 && conflicted === 0 && value.canIncludeLocalChanges && value.errorCode === void 0;
177
+ }
178
+ return !value.canIncludeLocalChanges && value.errorCode !== void 0;
179
+ }
149
180
  function isAgentJobSnapshot(value) {
150
181
  if (!isRecord(value) || !hasOnlyKeys(value, [
151
182
  "jobId",
@@ -486,6 +517,17 @@ function createRuntimeApi(options) {
486
517
  request
487
518
  );
488
519
  },
520
+ async agentWorkspaceHealth() {
521
+ const data = await requestJson(
522
+ SPOTPATCH_ENDPOINTS.agentWorkspaceHealth,
523
+ "POST",
524
+ emptyAction
525
+ );
526
+ if (!isAgentWorkspaceHealthSnapshot(data)) {
527
+ throw new RuntimeApiError();
528
+ }
529
+ return deepFreeze(data);
530
+ },
489
531
  async createAgentJob(request) {
490
532
  const serverRequest = omitBrowserCodeContext(request);
491
533
  return parseJobSnapshot(
@@ -1563,12 +1605,64 @@ var AGENT_PANEL_STYLES = `
1563
1605
  min-height: 46px;
1564
1606
  margin-top: 7px;
1565
1607
  border: 1px solid var(--spotpatch-border);
1566
- border-radius: 10px;
1567
- padding: 8px 30px 8px 10px;
1608
+ border-radius: 12px;
1609
+ padding: 10px 38px 10px 13px;
1568
1610
  color: #e8ebf2;
1569
- background: var(--spotpatch-bg-raised);
1611
+ color-scheme: dark;
1612
+ background: linear-gradient(180deg, rgb(255 255 255 / 4%), rgb(255 255 255 / 2%));
1570
1613
  font-size: 15px;
1614
+ font-weight: 560;
1615
+ line-height: 1.35;
1571
1616
  outline: none;
1617
+ transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
1618
+ }
1619
+ .spotpatch-agent select:hover { border-color: rgb(148 163 184 / 38%); background-color: rgb(255 255 255 / 5%); }
1620
+ .spotpatch-agent select:focus-visible { border-color: rgb(139 124 247 / 76%); box-shadow: 0 0 0 3px rgb(109 93 246 / 18%); }
1621
+ .spotpatch-agent select option { min-height: 42px; color: #e8ebf2; background: #10151e; }
1622
+ @supports (appearance: base-select) {
1623
+ .spotpatch-agent select,
1624
+ ::picker(select) { appearance: base-select; }
1625
+ .spotpatch-agent select { padding: 10px 13px; }
1626
+ .spotpatch-agent select::picker-icon {
1627
+ color: #9da6b5;
1628
+ transition: rotate 180ms cubic-bezier(.2, .8, .2, 1), color 160ms ease;
1629
+ }
1630
+ .spotpatch-agent select:open::picker-icon { rotate: 180deg; color: #c4b5fd; }
1631
+ .spotpatch-agent select::picker(select) {
1632
+ top: calc(anchor(bottom) + 7px);
1633
+ left: anchor(left);
1634
+ width: anchor-size(width);
1635
+ max-height: min(280px, 40vh);
1636
+ margin: 0;
1637
+ overflow: hidden;
1638
+ border: 1px solid rgb(139 124 247 / 28%);
1639
+ border-radius: 12px;
1640
+ padding: 6px;
1641
+ color: #e8ebf2;
1642
+ background: #10151e;
1643
+ box-shadow: 0 18px 50px rgb(0 0 0 / 38%), 0 0 0 1px rgb(255 255 255 / 3%);
1644
+ opacity: 0;
1645
+ transform: translateY(-5px) scale(.985);
1646
+ transform-origin: top center;
1647
+ transition: opacity 150ms ease, transform 180ms cubic-bezier(.2, .8, .2, 1), overlay 180ms allow-discrete, display 180ms allow-discrete;
1648
+ }
1649
+ ::picker(select):popover-open { opacity: 1; transform: translateY(0) scale(1); }
1650
+ @starting-style {
1651
+ ::picker(select):popover-open { opacity: 0; transform: translateY(-5px) scale(.985); }
1652
+ }
1653
+ .spotpatch-agent select option {
1654
+ min-height: 42px;
1655
+ border-radius: 8px;
1656
+ padding: 10px 12px;
1657
+ color: #dfe4ee;
1658
+ background: transparent;
1659
+ cursor: pointer;
1660
+ transition: color 120ms ease, background-color 120ms ease;
1661
+ }
1662
+ .spotpatch-agent select option:hover,
1663
+ .spotpatch-agent select option:focus { color: #fff; background: rgb(109 93 246 / 16%); }
1664
+ .spotpatch-agent select option:checked { color: #fff; background: rgb(109 93 246 / 24%); }
1665
+ .spotpatch-agent select option::checkmark { color: #8b7cf7; }
1572
1666
  }
1573
1667
  .spotpatch-agent select:focus-visible,
1574
1668
  .spotpatch-consent input:focus-visible {
@@ -1586,11 +1680,22 @@ var AGENT_PANEL_STYLES = `
1586
1680
  line-height: 1.6;
1587
1681
  }
1588
1682
  .spotpatch-consent input { width: 16px; height: 16px; flex: none; margin: 3px 0 0; accent-color: var(--spotpatch-accent); }
1683
+ .spotpatch-workspace-consent {
1684
+ margin-top: 12px;
1685
+ border: 1px solid rgb(251 191 36 / 18%);
1686
+ border-radius: 10px;
1687
+ padding: 11px 12px;
1688
+ background: rgb(120 53 15 / 9%);
1689
+ }
1690
+ .spotpatch-workspace-consent strong { display: block; color: #fde68a; font-size: 13px; font-weight: 620; }
1691
+ .spotpatch-workspace-consent small { display: block; margin-top: 2px; color: #aeb6c4; font-size: 12px; line-height: 1.5; }
1692
+ .spotpatch-agent-health-list { display: grid; gap: 8px; margin-top: 14px; }
1693
+ .spotpatch-agent-workspace,
1589
1694
  .spotpatch-agent-capability {
1590
1695
  display: flex;
1591
1696
  align-items: center;
1592
1697
  gap: 8px;
1593
- margin: 14px 0 0;
1698
+ margin: 0;
1594
1699
  color: #929bab;
1595
1700
  font-size: 13px;
1596
1701
  }
@@ -1607,6 +1712,21 @@ var AGENT_PANEL_STYLES = `
1607
1712
  .spotpatch-agent-capability[data-state="ready"]::before { background: var(--spotpatch-success); }
1608
1713
  .spotpatch-agent-capability[data-state="error"] { color: #fecaca; }
1609
1714
  .spotpatch-agent-capability[data-state="error"]::before { background: var(--spotpatch-danger); }
1715
+ .spotpatch-agent-workspace::before {
1716
+ width: 6px;
1717
+ height: 6px;
1718
+ flex: none;
1719
+ border-radius: 999px;
1720
+ background: #64748b;
1721
+ content: "";
1722
+ }
1723
+ .spotpatch-agent-workspace[data-state="checking"]::before { background: var(--spotpatch-warning); }
1724
+ .spotpatch-agent-workspace[data-state="ready"] { color: #a7f3d0; }
1725
+ .spotpatch-agent-workspace[data-state="ready"]::before { background: var(--spotpatch-success); }
1726
+ .spotpatch-agent-workspace[data-state="consent-required"] { color: #fde68a; }
1727
+ .spotpatch-agent-workspace[data-state="consent-required"]::before { background: #fbbf24; }
1728
+ .spotpatch-agent-workspace[data-state="blocked"] { color: #fecaca; }
1729
+ .spotpatch-agent-workspace[data-state="blocked"]::before { background: var(--spotpatch-danger); }
1610
1730
  .spotpatch-agent-job { padding: 16px; }
1611
1731
  .spotpatch-agent-job-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
1612
1732
  .spotpatch-agent-model { min-width: 0; overflow: hidden; color: #d6dafe; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
@@ -1695,10 +1815,26 @@ function createAgentPanel(document, ai, localizer) {
1695
1815
  consentCheckbox.type = "checkbox";
1696
1816
  const consentText = createMarkedElement(document, "span");
1697
1817
  consent.append(consentCheckbox, consentText);
1818
+ const workspaceConsent = createMarkedElement(document, "label");
1819
+ workspaceConsent.className = "spotpatch-consent spotpatch-workspace-consent";
1820
+ workspaceConsent.hidden = true;
1821
+ const workspaceConsentCheckbox = createMarkedElement(document, "input");
1822
+ workspaceConsentCheckbox.type = "checkbox";
1823
+ const workspaceConsentContent = createMarkedElement(document, "span");
1824
+ const workspaceConsentTitle = createMarkedElement(document, "strong");
1825
+ const workspaceConsentHelp = createMarkedElement(document, "small");
1826
+ workspaceConsentContent.append(workspaceConsentTitle, workspaceConsentHelp);
1827
+ workspaceConsent.append(workspaceConsentCheckbox, workspaceConsentContent);
1828
+ const healthList = createMarkedElement(document, "div");
1829
+ healthList.className = "spotpatch-agent-health-list";
1830
+ const workspace = createMarkedElement(document, "p");
1831
+ workspace.className = "spotpatch-agent-workspace";
1832
+ workspace.dataset.state = "idle";
1698
1833
  const capability = createMarkedElement(document, "p");
1699
1834
  capability.className = "spotpatch-agent-capability";
1700
1835
  capability.dataset.state = "idle";
1701
- setup.append(selectors, consent, capability);
1836
+ healthList.append(workspace, capability);
1837
+ setup.append(selectors, consent, workspaceConsent, healthList);
1702
1838
  const job = createMarkedElement(document, "div");
1703
1839
  job.className = "spotpatch-agent-job";
1704
1840
  job.hidden = true;
@@ -1757,6 +1893,9 @@ function createAgentPanel(document, ai, localizer) {
1757
1893
  let latestCapabilityState = "idle";
1758
1894
  let latestCapabilityMessage = messages.agent.connectionNotTested;
1759
1895
  let latestCapabilityErrorCode;
1896
+ let latestWorkspaceState = "idle";
1897
+ let latestWorkspaceSnapshot;
1898
+ let latestWorkspaceErrorCode;
1760
1899
  for (const provider of providers) {
1761
1900
  addOption(document, providerSelect, provider.id, provider.label);
1762
1901
  }
@@ -1784,7 +1923,7 @@ function createAgentPanel(document, ai, localizer) {
1784
1923
  testButton.hidden = !setupVisible;
1785
1924
  runButton.hidden = !setupVisible;
1786
1925
  testButton.disabled = !editingEnabled || capabilityProbing || selectedProvider() === void 0;
1787
- runButton.disabled = !editingEnabled || capabilityProbing || !contextReady || !consentCheckbox.checked || selectedProvider() === void 0 || modelSelect.value.length === 0;
1926
+ runButton.disabled = !editingEnabled || capabilityProbing || !contextReady || !consentCheckbox.checked || latestWorkspaceState === "idle" || latestWorkspaceState === "checking" || latestWorkspaceState === "blocked" || latestWorkspaceState === "consent-required" && !workspaceConsentCheckbox.checked || selectedProvider() === void 0 || modelSelect.value.length === 0;
1788
1927
  if (!jobPresented || !selectionVisible) {
1789
1928
  cancelButton.hidden = true;
1790
1929
  applyButton.hidden = true;
@@ -1792,8 +1931,25 @@ function createAgentPanel(document, ai, localizer) {
1792
1931
  resetButton.hidden = true;
1793
1932
  }
1794
1933
  };
1795
- populateModels();
1796
- providerSelect.addEventListener("change", () => {
1934
+ const renderWorkspaceMessage = () => {
1935
+ if (latestWorkspaceState === "idle") {
1936
+ workspace.textContent = messages.agent.workspaceNotChecked;
1937
+ } else if (latestWorkspaceState === "checking") {
1938
+ workspace.textContent = messages.agent.checkingWorkspace;
1939
+ } else if (latestWorkspaceState === "ready") {
1940
+ workspace.textContent = messages.agent.workspaceReady;
1941
+ } else if (latestWorkspaceState === "consent-required") {
1942
+ const changes = latestWorkspaceSnapshot?.changes;
1943
+ workspace.textContent = messages.agent.workspaceDirty(
1944
+ changes?.staged ?? 0,
1945
+ changes?.unstaged ?? 0,
1946
+ changes?.untracked ?? 0
1947
+ );
1948
+ } else {
1949
+ workspace.textContent = latestWorkspaceErrorCode === void 0 ? messages.errors.INTERNAL_ERROR : messages.errors[latestWorkspaceErrorCode];
1950
+ }
1951
+ };
1952
+ function handleProviderChange() {
1797
1953
  populateModels();
1798
1954
  consentCheckbox.checked = false;
1799
1955
  capability.dataset.state = "idle";
@@ -1803,8 +1959,8 @@ function createAgentPanel(document, ai, localizer) {
1803
1959
  latestCapabilityErrorCode = void 0;
1804
1960
  capabilityReady = false;
1805
1961
  refreshActions();
1806
- });
1807
- modelSelect.addEventListener("change", () => {
1962
+ }
1963
+ function handleModelChange() {
1808
1964
  capability.dataset.state = "idle";
1809
1965
  capability.textContent = messages.agent.connectionNotTested;
1810
1966
  latestCapabilityState = "idle";
@@ -1812,8 +1968,12 @@ function createAgentPanel(document, ai, localizer) {
1812
1968
  latestCapabilityErrorCode = void 0;
1813
1969
  capabilityReady = false;
1814
1970
  refreshActions();
1815
- });
1971
+ }
1972
+ populateModels();
1973
+ providerSelect.addEventListener("change", handleProviderChange);
1974
+ modelSelect.addEventListener("change", handleModelChange);
1816
1975
  consentCheckbox.addEventListener("change", refreshActions);
1976
+ workspaceConsentCheckbox.addEventListener("change", refreshActions);
1817
1977
  function renderCurrentJob() {
1818
1978
  if (latestSnapshot === void 0) {
1819
1979
  return;
@@ -1868,6 +2028,8 @@ function createAgentPanel(document, ai, localizer) {
1868
2028
  modelText.textContent = messages.agent.model;
1869
2029
  providerSelect.setAttribute("aria-label", messages.agent.providerAriaLabel);
1870
2030
  modelSelect.setAttribute("aria-label", messages.agent.modelAriaLabel);
2031
+ workspaceConsentTitle.textContent = messages.agent.includeLocalChanges;
2032
+ workspaceConsentHelp.textContent = messages.agent.includeLocalChangesHelp;
1871
2033
  diff.setAttribute("aria-label", messages.agent.diffAriaLabel);
1872
2034
  testButton.textContent = messages.agent.testConnection;
1873
2035
  applyButton.textContent = messages.agent.apply;
@@ -1888,6 +2050,7 @@ function createAgentPanel(document, ai, localizer) {
1888
2050
  } else {
1889
2051
  capability.textContent = latestCapabilityMessage;
1890
2052
  }
2053
+ renderWorkspaceMessage();
1891
2054
  refreshActions();
1892
2055
  renderCurrentJob();
1893
2056
  }
@@ -1898,6 +2061,7 @@ function createAgentPanel(document, ai, localizer) {
1898
2061
  providerSelect,
1899
2062
  modelSelect,
1900
2063
  consentCheckbox,
2064
+ workspaceConsentCheckbox,
1901
2065
  testButton,
1902
2066
  runButton,
1903
2067
  cancelButton,
@@ -1907,6 +2071,9 @@ function createAgentPanel(document, ai, localizer) {
1907
2071
  consentGranted() {
1908
2072
  return consentCheckbox.checked;
1909
2073
  },
2074
+ workspaceConsentGranted() {
2075
+ return workspaceConsentCheckbox.checked;
2076
+ },
1910
2077
  readSelection() {
1911
2078
  const provider = selectedProvider();
1912
2079
  const model = provider?.models.find(
@@ -1927,6 +2094,20 @@ function createAgentPanel(document, ai, localizer) {
1927
2094
  capability.textContent = capabilitySnapshot?.state === "agent-ready" ? `${message} \xB7 ${messages.agent.toolsReady}` : message;
1928
2095
  refreshActions();
1929
2096
  },
2097
+ renderWorkspaceHealth(state, snapshot, errorCode) {
2098
+ latestWorkspaceState = state;
2099
+ latestWorkspaceSnapshot = snapshot;
2100
+ latestWorkspaceErrorCode = errorCode ?? snapshot?.errorCode;
2101
+ workspace.dataset.state = state;
2102
+ if (state !== "checking") {
2103
+ workspaceConsent.hidden = state !== "consent-required";
2104
+ }
2105
+ if (state === "idle" || state === "ready" || state === "blocked") {
2106
+ workspaceConsentCheckbox.checked = false;
2107
+ }
2108
+ renderWorkspaceMessage();
2109
+ refreshActions();
2110
+ },
1930
2111
  renderJob(snapshot, result, activities, errorCode) {
1931
2112
  jobPresented = true;
1932
2113
  setup.hidden = true;
@@ -1934,6 +2115,7 @@ function createAgentPanel(document, ai, localizer) {
1934
2115
  providerSelect.disabled = true;
1935
2116
  modelSelect.disabled = true;
1936
2117
  consentCheckbox.disabled = true;
2118
+ workspaceConsentCheckbox.disabled = true;
1937
2119
  latestSnapshot = snapshot;
1938
2120
  latestResult = result;
1939
2121
  latestActivities = activities;
@@ -1951,6 +2133,7 @@ function createAgentPanel(document, ai, localizer) {
1951
2133
  providerSelect.disabled = false;
1952
2134
  modelSelect.disabled = false;
1953
2135
  consentCheckbox.disabled = false;
2136
+ workspaceConsentCheckbox.disabled = false;
1954
2137
  activity.replaceChildren();
1955
2138
  files.replaceChildren();
1956
2139
  checks.replaceChildren();
@@ -1968,6 +2151,7 @@ function createAgentPanel(document, ai, localizer) {
1968
2151
  providerSelect.disabled = !enabled || jobPresented;
1969
2152
  modelSelect.disabled = !enabled || jobPresented;
1970
2153
  consentCheckbox.disabled = !enabled || jobPresented;
2154
+ workspaceConsentCheckbox.disabled = !enabled || jobPresented;
1971
2155
  refreshActions();
1972
2156
  },
1973
2157
  setProviderConsent(granted) {
@@ -1986,6 +2170,10 @@ function createAgentPanel(document, ai, localizer) {
1986
2170
  },
1987
2171
  dispose() {
1988
2172
  unsubscribeLocale();
2173
+ providerSelect.removeEventListener("change", handleProviderChange);
2174
+ modelSelect.removeEventListener("change", handleModelChange);
2175
+ consentCheckbox.removeEventListener("change", refreshActions);
2176
+ workspaceConsentCheckbox.removeEventListener("change", refreshActions);
1989
2177
  }
1990
2178
  });
1991
2179
  }
@@ -2155,8 +2343,16 @@ var ERROR_MESSAGES_EN = Object.freeze({
2155
2343
  [ERROR_CODES2.AGENT_BUSY]: "Another write Agent job is still active.",
2156
2344
  [ERROR_CODES2.AGENT_LIMIT_EXCEEDED]: "The Agent stopped at a configured time, turn, output, or size limit.",
2157
2345
  [ERROR_CODES2.AGENT_CANCELLED]: "The Agent job was cancelled.",
2158
- [ERROR_CODES2.WORKTREE_DIRTY]: "Commit or otherwise clean staged, unstaged, and untracked files before running AI.",
2346
+ [ERROR_CODES2.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent before running AI.",
2347
+ [ERROR_CODES2.WORKTREE_NOT_REPOSITORY]: "The Vite root is not the top level of an initialized Git repository.",
2348
+ [ERROR_CODES2.WORKTREE_OPERATION_IN_PROGRESS]: "Finish the active merge, rebase, cherry-pick, or revert before running AI.",
2349
+ [ERROR_CODES2.WORKTREE_CONFLICTED]: "Resolve all merge conflicts before running AI; SpotPatch will not guess a resolution.",
2350
+ [ERROR_CODES2.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "Local untracked files exceed the safe isolation limit. Reduce their count or total size.",
2351
+ [ERROR_CODES2.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked item is missing, linked, or not a regular file and cannot be isolated safely.",
2352
+ [ERROR_CODES2.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "Local changes include conflicts, unsupported files, or exceed the safe baseline limit.",
2159
2353
  [ERROR_CODES2.TOOL_DENIED]: "A model tool request violated the local safety policy.",
2354
+ [ERROR_CODES2.TOOL_INPUT_INVALID]: "The model sent invalid tool arguments or reused a tool call ID inconsistently.",
2355
+ [ERROR_CODES2.TOOL_PATH_DENIED]: "The model tried to access a protected, non-text, linked, or out-of-project path.",
2160
2356
  [ERROR_CODES2.PATCH_REJECTED]: "The proposed patch did not pass local policy.",
2161
2357
  [ERROR_CODES2.VALIDATION_FAILED]: "Required project checks failed. The change cannot be applied.",
2162
2358
  [ERROR_CODES2.APPLY_CONFLICT]: "Project files changed after the Agent baseline; no overwrite was performed.",
@@ -2180,8 +2376,16 @@ var ERROR_MESSAGES_ZH = Object.freeze({
2180
2376
  [ERROR_CODES2.AGENT_BUSY]: "\u5F53\u524D\u9879\u76EE\u5DF2\u6709\u4E00\u4E2A\u5199\u5165\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\u3002",
2181
2377
  [ERROR_CODES2.AGENT_LIMIT_EXCEEDED]: "Agent \u8FBE\u5230\u65F6\u95F4\u3001\u8F6E\u6B21\u3001\u8F93\u51FA\u6216\u53D8\u66F4\u89C4\u6A21\u9650\u5236\u3002",
2182
2378
  [ERROR_CODES2.AGENT_CANCELLED]: "Agent \u4EFB\u52A1\u5DF2\u53D6\u6D88\u3002",
2183
- [ERROR_CODES2.WORKTREE_DIRTY]: "\u8FD0\u884C AI \u524D\uFF0C\u8BF7\u5148\u63D0\u4EA4\u6216\u6E05\u7406\u6682\u5B58\u3001\u672A\u6682\u5B58\u4E0E\u672A\u8DDF\u8E2A\u6587\u4EF6\u3002",
2379
+ [ERROR_CODES2.WORKTREE_DIRTY]: "\u8FD0\u884C AI \u524D\uFF0C\u8BF7\u660E\u786E\u540C\u610F\u5C06\u5F53\u524D\u672C\u5730\u4FEE\u6539\u7EB3\u5165\u9694\u79BB\u57FA\u7EBF\u3002",
2380
+ [ERROR_CODES2.WORKTREE_NOT_REPOSITORY]: "Vite \u6839\u76EE\u5F55\u4E0D\u662F\u5DF2\u521D\u59CB\u5316 Git \u4ED3\u5E93\u7684\u9876\u5C42\u76EE\u5F55\u3002",
2381
+ [ERROR_CODES2.WORKTREE_OPERATION_IN_PROGRESS]: "\u8BF7\u5148\u5B8C\u6210\u5F53\u524D merge\u3001rebase\u3001cherry-pick \u6216 revert\uFF0C\u518D\u8FD0\u884C AI\u3002",
2382
+ [ERROR_CODES2.WORKTREE_CONFLICTED]: "\u8BF7\u5148\u89E3\u51B3\u5168\u90E8\u5408\u5E76\u51B2\u7A81\uFF1BSpotPatch \u4E0D\u4F1A\u731C\u6D4B\u51B2\u7A81\u89E3\u51B3\u65B9\u5F0F\u3002",
2383
+ [ERROR_CODES2.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "\u672C\u5730\u672A\u8DDF\u8E2A\u6587\u4EF6\u8D85\u8FC7\u5B89\u5168\u9694\u79BB\u4E0A\u9650\uFF0C\u8BF7\u51CF\u5C11\u6587\u4EF6\u6570\u91CF\u6216\u603B\u4F53\u79EF\u3002",
2384
+ [ERROR_CODES2.WORKTREE_UNTRACKED_UNSUPPORTED]: "\u67D0\u4E2A\u672A\u8DDF\u8E2A\u9879\u5DF2\u4E22\u5931\u3001\u662F\u7B26\u53F7\u94FE\u63A5\u6216\u4E0D\u662F\u666E\u901A\u6587\u4EF6\uFF0C\u65E0\u6CD5\u5B89\u5168\u9694\u79BB\u3002",
2385
+ [ERROR_CODES2.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "\u672C\u5730\u4FEE\u6539\u5B58\u5728\u51B2\u7A81\u3001\u4E0D\u652F\u6301\u7684\u6587\u4EF6\uFF0C\u6216\u8D85\u8FC7\u5B89\u5168\u57FA\u7EBF\u9650\u5236\u3002",
2184
2386
  [ERROR_CODES2.TOOL_DENIED]: "\u6A21\u578B\u5DE5\u5177\u8BF7\u6C42\u8FDD\u53CD\u672C\u5730\u5B89\u5168\u7B56\u7565\u3002",
2387
+ [ERROR_CODES2.TOOL_INPUT_INVALID]: "\u6A21\u578B\u53D1\u9001\u4E86\u65E0\u6548\u5DE5\u5177\u53C2\u6570\uFF0C\u6216\u4E0D\u4E00\u81F4\u5730\u590D\u7528\u4E86\u5DE5\u5177\u8C03\u7528 ID\u3002",
2388
+ [ERROR_CODES2.TOOL_PATH_DENIED]: "\u6A21\u578B\u5C1D\u8BD5\u8BBF\u95EE\u53D7\u4FDD\u62A4\u3001\u975E\u6587\u672C\u3001\u7B26\u53F7\u94FE\u63A5\u6216\u9879\u76EE\u5916\u8DEF\u5F84\u3002",
2185
2389
  [ERROR_CODES2.PATCH_REJECTED]: "\u5EFA\u8BAE\u8865\u4E01\u672A\u901A\u8FC7\u672C\u5730\u7B56\u7565\u68C0\u67E5\u3002",
2186
2390
  [ERROR_CODES2.VALIDATION_FAILED]: "\u9879\u76EE\u5FC5\u9700\u68C0\u67E5\u5931\u8D25\uFF0C\u4E0D\u80FD\u5E94\u7528\u672C\u6B21\u53D8\u66F4\u3002",
2187
2391
  [ERROR_CODES2.APPLY_CONFLICT]: "Agent \u5EFA\u7ACB\u57FA\u7EBF\u540E\u9879\u76EE\u6587\u4EF6\u5DF2\u53D8\u5316\uFF0C\u672A\u6267\u884C\u8986\u76D6\u3002",
@@ -2346,6 +2550,13 @@ var UI_MESSAGES = Object.freeze({
2346
2550
  providerUnavailable: "Provider configuration is unavailable.",
2347
2551
  consent: (provider) => `I understand selected context and allowed source may be sent to ${provider}; its data policy is my responsibility.`,
2348
2552
  connectionNotTested: "Connection not tested",
2553
+ workspaceNotChecked: "Local workspace not checked",
2554
+ checkingWorkspace: "Checking Git workspace and isolated execution\u2026",
2555
+ workspaceReady: "Local workspace is ready for isolated execution",
2556
+ workspaceDirty: (staged, unstaged, untracked) => `Local changes found \xB7 ${String(staged)} staged \xB7 ${String(unstaged)} unstaged \xB7 ${String(untracked)} untracked`,
2557
+ includeLocalChanges: "Allow the Agent to continue from my current local changes",
2558
+ includeLocalChangesHelp: "The Agent may edit these files. SpotPatch preserves the baseline and applies or reverts only the Agent delta.",
2559
+ localChangesConsentRequired: "Confirm inclusion of current local changes before running AI.",
2349
2560
  capabilityVerified: "Agent capability verified",
2350
2561
  capabilityVerifiedAnnouncement: "AI provider capability verified.",
2351
2562
  testingCapability: "Testing authentication, tools, continuation, and streaming\u2026",
@@ -2354,7 +2565,7 @@ var UI_MESSAGES = Object.freeze({
2354
2565
  reverting: "Reverting the applied Agent change.",
2355
2566
  consentRequired: "Confirm remote provider data transmission before running AI.",
2356
2567
  toolsReady: "tools and streaming ready",
2357
- testConnection: "Test connection",
2568
+ testConnection: "Check environment",
2358
2569
  verifyAndRun: "Verify & run",
2359
2570
  verifying: "Verifying\u2026",
2360
2571
  run: "Run AI",
@@ -2475,6 +2686,13 @@ var UI_MESSAGES = Object.freeze({
2475
2686
  providerUnavailable: "\u6A21\u578B\u670D\u52A1\u914D\u7F6E\u4E0D\u53EF\u7528\u3002",
2476
2687
  consent: (provider) => `\u6211\u4E86\u89E3\u9009\u4E2D\u4E0A\u4E0B\u6587\u4E0E\u83B7\u51C6\u6E90\u7801\u53EF\u80FD\u53D1\u9001\u5230 ${provider}\uFF0C\u5E76\u81EA\u884C\u8D1F\u8D23\u5176\u6570\u636E\u7B56\u7565\u3002`,
2477
2688
  connectionNotTested: "\u5C1A\u672A\u6D4B\u8BD5\u8FDE\u63A5",
2689
+ workspaceNotChecked: "\u5C1A\u672A\u68C0\u67E5\u672C\u5730\u5DE5\u4F5C\u533A",
2690
+ checkingWorkspace: "\u6B63\u5728\u68C0\u67E5 Git \u5DE5\u4F5C\u533A\u4E0E\u9694\u79BB\u6267\u884C\u73AF\u5883\u2026\u2026",
2691
+ workspaceReady: "\u672C\u5730\u5DE5\u4F5C\u533A\u5DF2\u6EE1\u8DB3\u9694\u79BB\u6267\u884C\u6761\u4EF6",
2692
+ workspaceDirty: (staged, unstaged, untracked) => `\u53D1\u73B0\u672C\u5730\u4FEE\u6539 \xB7 \u6682\u5B58 ${String(staged)} \xB7 \u672A\u6682\u5B58 ${String(unstaged)} \xB7 \u672A\u8DDF\u8E2A ${String(untracked)}`,
2693
+ includeLocalChanges: "\u5141\u8BB8 Agent \u57FA\u4E8E\u6211\u5F53\u524D\u7684\u672C\u5730\u4FEE\u6539\u7EE7\u7EED",
2694
+ includeLocalChangesHelp: "Agent \u53EF\u80FD\u7EE7\u7EED\u4FEE\u6539\u8FD9\u4E9B\u6587\u4EF6\uFF1BSpotPatch \u4F1A\u4FDD\u7559\u539F\u57FA\u7EBF\uFF0C\u4EC5\u5E94\u7528\u6216\u64A4\u9500 Agent \u81EA\u5DF1\u7684\u589E\u91CF\u3002",
2695
+ localChangesConsentRequired: "\u8FD0\u884C AI \u524D\uFF0C\u8BF7\u786E\u8BA4\u5141\u8BB8\u7EB3\u5165\u5F53\u524D\u672C\u5730\u4FEE\u6539\u3002",
2478
2696
  capabilityVerified: "Agent \u80FD\u529B\u9A8C\u8BC1\u901A\u8FC7",
2479
2697
  capabilityVerifiedAnnouncement: "AI \u6A21\u578B\u670D\u52A1\u80FD\u529B\u9A8C\u8BC1\u901A\u8FC7\u3002",
2480
2698
  testingCapability: "\u6B63\u5728\u9A8C\u8BC1\u9274\u6743\u3001\u5DE5\u5177\u8C03\u7528\u3001\u8FDE\u7EED\u8C03\u7528\u4E0E\u6D41\u5F0F\u54CD\u5E94\u2026\u2026",
@@ -2483,7 +2701,7 @@ var UI_MESSAGES = Object.freeze({
2483
2701
  reverting: "\u6B63\u5728\u64A4\u9500\u5DF2\u5E94\u7528\u7684 Agent \u53D8\u66F4\u3002",
2484
2702
  consentRequired: "\u8FD0\u884C AI \u524D\uFF0C\u8BF7\u5148\u786E\u8BA4\u5141\u8BB8\u5411\u8FDC\u7A0B\u6A21\u578B\u670D\u52A1\u4F20\u8F93\u6570\u636E\u3002",
2485
2703
  toolsReady: "\u5DE5\u5177\u8C03\u7528\u4E0E\u6D41\u5F0F\u54CD\u5E94\u5DF2\u5C31\u7EEA",
2486
- testConnection: "\u6D4B\u8BD5\u8FDE\u63A5",
2704
+ testConnection: "\u68C0\u67E5\u8FD0\u884C\u73AF\u5883",
2487
2705
  verifyAndRun: "\u9A8C\u8BC1\u5E76\u8FD0\u884C",
2488
2706
  verifying: "\u9A8C\u8BC1\u4E2D\u2026\u2026",
2489
2707
  run: "\u8FD0\u884C AI",
@@ -3733,6 +3951,7 @@ function createRuntimeView(document, shortcut, ai = Object.freeze({ enabled: fal
3733
3951
  agentProviderSelect: agentPanel.providerSelect,
3734
3952
  agentModelSelect: agentPanel.modelSelect,
3735
3953
  agentConsentCheckbox: agentPanel.consentCheckbox,
3954
+ agentWorkspaceConsentCheckbox: agentPanel.workspaceConsentCheckbox,
3736
3955
  agentTestButton: agentPanel.testButton,
3737
3956
  agentRunButton: agentPanel.runButton,
3738
3957
  agentCancelButton: agentPanel.cancelButton,
@@ -3844,6 +4063,10 @@ function createRuntimeView(document, shortcut, ai = Object.freeze({ enabled: fal
3844
4063
  previewButton.classList.toggle("spotpatch-primary", !agentReady);
3845
4064
  placeDialog();
3846
4065
  },
4066
+ renderAgentWorkspaceHealth(state, snapshot, errorCode) {
4067
+ agentPanel.renderWorkspaceHealth(state, snapshot, errorCode);
4068
+ placeDialog();
4069
+ },
3847
4070
  renderAgentJob(snapshot, result, activities, errorCode) {
3848
4071
  agentPanel.renderJob(snapshot, result, activities, errorCode);
3849
4072
  placeDialog();
@@ -3864,6 +4087,7 @@ function createRuntimeView(document, shortcut, ai = Object.freeze({ enabled: fal
3864
4087
  },
3865
4088
  locale: localizer.locale,
3866
4089
  messages: localizer.messages,
4090
+ agentWorkspaceConsentGranted: agentPanel.workspaceConsentGranted,
3867
4091
  subscribeLocale: localizer.subscribe,
3868
4092
  dispose() {
3869
4093
  diagnostics.removeEventListener("toggle", placeDialog);
@@ -3967,6 +4191,22 @@ function createAgentWorkflow(options) {
3967
4191
  let appliedDuringCurrentJob = false;
3968
4192
  let actionPending = false;
3969
4193
  const selectedProfiles = () => options.view.readAgentSelection();
4194
+ const refreshWorkspaceHealth = async (workflowRevision) => {
4195
+ options.view.renderAgentWorkspaceHealth("checking");
4196
+ try {
4197
+ const health = await options.api.agentWorkspaceHealth();
4198
+ if (workflowRevision !== revision) {
4199
+ return health;
4200
+ }
4201
+ options.view.renderAgentWorkspaceHealth(health.state, health, health.errorCode);
4202
+ return health;
4203
+ } catch (error) {
4204
+ if (workflowRevision === revision) {
4205
+ options.view.renderAgentWorkspaceHealth("blocked", void 0, errorCode(error));
4206
+ }
4207
+ throw error;
4208
+ }
4209
+ };
3970
4210
  const renderJob = (explicitErrorCode) => {
3971
4211
  if (snapshot === void 0) {
3972
4212
  return;
@@ -4142,6 +4382,10 @@ function createAgentWorkflow(options) {
4142
4382
  },
4143
4383
  beginSelection() {
4144
4384
  resetState();
4385
+ const workflowRevision = revision;
4386
+ if (options.ai.enabled) {
4387
+ void refreshWorkspaceHealth(workflowRevision).catch(() => void 0);
4388
+ }
4145
4389
  },
4146
4390
  cancel() {
4147
4391
  if (snapshot?.canCancel === true) {
@@ -4174,12 +4418,17 @@ function createAgentWorkflow(options) {
4174
4418
  providerOrModelChanged() {
4175
4419
  revision += 1;
4176
4420
  restoreProviderState();
4421
+ const workflowRevision = revision;
4422
+ void refreshWorkspaceHealth(workflowRevision).catch(() => void 0);
4177
4423
  },
4178
4424
  reset() {
4179
4425
  const mustReselect = appliedDuringCurrentJob;
4180
4426
  resetState();
4181
4427
  if (mustReselect) {
4182
4428
  options.onReselectRequired();
4429
+ } else if (options.ai.enabled) {
4430
+ const workflowRevision = revision;
4431
+ void refreshWorkspaceHealth(workflowRevision).catch(() => void 0);
4183
4432
  }
4184
4433
  },
4185
4434
  revert() {
@@ -4206,15 +4455,27 @@ function createAgentWorkflow(options) {
4206
4455
  providerConsents.add(selection.providerProfileId);
4207
4456
  const workflowRevision = ++revision;
4208
4457
  options.view.setAgentEditingEnabled(false);
4209
- void probe(workflowRevision).then(async () => {
4458
+ void Promise.all([
4459
+ probe(workflowRevision),
4460
+ refreshWorkspaceHealth(workflowRevision)
4461
+ ]).then(async ([, health]) => {
4210
4462
  if (workflowRevision !== revision) {
4211
4463
  return;
4212
4464
  }
4465
+ if (health.state === "blocked") {
4466
+ throw new RuntimeApiError(
4467
+ health.errorCode ?? ERROR_CODES3.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
4468
+ );
4469
+ }
4470
+ if (health.state === "consent-required" && !options.view.agentWorkspaceConsentGranted()) {
4471
+ throw new RuntimeApiError(ERROR_CODES3.WORKTREE_DIRTY);
4472
+ }
4213
4473
  const created = await options.api.createAgentJob({
4214
4474
  annotation,
4215
4475
  providerProfileId: selection.providerProfileId,
4216
4476
  modelProfileId: selection.modelProfileId,
4217
- providerDataConsent: true
4477
+ providerDataConsent: true,
4478
+ workingTreeMode: health.state === "consent-required" ? "include-local-changes" : "require-clean"
4218
4479
  });
4219
4480
  if (workflowRevision !== revision) {
4220
4481
  return;
@@ -4243,7 +4504,10 @@ function createAgentWorkflow(options) {
4243
4504
  return;
4244
4505
  }
4245
4506
  const workflowRevision = ++revision;
4246
- void probe(workflowRevision).catch((error) => {
4507
+ void Promise.all([
4508
+ probe(workflowRevision),
4509
+ refreshWorkspaceHealth(workflowRevision)
4510
+ ]).catch((error) => {
4247
4511
  if (workflowRevision === revision) {
4248
4512
  options.view.renderAgentCapability(
4249
4513
  "error",