From cc35147c2b54cc9b2a0646522877fc29c77520ae Mon Sep 17 00:00:00 2001 From: Armando Fernandez Date: Tue, 28 Jul 2026 12:09:25 -0700 Subject: [PATCH 1/6] Make file paths clickable when the assistant mentions them in plain text Op cards (Write/Diff) already linked their file paths, but a path named in ordinary prose ("I've created xbox.txt at C:\...") stayed dead text. The transcript now walks each assistant message's text and wraps path-shaped runs in the same clickable link the op cards use, so clicking opens the file in its default app either way. The matching regex is deliberately conservative: it only treats something as a path if it's drive/UNC-rooted or ends in a known file extension, which is what keeps prices, version numbers, and bare URLs from being mistaken for file paths. --- .../Assets/web/transcript/transcript.css | 5 + .../Assets/web/transcript/transcript.js | 94 ++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/MandoCode.Desktop/Assets/web/transcript/transcript.css b/src/MandoCode.Desktop/Assets/web/transcript/transcript.css index 04278f4..fe065d7 100644 --- a/src/MandoCode.Desktop/Assets/web/transcript/transcript.css +++ b/src/MandoCode.Desktop/Assets/web/transcript/transcript.css @@ -347,6 +347,11 @@ a.file-link { color: var(--sky); text-decoration: none; border-bottom: 1px dotted color-mix(in srgb, var(--sky) 55%, transparent); cursor: pointer; } a.file-link:hover { color: var(--accent); border-bottom-color: var(--accent); } + /* Paths linkified out of assistant prose (transcript.js linkifyPaths) can be a full + absolute path — one unbroken 100-char token that would otherwise push the card wider + than the column. Break anywhere rather than overflow; only in .md, so the op cards' + short relative paths keep wrapping normally. */ + .md a.file-link { overflow-wrap: anywhere; } .op { margin: 2px 0; } .op-head { font-weight: 600; } .op-path { font-family: "Cascadia Code", Consolas, monospace; font-size: 13px; } diff --git a/src/MandoCode.Desktop/Assets/web/transcript/transcript.js b/src/MandoCode.Desktop/Assets/web/transcript/transcript.js index be523bb..ac4f634 100644 --- a/src/MandoCode.Desktop/Assets/web/transcript/transcript.js +++ b/src/MandoCode.Desktop/Assets/web/transcript/transcript.js @@ -45,6 +45,93 @@ try { hljs.highlightElement(c); } catch (err) { } }); } + // --- clickable file paths in assistant PROSE ----------------------------------------- + // Operation cards get their links from TranscriptHtmlBuilder.FileLink, but the model also + // names files in ordinary sentences ("I've created xbox.txt at C:\...\xbox.txt"). Those + // are plain text, so this pass walks the text nodes of each new .md block and wraps any + // path-shaped run in the same a.file-link the op cards use — one shared click handler, + // one shared style, and the host already resolves relative-or-absolute (OpenTranscriptPath). + // + // Text nodes only: no HTML parsing, so a path can never be found inside a tag or an + // attribute, and the walker refuses to descend into
 (code listings — linkifying
+  // there would fight highlight.js and read as noise) or an existing . Inline  IS
+  // linkified, because a single backticked filename is how the model usually cites one.
+  // No single-letter extensions (.c/.h): a two-char floor is what stops the "*.com/…" and
+  // "*.co/…" segment of a URL from reading as a filename. Matched case-insensitively, so
+  // C:\…\XBOX.TXT works — safe only because of that floor.
+  const FILE_EXT = 'cs|csproj|sln|props|targets|razor|cshtml|xaml|axaml|js|mjs|cjs|jsx|ts|tsx|' +
+    'json|jsonc|xml|xsd|resx|yml|yaml|toml|ini|cfg|conf|config|md|markdown|txt|log|csv|tsv|' +
+    'html|htm|css|scss|sass|less|py|rb|go|rs|java|kt|swift|cpp|hpp|cc|php|lua|sql|' +
+    'sh|bash|zsh|ps1|psm1|psd1|bat|cmd|env|lock|manifest|nuspec|dll|exe|pdb|' +
+    'zip|tar|gz|7z|png|jpg|jpeg|gif|svg|webp|ico|pdf|docx|xlsx|pptx';
+  // Two alternatives, deliberately conservative — a false link on ordinary prose is worse
+  // than a missed one:
+  //   1. drive-rooted (C:\… , C:/…) or UNC (\\server\…). The prefix is unambiguous, so no
+  //      extension is required and the whole run is taken. The leading (?"\'`|*?\\n]*[^\\s<>"\'`|*?.,;:!)\\]}\\n]' +
+      '|' +
+      '(?:(?:\\.{0,2}[\\\\/])?(?:[\\w.@+~-]+[\\\\/])+?[\\w.@+~-]+|[\\w@+~-][\\w.@+~-]*)' +
+        '\\.(?:' + FILE_EXT + ')(?![\\w./\\\\-])' +
+    ')',
+    'gi');
+
+  function linkifyIn(md) {
+    // Collect first, mutate after — replacing a node mid-walk invalidates the TreeWalker.
+    const targets = [];
+    const walker = document.createTreeWalker(md, NodeFilter.SHOW_TEXT, {
+      acceptNode: function (n) {
+        if (!n.nodeValue || n.nodeValue.length < 3) return NodeFilter.FILTER_REJECT;
+        for (let p = n.parentElement; p && p !== md; p = p.parentElement) {
+          const t = p.tagName;
+          if (t === 'PRE' || t === 'A' || t === 'CODE' && p.closest('pre'))
+            return NodeFilter.FILTER_REJECT;
+        }
+        return NodeFilter.FILTER_ACCEPT;
+      }
+    });
+    for (let n = walker.nextNode(); n; n = walker.nextNode())
+      if (PATH_RE.test(n.nodeValue)) targets.push(n);
+
+    targets.forEach(function (node) {
+      const text = node.nodeValue;
+      const frag = document.createDocumentFragment();
+      let last = 0, m;
+      PATH_RE.lastIndex = 0;
+      while ((m = PATH_RE.exec(text)) !== null) {
+        if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index)));
+        const a = document.createElement('a');
+        a.className = 'file-link';
+        a.href = '#';
+        a.title = 'Open in default app';
+        a.setAttribute('data-file', m[0]);
+        a.textContent = m[0];   // shown verbatim — the model's own wording is the record
+        frag.appendChild(a);
+        last = m.index + m[0].length;
+      }
+      if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
+      node.parentNode.replaceChild(frag, node);
+    });
+  }
+  // Runs after highlightNew() so code blocks are already tokenized (and skipped anyway).
+  function linkifyPaths() {
+    log.querySelectorAll('.md:not([data-fl])').forEach(function (md) {
+      md.setAttribute('data-fl', '1');
+      try { linkifyIn(md); } catch (err) { }
+    });
+  }
+
   function doCopy(text, chip) {
     // In the app, the host writes the clipboard (copy: message). In an EXPORTED transcript
     // there is no webview bridge, so fall back to the browser clipboard API — file:// pages
@@ -292,6 +379,7 @@
     wrap.innerHTML = html;
     while (wrap.firstChild) placeChild(wrap.firstChild);
     highlightNew();
+    linkifyPaths();
     addCopyChips();
     addReactionGhosts();
     addCollapsers();
@@ -305,7 +393,11 @@
     const link = e.target.closest('a[data-file]');
     if (!link) return;
     e.preventDefault();
-    window.chrome.webview.postMessage('open-file:' + link.getAttribute('data-file'));
+    // Guarded like doCopy: an EXPORTED transcript has no webview bridge, and prose
+    // linkification puts these links on far more lines than the op cards did — an
+    // unguarded call would throw on every path click in a saved page.
+    if (window.chrome && window.chrome.webview)
+      window.chrome.webview.postMessage('open-file:' + link.getAttribute('data-file'));
   });
 
   // Interactive diff-card chips (delegated — survives transcript export, like the toggles).

From 33ff9b15c0dd3344b11253de3bde94be6eee2c31 Mon Sep 17 00:00:00 2001
From: Armando Fernandez 
Date: Tue, 28 Jul 2026 12:09:35 -0700
Subject: [PATCH 2/6] Fix the music rail icon staying white in every theme

Every other rail icon gets its color from RefreshNavIcons, but the music
icon was never in that list, so it fell back to WinUI's default
foreground instead of following the current theme. It now pulls from the
same theme brush the gold equalizer bars already used, so it retints
automatically whenever the theme changes.
---
 src/MandoCode.Desktop/MainWindow.xaml | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/MandoCode.Desktop/MainWindow.xaml b/src/MandoCode.Desktop/MainWindow.xaml
index 7f4a49f..699bb73 100644
--- a/src/MandoCode.Desktop/MainWindow.xaml
+++ b/src/MandoCode.Desktop/MainWindow.xaml
@@ -92,12 +92,20 @@
             
             
+                 While something is playing the glyph hides entirely and the gold equalizer below
+                 takes its place, so the glyph itself is only ever the idle/paused state. -->
             
             
+