File size: 8,939 Bytes
79381e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""
vscode_controller.py
====================
Handles VS Code-specific automation:
  - Create / open files and folders
  - Write, append, or prepend code into files
  - Run commands in the integrated terminal
  - Install / list extensions

Dependencies: code CLI, wmctrl, xdotool
"""

import os
import time
import subprocess
from pathlib import Path


# ── File operations ───────────────────────────────────────────────────────────

def open_in_vscode(path: str, new_window: bool = False) -> dict:
    """Open a file or folder in VS Code."""
    expanded = os.path.expanduser(path)
    if not os.path.exists(expanded):
        return {"success": False, "message": f"Path not found: '{path}'", "action": "error"}

    cmd = ["code"] + (["--new-window"] if new_window else []) + [expanded]
    subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                     start_new_session=True)
    return {"success": True, "message": f"Opened '{path}' in VS Code.",
            "action": "opened", "path": expanded}


def create_file(filepath: str, content: str = "", open_after: bool = True) -> dict:
    # ── Ensure absolute path ──────────────────────────────────────────────
    if not filepath or not filepath.strip():
        return {"success": False, "message": "No file path provided.", "action": "error"}

    expanded = os.path.expanduser(filepath)

    # If still not absolute, reject β€” don't guess
    if not os.path.isabs(expanded):
        return {"success": False,
                "message": f"Could not resolve path: '{filepath}'. Please provide a full path.",
                "action": "error"}

    try:
        Path(expanded).parent.mkdir(parents=True, exist_ok=True)
        Path(expanded).write_text(content, encoding="utf-8")

        # ── Verify file actually exists on disk before opening ────────────
        if not Path(expanded).exists():
            return {"success": False, "message": f"File write failed: '{expanded}'", "action": "error"}

        if open_after:
            subprocess.Popen(
                ["code", "--goto", expanded],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                start_new_session=True,
            )

        return {"success": True, "message": f"Created '{expanded}'.",
                "action": "created", "path": expanded}

    except Exception as e:
        return {"success": False, "message": f"Failed to create file: {e}", "action": "error"}


def create_folder(folderpath: str, open_after: bool = True) -> dict:
    """Create a folder and open it as a VS Code workspace."""
    expanded = os.path.expanduser(folderpath)
    try:
        Path(expanded).mkdir(parents=True, exist_ok=True)
        if open_after:
            subprocess.Popen(["code", expanded],
                             stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                             start_new_session=True)
        return {"success": True, "message": f"Created folder '{folderpath}'.",
                "action": "folder_created", "path": expanded}
    except Exception as e:
        return {"success": False, "message": f"Failed to create folder: {e}", "action": "error"}


def write_to_file(filepath: str, content: str, mode: str = "overwrite") -> dict:
    """
    Write content to an existing file.
    mode: 'overwrite' | 'append' | 'prepend'
    """
    expanded = os.path.expanduser(filepath)
    p = Path(expanded)
    if not p.exists():
        return {"success": False, "message": f"File not found: '{filepath}'", "action": "error"}

    try:
        if mode == "overwrite":
            p.write_text(content, encoding="utf-8")
        elif mode == "append":
            with open(expanded, "a", encoding="utf-8") as f:
                f.write("\n" + content)
        elif mode == "prepend":
            existing = p.read_text(encoding="utf-8")
            p.write_text(content + "\n" + existing, encoding="utf-8")
        else:
            return {"success": False, "message": f"Unknown write mode: '{mode}'", "action": "error"}

        return {"success": True, "message": f"Written to '{filepath}' ({mode}).",
                "action": "written", "path": expanded, "mode": mode}
    except Exception as e:
        return {"success": False, "message": f"Failed to write: {e}", "action": "error"}


# ── Terminal operations ───────────────────────────────────────────────────────

def run_in_terminal(command: str) -> dict:
    """
    Focus VS Code, open the integrated terminal, and run a command.
    Uses wmctrl + xdotool.
    """
    try:
        # 1. Focus VS Code window
        subprocess.run(["wmctrl", "-x", "-a", "code.Code"],
                       capture_output=True, timeout=5)
        time.sleep(0.6)

        # 2. Open integrated terminal (Ctrl + `)
        subprocess.run(["xdotool", "key", "ctrl+grave"], timeout=5)
        time.sleep(0.8)

        # 3. Type command + Enter
        subprocess.run(
            ["xdotool", "type", "--clearmodifiers", "--delay", "40", command],
            timeout=15
        )
        time.sleep(0.2)
        subprocess.run(["xdotool", "key", "Return"], timeout=5)

        return {"success": True, "message": f"Ran in terminal: `{command}`",
                "action": "terminal_run", "command": command}
    except Exception as e:
        return {"success": False, "message": f"Terminal run failed: {e}", "action": "error"}


# ── Extension operations ──────────────────────────────────────────────────────

def install_extension(extension_id: str) -> dict:
    """Install a VS Code extension by its marketplace ID (e.g. 'ms-python.python')."""
    try:
        result = subprocess.run(
            ["code", "--install-extension", extension_id, "--force"],
            capture_output=True, text=True, timeout=120
        )
        if result.returncode == 0:
            return {"success": True,
                    "message": f"Installed extension '{extension_id}'.",
                    "action": "extension_installed", "extension": extension_id}
        return {"success": False, "message": result.stderr.strip() or "Install failed.",
                "action": "error"}
    except Exception as e:
        return {"success": False, "message": f"Extension install failed: {e}", "action": "error"}


def list_extensions() -> dict:
    """List all installed VS Code extensions."""
    try:
        result = subprocess.run(["code", "--list-extensions"],
                                capture_output=True, text=True, timeout=15)
        exts = [e.strip() for e in result.stdout.strip().splitlines() if e.strip()]
        return {"success": True, "extensions": exts, "count": len(exts)}
    except Exception as e:
        return {"success": False, "message": str(e), "extensions": []}
    

def get_open_vscode_windows() -> list[dict]:
    """
    Returns list of open VS Code windows with their workspace names.
    Each: {"id": "0x04000004", "title": "AI OS", "full_title": "main_agent.py - AI OS - Visual Studio Code"}
    """
    try:
        result = subprocess.run(
            ["wmctrl", "-l", "-x"],
            capture_output=True, text=True, timeout=5
        )
        windows = []
        for line in result.stdout.strip().splitlines():
            parts = line.split(None, 4)
            if len(parts) < 5:
                continue
            win_class = parts[2].lower()
            title     = parts[4].strip()
            if "code.code" in win_class or "visual studio code" in title.lower():
                # Extract workspace name β€” "main_agent.py - AI OS - Visual Studio Code"
                # β†’ "AI OS"
                segments = [s.strip() for s in title.split(" - ")]
                workspace = segments[-2] if len(segments) >= 2 else title
                windows.append({
                    "id":         parts[0],
                    "title":      workspace,
                    "full_title": title,
                })
        return windows
    except Exception as e:
        print(f"[VSCodeController] Window list failed: {e}")
        return []


def focus_vscode_window(window_id: str) -> dict:
    """Focus a specific VS Code window by its wmctrl ID."""
    try:
        subprocess.run(
            ["wmctrl", "-i", "-a", window_id],
            capture_output=True, timeout=5
        )
        return {"success": True, "message": f"Focused window {window_id}"}
    except Exception as e:
        return {"success": False, "message": str(e)}