{"id":533,"date":"2026-10-01T11:12:09","date_gmt":"2026-10-01T18:12:09","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/aspire\/?p=533"},"modified":"2026-10-01T11:12:09","modified_gmt":"2026-10-01T18:12:09","slug":"aspire-terminal-support","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/aspire\/aspire-terminal-support\/","title":{"rendered":"Bringing rich terminal experiences to Aspire"},"content":{"rendered":"<p>Think back to the first code that you ever wrote. For a significant majority of developers that first code was probably a &#8220;hello, world&#8221; program written in your language of choice.<\/p>\n<p>After that there is a pretty good chance that you modified that same program to prompt for a name and change the message to &#8220;hello, {world}&#8221;. For me my very next program was a number guessing game where I guessed a number between 1 and 100. Simple programs that teach some input concepts around input, output, variables, and data types.<\/p>\n<p>The tools that you would have used for these relatively simple programs included some kind of text editor, a compiler or interpreter, and a <em>terminal emulator<\/em>.<\/p>\n<h2>If not terminal emulator, why not terminal emulator shaped?<\/h2>\n<p>Despite the general utility of terminal emulators, Aspire itself was not capable of rendering the output of programs in a fully featured terminal emulation experience. The console logs view would allow you to see standard error and standard output streams from your terminal program and provided some limited inline coloring.<\/p>\n<p>That isn&#8217;t to say that the console logs view in the Aspire dashboard isn&#8217;t useful. It is &#8211; if you just need to throw some text on the screen, it&#8217;s probably the best option. However, if you need more complete escape sequence processing, or your program needs an interactive terminal, then the new terminal features in Aspire are for you!<\/p>\n<h2>Introducing WithTerminal\/withTerminal<\/h2>\n<p>With the release of Aspire 13.5, we introduced a new API that allows developers to attach a terminal emulator to their resources. The usage pattern is very simple:<\/p>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Experimental<\/strong><\/p>The terminal APIs and <code>aspire terminal<\/code> commands described in this post are experimental. Their APIs and behavior may change in a future release.<\/div><\/p>\n<pre><code class=\"language-typescript\">\/\/ TypeScript\nvar repl = await builder.addExecutable(\"noderepl\", \"node\", \".\", [])\n                        .withTerminal();<\/code><\/pre>\n<p>A terminal emulator can be attached to projects, executables, and containers. When the <code>WithTerminal(...)<\/code>\/<code>withTerminal()<\/code> extension method is applied, a special annotation is added to the resource in the app model, telling DCP to launch that program with a pseudo-terminal.<\/p>\n<p>In the dashboard, if you navigate to where you would normally find console logs, you will find that the view is now an interactive terminal experience by default.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/noderepl-scaled.webp\" alt=\"Interactive REPL via withTerminal\" \/><\/p>\n<p>The emulator has the typical options you would find along the bottom of the screen, including font sizing and dimensions, along with a button to snap the dimensions to fit the current window size. We also have the ability to detach the terminal from the dashboard, which can be useful when you want to navigate around the rest of the dashboard while still being able to interact with the terminal.<\/p>\n<p>One of the interesting things that Aspire supports for these resource-bound terminal experiences is the ability to have two or more views pointed to the same PTY, both interactable and kept in sync.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/detachedwindow-scaled.webp\" alt=\"Dual headed terminal\" \/><\/p>\n<p>This can be useful if you have a multi-workspace environment and you switch between workspaces but want to be able to have the terminal visible on all of them. One of the consequences of this capability is that if I open a detached window and resize it the terminal embedded in the dashboard will resize to match although the size of the font will scale to make maximum use of the vertical or horizontal space available.<\/p>\n<p>We have tried to make the terminal experience as capable as possible. You should be able to run some of the more exotic terminal programs within the Aspire terminal if you choose to. For example, it should have no problem with tmux or any number of the terminal-based coding agents (useful if, as part of your product, you ship skills and want to test their behavior).<\/p>\n<p>In fact &#8211; the Aspire terminal has support for the Kitty Graphics Protocol and Sixels. Here are a few cute little examples:<\/p>\n<p><div style=\"width: 1920px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-533-1\" width=\"1920\" height=\"1080\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/kgpdemo-ezgif.com-resize-video.mp4?_=1\" \/><a href=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/kgpdemo-ezgif.com-resize-video.mp4\">https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/kgpdemo-ezgif.com-resize-video.mp4<\/a><\/video><\/div><\/p>\n<h2>Show me the REPLs<\/h2>\n<p>Beyond attaching a terminal to resources in your app model, Aspire 13.6 adds the ability to dock a terminal at the bottom of the dashboard. This is ideal for REPL-like experiences. For a number of our integrations in Aspire, we have added the ability to enable a <em>REPL<\/em> command that opens a terminal-based REPL experience. The resources that support this so far are Redis, Mongo, SQL Server, Postgres, Valkey, and MySQL. The code is very straightforward:<\/p>\n<pre><code class=\"language-typescript\">var cache = await builder.addRedis(\"cache\")\n                         .withRepl();<\/code><\/pre>\n<p>Once you launch the AppHost, you will see a REPL command on the Redis resource. Here is a simple example:<\/p>\n<p><div style=\"width: 3840px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-533-2\" width=\"3840\" height=\"2160\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/repldemo.mp4?_=2\" \/><a href=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/repldemo.mp4\">https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/repldemo.mp4<\/a><\/video><\/div><\/p>\n<p>We have exposed this as an API that you can use on your own custom resources as well. Here is an example of some C# code that uses the new <code>TerminalService<\/code> API to create a terminal:<\/p>\n<pre><code class=\"language-csharp\">var myapp = builder.AddExecutable(\"myapp\", \"myapp\", \".\")\n                   .WithCommand(\"myapp-repl\", async context =&gt; {\n\n                        \/\/ Step 1: Get the terminal service.\n                        var ts = context.Services.GetRequiredService&lt;TerminalService&gt;();\n\n                        \/\/ Step 2: Create and start the terminal.\n                        var terminal = ts.CreateTerminal(new ()\n                        {\n                            Title = \"myapp-repl\",\n                            Executable = \"myapp\",\n                            Arguments = [\"repl\"]\n                        });\n                        terminal.Start();\n\n                        \/\/ Show it in the dock.\n                        terminal.Show();\n                   });<\/code><\/pre>\n<h2>Accessing the terminal from &#8230; the terminal?<\/h2>\n<p>When using <code>WithTerminal()\/withTerminal()<\/code> you are attaching a terminal to a resource but sometimes you already have your own terminal open and you just want to interact with that program in your usual terminal experience. Aspire helps you do this! Assuming you have a resource in your app model that has a terminal exposed you can use the <code>aspire terminal attach<\/code> command to attach to it directly.<\/p>\n<p><div style=\"width: 1920px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-533-3\" width=\"1920\" height=\"822\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/terminalattach-ezgif.com-resize-video.mp4?_=3\" \/><a href=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/terminalattach-ezgif.com-resize-video.mp4\">https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/terminalattach-ezgif.com-resize-video.mp4<\/a><\/video><\/div><\/p>\n<h2>Automating the terminal<\/h2>\n<p>Another capability that Aspire provides around its terminal experience is the ability to write automation scripts. Here is an example of automating a shell prompt<\/p>\n<p>Assuming the running shell is a resource named <code>shell<\/code> with <code>.WithTerminal()<\/code>, its first-replica terminal has the stable ID <code>resource:shell:0<\/code>. The automation waits for Vim&#8217;s welcome banner instead of relying on a fixed delay, then sends the <code>i<\/code> key and the text:<\/p>\n<pre><code class=\"language-csharp\">#pragma warning disable ASPIRETERMINAL001\nvar terminals = commandContext.Services.GetRequiredService&lt;TerminalService&gt;();\nconst string terminalId = \"resource:shell:0\";\n\nif (!terminals.TryGetTerminal(terminalId, out var terminal))\n{\n    return CommandResults.Failure($\"No terminal found: {terminalId}\");\n}\n\ntry\n{\n    await terminal.SendTextAsync(\"vi\\r\", commandContext.CancellationToken);\n    await terminal.WaitForTextAsync(\n        \"VIM - Vi IMproved\",\n        TimeSpan.FromSeconds(10),\n        commandContext.CancellationToken);\n    await terminal.SendKeyAsync(AspireTerminalKey.I, commandContext.CancellationToken);\n    await terminal.SendTextAsync(\n        \"Help, I'm stuck in vi and I can't escape!\",\n        commandContext.CancellationToken);\n\n    return CommandResults.Success();\n}\ncatch (Exception ex) when (ex is TimeoutException or InvalidOperationException)\n{\n    return CommandResults.Failure(ex.Message);\n}\n#pragma warning restore ASPIRETERMINAL001<\/code><\/pre>\n<h3>Automating with Tape files<\/h3>\n<p>For a longer or reusable sequence, you can put the same interactions in a <code>.tape<\/code> file. Tape files come from <a href=\"https:\/\/github.com\/charmbracelet\/vhs\">Charmbracelet VHS<\/a>, a tool for scripting terminal sessions and turning them into recordings. They are plain-text scripts: commands such as <code>Type<\/code> enter text, <code>Enter<\/code> presses a key, and <code>Wait+Screen<\/code> waits until a regular expression matches what is visible on the terminal screen.<\/p>\n<p>Aspire&#8217;s <code>aspire terminal tape play<\/code> command reuses that familiar format to automate a resource terminal; it does not render a video. The command connects to a terminal that is already running, plays the tape, and prints the final screen. For example, save this as <code>vi.tape<\/code>:<\/p>\n<pre><code class=\"language-text\">Type \"vi\"\nEnter\nWait+Screen \/VIM - Vi IMproved\/\nType \"i\"\nType \"Help, I'm stuck in vi and I can't escape!\"<\/code><\/pre>\n<p>With a running AppHost that has a <code>shell<\/code> resource configured with <code>.WithTerminal()<\/code>, play it like this:<\/p>\n<pre><code class=\"language-bash\">aspire terminal tape play shell --tape-file vi.tape<\/code><\/pre>\n<p>The command writes the final terminal screen to standard output after playback completes. It does not create <code>.txt<\/code> or <code>.cast<\/code> recording files; those are separate capture formats supported by the underlying Hex1b tape APIs.<\/p>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>See the <a href=\"https:\/\/github.com\/charmbracelet\/vhs\">VHS tape guide<\/a> for the broader format, and the <a href=\"https:\/\/github.com\/microsoft\/aspire\/blob\/main\/src\/Aspire.Cli\/Commands\/TerminalTapePlayCommand.cs\">Aspire CLI implementation<\/a> for the playback command and options.<\/div><\/p>\n<h2>Aspire 13.6 terminal enhancements<\/h2>\n<p>Aspire 13.6 builds on the terminal support introduced in 13.5 with richer terminal emulation and the ability to dock terminals at the bottom of the dashboard. These enhancements include:<\/p>\n<ol>\n<li>Iconography improvements<\/li>\n<li>Support for shell integration escape sequences (progress, title, path)<\/li>\n<li>Support for bookmark escape sequences.<\/li>\n<li>Support for independent dark\/light mode palette selection.<\/li>\n<\/ol>\n<p>Here is a short video showing a preview of some of these features being exercised.<\/p>\n<p><div style=\"width: 1920px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-533-4\" width=\"1920\" height=\"1080\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/terminalfutures-ezgif.com-resize-video.mp4?_=4\" \/><a href=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/terminalfutures-ezgif.com-resize-video.mp4\">https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/terminalfutures-ezgif.com-resize-video.mp4<\/a><\/video><\/div><\/p>\n<h2>Try it<\/h2>\n<p>If you have an Aspire app with any kind of interactive console resource, add one line to your AppHost, upgrade to 13.5 or later, and see what the terminal surface feels like in context. The feedback from early testing shaped a lot of the design here, and we want to hear what scenarios you run into that the current feature does not cover.<\/p>\n<pre><code class=\"language-csharp\">.WithTerminal()<\/code><\/pre>\n<p>That is all it takes to start.<\/p>\n<p>For the full feature documentation, see the Aspire terminal support docs at <a href=\"https:\/\/aspire.dev\">aspire.dev<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Aspire 13.5 introduced WithTerminal() for interactive terminal sessions on app resources. Aspire 13.6 builds on it with richer terminal emulation and docked terminals for REPLs. See how to enable, attach to, and automate terminal experiences in your app.<\/p>\n","protected":false},"author":656,"featured_media":534,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,17],"tags":[84,9,83,81,82],"class_list":["post-533","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aspire-category","category-deep-dives","tag-architecture","tag-aspire","tag-dcp","tag-terminal","tag-tui"],"acf":[],"blog_post_summary":"<p>Aspire 13.5 introduced WithTerminal() for interactive terminal sessions on app resources. Aspire 13.6 builds on it with richer terminal emulation and docked terminals for REPLs. See how to enable, attach to, and automate terminal experiences in your app.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/533","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/users\/656"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/comments?post=533"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/533\/revisions"}],"predecessor-version":[{"id":540,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/533\/revisions\/540"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media\/534"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media?parent=533"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/categories?post=533"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/tags?post=533"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}