The file C:\Users\MikeSwanson\Claude\guru-connect\server\static\dashboard.html has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   327→            border: none;
   328→            border-radius: 6px;
   329→            cursor: pointer;
   330→        }
   331→        .chat-send:hover { opacity: 0.9; }
   332→        .chat-send:disabled { opacity: 0.5; cursor: not-allowed; }
   333→
   334→        /* Connect Options Modal */
   335→        .connect-options { padding: 20px; }
   336→        .connect-option {
   337→            display: flex;
   338→            align-items: center;
   339→            gap: 16px;
   340→            padding: 16px;
   341→            margin-bottom: 12px;
   342→            background: hsl(var(--muted));
   343→            border: 1px solid hsl(var(--border));
   344→            border-radius: 8px;
   345→            cursor: pointer;
   346→            transition: all 0.2s;
   347→        }
   348→        .connect-option:hover { border-color: hsl(var(--primary)); background: hsla(var(--primary), 0.1); }
   349→        .connect-option-icon { font-size: 24px; width: 40px; text-align: center; }
   350→        .connect-option-info { flex: 1; }
   351→        .connect-option-title { font-weight: 600; margin-bottom: 4px; }
   352→        .connect-option-desc { font-size: 12px; color: hsl(var(--muted-foreground)); }
   353→
   354→        .native-command {
   355→            margin-top: 16px;
   356→            padding: 12px;
   357→            background: hsl(222.2 84% 2%);
   358→            border: 1px solid hsl(var(--border));
   359→            border-radius: 6px;
   360→            font-family: monospace;
   361→            font-size: 12px;
   362→            word-break: break-all;
   363→            position: relative;
   364→        }
   365→        .native-command code { color: hsl(142, 76%, 50%); }
   366→        .copy-btn {
   367→            position: absolute;
   368→            top: 8px;
   369→            right: 8px;
   370→            padding: 4px 8px;
   371→            font-size: 11px;
   372→            background: hsl(var(--primary));
   373→            color: hsl(var(--primary-foreground));
   374→            border: none;
   375→            border-radius: 4px;
   376→            cursor: pointer;
   377→        }
   378→        .copy-btn:hover { opacity: 0.9; }
   379→    </style>
   380→
   381→
   382→
   383→

        .chat-send:hover { opacity: 0.9; }
        .chat-send:disabled { opacity: 0.5; cursor: not-allowed; }
    </style>

        .chat-send:hover { opacity: 0.9; }
        .chat-send:disabled { opacity: 0.5; cursor: not-allowed; }

        /* Connect Options Modal */
        .connect-options { padding: 20px; }
        .connect-option {
            display: flex;
            align-items: center;
            gap: 16px;
            padding: 16px;
            margin-bottom: 12px;
            background: hsl(var(--muted));
            border: 1px solid hsl(var(--border));
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.2s;
        }
        .connect-option:hover { border-color: hsl(var(--primary)); background: hsla(var(--primary), 0.1); }
        .connect-option-icon { font-size: 24px; width: 40px; text-align: center; }
        .connect-option-info { flex: 1; }
        .connect-option-title { font-weight: 600; margin-bottom: 4px; }
        .connect-option-desc { font-size: 12px; color: hsl(var(--muted-foreground)); }

        .native-command {
            margin-top: 16px;
            padding: 12px;
            background: hsl(222.2 84% 2%);
            border: 1px solid hsl(var(--border));
            border-radius: 6px;
            font-family: monospace;
            font-size: 12px;
            word-break: break-all;
            position: relative;
        }
        .native-command code { color: hsl(142, 76%, 50%); }
        .copy-btn {
            position: absolute;
            top: 8px;
            right: 8px;
            padding: 4px 8px;
            font-size: 11px;
            background: hsl(var(--primary));
            color: hsl(var(--primary-foreground));
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .copy-btn:hover { opacity: 0.9; }
    </style>

<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GuruConnect - Dashboard</title>

GuruConnect

            Loading...
            <button class="logout-btn" id="logoutBtn">Sign Out</button>

        <button class="tab active" data-tab="support">Support</button>
        <button class="tab" data-tab="access">Access</button>
        <button class="tab" data-tab="build">Build</button>
        <button class="tab" data-tab="settings">Settings</button>
        <button class="tab admin-only" data-tab="users" style="display: none;">Users</button>

Active Support Sessions

Temporary sessions initiated by support codes

                    <button class="btn btn-primary" id="generateCodeBtn">Generate Code</button>

                                Code
                                Status
                                Created
                                Technician
                                Actions

No active sessions

Generate a code to start a support session

Status

                            All Machines
                            0

                            Online
                            0

                            Offline
                            0

By Company

No machines installed

No machines

Install the agent on a machine to see it here

Select a machine

Click a machine to view details

Installer Builder

Create customized agent installers for unattended access

                        Name
                        <input type="text" id="buildName" placeholder="Machine name (auto if blank)">

                        Company
                        <input type="text" id="buildCompany" placeholder="Client organization">

                        Site
                        <input type="text" id="buildSite" placeholder="Physical location">

                        Department
                        <input type="text" id="buildDepartment" placeholder="Business unit">

                        Device Type
                        <select id="buildDeviceType">
                            <option value="workstation">Workstation</option>
                            <option value="laptop">Laptop</option>
                            <option value="server">Server</option>
                        </select>

                        Tag
                        <input type="text" id="buildTag" placeholder="Custom label">

                    <button class="btn btn-primary" disabled>Build EXE (64-bit)</button>
                    <button class="btn btn-outline" disabled>Build EXE (32-bit)</button>
                    <button class="btn btn-outline" disabled>Build MSI</button>

                    Agent builds will be available once the agent is compiled.

Settings

Configure your GuruConnect preferences

                        Theme
                        <select disabled>
                            <option>Dark (Default)</option>
                            <option>Light</option>
                            <option>System</option>
                        </select>

                        Notifications
                        <select disabled>
                            <option>All notifications</option>
                            <option>Important only</option>
                            <option>None</option>
                        </select>

                    Additional settings coming soon.

User Management

Manage user accounts and permissions

                    Open User Management

Chat with Client

                <button class="modal-close" id="chatClose">×</button>

No messages yet. Start the conversation!

                <input type="text" class="chat-input" id="chatInput" placeholder="Type a message..." />
                <button class="chat-send" id="chatSend">Send</button>

+        .connect-options { padding: 20px; }

+            background: hsl(var(--muted));

+            border: 1px solid hsl(var(--border));

+        .connect-option:hover { border-color: hsl(var(--primary)); background: hsla(var(--primary), 0.1); }

+        .connect-option-icon { font-size: 24px; width: 40px; text-align: center; }

+        .connect-option-info { flex: 1; }

+        .connect-option-title { font-weight: 600; margin-bottom: 4px; }

+        .connect-option-desc { font-size: 12px; color: hsl(var(--muted-foreground)); }

+        .native-command code { color: hsl(142, 76%, 50%); }

+            background: hsl(var(--primary));

+            color: hsl(var(--primary-foreground));

+        .copy-btn:hover { opacity: 0.9; }


Now I need to add the connect options modal HTML and the JavaScript to handle it. Let me add the modal after the chat modal.


Now add the connect options modal:


Edit

    <!-- Chat Modal -->
    <div class="modal-overlay" id="chatModal">

Connect to Machine

                <button class="modal-close" id="connectClose">×</button>

🌐

Web Viewer

Opens in browser. Most keys work, but Win key and Alt+Tab are captured by your OS.

🖥

Native Viewer

Full keyboard capture including Win key, Alt+Tab, and Ctrl+Alt+Del.

                        <button class="copy-btn" onclick="copyNativeCommand()">Copy</button>

                        Run this command in PowerShell or CMD where guruconnect-viewer.exe is located.


The file C:\Users\MikeSwanson\Claude\guru-connect\server\static\dashboard.html has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   573→

   574→

   575→
   576→
   577→
   578→

   579→

   580→

   581→
Connect to Machine

   582→                <button class="modal-close" id="connectClose">×</button>
   583→

   584→

   585→

   586→
🌐

   587→

   588→
Web Viewer

   589→
Opens in browser. Most keys work, but Win key and Alt+Tab are captured by your OS.

   590→

   591→

   592→

   593→
🖥

   594→

   595→
Native Viewer

   596→
Full keyboard capture including Win key, Alt+Tab, and Ctrl+Alt+Del.

   597→

   598→

   599→

   600→

   601→                        <button class="copy-btn" onclick="copyNativeCommand()">Copy</button>
   602→
   603→

   604→

   605→                        Run this command in PowerShell or CMD where guruconnect-viewer.exe is located.
   606→

   607→

   608→

   609→

   610→

   611→
   612→
   613→

   614→

   615→

   616→
Chat with Client

   617→                <button class="modal-close" id="chatClose">×</button>

    <!-- Chat Modal -->
    <div class="modal-overlay" id="chatModal">

Connect to Machine

                <button class="modal-close" id="connectClose">×</button>

🌐

Web Viewer

Opens in browser. Most keys work, but Win key and Alt+Tab are captured by your OS.

🖥

Native Viewer

Full keyboard capture including Win key, Alt+Tab, and Ctrl+Alt+Del.

                        <button class="copy-btn" onclick="copyNativeCommand()">Copy</button>

                        Run this command in PowerShell or CMD where guruconnect-viewer.exe is located.

+    <div class="modal-overlay" id="connectModal">

+        <div class="modal" style="max-width: 450px;">

+                <button class="modal-close" id="connectClose">×</button>

+            <div class="connect-options">

+                <div class="connect-option" id="connectWeb">

+                    <div class="connect-option-info">

+
Opens in browser. Most keys work, but Win key and Alt+Tab are captured by your OS.

+                <div class="connect-option" id="connectNative">

+
Full keyboard capture including Win key, Alt+Tab, and Ctrl+Alt+Del.

+                <div id="nativeCommandContainer" style="display: none;">

+                    <div class="native-command">

+                        <button class="copy-btn" onclick="copyNativeCommand()">Copy</button>

+                    <p style="margin-top: 12px; font-size: 12px; color: hsl(var(--muted-foreground));">

+                        Run this command in PowerShell or CMD where guruconnect-viewer.exe is located.


Now I need to update the JavaScript to:
1. Change `connectToMachine()` to open the modal instead of directly opening the web viewer
2. Add handlers for the modal options
3. Add the native command generation

Let me find and update the JavaScript.


Now update the JavaScript to handle the connect modal:


        function connectToMachine(sessionId) {
            // Open viewer in new window
            const viewerUrl = "/viewer.html?session_id=" + sessionId;
            window.open(viewerUrl, "viewer_" + sessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
        }

        // Connect modal state
        let connectSessionId = null;
        let connectMachineName = null;

        const connectModal = document.getElementById("connectModal");
        const connectClose = document.getElementById("connectClose");
        const connectWeb = document.getElementById("connectWeb");
        const connectNative = document.getElementById("connectNative");
        const nativeCommandContainer = document.getElementById("nativeCommandContainer");
        const nativeCommandText = document.getElementById("nativeCommandText");
        const connectMachineNameEl = document.getElementById("connectMachineName");

        connectClose.addEventListener("click", () => {
            connectModal.classList.remove("active");
            nativeCommandContainer.style.display = "none";
        });

        connectModal.addEventListener("click", (e) => {
            if (e.target === connectModal) {
                connectModal.classList.remove("active");
                nativeCommandContainer.style.display = "none";
            }
        });

        connectWeb.addEventListener("click", () => {
            if (connectSessionId) {
                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;
                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
                connectModal.classList.remove("active");
                nativeCommandContainer.style.display = "none";
            }
        });

        connectNative.addEventListener("click", () => {
            if (connectSessionId) {
                const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
                const serverUrl = protocol + "//" + window.location.host + "/ws/viewer";
                const cmd = `guruconnect-viewer.exe -i ${connectSessionId} -s ${serverUrl}`;
                nativeCommandText.textContent = cmd;
                nativeCommandContainer.style.display = "block";
            }
        });

        function copyNativeCommand() {
            const cmd = nativeCommandText.textContent;
            navigator.clipboard.writeText(cmd).then(() => {
                const btn = document.querySelector(".copy-btn");
                btn.textContent = "Copied!";
                setTimeout(() => btn.textContent = "Copy", 2000);
            });
        }

        function connectToMachine(sessionId) {
            const machine = machines.find(m => m.id === sessionId);
            connectSessionId = sessionId;
            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);
            connectMachineNameEl.textContent = connectMachineName;
            nativeCommandContainer.style.display = "none";
            connectModal.classList.add("active");
        }


The file C:\Users\MikeSwanson\Claude\guru-connect\server\static\dashboard.html has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   923→                    '<button class="btn btn-outline" style="width: 100%; color: hsl(0, 62.8%, 50%);" onclick="disconnectMachine(\'' + m.id + '\', \'' + (m.agent_name || m.agent_id).replace(/'/g, "\\'") + '\')">Disconnect</button>' +
   924→                '</div>';
   925→        }
   926→
   927→        // Connect modal state
   928→        let connectSessionId = null;
   929→        let connectMachineName = null;
   930→
   931→        const connectModal = document.getElementById("connectModal");
   932→        const connectClose = document.getElementById("connectClose");
   933→        const connectWeb = document.getElementById("connectWeb");
   934→        const connectNative = document.getElementById("connectNative");
   935→        const nativeCommandContainer = document.getElementById("nativeCommandContainer");
   936→        const nativeCommandText = document.getElementById("nativeCommandText");
   937→        const connectMachineNameEl = document.getElementById("connectMachineName");
   938→
   939→        connectClose.addEventListener("click", () => {
   940→            connectModal.classList.remove("active");
   941→            nativeCommandContainer.style.display = "none";
   942→        });
   943→
   944→        connectModal.addEventListener("click", (e) => {
   945→            if (e.target === connectModal) {
   946→                connectModal.classList.remove("active");
   947→                nativeCommandContainer.style.display = "none";
   948→            }
   949→        });
   950→
   951→        connectWeb.addEventListener("click", () => {
   952→            if (connectSessionId) {
   953→                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;
   954→                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
   955→                connectModal.classList.remove("active");
   956→                nativeCommandContainer.style.display = "none";
   957→            }
   958→        });
   959→
   960→        connectNative.addEventListener("click", () => {
   961→            if (connectSessionId) {
   962→                const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
   963→                const serverUrl = protocol + "//" + window.location.host + "/ws/viewer";
   964→                const cmd = `guruconnect-viewer.exe -i ${connectSessionId} -s ${serverUrl}`;
   965→                nativeCommandText.textContent = cmd;
   966→                nativeCommandContainer.style.display = "block";
   967→            }
   968→        });
   969→
   970→        function copyNativeCommand() {
   971→            const cmd = nativeCommandText.textContent;
   972→            navigator.clipboard.writeText(cmd).then(() => {
   973→                const btn = document.querySelector(".copy-btn");
   974→                btn.textContent = "Copied!";
   975→                setTimeout(() => btn.textContent = "Copy", 2000);
   976→            });
   977→        }
   978→
   979→        function connectToMachine(sessionId) {
   980→            const machine = machines.find(m => m.id === sessionId);
   981→            connectSessionId = sessionId;
   982→            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);
   983→            connectMachineNameEl.textContent = connectMachineName;
   984→            nativeCommandContainer.style.display = "none";
   985→            connectModal.classList.add("active");
   986→        }
   987→
   988→        async function disconnectMachine(sessionId, machineName) {
   989→            if (!confirm("Disconnect " + machineName + "?\n\nThis will end the remote session.")) return;
   990→            try {

        function connectToMachine(sessionId) {
            // Open viewer in new window
            const viewerUrl = "/viewer.html?session_id=" + sessionId;
            window.open(viewerUrl, "viewer_" + sessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
        }

        // Connect modal state
        let connectSessionId = null;
        let connectMachineName = null;

        const connectModal = document.getElementById("connectModal");
        const connectClose = document.getElementById("connectClose");
        const connectWeb = document.getElementById("connectWeb");
        const connectNative = document.getElementById("connectNative");
        const nativeCommandContainer = document.getElementById("nativeCommandContainer");
        const nativeCommandText = document.getElementById("nativeCommandText");
        const connectMachineNameEl = document.getElementById("connectMachineName");

        connectClose.addEventListener("click", () => {
            connectModal.classList.remove("active");
            nativeCommandContainer.style.display = "none";
        });

        connectModal.addEventListener("click", (e) => {
            if (e.target === connectModal) {
                connectModal.classList.remove("active");
                nativeCommandContainer.style.display = "none";
            }
        });

        connectWeb.addEventListener("click", () => {
            if (connectSessionId) {
                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;
                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
                connectModal.classList.remove("active");
                nativeCommandContainer.style.display = "none";
            }
        });

        connectNative.addEventListener("click", () => {
            if (connectSessionId) {
                const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
                const serverUrl = protocol + "//" + window.location.host + "/ws/viewer";
                const cmd = `guruconnect-viewer.exe -i ${connectSessionId} -s ${serverUrl}`;
                nativeCommandText.textContent = cmd;
                nativeCommandContainer.style.display = "block";
            }
        });

        function copyNativeCommand() {
            const cmd = nativeCommandText.textContent;
            navigator.clipboard.writeText(cmd).then(() => {
                const btn = document.querySelector(".copy-btn");
                btn.textContent = "Copied!";
                setTimeout(() => btn.textContent = "Copy", 2000);
            });
        }

        function connectToMachine(sessionId) {
            const machine = machines.find(m => m.id === sessionId);
            connectSessionId = sessionId;
            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);
            connectMachineNameEl.textContent = connectMachineName;
            nativeCommandContainer.style.display = "none";
            connectModal.classList.add("active");
        }

<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GuruConnect - Dashboard</title>

GuruConnect

            Loading...
            <button class="logout-btn" id="logoutBtn">Sign Out</button>

        <button class="tab active" data-tab="support">Support</button>
        <button class="tab" data-tab="access">Access</button>
        <button class="tab" data-tab="build">Build</button>
        <button class="tab" data-tab="settings">Settings</button>
        <button class="tab admin-only" data-tab="users" style="display: none;">Users</button>

Active Support Sessions

Temporary sessions initiated by support codes

                    <button class="btn btn-primary" id="generateCodeBtn">Generate Code</button>

                                Code
                                Status
                                Created
                                Technician
                                Actions

No active sessions

Generate a code to start a support session

Status

                            All Machines
                            0

                            Online
                            0

                            Offline
                            0

By Company

No machines installed

No machines

Install the agent on a machine to see it here

Select a machine

Click a machine to view details

Installer Builder

Create customized agent installers for unattended access

                        Name
                        <input type="text" id="buildName" placeholder="Machine name (auto if blank)">

                        Company
                        <input type="text" id="buildCompany" placeholder="Client organization">

                        Site
                        <input type="text" id="buildSite" placeholder="Physical location">

                        Department
                        <input type="text" id="buildDepartment" placeholder="Business unit">

                        Device Type
                        <select id="buildDeviceType">
                            <option value="workstation">Workstation</option>
                            <option value="laptop">Laptop</option>
                            <option value="server">Server</option>
                        </select>

                        Tag
                        <input type="text" id="buildTag" placeholder="Custom label">

                    <button class="btn btn-primary" disabled>Build EXE (64-bit)</button>
                    <button class="btn btn-outline" disabled>Build EXE (32-bit)</button>
                    <button class="btn btn-outline" disabled>Build MSI</button>

                    Agent builds will be available once the agent is compiled.

Settings

Configure your GuruConnect preferences

                        Theme
                        <select disabled>
                            <option>Dark (Default)</option>
                            <option>Light</option>
                            <option>System</option>
                        </select>

                        Notifications
                        <select disabled>
                            <option>All notifications</option>
                            <option>Important only</option>
                            <option>None</option>
                        </select>

                    Additional settings coming soon.

User Management

Manage user accounts and permissions

                    Open User Management

Connect to Machine

                <button class="modal-close" id="connectClose">×</button>

🌐

Web Viewer

Opens in browser. Most keys work, but Win key and Alt+Tab are captured by your OS.

🖥

Native Viewer

Full keyboard capture including Win key, Alt+Tab, and Ctrl+Alt+Del.

                        <button class="copy-btn" onclick="copyNativeCommand()">Copy</button>

                        Run this command in PowerShell or CMD where guruconnect-viewer.exe is located.

Chat with Client

                <button class="modal-close" id="chatClose">×</button>

No messages yet. Start the conversation!

                <input type="text" class="chat-input" id="chatInput" placeholder="Type a message..." />
                <button class="chat-send" id="chatSend">Send</button>

+        const connectModal = document.getElementById("connectModal");

+        const connectClose = document.getElementById("connectClose");

+        const connectWeb = document.getElementById("connectWeb");

+        const connectNative = document.getElementById("connectNative");

+        const nativeCommandContainer = document.getElementById("nativeCommandContainer");

+        const nativeCommandText = document.getElementById("nativeCommandText");

+        const connectMachineNameEl = document.getElementById("connectMachineName");

+        connectClose.addEventListener("click", () => {

+            connectModal.classList.remove("active");

+            nativeCommandContainer.style.display = "none";

+        connectModal.addEventListener("click", (e) => {

+            if (e.target === connectModal) {

+        connectWeb.addEventListener("click", () => {

+                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;

+                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");

+        connectNative.addEventListener("click", () => {

+                const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";

+                const serverUrl = protocol + "//" + window.location.host + "/ws/viewer";

+                const cmd = `guruconnect-viewer.exe -i ${connectSessionId} -s ${serverUrl}`;

+                nativeCommandText.textContent = cmd;

+                nativeCommandContainer.style.display = "block";

+            const cmd = nativeCommandText.textContent;

+            navigator.clipboard.writeText(cmd).then(() => {

+                const btn = document.querySelector(".copy-btn");

+                btn.textContent = "Copied!";

+                setTimeout(() => btn.textContent = "Copy", 2000);

-            // Open viewer in new window

-            const viewerUrl = "/viewer.html?session_id=" + sessionId;

-            window.open(viewerUrl, "viewer_" + sessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");

+            const machine = machines.find(m => m.id === sessionId);

+            connectSessionId = sessionId;

+            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);

+            connectMachineNameEl.textContent = connectMachineName;

+            connectModal.classList.add("active");


Now I need to push this update to the server. Let me commit and push, then update the server.


Now let me push this update to the server:


Bash

cd /c/Users/MikeSwanson/Claude/guru-connect && git add server/static/dashboard.html && git commit -m "Add native viewer option to Connect button"

Commit native viewer option


[main 0c3435f] Add native viewer option to Connect button
 1 file changed, 139 insertions(+), 3 deletions(-)


Now push to remote and update the server. Since it's just static files, I only need to pull on the server - no rebuild needed.


cd /c/Users/MikeSwanson/Claude/guru-connect && git push origin main

Push to remote


ssh guru@172.16.3.30 "cd ~/guru-connect && git pull origin main"

Pull changes on server


remote: . Processing 1 references
remote: Processed 1 references in total
To https://git.azcomputerguru.com/azcomputerguru/guru-connect.git
   3fc4e1f..0c3435f  main -> main


Updating 3fc4e1f..0c3435f
Fast-forward
 server/static/dashboard.html | 142 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 139 insertions(+), 3 deletions(-)
From https://git.azcomputerguru.com/azcomputerguru/guru-connect
 * branch            main       -> FETCH_HEAD
   3fc4e1f..0c3435f  main       -> origin/main


The static files are updated. No rebuild needed since it's just HTML/JS. The server will serve the new file immediately.


Done. The Connect button now shows a modal with two options:

1. **Web Viewer** - Opens in browser (existing behavior)
2. **Native Viewer** - Shows the command to run `guruconnect-viewer.exe` with the correct session ID and server URL

Click "Native Viewer" and it displays the command with a Copy button. The command format is:
```
guruconnect-viewer.exe -i <session_id> -s wss://connect.azcomputerguru.com/ws/viewer
```

No server rebuild needed - static files update immediately.


This doesn't seem like the implementation we discussed


The user is right - they expected a different implementation. Looking at the screenshot, the native viewer shows a command line that the user has to copy and paste. That's not very user-friendly.

Looking back at our earlier work, we built a native viewer executable. A better approach would be to use a custom protocol handler (like `guruconnect://`) that would launch the native viewer directly when clicked.

Let me ask what implementation they expected, or just implement the custom protocol handler approach which is much more seamless.


You're right - having to copy/paste a command isn't great UX. I should implement a custom protocol handler (`guruconnect://`) so clicking "Native Viewer" launches the app directly.

This requires:
1. **Viewer registers the protocol** on install/first run (adds registry key on Windows)
2. **Dashboard uses the protocol link** - `guruconnect://connect?session=xxx&server=xxx`

Should I implement the protocol handler in the viewer, or did you have a different approach in mind?


yes implement the protocol handler - and if it's not found (not installed) it should offer to run the viewer and hopefully retry the protocol handler


The user wants me to implement a custom protocol handler for the native viewer. The flow should be:

1. User clicks "Native Viewer"
2. Dashboard tries to open `guruconnect://connect?session=xxx&server=xxx`
3. If the protocol is registered, the viewer launches
4. If not registered (viewer not installed), offer to download/run the viewer
5. After running the viewer (which registers the protocol), retry

I need to:
1. Update the viewer to register the `guruconnect://` protocol handler on Windows
2. Update the viewer to parse the protocol URL and extract session/server params
3. Update the dashboard to use the protocol and handle the "not installed" case

Let me start with the viewer changes.


Let me implement the full protocol handler flow:


<tool_use_error>File does not exist.</tool_use_error>


The viewer folder isn't in the Windows path I expected. Let me check where it is.


No files found


Let me check if there's a viewer directory at all.


Check guru-connect directory structure


total 245
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 29 20:57 ./
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 29 19:28 ../
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 28 10:11 .cargo/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 29 21:15 .git/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096    255 Dec 27 19:37 .gitignore
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 29 20:57 agent/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 138985 Dec 29 20:57 Cargo.lock
-rw-r--r-- 1 AzureAD+MikeSwanson 4096    649 Dec 27 19:37 Cargo.toml
-rw-r--r-- 1 AzureAD+MikeSwanson 4096   3240 Dec 27 19:37 CLAUDE.md
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 27 19:37 dashboard/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 29 20:57 proto/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096  31758 Dec 28 13:52 REQUIREMENTS.md
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 28 19:39 server/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 29 20:57 session-logs/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096      0 Dec 28 20:48 target/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096   4679 Dec 28 14:06 TODO.md


The viewer crate doesn't exist in this directory. Looking at the session log, the viewer was built on a different machine (Windows with localadmin user). The viewer code was created there but may not have been pushed to git.

Let me check if there's a viewer on the build server or if I need to create it here.


ssh guru@172.16.3.30 "ls -la ~/guru-connect/"

Check build server for viewer


total 344
drwxrwxr-x 10 guru guru   4096 Dec 30 04:07 .
drwxr-x--- 11 guru guru   4096 Dec 30 04:16 ..
drwxrwxr-x  2 guru guru   4096 Dec 28 19:35 .cargo
drwxrwxr-x  8 guru guru   4096 Dec 30 04:16 .git
-rw-rw-r--  1 guru guru    227 Dec 28 17:07 .gitignore
-rw-rw-r--  1 guru guru   3123 Dec 28 17:07 CLAUDE.md
-rw-rw-r--  1 guru guru 133590 Dec 30 02:15 Cargo.lock
-rw-rw-r--  1 guru guru    622 Dec 28 17:07 Cargo.toml
-rw-rw-r--  1 guru guru  30957 Dec 28 20:52 REQUIREMENTS.md
-rw-rw-r--  1 guru guru   4679 Dec 28 21:12 TODO.md
drwxrwxr-x  3 guru guru   4096 Dec 30 02:15 agent
drwxrwxr-x  3 guru guru   4096 Dec 28 17:07 dashboard
drwxrwxr-x  2 guru guru   4096 Dec 30 02:15 proto
drwxrwxr-x  5 guru guru   4096 Dec 30 04:01 server
-rw-rw-r--  1 guru guru 113930 Dec 29 01:20 server.log
drwxrwxr-x  2 guru guru   4096 Dec 30 02:32 session-logs
-rwxrwxr-x  1 guru guru    928 Dec 30 04:07 start-server.sh
lrwxrwxrwx  1 guru guru     13 Dec 28 23:47 static -> server/static
drwxrwxr-x  7 guru guru   4096 Dec 28 19:36 target


No viewer crate on the build server either. According to the session log, the viewer was created on the Windows machine at `C:\Users\localadmin\claude-projects\guru-connect`. But that's different from the current `C:\Users\MikeSwanson\Claude\guru-connect`.

I need to create the viewer crate here with the protocol handler support. Let me create it from scratch with the protocol handler built in.


The viewer crate doesn't exist in this repo. Let me create it with the protocol handler built in:


Create viewer directory


Now let me create the viewer crate with:
1. Protocol handler registration
2. Protocol URL parsing
3. The actual viewer functionality

Let me start with Cargo.toml.


Write

[package]
name = "guruconnect-viewer"
version = "0.1.0"
edition = "2021"
description = "GuruConnect Native Viewer with full keyboard capture"

[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
futures-util = "0.3"
url = "2"
clap = { version = "4", features = ["derive"] }
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
prost = "0.12"
zstd = "0.13"
winit = "0.29"
softbuffer = "0.4"
raw-window-handle = "0.6"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.52", features = [
    "Win32_Foundation",
    "Win32_UI_WindowsAndMessaging",
    "Win32_UI_Input_KeyboardAndMouse",
    "Win32_System_LibraryLoader",
    "Win32_System_Registry",
    "Win32_Security",
] }
winreg = "0.52"

[build-dependencies]
prost-build = "0.12"


File created successfully at: C:\Users\MikeSwanson\Claude\guru-connect\viewer\Cargo.toml

[package]
name = "guruconnect-viewer"
version = "0.1.0"
edition = "2021"
description = "GuruConnect Native Viewer with full keyboard capture"

[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
futures-util = "0.3"
url = "2"
clap = { version = "4", features = ["derive"] }
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
prost = "0.12"
zstd = "0.13"
winit = "0.29"
softbuffer = "0.4"
raw-window-handle = "0.6"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.52", features = [
    "Win32_Foundation",
    "Win32_UI_WindowsAndMessaging",
    "Win32_UI_Input_KeyboardAndMouse",
    "Win32_System_LibraryLoader",
    "Win32_System_Registry",
    "Win32_Security",
] }
winreg = "0.52"

[build-dependencies]
prost-build = "0.12"


[Request interrupted by user for tool use]


Viewer is integrated into the main exe now, double check your notes


The user is saying the viewer is integrated into the main agent exe now, not a separate crate. Let me check the session logs and the agent code to understand the current state.


Let me check the current agent structure:


Bash

Check agent source structure


Found 15 files
guru-connect\server\src\main.rs
guru-connect\server\src\api\users.rs
guru-connect\server\src\auth\jwt.rs
guru-connect\server\src\api\mod.rs
guru-connect\agent\src\viewer\render.rs
guru-connect\agent\src\viewer\transport.rs
guru-connect\server\src\db\events.rs
guru-connect\server\src\session\mod.rs
guru-connect\agent\src\session\mod.rs
guru-connect\agent\src\viewer\mod.rs
guru-connect\agent\src\install.rs
guru-connect\agent\src\main.rs
guru-connect\server\src\relay\mod.rs
guru-connect\agent\src\transport\websocket.rs
guru-connect\agent\src\capture\display.rs


total 64
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 29 20:57 ./
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 29 20:57 ../
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 28 20:53 bin/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 28 13:52 capture/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 28 16:23 chat/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096  6333 Dec 28 14:09 config.rs
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 27 19:37 encoder/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 28 20:32 input/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 11424 Dec 29 20:57 install.rs
-rw-r--r-- 1 AzureAD+MikeSwanson 4096 14283 Dec 29 20:57 main.rs
-rw-r--r-- 1 AzureAD+MikeSwanson 4096  3090 Dec 28 20:31 sas_client.rs
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 29 20:57 session/
-rw-r--r-- 1 AzureAD+MikeSwanson 4096  8466 Dec 28 20:34 startup.rs
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 28 14:10 transport/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 28 17:07 tray/
drwxr-xr-x 1 AzureAD+MikeSwanson 4096     0 Dec 29 20:57 viewer/


I see there's already a `viewer` folder inside the agent. The viewer is integrated into the agent binary. Let me check the agent's main.rs to understand how the viewer is invoked, and then check the viewer module.


Got it - viewer is in `agent/src/viewer/`. Let me check the current implementation:


Remove incorrect viewer directory


     1→//! Viewer module - Native remote desktop viewer with full keyboard capture
     2→//!
     3→//! This module provides the viewer functionality for connecting to remote
     4→//! GuruConnect sessions with low-level keyboard hooks for Win key capture.
     5→
     6→mod input;
     7→mod render;
     8→mod transport;
     9→
    10→use crate::proto;
    11→use anyhow::Result;
    12→use std::sync::Arc;
    13→use tokio::sync::{mpsc, Mutex};
    14→use tracing::{info, error, warn};
    15→
    16→#[derive(Debug, Clone)]
    17→pub enum ViewerEvent {
    18→    Connected,
    19→    Disconnected(String),
    20→    Frame(render::FrameData),
    21→    CursorPosition(i32, i32, bool),
    22→    CursorShape(proto::CursorShape),
    23→}
    24→
    25→#[derive(Debug, Clone)]
    26→pub enum InputEvent {
    27→    Mouse(proto::MouseEvent),
    28→    Key(proto::KeyEvent),
    29→    SpecialKey(proto::SpecialKeyEvent),
    30→}
    31→
    32→/// Run the viewer to connect to a remote session
    33→pub async fn run(server_url: &str, session_id: &str, api_key: &str) -> Result<()> {
    34→    info!("GuruConnect Viewer starting");
    35→    info!("Server: {}", server_url);
    36→    info!("Session: {}", session_id);
    37→
    38→    // Create channels for communication between components
    39→    let (viewer_tx, viewer_rx) = mpsc::channel::<ViewerEvent>(100);
    40→    let (input_tx, input_rx) = mpsc::channel::<InputEvent>(100);
    41→
    42→    // Connect to server
    43→    let ws_url = format!("{}?session_id={}", server_url, session_id);
    44→    info!("Connecting to {}", ws_url);
    45→
    46→    let (ws_sender, mut ws_receiver) = transport::connect(&ws_url, api_key).await?;
    47→    let ws_sender = Arc::new(Mutex::new(ws_sender));
    48→
    49→    info!("Connected to server");
    50→    let _ = viewer_tx.send(ViewerEvent::Connected).await;
    51→
    52→    // Clone sender for input forwarding
    53→    let ws_sender_input = ws_sender.clone();
    54→
    55→    // Spawn task to forward input events to server
    56→    let mut input_rx = input_rx;
    57→    let input_task = tokio::spawn(async move {
    58→        while let Some(event) = input_rx.recv().await {
    59→            let msg = match event {
    60→                InputEvent::Mouse(m) => proto::Message {
    61→                    payload: Some(proto::message::Payload::MouseEvent(m)),
    62→                },
    63→                InputEvent::Key(k) => proto::Message {
    64→                    payload: Some(proto::message::Payload::KeyEvent(k)),
    65→                },
    66→                InputEvent::SpecialKey(s) => proto::Message {
    67→                    payload: Some(proto::message::Payload::SpecialKey(s)),
    68→                },
    69→            };
    70→
    71→            if let Err(e) = transport::send_message(&ws_sender_input, &msg).await {
    72→                error!("Failed to send input: {}", e);
    73→                break;
    74→            }
    75→        }
    76→    });
    77→
    78→    // Spawn task to receive messages from server
    79→    let viewer_tx_recv = viewer_tx.clone();
    80→    let receive_task = tokio::spawn(async move {
    81→        while let Some(msg) = ws_receiver.recv().await {
    82→            match msg.payload {
    83→                Some(proto::message::Payload::VideoFrame(frame)) => {
    84→                    if let Some(proto::video_frame::Encoding::Raw(raw)) = frame.encoding {
    85→                        let frame_data = render::FrameData {
    86→                            width: raw.width as u32,
    87→                            height: raw.height as u32,
    88→                            data: raw.data,
    89→                            compressed: raw.compressed,
    90→                            is_keyframe: raw.is_keyframe,
    91→                        };
    92→                        let _ = viewer_tx_recv.send(ViewerEvent::Frame(frame_data)).await;
    93→                    }
    94→                }
    95→                Some(proto::message::Payload::CursorPosition(pos)) => {
    96→                    let _ = viewer_tx_recv.send(ViewerEvent::CursorPosition(
    97→                        pos.x, pos.y, pos.visible
    98→                    )).await;
    99→                }
   100→                Some(proto::message::Payload::CursorShape(shape)) => {
   101→                    let _ = viewer_tx_recv.send(ViewerEvent::CursorShape(shape)).await;
   102→                }
   103→                Some(proto::message::Payload::Disconnect(d)) => {
   104→                    warn!("Server disconnected: {}", d.reason);
   105→                    let _ = viewer_tx_recv.send(ViewerEvent::Disconnected(d.reason)).await;
   106→                    break;
   107→                }
   108→                _ => {}
   109→            }
   110→        }
   111→    });
   112→
   113→    // Run the window (this blocks until window closes)
   114→    render::run_window(viewer_rx, input_tx).await?;
   115→
   116→    // Cleanup
   117→    input_task.abort();
   118→    receive_task.abort();
   119→
   120→    Ok(())
   121→}
   122→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Viewer module - Native remote desktop viewer with full keyboard capture
//!
//! This module provides the viewer functionality for connecting to remote
//! GuruConnect sessions with low-level keyboard hooks for Win key capture.

mod input;
mod render;
mod transport;

use crate::proto;
use anyhow::Result;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tracing::{info, error, warn};

#[derive(Debug, Clone)]
pub enum ViewerEvent {
    Connected,
    Disconnected(String),
    Frame(render::FrameData),
    CursorPosition(i32, i32, bool),
    CursorShape(proto::CursorShape),
}

#[derive(Debug, Clone)]
pub enum InputEvent {
    Mouse(proto::MouseEvent),
    Key(proto::KeyEvent),
    SpecialKey(proto::SpecialKeyEvent),
}

/// Run the viewer to connect to a remote session
pub async fn run(server_url: &str, session_id: &str, api_key: &str) -> Result<()> {
    info!("GuruConnect Viewer starting");
    info!("Server: {}", server_url);
    info!("Session: {}", session_id);

    // Create channels for communication between components
    let (viewer_tx, viewer_rx) = mpsc::channel::<ViewerEvent>(100);
    let (input_tx, input_rx) = mpsc::channel::<InputEvent>(100);

    // Connect to server
    let ws_url = format!("{}?session_id={}", server_url, session_id);
    info!("Connecting to {}", ws_url);

    let (ws_sender, mut ws_receiver) = transport::connect(&ws_url, api_key).await?;
    let ws_sender = Arc::new(Mutex::new(ws_sender));

    info!("Connected to server");
    let _ = viewer_tx.send(ViewerEvent::Connected).await;

    // Clone sender for input forwarding
    let ws_sender_input = ws_sender.clone();

    // Spawn task to forward input events to server
    let mut input_rx = input_rx;
    let input_task = tokio::spawn(async move {
        while let Some(event) = input_rx.recv().await {
            let msg = match event {
                InputEvent::Mouse(m) => proto::Message {
                    payload: Some(proto::message::Payload::MouseEvent(m)),
                },
                InputEvent::Key(k) => proto::Message {
                    payload: Some(proto::message::Payload::KeyEvent(k)),
                },
                InputEvent::SpecialKey(s) => proto::Message {
                    payload: Some(proto::message::Payload::SpecialKey(s)),
                },
            };

            if let Err(e) = transport::send_message(&ws_sender_input, &msg).await {
                error!("Failed to send input: {}", e);
                break;
            }
        }
    });

    // Spawn task to receive messages from server
    let viewer_tx_recv = viewer_tx.clone();
    let receive_task = tokio::spawn(async move {
        while let Some(msg) = ws_receiver.recv().await {
            match msg.payload {
                Some(proto::message::Payload::VideoFrame(frame)) => {
                    if let Some(proto::video_frame::Encoding::Raw(raw)) = frame.encoding {
                        let frame_data = render::FrameData {
                            width: raw.width as u32,
                            height: raw.height as u32,
                            data: raw.data,
                            compressed: raw.compressed,
                            is_keyframe: raw.is_keyframe,
                        };
                        let _ = viewer_tx_recv.send(ViewerEvent::Frame(frame_data)).await;
                    }
                }
                Some(proto::message::Payload::CursorPosition(pos)) => {
                    let _ = viewer_tx_recv.send(ViewerEvent::CursorPosition(
                        pos.x, pos.y, pos.visible
                    )).await;
                }
                Some(proto::message::Payload::CursorShape(shape)) => {
                    let _ = viewer_tx_recv.send(ViewerEvent::CursorShape(shape)).await;
                }
                Some(proto::message::Payload::Disconnect(d)) => {
                    warn!("Server disconnected: {}", d.reason);
                    let _ = viewer_tx_recv.send(ViewerEvent::Disconnected(d.reason)).await;
                    break;
                }
                _ => {}
            }
        }
    });

    // Run the window (this blocks until window closes)
    render::run_window(viewer_rx, input_tx).await?;

    // Cleanup
    input_task.abort();
    receive_task.abort();

    Ok(())
}


Good, I can see the viewer is in `agent/src/viewer/`. Now I need to:

1. Check how the viewer is invoked in main.rs
2. Add protocol handler registration
3. Add protocol URL parsing

Let me check main.rs to see how the viewer subcommand works.


     1→//! GuruConnect - Remote Desktop Agent and Viewer
     2→//!
     3→//! Single binary for both agent (receiving connections) and viewer (initiating connections).
     4→//!
     5→//! Usage:
     6→//!   guruconnect agent              - Run as background agent
     7→//!   guruconnect view <session_id>  - View a remote session
     8→//!   guruconnect install            - Install and register protocol handler
     9→//!   guruconnect launch <url>       - Handle guruconnect:// URL
    10→//!   guruconnect [support_code]     - Legacy: run agent with support code
    11→
    12→// Hide console window by default on Windows (release builds)
    13→#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
    14→
    15→mod capture;
    16→mod chat;
    17→mod config;
    18→mod encoder;
    19→mod input;
    20→mod install;
    21→mod sas_client;
    22→mod session;
    23→mod startup;
    24→mod transport;
    25→mod tray;
    26→mod viewer;
    27→
    28→pub mod proto {
    29→    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
    30→}
    31→
    32→use anyhow::Result;
    33→use clap::{Parser, Subcommand};
    34→use tracing::{info, error, warn, Level};
    35→use tracing_subscriber::FmtSubscriber;
    36→
    37→#[cfg(windows)]
    38→use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, MB_ICONERROR};
    39→#[cfg(windows)]
    40→use windows::core::PCWSTR;
    41→#[cfg(windows)]
    42→use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
    43→#[cfg(windows)]
    44→use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};
    45→
    46→/// GuruConnect Remote Desktop
    47→#[derive(Parser)]
    48→#[command(name = "guruconnect")]
    49→#[command(version, about = "Remote desktop agent and viewer")]
    50→struct Cli {
    51→    #[command(subcommand)]
    52→    command: Option<Commands>,
    53→
    54→    /// Support code for legacy mode (runs agent with code)
    55→    #[arg(value_name = "SUPPORT_CODE")]
    56→    support_code: Option<String>,
    57→
    58→    /// Enable verbose logging
    59→    #[arg(short, long, global = true)]
    60→    verbose: bool,
    61→}
    62→
    63→#[derive(Subcommand)]
    64→enum Commands {
    65→    /// Run as background agent (receive remote connections)
    66→    Agent {
    67→        /// Support code for one-time session
    68→        #[arg(short, long)]
    69→        code: Option<String>,
    70→    },
    71→
    72→    /// View a remote session (connect to an agent)
    73→    View {
    74→        /// Session ID to connect to
    75→        session_id: String,
    76→
    77→        /// Server URL
    78→        #[arg(short, long, default_value = "wss://connect.azcomputerguru.com/ws/viewer")]
    79→        server: String,
    80→
    81→        /// API key for authentication
    82→        #[arg(short, long, default_value = "")]
    83→        api_key: String,
    84→    },
    85→
    86→    /// Install GuruConnect and register protocol handler
    87→    Install {
    88→        /// Skip UAC elevation, install for current user only
    89→        #[arg(long)]
    90→        user_only: bool,
    91→
    92→        /// Called internally when running elevated
    93→        #[arg(long, hide = true)]
    94→        elevated: bool,
    95→    },
    96→
    97→    /// Uninstall GuruConnect
    98→    Uninstall,
    99→
   100→    /// Handle a guruconnect:// protocol URL
   101→    Launch {
   102→        /// The guruconnect:// URL to handle
   103→        url: String,
   104→    },
   105→}
   106→
   107→fn main() -> Result<()> {
   108→    let cli = Cli::parse();
   109→
   110→    // Initialize logging
   111→    let level = if cli.verbose { Level::DEBUG } else { Level::INFO };
   112→    FmtSubscriber::builder()
   113→        .with_max_level(level)
   114→        .with_target(true)
   115→        .with_thread_ids(true)
   116→        .init();
   117→
   118→    info!("GuruConnect v{}", env!("CARGO_PKG_VERSION"));
   119→
   120→    match cli.command {
   121→        Some(Commands::Agent { code }) => {
   122→            run_agent_mode(code)
   123→        }
   124→        Some(Commands::View { session_id, server, api_key }) => {
   125→            run_viewer_mode(&server, &session_id, &api_key)
   126→        }
   127→        Some(Commands::Install { user_only, elevated }) => {
   128→            run_install(user_only || elevated)
   129→        }
   130→        Some(Commands::Uninstall) => {
   131→            run_uninstall()
   132→        }
   133→        Some(Commands::Launch { url }) => {
   134→            run_launch(&url)
   135→        }
   136→        None => {
   137→            // Legacy mode: if a support code was provided, run as agent
   138→            if let Some(code) = cli.support_code {
   139→                run_agent_mode(Some(code))
   140→            } else {
   141→                // No args: check if we should auto-detect mode
   142→                // For now, default to agent mode
   143→                run_agent_mode(None)
   144→            }
   145→        }
   146→    }
   147→}
   148→
   149→/// Run in agent mode (receive remote connections)
   150→fn run_agent_mode(support_code: Option<String>) -> Result<()> {
   151→    info!("Running in agent mode");
   152→
   153→    // Check elevation status
   154→    if install::is_elevated() {
   155→        info!("Running with elevated (administrator) privileges");
   156→    } else {
   157→        info!("Running with standard user privileges");
   158→    }
   159→
   160→    // Also check for support code in filename (legacy compatibility)
   161→    let code = support_code.or_else(extract_code_from_filename);
   162→    if let Some(ref c) = code {
   163→        info!("Support code: {}", c);
   164→    }
   165→
   166→    // Load configuration
   167→    let mut config = config::Config::load()?;
   168→    config.support_code = code;
   169→    info!("Server: {}", config.server_url);
   170→
   171→    // Run the agent
   172→    let rt = tokio::runtime::Runtime::new()?;
   173→    rt.block_on(run_agent(config))
   174→}
   175→
   176→/// Run in viewer mode (connect to remote session)
   177→fn run_viewer_mode(server: &str, session_id: &str, api_key: &str) -> Result<()> {
   178→    info!("Running in viewer mode");
   179→    info!("Connecting to session: {}", session_id);
   180→
   181→    let rt = tokio::runtime::Runtime::new()?;
   182→    rt.block_on(viewer::run(server, session_id, api_key))
   183→}
   184→
   185→/// Handle guruconnect:// URL launch
   186→fn run_launch(url: &str) -> Result<()> {
   187→    info!("Handling protocol URL: {}", url);
   188→
   189→    match install::parse_protocol_url(url) {
   190→        Ok((server, session_id, token)) => {
   191→            let api_key = token.unwrap_or_default();
   192→            run_viewer_mode(&server, &session_id, &api_key)
   193→        }
   194→        Err(e) => {
   195→            error!("Failed to parse URL: {}", e);
   196→            show_error_box("GuruConnect", &format!("Invalid URL: {}", e));
   197→            Err(e)
   198→        }
   199→    }
   200→}
   201→
   202→/// Install GuruConnect
   203→fn run_install(force_user_install: bool) -> Result<()> {
   204→    info!("Installing GuruConnect...");
   205→
   206→    match install::install(force_user_install) {
   207→        Ok(()) => {
   208→            show_message_box("GuruConnect", "Installation complete!\n\nYou can now use guruconnect:// links.");
   209→            Ok(())
   210→        }
   211→        Err(e) => {
   212→            error!("Installation failed: {}", e);
   213→            show_error_box("GuruConnect", &format!("Installation failed: {}", e));
   214→            Err(e)
   215→        }
   216→    }
   217→}
   218→
   219→/// Uninstall GuruConnect
   220→fn run_uninstall() -> Result<()> {
   221→    info!("Uninstalling GuruConnect...");
   222→
   223→    // Remove from startup
   224→    if let Err(e) = startup::remove_from_startup() {
   225→        warn!("Failed to remove from startup: {}", e);
   226→    }
   227→
   228→    // TODO: Remove registry keys for protocol handler
   229→    // TODO: Remove install directory
   230→
   231→    show_message_box("GuruConnect", "Uninstall complete.");
   232→    Ok(())
   233→}
   234→
   235→/// Extract a 6-digit support code from the executable's filename
   236→fn extract_code_from_filename() -> Option<String> {
   237→    let exe_path = std::env::current_exe().ok()?;
   238→    let filename = exe_path.file_stem()?.to_str()?;
   239→
   240→    // Look for a 6-digit number in the filename
   241→    for part in filename.split(|c| c == '-' || c == '_' || c == '.') {
   242→        let trimmed = part.trim();
   243→        if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) {
   244→            return Some(trimmed.to_string());
   245→        }
   246→    }
   247→
   248→    // Check if the last 6 characters are digits
   249→    if filename.len() >= 6 {
   250→        let last_six = &filename[filename.len() - 6..];
   251→        if last_six.chars().all(|c| c.is_ascii_digit()) {
   252→            return Some(last_six.to_string());
   253→        }
   254→    }
   255→
   256→    None
   257→}
   258→
   259→/// Show a message box (Windows only)
   260→#[cfg(windows)]
   261→fn show_message_box(title: &str, message: &str) {
   262→    use std::ffi::OsStr;
   263→    use std::os::windows::ffi::OsStrExt;
   264→
   265→    let title_wide: Vec<u16> = OsStr::new(title)
   266→        .encode_wide()
   267→        .chain(std::iter::once(0))
   268→        .collect();
   269→    let message_wide: Vec<u16> = OsStr::new(message)
   270→        .encode_wide()
   271→        .chain(std::iter::once(0))
   272→        .collect();
   273→
   274→    unsafe {
   275→        MessageBoxW(
   276→            None,
   277→            PCWSTR(message_wide.as_ptr()),
   278→            PCWSTR(title_wide.as_ptr()),
   279→            MB_OK | MB_ICONINFORMATION,
   280→        );
   281→    }
   282→}
   283→
   284→#[cfg(not(windows))]
   285→fn show_message_box(_title: &str, message: &str) {
   286→    println!("{}", message);
   287→}
   288→
   289→/// Show an error message box (Windows only)
   290→#[cfg(windows)]
   291→fn show_error_box(title: &str, message: &str) {
   292→    use std::ffi::OsStr;
   293→    use std::os::windows::ffi::OsStrExt;
   294→
   295→    let title_wide: Vec<u16> = OsStr::new(title)
   296→        .encode_wide()
   297→        .chain(std::iter::once(0))
   298→        .collect();
   299→    let message_wide: Vec<u16> = OsStr::new(message)
   300→        .encode_wide()
   301→        .chain(std::iter::once(0))
   302→        .collect();
   303→
   304→    unsafe {
   305→        MessageBoxW(
   306→            None,
   307→            PCWSTR(message_wide.as_ptr()),
   308→            PCWSTR(title_wide.as_ptr()),
   309→            MB_OK | MB_ICONERROR,
   310→        );
   311→    }
   312→}
   313→
   314→#[cfg(not(windows))]
   315→fn show_error_box(_title: &str, message: &str) {
   316→    eprintln!("ERROR: {}", message);
   317→}
   318→
   319→/// Show debug console window (Windows only)
   320→#[cfg(windows)]
   321→#[allow(dead_code)]
   322→fn show_debug_console() {
   323→    unsafe {
   324→        let hwnd = GetConsoleWindow();
   325→        if hwnd.0 == std::ptr::null_mut() {
   326→            let _ = AllocConsole();
   327→        } else {
   328→            let _ = ShowWindow(hwnd, SW_SHOW);
   329→        }
   330→    }
   331→}
   332→
   333→#[cfg(not(windows))]
   334→#[allow(dead_code)]
   335→fn show_debug_console() {}
   336→
   337→/// Clean up before exiting
   338→fn cleanup_on_exit() {
   339→    info!("Cleaning up before exit");
   340→    if let Err(e) = startup::remove_from_startup() {
   341→        warn!("Failed to remove from startup: {}", e);
   342→    }
   343→}
   344→
   345→/// Run the agent main loop
   346→async fn run_agent(config: config::Config) -> Result<()> {
   347→    let elevated = install::is_elevated();
   348→    let mut session = session::SessionManager::new(config.clone(), elevated);
   349→    let is_support_session = config.support_code.is_some();
   350→    let hostname = config.hostname();
   351→
   352→    // Add to startup
   353→    if let Err(e) = startup::add_to_startup() {
   354→        warn!("Failed to add to startup: {}", e);
   355→    }
   356→
   357→    // Create tray icon
   358→    let tray = match tray::TrayController::new(&hostname, config.support_code.as_deref(), is_support_session) {
   359→        Ok(t) => {
   360→            info!("Tray icon created");
   361→            Some(t)
   362→        }
   363→        Err(e) => {
   364→            warn!("Failed to create tray icon: {}", e);
   365→            None
   366→        }
   367→    };
   368→
   369→    // Create chat controller
   370→    let chat_ctrl = chat::ChatController::new();
   371→
   372→    // Connect to server and run main loop
   373→    loop {
   374→        info!("Connecting to server...");
   375→
   376→        if is_support_session {
   377→            if let Some(ref t) = tray {
   378→                if t.exit_requested() {
   379→                    info!("Exit requested by user");
   380→                    cleanup_on_exit();
   381→                    return Ok(());
   382→                }
   383→            }
   384→        }
   385→
   386→        match session.connect().await {
   387→            Ok(_) => {
   388→                info!("Connected to server");
   389→
   390→                if let Some(ref t) = tray {
   391→                    t.update_status("Status: Connected");
   392→                }
   393→
   394→                if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await {
   395→                    let error_msg = e.to_string();
   396→
   397→                    if error_msg.contains("USER_EXIT") {
   398→                        info!("Session ended by user");
   399→                        cleanup_on_exit();
   400→                        return Ok(());
   401→                    }
   402→
   403→                    if error_msg.contains("SESSION_CANCELLED") {
   404→                        info!("Session was cancelled by technician");
   405→                        cleanup_on_exit();
   406→                        show_message_box("Support Session Ended", "The support session was cancelled.");
   407→                        return Ok(());
   408→                    }
   409→
   410→                    if error_msg.contains("ADMIN_DISCONNECT") {
   411→                        info!("Session disconnected by administrator - uninstalling");
   412→                        if let Err(e) = startup::uninstall() {
   413→                            warn!("Uninstall failed: {}", e);
   414→                        }
   415→                        show_message_box("Remote Session Ended", "The session was ended by the administrator.");
   416→                        return Ok(());
   417→                    }
   418→
   419→                    if error_msg.contains("ADMIN_UNINSTALL") {
   420→                        info!("Uninstall command received from server - uninstalling");
   421→                        if let Err(e) = startup::uninstall() {
   422→                            warn!("Uninstall failed: {}", e);
   423→                        }
   424→                        show_message_box("GuruConnect Removed", "This computer has been removed from remote management.");
   425→                        return Ok(());
   426→                    }
   427→
   428→                    if error_msg.contains("ADMIN_RESTART") {
   429→                        info!("Restart command received - will reconnect");
   430→                        // Don't exit, just let the loop continue to reconnect
   431→                    } else {
   432→                        error!("Session error: {}", e);
   433→                    }
   434→                }
   435→            }
   436→            Err(e) => {
   437→                let error_msg = e.to_string();
   438→
   439→                if error_msg.contains("cancelled") {
   440→                    info!("Support code was cancelled");
   441→                    cleanup_on_exit();
   442→                    show_message_box("Support Session Cancelled", "This support session has been cancelled.");
   443→                    return Ok(());
   444→                }
   445→
   446→                error!("Connection failed: {}", e);
   447→            }
   448→        }
   449→
   450→        if is_support_session {
   451→            info!("Support session ended, not reconnecting");
   452→            cleanup_on_exit();
   453→            return Ok(());
   454→        }
   455→
   456→        info!("Reconnecting in 5 seconds...");
   457→        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
   458→    }
   459→}
   460→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! GuruConnect - Remote Desktop Agent and Viewer
//!
//! Single binary for both agent (receiving connections) and viewer (initiating connections).
//!
//! Usage:
//!   guruconnect agent              - Run as background agent
//!   guruconnect view <session_id>  - View a remote session
//!   guruconnect install            - Install and register protocol handler
//!   guruconnect launch <url>       - Handle guruconnect:// URL
//!   guruconnect [support_code]     - Legacy: run agent with support code

// Hide console window by default on Windows (release builds)
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod capture;
mod chat;
mod config;
mod encoder;
mod input;
mod install;
mod sas_client;
mod session;
mod startup;
mod transport;
mod tray;
mod viewer;

pub mod proto {
    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
}

use anyhow::Result;
use clap::{Parser, Subcommand};
use tracing::{info, error, warn, Level};
use tracing_subscriber::FmtSubscriber;

#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, MB_ICONERROR};
#[cfg(windows)]
use windows::core::PCWSTR;
#[cfg(windows)]
use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};

/// GuruConnect Remote Desktop
#[derive(Parser)]
#[command(name = "guruconnect")]
#[command(version, about = "Remote desktop agent and viewer")]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Support code for legacy mode (runs agent with code)
    #[arg(value_name = "SUPPORT_CODE")]
    support_code: Option<String>,

    /// Enable verbose logging
    #[arg(short, long, global = true)]
    verbose: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Run as background agent (receive remote connections)
    Agent {
        /// Support code for one-time session
        #[arg(short, long)]
        code: Option<String>,
    },

    /// View a remote session (connect to an agent)
    View {
        /// Session ID to connect to
        session_id: String,

        /// Server URL
        #[arg(short, long, default_value = "wss://connect.azcomputerguru.com/ws/viewer")]
        server: String,

        /// API key for authentication
        #[arg(short, long, default_value = "")]
        api_key: String,
    },

    /// Install GuruConnect and register protocol handler
    Install {
        /// Skip UAC elevation, install for current user only
        #[arg(long)]
        user_only: bool,

        /// Called internally when running elevated
        #[arg(long, hide = true)]
        elevated: bool,
    },

    /// Uninstall GuruConnect
    Uninstall,

    /// Handle a guruconnect:// protocol URL
    Launch {
        /// The guruconnect:// URL to handle
        url: String,
    },
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    // Initialize logging
    let level = if cli.verbose { Level::DEBUG } else { Level::INFO };
    FmtSubscriber::builder()
        .with_max_level(level)
        .with_target(true)
        .with_thread_ids(true)
        .init();

    info!("GuruConnect v{}", env!("CARGO_PKG_VERSION"));

    match cli.command {
        Some(Commands::Agent { code }) => {
            run_agent_mode(code)
        }
        Some(Commands::View { session_id, server, api_key }) => {
            run_viewer_mode(&server, &session_id, &api_key)
        }
        Some(Commands::Install { user_only, elevated }) => {
            run_install(user_only || elevated)
        }
        Some(Commands::Uninstall) => {
            run_uninstall()
        }
        Some(Commands::Launch { url }) => {
            run_launch(&url)
        }
        None => {
            // Legacy mode: if a support code was provided, run as agent
            if let Some(code) = cli.support_code {
                run_agent_mode(Some(code))
            } else {
                // No args: check if we should auto-detect mode
                // For now, default to agent mode
                run_agent_mode(None)
            }
        }
    }
}

/// Run in agent mode (receive remote connections)
fn run_agent_mode(support_code: Option<String>) -> Result<()> {
    info!("Running in agent mode");

    // Check elevation status
    if install::is_elevated() {
        info!("Running with elevated (administrator) privileges");
    } else {
        info!("Running with standard user privileges");
    }

    // Also check for support code in filename (legacy compatibility)
    let code = support_code.or_else(extract_code_from_filename);
    if let Some(ref c) = code {
        info!("Support code: {}", c);
    }

    // Load configuration
    let mut config = config::Config::load()?;
    config.support_code = code;
    info!("Server: {}", config.server_url);

    // Run the agent
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(run_agent(config))
}

/// Run in viewer mode (connect to remote session)
fn run_viewer_mode(server: &str, session_id: &str, api_key: &str) -> Result<()> {
    info!("Running in viewer mode");
    info!("Connecting to session: {}", session_id);

    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(viewer::run(server, session_id, api_key))
}

/// Handle guruconnect:// URL launch
fn run_launch(url: &str) -> Result<()> {
    info!("Handling protocol URL: {}", url);

    match install::parse_protocol_url(url) {
        Ok((server, session_id, token)) => {
            let api_key = token.unwrap_or_default();
            run_viewer_mode(&server, &session_id, &api_key)
        }
        Err(e) => {
            error!("Failed to parse URL: {}", e);
            show_error_box("GuruConnect", &format!("Invalid URL: {}", e));
            Err(e)
        }
    }
}

/// Install GuruConnect
fn run_install(force_user_install: bool) -> Result<()> {
    info!("Installing GuruConnect...");

    match install::install(force_user_install) {
        Ok(()) => {
            show_message_box("GuruConnect", "Installation complete!\n\nYou can now use guruconnect:// links.");
            Ok(())
        }
        Err(e) => {
            error!("Installation failed: {}", e);
            show_error_box("GuruConnect", &format!("Installation failed: {}", e));
            Err(e)
        }
    }
}

/// Uninstall GuruConnect
fn run_uninstall() -> Result<()> {
    info!("Uninstalling GuruConnect...");

    // Remove from startup
    if let Err(e) = startup::remove_from_startup() {
        warn!("Failed to remove from startup: {}", e);
    }

    // TODO: Remove registry keys for protocol handler
    // TODO: Remove install directory

    show_message_box("GuruConnect", "Uninstall complete.");
    Ok(())
}

/// Extract a 6-digit support code from the executable's filename
fn extract_code_from_filename() -> Option<String> {
    let exe_path = std::env::current_exe().ok()?;
    let filename = exe_path.file_stem()?.to_str()?;

    // Look for a 6-digit number in the filename
    for part in filename.split(|c| c == '-' || c == '_' || c == '.') {
        let trimmed = part.trim();
        if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) {
            return Some(trimmed.to_string());
        }
    }

    // Check if the last 6 characters are digits
    if filename.len() >= 6 {
        let last_six = &filename[filename.len() - 6..];
        if last_six.chars().all(|c| c.is_ascii_digit()) {
            return Some(last_six.to_string());
        }
    }

    None
}

/// Show a message box (Windows only)
#[cfg(windows)]
fn show_message_box(title: &str, message: &str) {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    let title_wide: Vec<u16> = OsStr::new(title)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let message_wide: Vec<u16> = OsStr::new(message)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        MessageBoxW(
            None,
            PCWSTR(message_wide.as_ptr()),
            PCWSTR(title_wide.as_ptr()),
            MB_OK | MB_ICONINFORMATION,
        );
    }
}

#[cfg(not(windows))]
fn show_message_box(_title: &str, message: &str) {
    println!("{}", message);
}

/// Show an error message box (Windows only)
#[cfg(windows)]
fn show_error_box(title: &str, message: &str) {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    let title_wide: Vec<u16> = OsStr::new(title)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let message_wide: Vec<u16> = OsStr::new(message)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        MessageBoxW(
            None,
            PCWSTR(message_wide.as_ptr()),
            PCWSTR(title_wide.as_ptr()),
            MB_OK | MB_ICONERROR,
        );
    }
}

#[cfg(not(windows))]
fn show_error_box(_title: &str, message: &str) {
    eprintln!("ERROR: {}", message);
}

/// Show debug console window (Windows only)
#[cfg(windows)]
#[allow(dead_code)]
fn show_debug_console() {
    unsafe {
        let hwnd = GetConsoleWindow();
        if hwnd.0 == std::ptr::null_mut() {
            let _ = AllocConsole();
        } else {
            let _ = ShowWindow(hwnd, SW_SHOW);
        }
    }
}

#[cfg(not(windows))]
#[allow(dead_code)]
fn show_debug_console() {}

/// Clean up before exiting
fn cleanup_on_exit() {
    info!("Cleaning up before exit");
    if let Err(e) = startup::remove_from_startup() {
        warn!("Failed to remove from startup: {}", e);
    }
}

/// Run the agent main loop
async fn run_agent(config: config::Config) -> Result<()> {
    let elevated = install::is_elevated();
    let mut session = session::SessionManager::new(config.clone(), elevated);
    let is_support_session = config.support_code.is_some();
    let hostname = config.hostname();

    // Add to startup
    if let Err(e) = startup::add_to_startup() {
        warn!("Failed to add to startup: {}", e);
    }

    // Create tray icon
    let tray = match tray::TrayController::new(&hostname, config.support_code.as_deref(), is_support_session) {
        Ok(t) => {
            info!("Tray icon created");
            Some(t)
        }
        Err(e) => {
            warn!("Failed to create tray icon: {}", e);
            None
        }
    };

    // Create chat controller
    let chat_ctrl = chat::ChatController::new();

    // Connect to server and run main loop
    loop {
        info!("Connecting to server...");

        if is_support_session {
            if let Some(ref t) = tray {
                if t.exit_requested() {
                    info!("Exit requested by user");
                    cleanup_on_exit();
                    return Ok(());
                }
            }
        }

        match session.connect().await {
            Ok(_) => {
                info!("Connected to server");

                if let Some(ref t) = tray {
                    t.update_status("Status: Connected");
                }

                if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await {
                    let error_msg = e.to_string();

                    if error_msg.contains("USER_EXIT") {
                        info!("Session ended by user");
                        cleanup_on_exit();
                        return Ok(());
                    }

                    if error_msg.contains("SESSION_CANCELLED") {
                        info!("Session was cancelled by technician");
                        cleanup_on_exit();
                        show_message_box("Support Session Ended", "The support session was cancelled.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_DISCONNECT") {
                        info!("Session disconnected by administrator - uninstalling");
                        if let Err(e) = startup::uninstall() {
                            warn!("Uninstall failed: {}", e);
                        }
                        show_message_box("Remote Session Ended", "The session was ended by the administrator.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_UNINSTALL") {
                        info!("Uninstall command received from server - uninstalling");
                        if let Err(e) = startup::uninstall() {
                            warn!("Uninstall failed: {}", e);
                        }
                        show_message_box("GuruConnect Removed", "This computer has been removed from remote management.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_RESTART") {
                        info!("Restart command received - will reconnect");
                        // Don't exit, just let the loop continue to reconnect
                    } else {
                        error!("Session error: {}", e);
                    }
                }
            }
            Err(e) => {
                let error_msg = e.to_string();

                if error_msg.contains("cancelled") {
                    info!("Support code was cancelled");
                    cleanup_on_exit();
                    show_message_box("Support Session Cancelled", "This support session has been cancelled.");
                    return Ok(());
                }

                error!("Connection failed: {}", e);
            }
        }

        if is_support_session {
            info!("Support session ended, not reconnecting");
            cleanup_on_exit();
            return Ok(());
        }

        info!("Reconnecting in 5 seconds...");
        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
    }
}


The main.rs already has:
1. `guruconnect install` - Install and register protocol handler
2. `guruconnect launch <url>` - Handle guruconnect:// URL
3. `guruconnect view <session_id>` - Direct viewer mode

Now I need to check install.rs to see how the protocol handler registration and URL parsing work, and then update the dashboard to use the protocol properly.


The main.rs already has `install` and `launch` commands. Let me check install.rs for the protocol handler: