Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
160249 stories
·
33 followers

368: Push, Pull, and Pray: GitHub Outage Strikes

1 Share




Download audio: https://episodes.castos.com/5e2d2c4b117f29-10227663/2590101/c1e-rodobo6d41sd3d64-gpd7k5d0sxzd-eixmv8.mp3
Read the whole story
alvinashcraft
17 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

5 Rules for AI Writing

1 Share
From: AIDailyBrief
Duration: 22:57
Views: 2,316

Stanley Druckenmiller's obviously AI-written Wall Street Journal op-ed sparked a fierce debate about disclosure, plagiarism, and whether the tool used matters more than the thinking behind it. NLW lays out five rules for AI writing, including why the purity test will fade but the quality test never will, and why perceived laziness undermines the argument itself. Also covered: a practical crib sheet for emails, meeting notes, strategy memos, social copy, marketing, and op-eds.

The AI Daily Brief helps you understand the most important news and discussions in AI.
Subscribe to the podcast version of The AI Daily Brief wherever you listen: https://pod.link/1680633614
Get it ad free at http://patreon.com/aidailybrief
Learn more about the show https://aidailybrief.ai/

Read the whole story
alvinashcraft
22 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

PPP 521 | Neuroscience-Based Principles to Resolve Conflicts, with Dr. Jeremy Pollack

1 Share

Summary

In this episode, Andy sits down with Dr. Jeremy Pollack, a social-organizational psychologist and founder of Pollack Peacebuilding Systems, author of Wired for Peace: Using 7 Neuroscience-Based Principles to Resolve Conflicts. Jeremy looks at conflict through the lens of the brain and nervous system, which helps explain why disagreements escalate so quickly and why the fixes we reach for often make things worse.

Andy and Jeremy talk about why de-escalation has to come before problem-solving, since solutions and stress do not mix. Jeremy walks us through his Sapien Model of six basic psychological needs, giving us a far more useful lens than labeling a stakeholder difficult, defensive, or resistant. You will hear how memory and prediction shape the way we read the person in front of us, what a team learns when its leader consistently avoids conflict, and why changing our experience of conflict requires new experiences, not just new beliefs. Jeremy also shares how this shows up at home, in marriage and parenting.

If you're looking for a practical, evidence-based way to handle the conflicts that come with leading, this episode is for you!

Sound Bites

  • "The most fruitful relationships from what I've seen at work, at home, et cetera, are those that can weather storms without the real fear of abandonment."
  • "That was pretty interesting to me to see how much of the science is telling us that, like, our brain is mostly a predictive machine."
  • "Anytime we're in an escalated or emotionally charged state, there's something going on in our brains, usually subconsciously, that's tracking a threat or an impediment to our basic needs."
  • "It's de-escalate first because solutions and stress just don't mix."
  • "What I'm feeling right now is not a result of what the person's saying. It's a result of the way my body is responding."
  • "I call it an experiential thumbprint. It's completely unique to you and no one else has your nervous system, and you don't have anyone else's."
  • "If we have a negative experience with someone and we go into a place of fear or pain or anger or something like that, the way that we see them going forward can change very, very quickly, and it's a lot harder to get us to unsee that."
  • "We access memories in edit mode. And so after we retrieve it, and when we write it back, it's not always quite the same thing, and that's always made me question not is that memory correct as much as is it completely correct."
  • "All conflict is an opportunity for learning."
  • "The threat of abandonment is the big thing to really watch for when we're having arguments."

Chapters

  • 00:00 Introduction
  • 02:27 Start of Interview
  • 02:35 The Family Cultures That Shaped How Jeremy Sees Conflict
  • 05:51 In Awe of How Much We Still Do Not Know About the Brain
  • 07:49 The Brain as a Prediction Machine
  • 09:55 The Salad Lady and What Sits Underneath Defensive Behavior
  • 12:45 An Introduction to the Sapien Model
  • 16:19 Why a Framework Beats a Label
  • 16:58 De-escalate First: Catching Yourself Before It Is Too Late
  • 20:54 When Our De-escalation Attempts Backfire
  • 22:43 Empathy Without Having to Agree
  • 25:03 How Memory Distorts the Person in Front of Us
  • 27:46 We Do Not Access Memory as Read-Only
  • 30:45 Joe, the Conflict-Avoidant CEO, and What a Team Learns
  • 33:03 Taking Inventory When Nobody Pushes Back
  • 34:53 New Experiences of Conflict, Not Just New Beliefs
  • 38:08 Tell Me More: Creating a Gap Between Stimulus and Response
  • 40:31 Peace Starts in Small Ways
  • 42:49 Interrupting the Pavlovian Pattern
  • 44:19 Bringing It Home: Marriage, Parenting, and Attachment
  • 47:17 End of Interview
  • 47:57 Andy Comments After the Interview
  • 51:57 Outtakes

Learn More

You can learn more about Jeremy and his work at CoachJeremyPollack.com.

For more learning on this topic, check out:

  • Episode 371 with Amanda Ripley. Amanda is an amazing writer, and her book High Conflict is a great follow-up to today's discussion.
  • Episode 148 with Dan Shapiro. The title of his book Negotiating the Nonnegotiable sounds like it's about negotiating, but it's really about navigating the conflicts that suck the life out of us.
  • Episode 61 with Mike Roberto. That discussion was over 15 years ago, and there are ideas from it that Andy still uses to this day.

Chat with PMeLa

You can chat directly with PMeLa, the podcast's AI persona, to get episode recommendations and answers to your project management and leadership questions. Visit PeopleAndProjectsPodcast.com/PMeLa to chat with her.

Join Us for LEAD52

I know you want to be a more confident leader–that's why you listen to this podcast. LEAD52 is a global community of people like you who are committed to transforming their ability to lead and deliver. It's 52 weeks of leadership learning, delivered right to your inbox, taking less than 5 minutes a week. And it's all for free. Learn more and sign up at GetLEAD52.com. Thanks!

Thank you for joining me for this episode of The People and Projects Podcast!

Talent Triangle: Power Skills

Topics: Leadership, Project Management, Conflict Resolution, Neuroscience, De-escalation, Psychological Safety, Emotional Regulation, Memory, Stakeholder Management, Team Culture, Accountability, Parenting

The following music was used for this episode:

Music: On Point by Steven O'Brien
License (CC BY 4.0): https://filmmusic.io/standard-license

Music: Energetic Drive Indie Rock by WinnieTheMoog
License (CC BY 4.0): https://filmmusic.io/standard-license





Download audio: https://traffic.libsyn.com/secure/peopleandprojectspodcast/521-JeremyPollack.mp3?dest-id=107017
Read the whole story
alvinashcraft
28 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

KONVO: Create Clearer Technical Content with AI

1 Share
KONVO is an agent skill that helps you create clearer technical content with AI as an editor and collaborator, without replacing your voice.
Read the whole story
alvinashcraft
38 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Mechanically Eliminating FutureBuilder & StreamBuilder: Universal Signal, Future, and Stream Adapters in BlocSignal

1 Share

Making the Migration from In-View Asynchrony to Synchronous State Management Truly Mechanical

After our recent discussions on why FutureBuilder and StreamBuilder are architectural anti-patterns when placed inside Flutter widget trees, I started thinking: how can we make it even easierβ€”even completely mechanicalβ€”to convert from a FutureBuilder or StreamBuilder to a BlocSignalBuilder?

Every Flutter developer knows the history. Years ago, I recorded a video breaking down the hidden traps of placing asynchronous builders in UI views: Why you shouldn't put FutureBuilder in your build method. Even the original official Flutter video on FutureBuilder initially instantiated the network future directly inside the build() method, until I filed an issue to get it corrected (which is why the official Flutter YouTube video still proudly bears "Take 2" on its clapperboard!).

The fundamental issue has never been that developers want bad architecture. The issue was friction.

FutureBuilder was simply the path of least resistance. To do it "properly" in traditional state management, developers had to create an entire BLoC or Cubit, declare separate Event and State classes (or union types), write boilerplate event handlers, wire asynchronous repository methods, manage subscription lifecycles, and inject everything into the widget tree.

With bloc_signals 1.1.0, that friction disappears completely.

We have introduced universal, symmetrical adapter extensions that allow any Dart Future, Stream, ReadonlySignal, or lifted primitive (value.$) to adapt into a synchronous BlocSignalBase container with a single method call.

🧭 The Universal Dual-Track Mental Model

When bridging asynchronous sources into synchronous state management, developers typically have one of two distinct intents:

  1. Raw Domain Values (T): You want raw domain objects (for example int, UserProfile, ThemeMode) with zero wrapper ceremony, and you have an immediate default or fallback value for frame 0.
  2. Rich Asynchronous Lifecycle States (AsyncState<T>): You want first-class lifecycle tracking (AsyncLoading β†’ AsyncData or AsyncError) with exhaustive pattern matching.

To make the API completely intuitive and predictable, BlocSignal adheres to the Universal Dual-Track Principle:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   BlocSignal Universal Adapter Matrix                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Method                        β”‚ Resulting Container Type               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ .toBlocSignal(...)            β”‚ BlocSignalBase<T> (Raw domain state)   β”‚
β”‚ .toAsyncBlocSignal(...)       β”‚ BlocSignalBase<AsyncState<T>> (Async)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Let us examine how each track works.

⚑ Track 1: Direct Raw Domain State (.toBlocSignal)

The .toBlocSignal(...) family strictly yields a BlocSignalBase<T> holding raw values of type T.

1. Adapting ReadonlySignal<T> & Lifted Primitives

Because signals in the signals_core graph always hold an immediate synchronous value, converting any signal to a BlocSignalBase requires no arguments:

// 1. Raw Signal -> BlocSignalBase<int>
final countSignal = signal(0);
final countBloc = countSignal.toBlocSignal();

// 2. Computed Expression -> BlocSignalBase<bool>
final isValidSignal = computed(() => username().isNotEmpty && password().length >= 8);
final authBloc = isValidSignal.toBlocSignal();

// 3. Lifted Primitive -> BlocSignalBase<double>
final priceBloc = 49.99.$.toBlocSignal();

The resulting SignalBlocSignal<T> subscribes directly to the underlying signal graph and emits synchronous state transitions whenever the source signal mutates. Calling close() on the container automatically unsubscribes the effect.

2. Adapting Future<T> with a Required Initial State

Futures do not have a synchronous value on frame 0. Therefore, when requesting raw domain values of type T, Future.toBlocSignal() requires an explicit initialState::

// Future<User> -> BlocSignalBase<User>
final userBloc = api.fetchUserProfile(userId).toBlocSignal(
  initialState: User.anonymous(),
);

On frame 0, userBloc.stateValue is immediately User.anonymous(). As soon as the future completes, it emits the resolved User synchronously into the state machine. If the future throws, the error is routed safely to onError() and the container's registered BlocObserver.

3. Adapting Stream<T> with a Required Initial State

Similarly, any multi-value Stream<T> (such as a WebSocket, sensor stream, or legacy BLoC/Redux stream) adapts with a required initial value:

// Stream<int> -> BlocSignalBase<int>
final counterBloc = myStream.toBlocSignal(initialState: 0);

// Redux Store -> BlocSignalBase<AppState>
final reduxBloc = store.onChange.toBlocSignal(
  initialState: store.state,
);

πŸ”„ Track 2: Rich Asynchronous State (.toAsyncBlocSignal)

When you do not have an initial fallback value and want first-class loading, data, and error states, .toAsyncBlocSignal() converts Future<T> and Stream<T> into a BlocSignalBase<AsyncState<T>>.

// 1. Future<UserProfile> -> BlocSignalBase<AsyncState<UserProfile>>
final userProfileBloc = api.fetchUserProfile(userId).toAsyncBlocSignal();

// 2. Stream<List<ChatMessage>> -> BlocSignalBase<AsyncState<List<ChatMessage>>>
final chatBloc = chatSocket.messageStream.toAsyncBlocSignal();

The state lifecycle is completely automatic:

  • Frame 0: Starts synchronously in AsyncLoading().
  • On Resolution / Emission: Synchronously transitions to AsyncData(value).
  • On Failure / Error: Synchronously transitions to AsyncError(error, stackTrace).

πŸ’‘ Bonus Win: Goodbye to Async Singleton Boilerplate (Inherently Lazy State)

Think for a moment about how Flutter applications traditionally handled an asynchronous resource with a fallback default. You often had to write 30+ lines of fragile singleton and ChangeNotifier plumbing:

// 😩 The old way: Fragile singleton boilerplate with mutable async init
class UserManager extends ChangeNotifier {
  UserManager._() { _init(); }
  static final instance = UserManager._();

  User _user = User.anonymous();
  User get user => _user;
  bool _loading = true;

  Future<void> _init() async {
    try {
      _user = await api.fetchUser();
    } finally {
      _loading = false;
      notifyListeners();
    }
  }
}

Compare that ceremony with BlocSignal:

// πŸͺ„ The BlocSignal way: 1 declarative expression
final userBloc = api.fetchUserProfile(userId).toBlocSignal(
  initialState: User.anonymous(),
);

Even better: it is inherently lazy.

Because top-level variables and late fields in Dart are evaluated on demand:

  • Zero Application Startup Cost: Defining top-level or module-level state containers does not execute during void main(). If the user never navigates to that screen or feature, the network request is never fired and no resources are wasted.
  • Signal-Graph Laziness: In .toAsyncBlocSignal(), the underlying FutureSignal defaults to lazy: true, meaning evaluation only triggers when an active UI consumer or test actually reads the signal.
  • No async main() Initialization Bottlenecks: You no longer need to stall app startup with await initAllServices() before runApp(). Features initialize on demand on the exact frame they are requested.

The Mindblowing Part: Reactive Dependency Chains

What if userId is dynamic (for example derived from an auth session, a route parameter, or a dropdown selection)?

You can wire it directly into the reactive dependency graph:

// 1. Upstream reactive dependency:
final selectedUserId = signal<String>('42');

// 2. Downstream async pipeline β€” automatically refetches when selectedUserId changes:
final userProfileBloc = futureSignal(
  () => api.fetchUserProfile(selectedUserId()),
).toBlocSignal();

When selectedUserId.value = '99' changes anywhere in your app, the futureSignal automatically detects the dependency change, refetches the profile, and emits state transitions through userProfileBloc directly into your BlocSignalBuilderβ€”with zero manual listeners, zero event dispatch plumbing, and zero didUpdateWidget lifecycle gymnastics.

πŸ› οΈ Step-by-Step Conversion 1: Eliminating FutureBuilder

Let us take a real-world example of FutureBuilder and see how clean and non-scary it is to replace it.

The Legacy Anti-Pattern (FutureBuilder)

Here is the classic code that lives in thousands of Flutter codebases:

// ❌ ANTI-PATTERN: Trapping async I/O inside the widget tree
class UserProfilePage extends StatelessWidget {
  const UserProfilePage({super.key, required this.userId});
  final String userId;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User Profile')),
      body: FutureBuilder<UserProfile>(
        // ⚠️ DANGER: Re-invoked on every parent rebuild!
        future: apiClient.fetchUserProfile(userId),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          }
          if (snapshot.hasData) {
            final user = snapshot.data!;
            return ProfileView(user: user);
          }
          return const SizedBox.shrink();
        },
      ),
    );
  }
}

The Clean, Non-Scary Refactor

Instead of wrapping the view in complex provider nesting, we simply pass the adapted container directly into our view widget via constructor parameters (clean prop-drilling):

// 1. At the route boundary, controller, or parent widget:
final userProfileBloc = apiClient.fetchUserProfile(userId).toAsyncBlocSignal();

// 2. Pass it directly to the view:
UserProfilePage(userProfileBloc: userProfileBloc);

Our presentation widget becomes a pure, lightweight StatelessWidget:

// βœ… BLOCSIGNAL PATTERN: Clean, synchronous projection of state
class UserProfilePage extends StatelessWidget {
  const UserProfilePage({
    super.key, 
    required this.userProfileBloc,
  });

  final BlocSignalBase<AsyncState<UserProfile>> userProfileBloc;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User Profile')),
      body: BlocSignalBuilder(
        bloc: userProfileBloc,
        builder: (context, state) => switch (state) {
          AsyncData(:final value) => ProfileView(user: value),
          AsyncLoading() => const Center(child: CircularProgressIndicator()),
          AsyncError(:final error) => Center(child: Text('Error: $error')),
        },
      ),
    );
  }
}

Look at how clear that is!

There are no nested InheritedWidget provider trees or noisy generic boilerplate. Type inference handles the builder automatically.

Note on Dependency Injection: Because BlocSignal containers are pure Dart objects with zero framework baggage, you can supply them using whichever DI strategy fits your project: simple constructor prop-drilling (as shown above), top-level globals, service locators (such as GetIt), BlocSignalProvider, or Riverpod adapters.

What You Just Won:

  1. Zero Accidental Refetches: If the software keyboard opens, an animation ticks, or the device rotates, build() re-runs synchronously without dispatching a new HTTP request.
  2. Exhaustive Safety: The compiler verifies that you handled AsyncLoading, AsyncData, and AsyncError. No more forgetting edge cases or dealing with force unwraps (snapshot.data!).
  3. 0ms Synchronous Testability: In unit tests, you can test userProfileBloc with blocSignalTest in 0ms without mocking microtask timers or calling tester.pumpAndSettle().

🌊 Step-by-Step Conversion 2: Eliminating StreamBuilder

Now let us look at StreamBuilder.

The Legacy Anti-Pattern (StreamBuilder)

// ❌ ANTI-PATTERN: Stream subscription lifecycle entangled with widget tree
class DeviceTemperatureWidget extends StatelessWidget {
  const DeviceTemperatureWidget({super.key, required this.sensorService});
  final SensorService sensorService;

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<double>(
      stream: sensorService.temperatureStream,
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return const Text('Reading sensor...');
        }
        return Text('${snapshot.data!.toStringAsFixed(1)}Β°C');
      },
    );
  }
}

The Clean Refactor with .toBlocSignal()

If you have a sensible initial value (for example 0.0), adapt the stream directly to a raw domain container and pass it straight into your widget:

// βœ… OPTION A: Raw Domain State with explicit initial value
class DeviceTemperatureWidget extends StatelessWidget {
  const DeviceTemperatureWidget({
    super.key, 
    required this.temperatureBloc,
  });

  final BlocSignalBase<double> temperatureBloc;

  @override
  Widget build(BuildContext context) {
    return BlocSignalBuilder(
      bloc: temperatureBloc,
      builder: (context, temperature) {
        return Text('${temperature.toStringAsFixed(1)}Β°C');
      },
    );
  }
}

Or, if you prefer rich async lifecycle tracking:

// βœ… OPTION B: Rich Async Lifecycle State
class DeviceTemperatureWidget extends StatelessWidget {
  const DeviceTemperatureWidget({
    super.key, 
    required this.temperatureBloc,
  });

  final BlocSignalBase<AsyncState<double>> temperatureBloc;

  @override
  Widget build(BuildContext context) {
    return BlocSignalBuilder(
      bloc: temperatureBloc,
      builder: (context, state) => switch (state) {
        AsyncData(:final value) => Text('${value.toStringAsFixed(1)}Β°C'),
        AsyncLoading() => const Text('Reading sensor...'),
        AsyncError(:final error) => Text('Sensor offline: $error'),
      },
    );
  }
}

πŸ§ͺ Testing: From Async Pump Hell to 0ms Determinism

Consider the difference in testability.

Testing a FutureBuilder Widget

// ⚠️ Fragile, slow, timer-dependent
testWidgets('renders user profile', (tester) async {
  await tester.pumpWidget(UserProfilePage(
    userProfileBloc: Future.value(mockUser).toAsyncBlocSignal(),
  ));

  // Instant synchronous verification of the initial loading state:
  expect(find.byType(CircularProgressIndicator), findsOneWidget);

  // Settle completion cleanly:
  await tester.pump();
  expect(find.text('Alice'), findsOneWidget);
});

Testing the BlocSignal Adapter Unit

Because BlocSignal operates synchronously with 0ms microtask delays, testing state transitions is declarative and instantaneous:

// βœ… Fast, synchronous, 100% deterministic
blocSignalTest<SignalBlocSignal<AsyncState<String>>, AsyncState<String>>(
  'emits AsyncLoading then AsyncData on future completion',
  build: () => Future.value('Alice').toAsyncBlocSignal(),
  wait: const Duration(milliseconds: 10),
  expect: () => [
    isA<AsyncLoading<String>>(),
    isA<AsyncData<String>>().having((s) => s.value, 'value', 'Alice'),
  ],
);

🌌 Like the Force: Binding the Galaxy Together

In modern application engineering, you will inevitably interact with diverse reactive abstractions:

  • Third-party SDKs that return Future<T>.
  • Hardware APIs and WebSockets that stream Stream<T>.
  • Reactive view models built with Signal<T> or Computed<T>.
  • Classic legacy state stores built with BLoC or Redux.

BlocSignal does not force you to rewrite your entire data layer or choose between the discipline of unidirectional data flow and the speed of fine-grained signals.

With .toBlocSignal() and .toAsyncBlocSignal(), every asynchronous stream, future, and signal in your architecture adapts seamlessly into a synchronous, predictable, 0ms reactive state machine.

Try out bloc_signals 1.1.0 today:

dependencies:
  bloc_signals: ^1.1.0
  bloc_signals_flutter: ^1.1.0

Happy coding!

Read the whole story
alvinashcraft
48 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Lore for Visual Studio: Source control without leaving the IDE

1 Share

Lore is a new source control system from Epic Games. It is designed for fast, distributed workflows, with local repositories that work offline and optional Lore servers when you need to collaborate.

Lore for Visual Studio brings that workflow directly into the IDE. It is also a practical example of what it takes to add a new source-control provider to Visual Studio: integrate onboarding, status, changes, commits, history, branches, authentication, and native diff and merge experiences into the places developers already use.

The extension lets you onboard a solution or folder, see file status in Solution Explorer, review changes, commit, manage branches, inspect history, and synchronize with a Lore server - all without dropping to a terminal.

A source-control provider that feels native

Visual Studio already provides the integration points a source-control provider needs to participate in the everyday development workflow. Lore for Visual Studio uses them to surface repository status in Solution Explorer, provide commands in solution and file context menus, and offer dedicated Changes and History tool windows. Rather than making developers learn a separate workflow, the extension makes Lore available alongside the IDE experiences they already know.

Start with a local repository

Open a solution or folder and select Add to Source Control in the Visual Studio status bar. Choose Lore, then create a local repository.

Local repositories are fully offline. You can commit, create branches, switch branches, and browse history while all content remains on your machine. When you are ready to collaborate, create or connect to a Lore server and push your work.

Add a solution to Lore

You can also clone an existing Lore repository directly from Solution Explorer. The extension checks that the server is reachable, clones the working tree, and records the remote configuration so push and pull are ready immediately.

Clone a Lore repository

Source-control status where you expect it

Once a solution is under Lore source control, status glyphs appear beside files in Solution Explorer. They refresh automatically as documents are saved, and the Lore Changes window provides a manual refresh command whenever you need one.

File context menus include the essentials:

  • Undo Changes to restore the committed version
  • Compare with Unmodified to view a native Visual Studio diff
  • Ignore and Untrack Item to add a file to .loreignore

Moves and renames performed in Solution Explorer are tracked automatically. The extension also uses Visual Studio's shared file-change service to detect changes made outside the IDE.

Review and commit changes

The Lore Changes window provides a dedicated commit experience modeled after Visual Studio's Git Changes window.

Lore Changes window

It presents changed files in a repository-root folder tree, including status badges for modified, added, deleted, renamed, and copied files.

You can select exactly which files belong in the next commit. Folder checkboxes are tri-state, the header checkbox selects everything at once, and your selection survives automatic refreshes while you write a commit message.

From the window, you can:

  • Commit selected files or commit and push in one operation
  • Amend the latest revision
  • Open native Visual Studio diffs for individual files
  • Discard a file's changes
  • Pull, push, and refresh from the toolbar

Browse history without touching the working tree

The Lore History tool window shows a read-only log for the repository associated with the open solution or folder.

History is paged, newest first, so even repositories with thousands of revisions remain responsive. Each revision shows its message, short hash, author, and local commit time. Select a revision to load only the files that it changed, then double-click a file to compare it with its direct parent in Visual Studio's native diff viewer.

The window stays in sync with the workspace and refreshes automatically after relevant Lore operations, such as commits, pulls, branch changes, and merges.

Branching and merging

The branch picker in the Lore Changes window lets you switch branches, create a branch from the current revision, and merge another branch into the one you are working on.

Lore is offline-first. When the needed content is already cached locally, switching and merging do not require a network round trip. If content has been evicted from the local cache, Lore fetches it from the configured server and clearly reports when the server is unavailable.

When a merge conflicts, Visual Studio's built-in three-way merge experience opens for each conflicted file. Resolve the conflicts, accept the results, and Lore completes the merge commit.

Sign in only when it matters

There is no separate sign-in command. If a server-backed operation such as cloning, pushing, or pulling requires authentication, Lore asks whether to sign in and opens the default browser. After sign-in completes, the original operation is retried.

That keeps the experience simple: work locally when you can, authenticate only when a server operation actually needs it.

Try it out

Lore for Visual Studio is available from the Visual Studio Marketplace.

The extension is open source, and contributions are welcome on GitHub. If you are building a source-control integration of your own, the project is also an example of how a provider can fit into Visual Studio's existing source-control workflow.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories