diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9b74cce7..e9fec793f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,11 @@ Changes to Calva.
## [Unreleased]
+- [Add flare handler and webview](https://github.com/BetterThanTomorrow/calva/issues/2679)
+
## [2.0.482] - 2024-12-03
-- Fix: [Added 'replace-refer-all-with-alias' & 'replace-refer-all-with-refer' actions to calva.](https://github.com/BetterThanTomorrow/calva/issues/2667)
+- Fix: [Added 'replace-refer-all-with-alias' & 'replace-refer-all-with-refer' actions to calva.](https://github.com/BetterThanTomorrow/calva/issues/2667)
## [2.0.481] - 2024-10-29
diff --git a/docs/site/flares.md b/docs/site/flares.md
new file mode 100644
index 000000000..e505cc3d8
--- /dev/null
+++ b/docs/site/flares.md
@@ -0,0 +1,129 @@
+---
+title: Calva Flares Documentation
+description: Learn how to use Calva Flares to enhance your development experience.
+---
+
+# Calva Flares
+
+Flares are a mechanism in Calva that allow the REPL server (where your Clojure code runs) to send requests to the REPL client (your Calva IDE) to trigger specific behaviors.
+They bridge the gap between user-space code and IDE features, enabling dynamic and interactive workflows.
+
+Flares are special values that, when encountered by the IDE, prompt it to perform predefined actions such as rendering HTML, showing notifications, or visualizing data.
+
+> **TIP:**
+> Don't put flares in your project code.
+> Flares are IDE specific, so they should be created by tooling code.
+> Flares will be created when invoking a tool or custom action from your IDE.
+
+## How to Create Flares
+
+Flares take the form of a map with a single key-value pair.
+The key specifies the flare is for Calva `:calva/flare`, while the value contains the details of the request.
+
+```clojure
+{:calva/flare {:type :info
+ :message "Congratulations, you sent a flare!"}}
+```
+
+- **Key**: `:calva/flare` – Identifies this as a flare for Calva.
+- **Value**: A map defining the specific request, such as showing a message, rendering HTML, or invoking an IDE command.
+
+Here’s a flare to display a HTML greeting:
+
+```clojure
+{:calva/flare {:type :webview
+ :html "
Hello, Calva!
",
+ :title "Greeting"}}
+```
+
+## Typical Uses of Flares
+
+Flares enhance your development experience by enabling IDE features directly from user-space code. Below are common use cases:
+
+### 1. Data Visualization
+
+Used with tools like Clay, you can render HTML, SVG, or other visual elements directly in the IDE:
+
+```clojure
+(calva.clay/webview $current-form $file)
+```
+
+Produces a flare:
+
+```clojure
+{:calva/flare {:type :webview
+ :url "https://localhost:1971"}}}
+```
+
+Enabling you to create a custom action "Send to Clay" to visualize Kindly annotated visualizations.
+
+### 2. Notifications
+
+Test results or task completion:
+
+```clojure
+{:calva/flare {:type :info
+ :message "Tests Passed 🎉"}}}
+```
+
+### 3. VSCode Commands
+
+Developers can define custom workflows or integrate with external tools:
+
+```clojure
+{:calva/flare {:type :command
+ :command "workbench.action.toggleLightDarkThemes" }}
+```
+
+### 4. Debugging and Status Updates
+
+Send contextual data back to the IDE for live updates or inline annotations.
+
+## Why Use Flares?
+
+Flares enhance the feedback loop between your code and the IDE, reducing context switching and enabling a more interactive development experience.
+
+### Key Benefits
+
+- **Immediate Feedback**: See results, warnings, or visualizations inline as part of your workflow.
+- **Custom Workflows**: Tailor IDE behavior to suit your needs using tools like Clay or by creating custom flares.
+- **IDE-Specific Features**: Leverage the unique capabilities of Calva while maintaining the flexibility to extend or modify functionality.
+
+## Allowed Commands
+
+Calva supports a predefined set of flare actions and commands that are allowed.
+If you want to access other commands, enable them in settings.
+
+Be mindful that flares are values, and values may originate from sources outside of your code.
+For example if you read a value out of a logfile into a map, it could be a flare!
+
+If you want to experiment with new flare handlers, consider using Joyride to inject them.
+
+## Flare Reference
+
+All flares may have a `:then` in them which is a fully qualified symbol of a function to invoke with the result of the processed flare.
+
+| type | keys |
+|------|-----|
+| `:info` | `:message`, `items` |
+| `:warn` | `:message`, `items` |
+| `:error` | `:message`, `items` |
+| `:webview` | `:title`, `:html`, `:url`, `:key` |
+| `:command` | `:command`, `:args` |
+
+VSCode commands aren't comprehensively documented, you'll have to discover their ids and arguments with some guesswork and research.
+
+## Recap of how to use Flares
+
+To start using flares in your Calva environment, follow these steps:
+
+1. Ensure you have the latest version of Calva installed.
+2. Open your Clojure project in Calva.
+3. Connect to your REPL.
+4. Use the provided examples to experiment with flares from the REPL.
+5. Create custom user actions that trigger flares.
+6. Request toolmakers provide flare producing actions.
+
+Flares enhance our development experience in Calva.
+Whether you're visualizing data or creating custom workflows, they open up more possibilities for interactive development.
+Let us know how you’re using flares, and share your feedback to make this feature even better.
diff --git a/mkdocs.yml b/mkdocs.yml
index fc8688eff..5bd3ef1c8 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -64,6 +64,7 @@ nav:
- fiddle-files.md
- connect-sequences.md
- custom-commands.md
+ - flares.md
- refactoring.md
- notebooks.md
- clojuredocs.md
diff --git a/src/evaluate.ts b/src/evaluate.ts
index 7806aa6f7..98f8015cc 100644
--- a/src/evaluate.ts
+++ b/src/evaluate.ts
@@ -19,6 +19,7 @@ import * as output from './results-output/output';
import * as inspector from './providers/inspector';
import { resultAsComment } from './util/string-result';
import { highlight } from './highlight/src/extension';
+import * as flareHandler from './flare-handler';
let inspectorDataProvider: inspector.InspectorDataProvider;
@@ -139,6 +140,8 @@ async function evaluateCodeUpdatingUI(
result = value;
+ flareHandler.inspect(value, (code) => evaluateCodeUpdatingUI(code, options, selection));
+
if (showResult) {
inspectorDataProvider.addItem(value, false, `[${session.replType}] ${ns}`);
output.appendClojureEval(value, { ns, replSessionType: session.replType }, async () => {
diff --git a/src/flare-handler.ts b/src/flare-handler.ts
new file mode 100644
index 000000000..299539b04
--- /dev/null
+++ b/src/flare-handler.ts
@@ -0,0 +1,129 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import * as webview from './webview';
+import { parseEdn } from '../out/cljs-lib/cljs-lib';
+
+type EvaluateFunction = (code: string) => Promise;
+
+type InfoRequest = { type: 'info'; message: string; items?: string[]; then?: string };
+type WarnRequest = { type: 'warn'; message: string; items?: string[]; then?: string };
+type ErrorRequest = { type: 'error'; message: string; items?: string[]; then?: string };
+type WebviewRequest = {
+ type: 'webview';
+ title?: string;
+ html?: string;
+ url?: string;
+ key?: string;
+ column?: vscode.ViewColumn;
+ opts?: any;
+ then?: string;
+};
+type CommandRequest = { type: 'command'; command: string; args?: string[]; then?: string };
+type CommandsRequest = { type: 'commands'; then?: string };
+type DefaultRequest = { type: 'default'; [key: string]: any };
+
+type ActRequest =
+ | InfoRequest
+ | WarnRequest
+ | ErrorRequest
+ | WebviewRequest
+ | CommandRequest
+ | CommandsRequest
+ | DefaultRequest;
+
+function callback(p, thensym: string, evaluate: EvaluateFunction): void {
+ if (thensym) {
+ p.then((x: any) => evaluate(`((resolve '${thensym}) ${JSON.stringify(x)})`)).error((e) => {
+ // TODO: I haven't seen this work yet, test it somehow
+ vscode.window.showErrorMessage('failed callback: ' + e);
+ console.log('OH NO', e);
+ });
+ }
+}
+
+function parseArg(arg: string) {
+ try {
+ // Try to parse JSON (handles numbers, booleans, arrays, objects)
+ return JSON.parse(arg);
+ } catch {
+ // If parsing fails, check if it's a valid file path and convert to URI
+ if (path.isAbsolute(arg) || arg.startsWith('./') || arg.startsWith('../')) {
+ return vscode.Uri.file(arg);
+ }
+ // Return the argument as a string if it's not a valid file path
+ return arg;
+ }
+}
+
+const actHandlers: Record void> = {
+ default: (request: DefaultRequest, evaluate: EvaluateFunction) => {
+ void vscode.window.showErrorMessage(
+ `Unknown flare request type: ${JSON.stringify(request.type)}`
+ );
+ },
+ info: ({ message, items = [], then }: InfoRequest, evaluate: EvaluateFunction) => {
+ const p = vscode.window.showInformationMessage(message, ...items);
+ callback(p, then, evaluate);
+ },
+ warn: ({ message, items = [], then }: WarnRequest, evaluate: EvaluateFunction) => {
+ const p = vscode.window.showWarningMessage(message, ...items);
+ callback(p, then, evaluate);
+ },
+ error: ({ message, items = [], then }: ErrorRequest, evaluate: EvaluateFunction) => {
+ const p = vscode.window.showErrorMessage(message, ...items);
+ callback(p, then, evaluate);
+ },
+ webview: ({ then, ...request }: WebviewRequest, evaluate: EvaluateFunction) => {
+ const p = webview.show(request);
+ // TODO: p here is a panel, not a promise, so then clause will fail
+ callback(p, then, evaluate);
+ },
+ // TODO: handling commands sounds like fun, but there aren't actually many that are useful afaik,
+ // there are some that could be considered dangerous
+ // there are so many and they aren't documented anywhere afaik
+ command: ({ command, args = [], then }: CommandRequest, evaluate: EvaluateFunction) => {
+ // TODO: args might not be an array like we expect
+ const parsedArgs = (args || []).map(parseArg);
+ console.log('ARGS', parsedArgs);
+ const p = vscode.commands.executeCommand(command, ...parsedArgs);
+ callback(p, then, evaluate);
+ },
+ commands: ({ then }: CommandsRequest, evaluate: EvaluateFunction) => {
+ const p = vscode.commands.getCommands();
+ callback(p, then, evaluate);
+ },
+};
+
+function act(request: ActRequest, evaluate: EvaluateFunction): void {
+ const handler = actHandlers[request.type] || actHandlers.default;
+ handler(request, evaluate);
+}
+
+function isFlare(x: any): boolean {
+ return typeof x === 'object' && x !== null && 'calva/flare' in x;
+}
+
+function getFlareRequest(flare: Record): any {
+ return Object.values(flare)[0];
+}
+
+export function inspect(edn: string, evaluate: EvaluateFunction): any {
+ console.log('INSPECT', edn);
+ if (
+ edn &&
+ typeof edn === 'string' &&
+ (edn.startsWith('{:calva/flare') || edn.startsWith('#:calva{:flare'))
+ ) {
+ try {
+ const x = parseEdn(edn);
+ console.log('PARSED', x);
+ if (isFlare(x)) {
+ console.log('FLARE', x);
+ const request = getFlareRequest(x);
+ act(request, evaluate);
+ }
+ } catch (e) {
+ console.log('ERROR: inspect failed', e);
+ }
+ }
+}
diff --git a/src/webview.ts b/src/webview.ts
new file mode 100644
index 000000000..c6d0ac2f8
--- /dev/null
+++ b/src/webview.ts
@@ -0,0 +1,75 @@
+import * as vscode from 'vscode';
+
+const defaultOpts = {
+ enableScripts: true,
+};
+
+// keep track of open webviews that have a key,
+// so that they can be updated
+const webviewRegistry: Record = {};
+
+function setHtml(panel: vscode.WebviewPanel, title: string, html: string): vscode.WebviewPanel {
+ if (panel.title !== title) {
+ panel.title = title;
+ }
+ if (panel.webview.html !== html) {
+ panel.webview.html = html;
+ }
+ panel.reveal();
+ return panel;
+}
+
+function urlInIframe(uri: string): string {
+ return `
+
+
+
+
+
+
+
+`;
+}
+
+export function show({
+ title = 'Webview',
+ html,
+ url,
+ key,
+ column = vscode.ViewColumn.Beside,
+ opts = defaultOpts,
+}: {
+ title?: string;
+ html?: string;
+ url?: string;
+ key?: string;
+ column?: vscode.ViewColumn;
+ opts?: typeof defaultOpts;
+}): vscode.WebviewPanel {
+ const finalHtml = url ? urlInIframe(url) : html || '';
+ if (key) {
+ const existingPanel = webviewRegistry[key];
+ if (existingPanel) {
+ return setHtml(existingPanel, title, finalHtml);
+ }
+ }
+
+ const panel = vscode.window.createWebviewPanel('calva-webview', title, column, opts);
+ setHtml(panel, title, finalHtml);
+
+ if (key) {
+ webviewRegistry[key] = panel;
+ panel.onDidDispose(() => delete webviewRegistry[key]);
+ }
+
+ return panel;
+}
+
+// TODO: register a command for creating a webview, because why not?