Skip to content

Add Discard changes button to the Agent Changes view#349

Open
anderson-joyle wants to merge 5 commits into
mainfrom
andersonf/discard-changes
Open

Add Discard changes button to the Agent Changes view#349
anderson-joyle wants to merge 5 commits into
mainfrom
andersonf/discard-changes

Conversation

@anderson-joyle

@anderson-joyle anderson-joyle commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Discard changes button (arrow-back $(discard) icon) to the Agent Changes view title bar. After a modal confirmation, it restores the selected agent's local workspace to its last-synced baseline through a local-only, projection-aware Sync/LSP operation.

Resolves #343.

Behavior

  • Enabled only when local changes exist and no sync is running: mcs.agentChangesView.hasLocalChanges && !mcs.isSyncing.
  • Prompts for the agent when multiple workspaces are connected, then shows a modal confirmation with the local-change count.
  • Surfaces picker cancellation as a warning and the absence of connected workspaces as an error, consistent with the existing sync commands.
  • Recomputes local changes from disk before discarding them rather than trusting stale tree state.
  • Locally created component files are deleted; locally updated or deleted text resources are restored from .mcs/botdefinition.json.
  • Handles projection-specific resources safely:
    • edits only the affected entries in shared references.mcs.yml;
    • restores both workflow.json and metadata.yml for deleted workflows;
    • restores per-reference CLI infrastructure/connections/*.sync.yaml files;
    • recreates missing parent directories.
  • Binary resources that cannot be reconstructed from the cache are skipped and reported so they can be restored with Get.
  • Returns the remaining local changes from the same LSP request and updates the Agent Changes tree directly. Failures are propagated so existing tree state is not replaced with an empty result.

Design

  • WorkspaceSynchronizer.DiscardLocalChanges owns cache restoration and projection-aware file handling in the shared Sync layer.
  • powerplatformls/discardLocalChanges is a local-only LSP endpoint that reads the current workspace, computes changes, applies the discard, and returns any unresolved changes.
  • Local change calculation no longer requires access tokens, Dataverse client setup, or workflow enrichment calls.
  • The TypeScript command is orchestration-only: workspace selection, confirmation, busy state/progress, LSP invocation, result reporting, and tree replacement.
  • Agent names, skipped paths, and command errors remain PII-tagged for telemetry. Non-Error rejections retain useful text through the repository-standard String(error) fallback.

Key files

Area Change
src/CopilotStudio.Sync/WorkspaceSynchronizer.cs Local-only diff and projection-aware discard implementation
src/CopilotStudio.Sync/IWorkspaceSynchronizer.cs, Models.cs Discard API and result contracts
src/CopilotStudio.Sync.UnitTests/DiscardLocalChangesTests.cs Shared references, workflows, CLI references, missing parents, malformed manifests, binary skips, and partial-failure coverage
src/LanguageServers/PowerPlatformLS/Impl.PullAgent/DiscardLocalChangesHandler.cs New local-only discard endpoint
client/src/commands/discardChanges.ts Command orchestration, user feedback, and telemetry-safe reporting
client/src/sync/workspaceScm.ts Credential-free local diff requests, propagated failures, and direct local-state replacement
client/src/sync/discardChanges.ts Removed; restoration now belongs to the projection-aware Sync layer

Testing

  • Full .NET Debug build: 0 warnings / 0 errors
  • Full .NET traversal: 1,485 passed / 0 failed / 9 skipped (external E2E scenarios)
  • Extension host tests: 156 passed / 0 failed
  • TypeScript type-check and ESLint pass as part of the host-test pretest flow

Known limitation

  • Newly added knowledge attachments are not fully discarded yet: the generated metadata change can be removed while the payload remains on disk. This is intentionally deferred to a follow-up.
  • A live F5 smoke test against a real cloned agent has not been completed.

Copilot AI review requested due to automatic review settings July 17, 2026 22:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an offline “Discard changes” command to the Agent Changes view, enabling users to revert an agent workspace’s local edits back to the last-synced baseline using the LSP cached-file API.

Changes:

  • Adds a new title-bar command (microsoft-copilot-studio.discardChanges) and menu contribution for the Agent Changes view.
  • Implements discard logic in a dependency-injected module (discardLocalChanges) plus command wiring (confirmation, progress, busy state, refresh).
  • Adds host tests covering restorable vs non-restorable changes and batch behavior; adds a helper to recompute local changes after discard.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/vscode-extensions/microsoft-powerplatformlang-extension/package.json Adds the Discard Changes command + Agent Changes title-bar button contribution.
src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/tests/host/discardChanges.test.ts New host tests for restorable checks and discard batch behavior.
src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/sync/workspaceScm.ts Adds refreshLocalChanges() to refresh the tree/badge after discard.
src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/sync/discardChanges.ts New pure discard/revert logic with restorable filtering and skip reporting.
src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/extension.ts Registers the new discard command during activation.
src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/commands/discardChanges.ts New command wiring: workspace selection, confirmation, LSP cache reads, file writes/deletes, and result reporting.

Copilot AI review requested due to automatic review settings July 20, 2026 13:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/commands/discardChanges.ts:116

  • DiscardDependencies.writeFile is documented as creating parent directories as needed, but the concrete implementation here calls workspace.fs.writeFile directly. If the user deleted a directory along with a file (or a deleted file lives under a now-missing folder), restore will fail and the change will be skipped unnecessarily. Create the parent directory before writing.
            writeFile: async (uri: Uri, content: string) => {
              await VSworkspace.fs.writeFile(uri, Buffer.from(content, 'utf8'));
            },

@anderson-joyle
anderson-joyle marked this pull request as ready for review July 20, 2026 14:26
nlivie
nlivie previously approved these changes Jul 20, 2026
Copilot AI review requested due to automatic review settings July 23, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/commands/discardChanges.ts:56

  • When no workspace is resolved, logWarning is called with message passed only via the telemetry data object. This means no warning is shown in the VS Code UI (logger only shows UI messages when the message argument is provided), so the command silently does nothing from the user's perspective.
      if (!selectedWorkspace) {
        const message = getAllWorkspaces().length > 0
          ? `No workspace selected. ${DISCARD_OPERATION} operation cancelled.`
          : `No workspace found for ${DISCARD_OPERATION} operation`;
        logger.logWarning(TelemetryEventsKeys.SyncWorkspaceCancel, undefined, { message });

Comment thread src/CopilotStudio.Sync/WorkspaceSynchronizer.cs
Copilot AI review requested due to automatic review settings July 23, 2026 13:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/commands/discardChanges.ts:57

  • When no workspace is resolved, the command logs with message only in the telemetry data object (logWarning(..., undefined, { message })). Since logger only shows a VS Code UI notification when the message parameter is provided, this results in no user-visible feedback. Also, this differs from the existing sync commands, which treat "no workspace found" as an error and "no workspace selected" as a cancel with a visible message.
      if (!selectedWorkspace) {
        const message = getAllWorkspaces().length > 0
          ? `No workspace selected. ${DISCARD_OPERATION} operation cancelled.`
          : `No workspace found for ${DISCARD_OPERATION} operation`;
        logger.logWarning(TelemetryEventsKeys.SyncWorkspaceCancel, undefined, { message });
        return;

src/CopilotStudio.Sync/WorkspaceSynchronizer.cs:4256

  • DiscardSkippedChange.Path is currently populated with Path.GetFileName(change.Uri), which drops the relative directory (e.g., knowledge/files/foo.pdf becomes just foo.pdf). The VS Code command surfaces skipped items to the user so they can restore them with Get; without the relative path this can be ambiguous and harder to locate (and loses context for similarly-named files).
    private static DiscardSkippedChange CreateDiscardSkippedChange(Change change, string reason) => new()
    {
        SchemaName = change.SchemaName,
        Path = Path.GetFileName(change.Uri),
        Reason = reason,
    };

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/CopilotStudio.Sync/WorkspaceSynchronizer.cs:4256

  • CreateDiscardSkippedChange reports Path as Path.GetFileName(change.Uri), which drops the relative directory. This makes the user-facing skipped-items list ambiguous (e.g., multiple workflow.json / metadata.yml files under different workflow folders) and harder to act on when the warning says the items can be restored with Get.
    private static DiscardSkippedChange CreateDiscardSkippedChange(Change change, string reason) => new()
    {
        SchemaName = change.SchemaName,
        Path = Path.GetFileName(change.Uri),
        Reason = reason,
    };

src/vscode-extensions/microsoft-powerplatformlang-extension/client/src/tests/host/discardChanges.test.ts:22

  • The PR description lists a new pure module client/src/sync/discardChanges.ts and "12 unit tests" in this host test file, but the branch only contains client/src/commands/discardChanges.ts and this test file currently has 2 formatting/PII tests. Please either update the PR description to match the actual implementation/testing, or add the missing module/tests if they were intended.
import * as assert from 'node:assert';
import { describe, test } from 'node:test';

import { formatDiscardErrorMessage, formatDiscardResultMessage } from '../../commands/discardChanges';

describe('discardChanges: telemetry', () => {
	test('agent names are marked as PII while remaining visible to the user', () => {
		const message = formatDiscardResultMessage('Contoso Support', {
			restored: 1,
			deleted: 0,
			skipped: [],
		});

		assert.match(message, /<pii>Contoso Support<\/pii>/);
	});

	test('non-Error rejections retain their message and PII protection', () => {
		const message = formatDiscardErrorMessage('request rejected');

		assert.match(message, /<pii>request rejected<\/pii>/);
	});
});

anderson-joyle and others added 5 commits July 23, 2026 11:54
Adds a "Discard changes" title-bar button (arrow-back icon) to the Agent
Changes view that reverts all of the selected agent's local changes back
to their last-synced baseline, entirely offline. Resolves #343.

Behavior:
- Enabled only when local changes exist and no sync is running
  (mcs.agentChangesView.hasLocalChanges && !mcs.isSyncing).
- Prompts for the agent when more than one workspace is connected, then
  shows a modal confirmation with the local-change count.
- Per change: locally added files are deleted; locally updated/deleted
  files are rewritten from the cached (getCachedFile) baseline.
- Binary changes the text-only cache cannot restore (the agent icon and
  knowledge file attachments) are skipped and reported so the user can
  restore them with Get.

The revert logic lives in a pure, dependency-injected module
(sync/discardChanges.ts) with host tests; the command module wires the
real language-server cache read and workspace filesystem, holds the sync
busy state for the operation, and refreshes the Agent Changes tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 29babeb3-e4ad-4c53-a6e8-60b0a5dbbe38
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Move discard restoration into the Sync/LSP layer so shared manifests,
workflow pairs, CLI connection references, and missing directories are
handled from the cached projection without authenticated refresh work.
Preserve unresolved changes on refresh failures and redact agent names in
telemetry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35147f40-12f6-448f-81cf-915987e9414c
Run the discard integration test against a temporary workspace copy so it
cannot mutate shared test output or race workspace compiler tests. Assert the
restored topic outcome without depending on unrelated baseline projection
change counts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35147f40-12f6-448f-81cf-915987e9414c
Show picker cancellation and missing-workspace failures in the VS Code UI,
matching existing sync command behavior. Preserve useful text when a command
rejects with a non-Error value while keeping telemetry PII redaction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35147f40-12f6-448f-81cf-915987e9414c
Copilot AI review requested due to automatic review settings July 23, 2026 14:54
@anderson-joyle
anderson-joyle force-pushed the andersonf/discard-changes branch from e821ff2 to 0a51e2a Compare July 23, 2026 14:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/CopilotStudio.Sync/WorkspaceSynchronizer.cs:4256

  • DiscardLocalChanges reports skipped items with Path = Path.GetFileName(change.Uri), which drops the relative directory (e.g. infrastructure/connections/foo.sync.yaml becomes foo.sync.yaml). This makes the user-facing message ambiguous and less actionable, especially when multiple files share a name across folders. Use the full relative change.Uri instead.
    private static DiscardSkippedChange CreateDiscardSkippedChange(Change change, string reason) => new()
    {
        SchemaName = change.SchemaName,
        Path = Path.GetFileName(change.Uri),
        Reason = reason,
    };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discard changes option

3 participants