Toby Chui 2 недель назад
Родитель
Сommit
550a139723
4 измененных файлов с 1296 добавлено и 438 удалено
  1. 8 0
      src/mod/agi/README.md
  2. 16 0
      src/web/Terminal/backend/getdocs.agi
  3. 702 0
      src/web/Terminal/docs/api.json
  4. 570 438
      src/web/Terminal/index.html

+ 8 - 0
src/mod/agi/README.md

@@ -5,6 +5,14 @@ Scripts run in an Otto VM with sandboxed access to ArozOS functions.
 
 
 This document is updated to match the current AGI implementation in `mod/agi/agi*.go`.
 This document is updated to match the current AGI implementation in `mod/agi/agi*.go`.
 
 
+> **Maintainer note — keep Terminal in sync**
+> The Terminal webapp ships an in-app API reference panel that is driven by a separate
+> structured data file: **`src/web/Terminal/docs/api.json`**.
+> Whenever this README is updated (new functions, changed signatures, new library, etc.)
+> that file **must also be updated** to keep the in-app help accurate.
+> The JSON mirrors this README's section structure — one object per library section,
+> each with a `functions` array of `{ name, sig, desc, ret, example }` entries.
+
 ## AGI Version
 ## AGI Version
 
 
 - Runtime version: `3.0` (`AgiVersion` in `agi.go`)
 - Runtime version: `3.0` (`AgiVersion` in `agi.go`)

+ 16 - 0
src/web/Terminal/backend/getdocs.agi

@@ -0,0 +1,16 @@
+/*
+    Terminal — Serve API Documentation
+
+    Reads Terminal/docs/api.json from the web root and returns it as JSON.
+    This keeps the docs file editable without touching Go code.
+*/
+
+requirelib("appdata");
+
+var content = appdata.readFile("Terminal/docs/api.json");
+if (content === false || content === null || content === undefined) {
+    sendJSONResp(JSON.stringify({error: "Documentation file not found"}));
+} else {
+    HTTP_HEADER = "application/json";
+    sendResp(content);
+}

+ 702 - 0
src/web/Terminal/docs/api.json

@@ -0,0 +1,702 @@
+{
+  "_note": "This file is the source for the Terminal in-app documentation panel. It is derived from mod/agi/README.md. Whenever README.md is updated, this file MUST also be updated to keep the in-app help in sync.",
+  "_version": "3.0",
+  "sections": [
+    {
+      "id": "core",
+      "name": "Core",
+      "desc": "Built-in functions always available without requirelib()",
+      "functions": [
+        {
+          "name": "sendResp",
+          "sig": "sendResp(content)",
+          "desc": "Set the HTTP response body.",
+          "ret": "void",
+          "example": "sendResp(\"Hello from AGI\");"
+        },
+        {
+          "name": "sendJSONResp",
+          "sig": "sendJSONResp(objectOrJsonString)",
+          "desc": "Set Content-Type to application/json and write a JSON response.",
+          "ret": "void",
+          "example": "sendJSONResp({ ok: true, items: [1, 2, 3] });"
+        },
+        {
+          "name": "echo",
+          "sig": "echo(content)",
+          "desc": "Append text to the current HTTP_RESP.",
+          "ret": "void",
+          "example": "echo(\"Hello \");\necho(\"World\");"
+        },
+        {
+          "name": "sendOK",
+          "sig": "sendOK()",
+          "desc": "Set response to the string \"ok\".",
+          "ret": "void",
+          "example": "sendOK();"
+        },
+        {
+          "name": "requirelib",
+          "sig": "requirelib(libname)",
+          "desc": "Load an AGI library into the current VM. Returns true on success.",
+          "ret": "bool",
+          "example": "if (!requirelib(\"filelib\")) {\n    sendResp(\"filelib not available\");\n}"
+        },
+        {
+          "name": "includes",
+          "sig": "includes(scriptName)",
+          "desc": "Load and execute another script relative to the current script's directory.",
+          "ret": "void",
+          "example": "includes(\"helpers.js\");"
+        },
+        {
+          "name": "delay",
+          "sig": "delay(ms)",
+          "desc": "Sleep for the given number of milliseconds. After websocket.upgrade(), also pumps incoming WebSocket messages.",
+          "ret": "void",
+          "example": "delay(500);"
+        },
+        {
+          "name": "exit",
+          "sig": "exit()",
+          "desc": "Stop script execution immediately.",
+          "ret": "void",
+          "example": "if (!userIsAdmin()) exit();"
+        },
+        {
+          "name": "execd",
+          "sig": "execd(scriptName, payload)",
+          "desc": "Execute another AGI script asynchronously as a detached process.",
+          "ret": "void",
+          "example": "execd(\"worker.agi\", JSON.stringify({ job: \"thumbs\" }));"
+        },
+        {
+          "name": "console.log",
+          "sig": "console.log(...args)",
+          "desc": "Write a line to the server log. In Terminal sessions, output is captured and shown in yellow.",
+          "ret": "void",
+          "example": "console.log(\"Debug:\", someVariable);"
+        }
+      ]
+    },
+    {
+      "id": "db",
+      "name": "DB",
+      "desc": "Key-value database functions, always available.",
+      "functions": [
+        {
+          "name": "newDBTableIfNotExists",
+          "sig": "newDBTableIfNotExists(tableName)",
+          "desc": "Create a database table if it does not already exist.",
+          "ret": "bool",
+          "example": "newDBTableIfNotExists(\"my_table\");"
+        },
+        {
+          "name": "DBTableExists",
+          "sig": "DBTableExists(tableName)",
+          "desc": "Return true if the table exists.",
+          "ret": "bool",
+          "example": "if (DBTableExists(\"my_table\")) sendOK();"
+        },
+        {
+          "name": "writeDBItem",
+          "sig": "writeDBItem(tableName, key, value)",
+          "desc": "Write a string value to the given key.",
+          "ret": "bool",
+          "example": "writeDBItem(\"my_table\", \"theme\", \"dark\");"
+        },
+        {
+          "name": "readDBItem",
+          "sig": "readDBItem(tableName, key)",
+          "desc": "Read a string value from the given key.",
+          "ret": "string",
+          "example": "var theme = readDBItem(\"my_table\", \"theme\");"
+        },
+        {
+          "name": "listDBTable",
+          "sig": "listDBTable(tableName)",
+          "desc": "Return all key-value pairs in the table as an object.",
+          "ret": "object",
+          "example": "var kv = listDBTable(\"my_table\");\nsendJSONResp(kv);"
+        },
+        {
+          "name": "deleteDBItem",
+          "sig": "deleteDBItem(tableName, key)",
+          "desc": "Delete a single key from the table.",
+          "ret": "bool",
+          "example": "deleteDBItem(\"my_table\", \"theme\");"
+        },
+        {
+          "name": "dropDBTable",
+          "sig": "dropDBTable(tableName)",
+          "desc": "Delete the entire table.",
+          "ret": "bool",
+          "example": "dropDBTable(\"my_table\");"
+        }
+      ]
+    },
+    {
+      "id": "user",
+      "name": "User",
+      "desc": "User management and permission functions.",
+      "functions": [
+        {
+          "name": "pathCanWrite",
+          "sig": "pathCanWrite(vpath)",
+          "desc": "Return true if the current user can write to the virtual path.",
+          "ret": "bool",
+          "example": "if (pathCanWrite(\"user:/Documents\")) sendOK();"
+        },
+        {
+          "name": "getUserPermissionGroup",
+          "sig": "getUserPermissionGroup()",
+          "desc": "Return a JSON string describing the current user's permission group.",
+          "ret": "string (JSON)",
+          "example": "var group = JSON.parse(getUserPermissionGroup());"
+        },
+        {
+          "name": "userIsAdmin",
+          "sig": "userIsAdmin()",
+          "desc": "Return true if the current user is an administrator.",
+          "ret": "bool",
+          "example": "if (!userIsAdmin()) {\n    sendResp(\"Admin only\");\n    exit();\n}"
+        },
+        {
+          "name": "userExists",
+          "sig": "userExists(username)",
+          "desc": "(Admin only) Return true if the username exists.",
+          "ret": "bool",
+          "example": "if (userExists(\"alice\")) echo(\"exists\");"
+        },
+        {
+          "name": "createUser",
+          "sig": "createUser(username, password, defaultGroup)",
+          "desc": "(Admin only) Create a new user account.",
+          "ret": "bool",
+          "example": "createUser(\"alice\", \"StrongPass\", \"default\");"
+        },
+        {
+          "name": "removeUser",
+          "sig": "removeUser(username)",
+          "desc": "(Admin only) Delete a user account.",
+          "ret": "bool",
+          "example": "removeUser(\"alice\");"
+        }
+      ]
+    },
+    {
+      "id": "filelib",
+      "name": "filelib",
+      "desc": "Virtual filesystem read, write, and metadata operations.",
+      "load": "requirelib(\"filelib\");",
+      "functions": [
+        {
+          "name": "filelib.writeFile",
+          "sig": "filelib.writeFile(vpath, content)",
+          "desc": "Write text content to a virtual path. Creates the file if it does not exist.",
+          "ret": "bool",
+          "example": "requirelib(\"filelib\");\nfilelib.writeFile(\"user:/notes.txt\", \"Hello World\");"
+        },
+        {
+          "name": "filelib.readFile",
+          "sig": "filelib.readFile(vpath)",
+          "desc": "Read text content from a virtual path.",
+          "ret": "string | false",
+          "example": "requirelib(\"filelib\");\nvar text = filelib.readFile(\"user:/notes.txt\");"
+        },
+        {
+          "name": "filelib.deleteFile",
+          "sig": "filelib.deleteFile(vpath)",
+          "desc": "Delete a file at the virtual path.",
+          "ret": "bool",
+          "example": "requirelib(\"filelib\");\nfilelib.deleteFile(\"user:/notes.txt\");"
+        },
+        {
+          "name": "filelib.walk",
+          "sig": "filelib.walk(vpath, mode)",
+          "desc": "Recursively list entries. mode: \"all\", \"file\", or \"folder\".",
+          "ret": "string[]",
+          "example": "requirelib(\"filelib\");\nvar files = filelib.walk(\"user:/\", \"file\");"
+        },
+        {
+          "name": "filelib.glob",
+          "sig": "filelib.glob(pattern, sortMode)",
+          "desc": "Glob match files. sortMode: \"default\" or user-defined sort. Does not support ** patterns.",
+          "ret": "string[]",
+          "example": "requirelib(\"filelib\");\nvar jpgs = filelib.glob(\"user:/Desktop/*.jpg\", \"default\");"
+        },
+        {
+          "name": "filelib.aglob",
+          "sig": "filelib.aglob(pattern, sortMode)",
+          "desc": "Advanced glob supporting ** recursive patterns. Cannot scan bare root dirs.",
+          "ret": "string[]",
+          "example": "requirelib(\"filelib\");\nvar pngs = filelib.aglob(\"user:/Desktop/**/*.png\", \"default\");"
+        },
+        {
+          "name": "filelib.readdir",
+          "sig": "filelib.readdir(vpath, sortMode)",
+          "desc": "List directory entries. Returns array of {Filename, Filepath, Ext, Filesize, Modtime, IsDir}.",
+          "ret": "object[]",
+          "example": "requirelib(\"filelib\");\nvar entries = filelib.readdir(\"user:/Desktop\", \"default\");\nsendJSONResp(entries);"
+        },
+        {
+          "name": "filelib.filesize",
+          "sig": "filelib.filesize(vpath)",
+          "desc": "Return file size in bytes.",
+          "ret": "number",
+          "example": "requirelib(\"filelib\");\nvar sz = filelib.filesize(\"user:/movie.mp4\");"
+        },
+        {
+          "name": "filelib.fileExists",
+          "sig": "filelib.fileExists(vpath)",
+          "desc": "Return true if the path exists as a file.",
+          "ret": "bool",
+          "example": "requirelib(\"filelib\");\nif (filelib.fileExists(\"user:/a.txt\")) sendOK();"
+        },
+        {
+          "name": "filelib.isDir",
+          "sig": "filelib.isDir(vpath)",
+          "desc": "Return true if the path is a directory.",
+          "ret": "bool",
+          "example": "requirelib(\"filelib\");\nif (filelib.isDir(\"user:/Desktop\")) sendOK();"
+        },
+        {
+          "name": "filelib.mkdir",
+          "sig": "filelib.mkdir(vpath)",
+          "desc": "Create a directory (and parents) at the virtual path.",
+          "ret": "bool",
+          "example": "requirelib(\"filelib\");\nfilelib.mkdir(\"user:/newfolder\");"
+        },
+        {
+          "name": "filelib.md5",
+          "sig": "filelib.md5(vpath)",
+          "desc": "Return the MD5 hash string of a file.",
+          "ret": "string",
+          "example": "requirelib(\"filelib\");\nvar hash = filelib.md5(\"user:/a.txt\");"
+        },
+        {
+          "name": "filelib.mtime",
+          "sig": "filelib.mtime(vpath, parseToUnix)",
+          "desc": "Return file modification time. parseToUnix=true returns a Unix timestamp; otherwise a formatted string.",
+          "ret": "number | string",
+          "example": "requirelib(\"filelib\");\nvar ts = filelib.mtime(\"user:/a.txt\", true);"
+        },
+        {
+          "name": "filelib.rootName",
+          "sig": "filelib.rootName(vpath)",
+          "desc": "Return the display name of the storage root that owns this path.",
+          "ret": "string",
+          "example": "requirelib(\"filelib\");\nvar root = filelib.rootName(\"user:/Desktop/a.txt\");"
+        }
+      ]
+    },
+    {
+      "id": "imagelib",
+      "name": "imagelib",
+      "desc": "Image dimension, resize, crop, and EXIF operations.",
+      "load": "requirelib(\"imagelib\");",
+      "functions": [
+        {
+          "name": "imagelib.getImageDimension",
+          "sig": "imagelib.getImageDimension(vpath)",
+          "desc": "Return [width, height] of the image.",
+          "ret": "[number, number]",
+          "example": "requirelib(\"imagelib\");\nvar dim = imagelib.getImageDimension(\"user:/img.jpg\");"
+        },
+        {
+          "name": "imagelib.resizeImage",
+          "sig": "imagelib.resizeImage(src, dest, width, height)",
+          "desc": "Resize an image and save to dest.",
+          "ret": "bool",
+          "example": "requirelib(\"imagelib\");\nimagelib.resizeImage(\"user:/img.jpg\", \"user:/img_small.jpg\", 800, 600);"
+        },
+        {
+          "name": "imagelib.resizeImageBase64",
+          "sig": "imagelib.resizeImageBase64(src, width, height, format)",
+          "desc": "Resize an image and return it as a base64 data URL. format: \"jpeg\", \"png\".",
+          "ret": "string (data URL)",
+          "example": "requirelib(\"imagelib\");\nvar b64 = imagelib.resizeImageBase64(\"user:/img.jpg\", 320, 240, \"jpeg\");"
+        },
+        {
+          "name": "imagelib.cropImage",
+          "sig": "imagelib.cropImage(src, dest, x, y, width, height)",
+          "desc": "Crop a region from an image and save to dest.",
+          "ret": "bool",
+          "example": "requirelib(\"imagelib\");\nimagelib.cropImage(\"user:/img.jpg\", \"user:/crop.jpg\", 10, 10, 200, 200);"
+        },
+        {
+          "name": "imagelib.loadThumbString",
+          "sig": "imagelib.loadThumbString(vpath)",
+          "desc": "Return the cached thumbnail as a base64 string.",
+          "ret": "string",
+          "example": "requirelib(\"imagelib\");\nvar thumb = imagelib.loadThumbString(\"user:/img.jpg\");"
+        },
+        {
+          "name": "imagelib.hasExif",
+          "sig": "imagelib.hasExif(vpath)",
+          "desc": "Return true if the image has EXIF metadata.",
+          "ret": "bool",
+          "example": "requirelib(\"imagelib\");\nif (imagelib.hasExif(\"user:/img.jpg\")) echo(\"has exif\");"
+        },
+        {
+          "name": "imagelib.getExif",
+          "sig": "imagelib.getExif(vpath)",
+          "desc": "Return EXIF data as a JSON string.",
+          "ret": "string (JSON)",
+          "example": "requirelib(\"imagelib\");\nvar exif = JSON.parse(imagelib.getExif(\"user:/img.jpg\"));"
+        }
+      ]
+    },
+    {
+      "id": "http",
+      "name": "http",
+      "desc": "Outbound HTTP request helpers.",
+      "load": "requirelib(\"http\");",
+      "functions": [
+        {
+          "name": "http.get",
+          "sig": "http.get(url)",
+          "desc": "Perform an HTTP GET and return the response body as a string.",
+          "ret": "string",
+          "example": "requirelib(\"http\");\nvar body = http.get(\"https://example.com/api\");"
+        },
+        {
+          "name": "http.post",
+          "sig": "http.post(url, jsonString)",
+          "desc": "Perform an HTTP POST with JSON body and return the response body.",
+          "ret": "string",
+          "example": "requirelib(\"http\");\nvar resp = http.post(\"https://example.com/api\", JSON.stringify({a:1}));"
+        },
+        {
+          "name": "http.head",
+          "sig": "http.head(url, headerKey)",
+          "desc": "Fetch response headers. Without headerKey returns all headers as JSON; with headerKey returns just that header.",
+          "ret": "string (JSON)",
+          "example": "requirelib(\"http\");\nvar headers = JSON.parse(http.head(\"https://example.com\"));"
+        },
+        {
+          "name": "http.getCode",
+          "sig": "http.getCode(url)",
+          "desc": "Return the HTTP status code for the URL.",
+          "ret": "number",
+          "example": "requirelib(\"http\");\nvar code = http.getCode(\"https://example.com\");"
+        },
+        {
+          "name": "http.download",
+          "sig": "http.download(url, destDirVpath, filenameOptional)",
+          "desc": "Download a URL into the destination virtual directory.",
+          "ret": "bool",
+          "example": "requirelib(\"http\");\nhttp.download(\"https://example.com/a.zip\", \"user:/Downloads\", \"a.zip\");"
+        },
+        {
+          "name": "http.getb64",
+          "sig": "http.getb64(url)",
+          "desc": "Fetch a URL and return the raw bytes as a base64 string.",
+          "ret": "string",
+          "example": "requirelib(\"http\");\nvar raw = http.getb64(\"https://example.com/logo.png\");"
+        },
+        {
+          "name": "http.redirect",
+          "sig": "http.redirect(targetUrl, statusCode)",
+          "desc": "Redirect the client. Default statusCode is 307.",
+          "ret": "void",
+          "example": "requirelib(\"http\");\nhttp.redirect(\"https://example.com/new\", 302);"
+        }
+      ]
+    },
+    {
+      "id": "share",
+      "name": "share",
+      "desc": "Create and manage public file share links.",
+      "load": "requirelib(\"share\");",
+      "functions": [
+        {
+          "name": "share.shareFile",
+          "sig": "share.shareFile(vpath, timeoutSec)",
+          "desc": "Create a public share link. timeoutSec=0 means no expiry. Returns the share UUID.",
+          "ret": "string (uuid)",
+          "example": "requirelib(\"share\");\nvar uuid = share.shareFile(\"user:/report.pdf\", 3600);"
+        },
+        {
+          "name": "share.removeShare",
+          "sig": "share.removeShare(shareUUID)",
+          "desc": "Remove an existing share link.",
+          "ret": "bool",
+          "example": "requirelib(\"share\");\nshare.removeShare(uuid);"
+        },
+        {
+          "name": "share.checkShareExists",
+          "sig": "share.checkShareExists(shareUUID)",
+          "desc": "Return true if the share UUID is still valid.",
+          "ret": "bool",
+          "example": "requirelib(\"share\");\nif (share.checkShareExists(uuid)) sendOK();"
+        },
+        {
+          "name": "share.fileIsShared",
+          "sig": "share.fileIsShared(vpath)",
+          "desc": "Return true if the file already has an active share link.",
+          "ret": "bool",
+          "example": "requirelib(\"share\");\nif (share.fileIsShared(\"user:/report.pdf\")) sendOK();"
+        },
+        {
+          "name": "share.getFileShareUUID",
+          "sig": "share.getFileShareUUID(vpath)",
+          "desc": "Return the share UUID for the given file, or false if not shared.",
+          "ret": "string | false",
+          "example": "requirelib(\"share\");\nvar sid = share.getFileShareUUID(\"user:/report.pdf\");"
+        }
+      ]
+    },
+    {
+      "id": "appdata",
+      "name": "appdata",
+      "desc": "Read-only access to web-root application data files.",
+      "load": "requirelib(\"appdata\");",
+      "functions": [
+        {
+          "name": "appdata.readFile",
+          "sig": "appdata.readFile(relativePathFromWebRoot)",
+          "desc": "Read a file relative to ./web/. Returns false on error.",
+          "ret": "string | false",
+          "example": "requirelib(\"appdata\");\nvar conf = appdata.readFile(\"MyApp/config.json\");"
+        },
+        {
+          "name": "appdata.listDir",
+          "sig": "appdata.listDir(relativeDirFromWebRoot)",
+          "desc": "List files in a directory relative to ./web/. Returns an array of relative paths.",
+          "ret": "string[]",
+          "example": "requirelib(\"appdata\");\nvar files = appdata.listDir(\"MyApp\");\nsendJSONResp(files);"
+        },
+        {
+          "name": "appdata.getModuleList",
+          "sig": "appdata.getModuleList()",
+          "desc": "Return an array of registered module objects.",
+          "ret": "object[]",
+          "example": "requirelib(\"appdata\");\nvar mods = appdata.getModuleList();"
+        }
+      ]
+    },
+    {
+      "id": "sysinfo",
+      "name": "sysinfo",
+      "desc": "Real-time system resource information.",
+      "load": "requirelib(\"sysinfo\");",
+      "functions": [
+        {
+          "name": "sysinfo.getCPUUsage",
+          "sig": "sysinfo.getCPUUsage()",
+          "desc": "Return CPU usage as a percentage (0–100).",
+          "ret": "number",
+          "example": "requirelib(\"sysinfo\");\nvar cpu = sysinfo.getCPUUsage();"
+        },
+        {
+          "name": "sysinfo.getRAMUsage",
+          "sig": "sysinfo.getRAMUsage()",
+          "desc": "Return {used, total, percent} memory statistics.",
+          "ret": "object",
+          "example": "requirelib(\"sysinfo\");\nvar ram = sysinfo.getRAMUsage();\nsendJSONResp(ram);"
+        },
+        {
+          "name": "sysinfo.getNetworkUsage",
+          "sig": "sysinfo.getNetworkUsage()",
+          "desc": "Return {rxRate, txRate, rxTotal, txTotal} in bytes/bytes-per-second.",
+          "ret": "object",
+          "example": "requirelib(\"sysinfo\");\nvar net = sysinfo.getNetworkUsage();"
+        },
+        {
+          "name": "sysinfo.getDiskInfo",
+          "sig": "sysinfo.getDiskInfo()",
+          "desc": "Return an array of logical disk info objects.",
+          "ret": "object[]",
+          "example": "requirelib(\"sysinfo\");\nvar disks = sysinfo.getDiskInfo();\nsendJSONResp(disks);"
+        }
+      ]
+    },
+    {
+      "id": "ziplib",
+      "name": "ziplib",
+      "desc": "Archive creation and extraction (zip, tar, tar.gz, gz).",
+      "load": "requirelib(\"ziplib\");",
+      "functions": [
+        {
+          "name": "ziplib.extractZipFile",
+          "sig": "ziplib.extractZipFile(src, destDir)",
+          "desc": "Extract a ZIP archive into destDir.",
+          "ret": "bool",
+          "example": "requirelib(\"ziplib\");\nziplib.extractZipFile(\"user:/a.zip\", \"user:/out/\");"
+        },
+        {
+          "name": "ziplib.createZipFile",
+          "sig": "ziplib.createZipFile(sourcesArrayOrString, outputZip)",
+          "desc": "Create a ZIP archive from one or more source paths.",
+          "ret": "bool",
+          "example": "requirelib(\"ziplib\");\nziplib.createZipFile([\"user:/a.txt\", \"user:/b.txt\"], \"user:/bundle.zip\");"
+        },
+        {
+          "name": "ziplib.extractAnyFile",
+          "sig": "ziplib.extractAnyFile(srcArchive, destDir)",
+          "desc": "Auto-detect archive format and extract into destDir.",
+          "ret": "bool",
+          "example": "requirelib(\"ziplib\");\nziplib.extractAnyFile(\"user:/archive.tar.gz\", \"user:/out/\");"
+        },
+        {
+          "name": "ziplib.createAnyZipFile",
+          "sig": "ziplib.createAnyZipFile(sourcesArrayOrString, outputPath, format)",
+          "desc": "Create an archive in any supported format. format: \"zip\", \"tar\", \"tar.gz\", \"gz\".",
+          "ret": "bool",
+          "example": "requirelib(\"ziplib\");\nziplib.createAnyZipFile([\"user:/folder\"], \"user:/bundle.tar.gz\", \"tar.gz\");"
+        },
+        {
+          "name": "ziplib.isValidZipFile",
+          "sig": "ziplib.isValidZipFile(vpath)",
+          "desc": "Return true if the file is a recognisable archive.",
+          "ret": "bool",
+          "example": "requirelib(\"ziplib\");\nvar ok = ziplib.isValidZipFile(\"user:/a.zip\");"
+        },
+        {
+          "name": "ziplib.listZipFileContents",
+          "sig": "ziplib.listZipFileContents(zipPath)",
+          "desc": "Return a JSON tree string of the archive's contents.",
+          "ret": "string (JSON)",
+          "example": "requirelib(\"ziplib\");\nvar tree = JSON.parse(ziplib.listZipFileContents(\"user:/a.zip\"));"
+        },
+        {
+          "name": "ziplib.getFileFromZip",
+          "sig": "ziplib.getFileFromZip(zipPath, filePathInZip)",
+          "desc": "Extract one file to tmp:/ and return its virtual path.",
+          "ret": "string (vpath)",
+          "example": "requirelib(\"ziplib\");\nvar tmp = ziplib.getFileFromZip(\"user:/a.zip\", \"docs/readme.txt\");"
+        }
+      ]
+    },
+    {
+      "id": "websocket",
+      "name": "websocket",
+      "desc": "Upgrade the HTTP connection to a persistent WebSocket session.",
+      "load": "requirelib(\"websocket\");",
+      "functions": [
+        {
+          "name": "websocket.upgrade",
+          "sig": "websocket.upgrade(timeoutSec)",
+          "desc": "Upgrade to WebSocket. Also overrides delay() with a message-pumping version. Returns false on failure.",
+          "ret": "bool",
+          "example": "requirelib(\"websocket\");\nif (!websocket.upgrade(120)) exit();\nwebsocket.send(\"Connected!\");"
+        },
+        {
+          "name": "websocket.send",
+          "sig": "websocket.send(text)",
+          "desc": "Send a text frame to the client. Returns false if the connection is closed.",
+          "ret": "bool",
+          "example": "websocket.send(\"Hello client\");"
+        },
+        {
+          "name": "websocket.read",
+          "sig": "websocket.read(timeoutMs)",
+          "desc": "Read next message. Returns string on message, null on timeout (still open), false if closed. Omit timeoutMs to block.",
+          "ret": "string | null | false",
+          "example": "var msg = websocket.read(5000);\nif (msg === false) { /* closed */ }\nif (msg === null)  { /* timeout  */ }"
+        },
+        {
+          "name": "websocket.available",
+          "sig": "websocket.available()",
+          "desc": "Return count of buffered unread messages. Non-blocking.",
+          "ret": "number",
+          "example": "if (websocket.available() > 0) {\n    var msg = websocket.read();\n}"
+        },
+        {
+          "name": "websocket.isClosed",
+          "sig": "websocket.isClosed()",
+          "desc": "Return true when the connection is no longer active.",
+          "ret": "bool",
+          "example": "while (!websocket.isClosed()) {\n    websocket.send(\"tick\");\n    delay(1000);\n}"
+        },
+        {
+          "name": "websocket.onMessage",
+          "sig": "websocket.onMessage = function(msg) { ... }",
+          "desc": "Assign a callback fired inside delay(). msg = { data, timestamp, type }. Set to null to stop and leave messages in buffer.",
+          "ret": "void",
+          "example": "var last = \"\";\nwebsocket.onMessage = function(msg) {\n    last = msg.data;\n};\nwhile (!websocket.isClosed()) {\n    if (last !== \"\") {\n        websocket.send(\"Echo: \" + last);\n        last = \"\";\n    }\n    delay(100);\n}"
+        },
+        {
+          "name": "websocket.close",
+          "sig": "websocket.close()",
+          "desc": "Send a normal-closure frame and close the connection.",
+          "ret": "bool",
+          "example": "websocket.close();"
+        }
+      ]
+    },
+    {
+      "id": "scheduler",
+      "name": "scheduler",
+      "desc": "Register and manage recurring background tasks for a webapp.",
+      "load": "requirelib(\"scheduler\");",
+      "functions": [
+        {
+          "name": "scheduler.hasPermission",
+          "sig": "scheduler.hasPermission()",
+          "desc": "Return true if the current user is allowed to create scheduled tasks.",
+          "ret": "bool",
+          "example": "requirelib(\"scheduler\");\nif (!scheduler.hasPermission()) sendResp(\"no_permission\");"
+        },
+        {
+          "name": "scheduler.registered",
+          "sig": "scheduler.registered(taskName, appName)",
+          "desc": "Return true if the task is already registered for this user+app.",
+          "ret": "bool",
+          "example": "requirelib(\"scheduler\");\nif (scheduler.registered(\"MyApp_Sync\", \"MyApp\")) sendResp(\"already_registered\");"
+        },
+        {
+          "name": "scheduler.register",
+          "sig": "scheduler.register(taskName, appName, intervalSecs, description, scriptName)",
+          "desc": "Register a new background task. scriptName defaults to \"cron.agi\".",
+          "ret": "bool",
+          "example": "requirelib(\"scheduler\");\nvar ok = scheduler.register(\"MyApp_Sync\", \"MyApp\", 3600, \"Hourly sync\", \"cron.agi\");"
+        },
+        {
+          "name": "scheduler.unregister",
+          "sig": "scheduler.unregister(taskName)",
+          "desc": "Remove a registered task.",
+          "ret": "bool",
+          "example": "requirelib(\"scheduler\");\nscheduler.unregister(\"MyApp_Sync\");"
+        }
+      ]
+    },
+    {
+      "id": "ffmpeg",
+      "name": "ffmpeg",
+      "desc": "Media conversion via ffmpeg. Only available when ffmpeg is installed on the host.",
+      "load": "requirelib(\"ffmpeg\");",
+      "functions": [
+        {
+          "name": "ffmpeg.convert",
+          "sig": "ffmpeg.convert(input, output, compression)",
+          "desc": "Generic media conversion.",
+          "ret": "bool",
+          "example": "requirelib(\"ffmpeg\");\nffmpeg.convert(\"user:/in.mov\", \"user:/out.mp4\", 0);"
+        },
+        {
+          "name": "ffmpeg.videoConvert",
+          "sig": "ffmpeg.videoConvert(input, output, resolution, compressionRate, progressFile)",
+          "desc": "Convert video to the given resolution. progressFile is a virtual path for JSON progress updates.",
+          "ret": "bool",
+          "example": "requirelib(\"ffmpeg\");\nffmpeg.videoConvert(\"user:/in.mp4\", \"user:/out.mp4\", \"720p\", 55, \"tmp:/progress.json\");"
+        },
+        {
+          "name": "ffmpeg.audioConvert",
+          "sig": "ffmpeg.audioConvert(input, output, sampleRate, progressFile)",
+          "desc": "Convert audio to the given sample rate.",
+          "ret": "bool",
+          "example": "requirelib(\"ffmpeg\");\nffmpeg.audioConvert(\"user:/in.wav\", \"user:/out.mp3\", 44100, \"tmp:/audio_progress.json\");"
+        },
+        {
+          "name": "ffmpeg.imageConvert",
+          "sig": "ffmpeg.imageConvert(input, output, scaleFactor, compressionRate)",
+          "desc": "Convert/resize an image. scaleFactor 0.5 = 50% size.",
+          "ret": "bool",
+          "example": "requirelib(\"ffmpeg\");\nffmpeg.imageConvert(\"user:/in.png\", \"user:/out.jpg\", 0.5, 80);"
+        }
+      ]
+    }
+  ]
+}

+ 570 - 438
src/web/Terminal/index.html

@@ -10,180 +10,145 @@
             *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
             *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
 
 
             :root {
             :root {
-                --bg:       #1e1e1e;
-                --bg2:      #252526;
-                --bg3:      #2d2d2d;
-                --border:   #3c3c3c;
-                --fg:       #d4d4d4;
-                --fg-dim:   #858585;
-                --green:    #4ec94e;
-                --red:      #f44747;
-                --yellow:   #dcdcaa;
-                --blue:     #569cd6;
-                --orange:   #ce9178;
-                --prompt:   #4ec94e;
-            }
-
-            html, body {
-                height: 100%;
-                background: var(--bg);
-                color: var(--fg);
-                font-family: 'Cascadia Code', 'Consolas', 'Courier New', monospace;
-                font-size: 13px;
-                overflow: hidden;
-            }
-
-            /* ── Shell layout ──────────────────────────────────────── */
-            .shell {
-                display: flex;
-                flex-direction: column;
-                height: 100vh;
-            }
+                --bg:      #1e1e1e;
+                --bg2:     #252526;
+                --bg3:     #2d2d2d;
+                --border:  #3c3c3c;
+                --fg:      #d4d4d4;
+                --dim:     #858585;
+                --green:   #4ec94e;
+                --red:     #f44747;
+                --yellow:  #dcdcaa;
+                --blue:    #569cd6;
+                --orange:  #ce9178;
+                --purple:  #c586c0;
+            }
+
+            html, body { height: 100%; background: var(--bg); color: var(--fg);
+                font-family: 'Cascadia Code','Consolas','Courier New',monospace;
+                font-size: 13px; overflow: hidden; }
+
+            /* ── Shell ─────────────────────────────────────────────── */
+            .shell { display: flex; flex-direction: column; height: 100vh; }
 
 
             /* ── Toolbar ───────────────────────────────────────────── */
             /* ── Toolbar ───────────────────────────────────────────── */
-            .toolbar {
-                flex-shrink: 0;
-                background: var(--bg3);
-                border-bottom: 1px solid var(--border);
-                display: flex;
-                align-items: center;
-                gap: 4px;
-                padding: 4px 10px;
-                height: 34px;
-            }
-
-            .toolbar-title {
-                font-size: 12px;
-                color: var(--fg-dim);
-                margin-right: 6px;
-                white-space: nowrap;
-            }
-
-            .tb-btn {
-                padding: 2px 10px;
-                background: transparent;
-                border: 1px solid var(--border);
-                color: var(--fg-dim);
-                cursor: pointer;
-                border-radius: 3px;
-                font-family: inherit;
-                font-size: 11px;
-                transition: background 0.1s, color 0.1s;
-                white-space: nowrap;
-            }
-            .tb-btn:hover { background: var(--bg2); color: var(--fg); }
-
-            .conn-badge {
-                margin-left: auto;
-                display: flex;
-                align-items: center;
-                gap: 5px;
-                font-size: 11px;
-                color: var(--fg-dim);
-            }
-            .conn-dot {
-                width: 7px; height: 7px;
-                border-radius: 50%;
-                background: var(--fg-dim);
-                flex-shrink: 0;
-                transition: background 0.2s;
-            }
-            .conn-dot.connecting { background: var(--yellow); animation: blink 1s ease-in-out infinite; }
-            .conn-dot.connected  { background: var(--green); }
-            .conn-dot.error      { background: var(--red); }
+            .toolbar { flex-shrink:0; background:var(--bg3); border-bottom:1px solid var(--border);
+                display:flex; align-items:center; gap:4px; padding:4px 10px; height:34px; }
+            .tb-title { font-size:12px; color:var(--dim); margin-right:6px; white-space:nowrap; }
+            .tb-btn { padding:2px 10px; background:transparent; border:1px solid var(--border);
+                color:var(--dim); cursor:pointer; border-radius:3px; font-family:inherit;
+                font-size:11px; transition:background .1s,color .1s; white-space:nowrap; }
+            .tb-btn:hover { background:var(--bg2); color:var(--fg); }
+            .tb-btn.active { border-color:var(--green); color:var(--green); }
+            .conn-badge { margin-left:auto; display:flex; align-items:center; gap:5px;
+                font-size:11px; color:var(--dim); }
+            .conn-dot { width:7px; height:7px; border-radius:50%; background:var(--dim);
+                flex-shrink:0; transition:background .2s; }
+            .conn-dot.connecting { background:var(--yellow); animation:blink 1s ease-in-out infinite; }
+            .conn-dot.connected  { background:var(--green); }
+            .conn-dot.error      { background:var(--red); }
             @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
             @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
 
 
-            /* ── Output area ───────────────────────────────────────── */
-            .term-output {
-                flex: 1;
-                overflow-y: auto;
-                padding: 10px 14px 6px;
-                cursor: text;
-                /* custom scrollbar */
-                scrollbar-width: thin;
-                scrollbar-color: var(--border) transparent;
-            }
-            .term-output::-webkit-scrollbar       { width: 6px; }
-            .term-output::-webkit-scrollbar-track  { background: transparent; }
-            .term-output::-webkit-scrollbar-thumb  { background: var(--border); border-radius: 3px; }
-
-            /* ── Lines ─────────────────────────────────────────────── */
-            .tl {
-                line-height: 1.65;
-                white-space: pre-wrap;
-                word-break: break-all;
-                min-height: 1.65em;
-            }
-            /* type variants */
-            .tl-in  { color: var(--fg); }                        /* user input echo */
-            .tl-ok  { color: var(--green); }                      /* successful result */
-            .tl-err { color: var(--red); }                        /* error result */
-            .tl-log { color: var(--yellow); }                     /* console.log */
-            .tl-sys { color: var(--blue); }                       /* system / session messages */
-            .tl-file{ color: var(--orange); }                     /* file preview lines */
-            .tl-dim { color: var(--fg-dim); }                     /* decorative separators */
-
-            /* Blinking cursor on the last line when idle */
-            .tl-cursor::after {
-                content: '▋';
-                animation: blink 1s step-end infinite;
-                color: var(--prompt);
-            }
-
-            /* ── Pending file banner ───────────────────────────────── */
-            .pending-banner {
-                display: none;
-                background: #2a2d2e;
-                border-top: 1px solid var(--border);
-                border-bottom: 1px solid var(--border);
-                padding: 5px 14px;
-                font-size: 11px;
-                color: var(--yellow);
-                flex-shrink: 0;
-            }
-
-            /* ── Input row ─────────────────────────────────────────── */
-            .term-input-row {
-                flex-shrink: 0;
-                display: flex;
-                align-items: center;
-                padding: 7px 14px;
-                background: var(--bg2);
-                border-top: 1px solid var(--border);
-                gap: 8px;
-            }
-
-            .term-prompt {
-                color: var(--prompt);
-                flex-shrink: 0;
-                user-select: none;
-                font-size: 13px;
-            }
-
-            .term-input {
-                flex: 1;
-                background: transparent;
-                border: none;
-                color: var(--fg);
-                font-family: inherit;
-                font-size: 13px;
-                outline: none;
-                caret-color: var(--prompt);
-            }
-            .term-input::placeholder { color: var(--fg-dim); opacity: 0.5; }
-
-            .run-btn {
-                padding: 3px 12px;
-                background: transparent;
-                border: 1px solid var(--border);
-                color: var(--fg-dim);
-                cursor: pointer;
-                border-radius: 3px;
-                font-family: inherit;
-                font-size: 11px;
-                flex-shrink: 0;
-            }
-            .run-btn:hover { background: var(--bg3); color: var(--green); border-color: var(--green); }
+            /* ── Main area (terminal + optional docs panel) ─────────── */
+            .main-area { flex:1; display:flex; overflow:hidden; }
+
+            /* ── Terminal output ────────────────────────────────────── */
+            .term-output { flex:1; overflow-y:auto; padding:10px 14px 6px; cursor:text;
+                scrollbar-width:thin; scrollbar-color:var(--border) transparent; }
+            .term-output::-webkit-scrollbar { width:6px; }
+            .term-output::-webkit-scrollbar-track { background:transparent; }
+            .term-output::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
+
+            /* ── Output line types ─────────────────────────────────── */
+            .tl { line-height:1.65; white-space:pre-wrap; word-break:break-all; min-height:1.65em; }
+            .tl-in   { color:var(--fg); }
+            .tl-ok   { color:var(--green); }
+            .tl-err  { color:var(--red); }
+            .tl-log  { color:var(--yellow); }
+            .tl-sys  { color:var(--blue); }
+            .tl-file { color:var(--orange); }
+            .tl-dim  { color:var(--dim); }
+
+            /* ── Pending-file banner ────────────────────────────────── */
+            .pending-banner { display:none; background:#2a2d2e; border-top:1px solid var(--border);
+                border-bottom:1px solid var(--border); padding:5px 14px; font-size:11px;
+                color:var(--yellow); flex-shrink:0; }
+
+            /* ── Input row ──────────────────────────────────────────── */
+            .term-input-row { flex-shrink:0; display:flex; align-items:center; padding:7px 14px;
+                background:var(--bg2); border-top:1px solid var(--border); gap:8px; }
+            .term-prompt { color:var(--green); flex-shrink:0; user-select:none; }
+            .term-input { flex:1; background:transparent; border:none; color:var(--fg);
+                font-family:inherit; font-size:13px; outline:none; caret-color:var(--green); }
+            .term-input::placeholder { color:var(--dim); opacity:.5; }
+            .run-btn { padding:3px 12px; background:transparent; border:1px solid var(--border);
+                color:var(--dim); cursor:pointer; border-radius:3px; font-family:inherit;
+                font-size:11px; flex-shrink:0; }
+            .run-btn:hover { color:var(--green); border-color:var(--green); }
+
+            /* ══ DOCS PANEL ════════════════════════════════════════════ */
+            .docs-panel { width:320px; flex-shrink:0; background:var(--bg2);
+                border-left:1px solid var(--border); display:none; flex-direction:column; overflow:hidden; }
+            .docs-panel.open { display:flex; }
+
+            /* search */
+            .docs-search { padding:7px 8px; border-bottom:1px solid var(--border); flex-shrink:0; }
+            .docs-search input { width:100%; background:var(--bg); border:1px solid var(--border);
+                color:var(--fg); padding:4px 8px; border-radius:3px; font-family:inherit;
+                font-size:11px; outline:none; }
+            .docs-search input:focus { border-color:var(--blue); }
+
+            /* library tabs */
+            .docs-tabs { display:flex; flex-wrap:wrap; gap:3px; padding:6px 8px;
+                border-bottom:1px solid var(--border); max-height:72px; overflow-y:auto; flex-shrink:0;
+                scrollbar-width:thin; scrollbar-color:var(--border) transparent; }
+            .docs-tab { padding:2px 8px; background:transparent; border:1px solid var(--border);
+                color:var(--dim); cursor:pointer; border-radius:3px; font-family:inherit;
+                font-size:10px; transition:background .1s; }
+            .docs-tab:hover  { background:var(--bg3); color:var(--fg); }
+            .docs-tab.active { background:var(--green); border-color:var(--green); color:#0a0a0a; font-weight:700; }
+
+            /* function list */
+            .docs-list { flex:1; overflow-y:auto; scrollbar-width:thin;
+                scrollbar-color:var(--border) transparent; }
+            .docs-list::-webkit-scrollbar { width:4px; }
+            .docs-list::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
+
+            /* section header inside list */
+            .docs-section-label { padding:5px 10px 3px; font-size:10px; font-weight:700;
+                color:var(--dim); text-transform:uppercase; letter-spacing:.05em;
+                border-bottom:1px solid var(--border); background:var(--bg); position:sticky; top:0; }
+
+            /* individual function card */
+            .docs-fn { padding:6px 10px 5px; border-bottom:1px solid #2e2e2e; }
+            .docs-fn:hover { background:#2a2a2a; }
+
+            .docs-fn-row { display:flex; align-items:baseline; gap:5px; }
+            .docs-fn-sig { flex:1; font-size:11px; color:var(--green); cursor:pointer;
+                white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+            .docs-fn-sig:hover { color:#fff; }
+            .docs-ins-btn { padding:1px 7px; background:transparent; border:1px solid var(--border);
+                color:var(--dim); cursor:pointer; border-radius:2px; font-size:10px; flex-shrink:0;
+                font-family:inherit; white-space:nowrap; }
+            .docs-ins-btn:hover { background:var(--green); color:#000; border-color:var(--green); }
+
+            .docs-fn-desc { font-size:10px; color:var(--dim); margin-top:2px; line-height:1.4; }
+            .docs-fn-ret  { font-size:10px; color:var(--blue); margin-top:1px; }
+
+            .docs-ex-toggle { font-size:10px; color:var(--yellow); cursor:pointer;
+                display:inline-flex; align-items:center; gap:3px; margin-top:4px; user-select:none; }
+            .docs-ex-toggle:hover { color:#fff; }
+
+            .docs-ex-block { display:none; margin-top:5px; background:var(--bg);
+                border:1px solid var(--border); border-radius:3px; padding:6px 8px;
+                font-size:11px; color:var(--orange); white-space:pre-wrap; word-break:break-all;
+                line-height:1.5; }
+            .docs-ex-run { display:block; margin-top:5px; padding:2px 10px; background:transparent;
+                border:1px solid var(--border); color:var(--dim); cursor:pointer;
+                border-radius:2px; font-size:10px; font-family:inherit; }
+            .docs-ex-run:hover { background:var(--green); color:#000; border-color:var(--green); }
+
+            .docs-empty { padding:20px 10px; text-align:center; color:var(--dim); font-size:11px; }
         </style>
         </style>
     </head>
     </head>
     <body>
     <body>
@@ -191,9 +156,10 @@
 
 
             <!-- Toolbar -->
             <!-- Toolbar -->
             <div class="toolbar">
             <div class="toolbar">
-                <span class="toolbar-title">&#9654; AGI Terminal</span>
+                <span class="tb-title">&#9654; AGI Terminal</span>
                 <button class="tb-btn" onclick="clearOutput()">Clear</button>
                 <button class="tb-btn" onclick="clearOutput()">Clear</button>
                 <button class="tb-btn" onclick="reconnect()">Reconnect</button>
                 <button class="tb-btn" onclick="reconnect()">Reconnect</button>
+                <button class="tb-btn" id="btnDocs" onclick="toggleDocs()">Docs</button>
                 <button class="tb-btn" onclick="showHelp()">Help</button>
                 <button class="tb-btn" onclick="showHelp()">Help</button>
                 <div class="conn-badge">
                 <div class="conn-badge">
                     <span class="conn-dot connecting" id="connDot"></span>
                     <span class="conn-dot connecting" id="connDot"></span>
@@ -201,12 +167,33 @@
                 </div>
                 </div>
             </div>
             </div>
 
 
-            <!-- Output -->
-            <div class="term-output" id="termOutput" onclick="focusInput()"></div>
+            <!-- Main split area -->
+            <div class="main-area">
+
+                <!-- Terminal output -->
+                <div class="term-output" id="termOutput" onclick="focusInput()"></div>
+
+                <!-- ── Docs panel ────────────────────────────────── -->
+                <div class="docs-panel" id="docsPanel">
+                    <div class="docs-search">
+                        <input type="text" id="docsSearch" placeholder="Search functions&hellip;"
+                               oninput="filterDocs(this.value)" autocomplete="off" spellcheck="false">
+                    </div>
+                    <div class="docs-tabs" id="docsTabs">
+                        <button class="tb-btn" style="font-size:10px;color:var(--dim)">Loading&hellip;</button>
+                    </div>
+                    <div class="docs-list" id="docsList">
+                        <div class="docs-empty">Loading documentation&hellip;</div>
+                    </div>
+                </div>
+
+            </div>
 
 
             <!-- Pending-file banner -->
             <!-- Pending-file banner -->
             <div class="pending-banner" id="pendingBanner">
             <div class="pending-banner" id="pendingBanner">
-                &#9658; File loaded &mdash; press <kbd style="background:#3c3c3c;padding:0 4px;border-radius:2px">Enter</kbd> to run, or type a new command to cancel
+                &#9658; File loaded &mdash; press
+                <kbd style="background:#3c3c3c;padding:0 4px;border-radius:2px">Enter</kbd>
+                to run, or type a new command to cancel
             </div>
             </div>
 
 
             <!-- Input row -->
             <!-- Input row -->
@@ -219,295 +206,440 @@
                 <button class="run-btn" onclick="submitInput()">&#9654; Run</button>
                 <button class="run-btn" onclick="submitInput()">&#9654; Run</button>
             </div>
             </div>
 
 
-        </div>
+        </div><!-- .shell -->
 
 
         <script>
         <script>
-            // ── State ─────────────────────────────────────────────────────────
-            var ws          = null;
-            var cmdHistory  = [];
-            var histIdx     = -1;
-            var pendingCode = null;   // code loaded from a file, awaiting confirm
-            var sessionReady = false;
-            var pendingFiles = [];
-
-            // ── WebSocket helpers ─────────────────────────────────────────────
-            function wsEndpoint() {
-                var proto = location.protocol === "https:" ? "wss://" : "ws://";
-                return proto + location.hostname + ":" + location.port;
-            }
-
-            function connect() {
-                setConn("connecting", "Connecting…");
-                ws = new WebSocket(wsEndpoint() + "/system/ajgi/interface?script=Terminal/backend/session.agi");
-
-                ws.onopen = function() {
-                    // Wait for "ready" message before marking connected
-                };
-
-                ws.onmessage = function(e) {
-                    var msg;
-                    try { msg = JSON.parse(e.data); } catch(_) { return; }
-                    handleServerMsg(msg);
-                };
-
-                ws.onclose = function() {
-                    sessionReady = false;
-                    setConn("error", "Disconnected");
-                    line("sys", "# Session closed.  Click Reconnect to start a new one.");
-                    ws = null;
-                };
-
-                ws.onerror = function() {
-                    setConn("error", "Connection error");
-                };
-            }
-
-            function send(obj) {
-                if (ws && ws.readyState === WebSocket.OPEN) {
-                    ws.send(JSON.stringify(obj));
-                }
-            }
-
-            // Keep session alive
-            setInterval(function() {
-                if (ws && ws.readyState === WebSocket.OPEN) send({ type: "ping" });
-            }, 30000);
-
-            // ── Server message handler ────────────────────────────────────────
-            function handleServerMsg(msg) {
-                if (msg.type === "ready") {
-                    sessionReady = true;
-                    setConn("connected", "Connected — " + msg.user);
-                    line("sys", "# AGI Terminal — ArozOS " + (msg.build || "") + "  |  user: " + msg.user);
-                    line("sys", "# All AGI globals and requirelib() are available.");
-                    line("sys", "# Type .help for commands.  Variables persist for this session.");
-                    line("dim", "");
-                    // Now it is safe to load any files passed at launch
-                    if (pendingFiles.length > 0) {
-                        pendingFiles.forEach(function(vpath) { loadFile(vpath); });
-                        pendingFiles = [];
-                    }
-                    focusInput();
-                    return;
+        // ── State ───────────────────────────────────────────────────────────
+        var ws           = null;
+        var cmdHistory   = [];
+        var histIdx      = -1;
+        var pendingCode  = null;
+        var sessionReady = false;
+        var pendingFiles = [];
+
+        // ── WebSocket ────────────────────────────────────────────────────────
+        function wsEndpoint() {
+            var p = location.protocol === "https:" ? "wss://" : "ws://";
+            return p + location.hostname + ":" + location.port;
+        }
+
+        function connect() {
+            setConn("connecting", "Connecting…");
+            ws = new WebSocket(wsEndpoint() + "/system/ajgi/interface?script=Terminal/backend/session.agi");
+
+            ws.onopen    = function() { /* wait for "ready" JSON message */ };
+            ws.onmessage = function(e) {
+                var msg; try { msg = JSON.parse(e.data); } catch(_) { return; }
+                handleServerMsg(msg);
+            };
+            ws.onclose   = function() {
+                sessionReady = false;
+                setConn("error", "Disconnected");
+                line("sys", "# Session closed.  Click Reconnect to start a new one.");
+                ws = null;
+            };
+            ws.onerror   = function() { setConn("error", "Connection error"); };
+        }
+
+        function wsSend(obj) {
+            if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj));
+        }
+
+        setInterval(function() { wsSend({ type: "ping" }); }, 30000);
+
+        // ── Server messages ──────────────────────────────────────────────────
+        function handleServerMsg(msg) {
+            if (msg.type === "ready") {
+                sessionReady = true;
+                setConn("connected", "Connected — " + msg.user);
+                line("sys", "# AGI Terminal — ArozOS " + (msg.build || "") + "  |  user: " + msg.user);
+                line("sys", "# All AGI globals and requirelib() are available.");
+                line("sys", "# Type .help for commands.  Variables persist for this session.");
+                line("dim", "");
+                if (pendingFiles.length > 0) {
+                    pendingFiles.forEach(function(p) { loadFile(p); });
+                    pendingFiles = [];
                 }
                 }
-
-                if (msg.type === "result") {
-                    // console.log lines
-                    if (msg.logs && msg.logs.length > 0) {
-                        msg.logs.forEach(function(l) { line("log", "· " + l); });
-                    }
-                    // result value / error
-                    if (msg.error) {
-                        line("err", "✕ " + msg.output);
-                    } else if (msg.output !== "" && msg.output !== undefined) {
-                        // Pretty-print multi-line results (e.g. JSON.stringify'd arrays)
-                        var lines = String(msg.output).split("\n");
-                        lines.forEach(function(l, i) {
-                            line("ok", (i === 0 ? "← " : "  ") + l);
-                        });
-                    } else {
-                        line("dim", "← undefined");
-                    }
-                    return;
+                focusInput();
+                return;
+            }
+            if (msg.type === "result") {
+                if (msg.logs && msg.logs.length) msg.logs.forEach(function(l) { line("log", "· " + l); });
+                if (msg.error) {
+                    line("err", "✕ " + msg.output);
+                } else if (msg.output !== "" && msg.output !== undefined) {
+                    String(msg.output).split("\n").forEach(function(l, i) {
+                        line("ok", (i === 0 ? "← " : "  ") + l);
+                    });
+                } else {
+                    line("dim", "← undefined");
                 }
                 }
-                // pong: silently ignored
             }
             }
-
-            // ── Execution ────────────────────────────────────────────────────
-            function execCode(code) {
-                if (!sessionReady) {
-                    line("err", "✕ Session not ready.  Please wait or click Reconnect.");
-                    return;
-                }
-                send({ type: "exec", code: code });
+        }
+
+        function execCode(code) {
+            if (!sessionReady) { line("err", "✕ Session not ready. Click Reconnect."); return; }
+            wsSend({ type: "exec", code: code });
+        }
+
+        // ── Input ────────────────────────────────────────────────────────────
+        function handleKey(e) {
+            if (e.key === "Enter") {
+                submitInput();
+            } else if (e.key === "ArrowUp") {
+                e.preventDefault();
+                if (histIdx < cmdHistory.length - 1) { histIdx++; inp().value = cmdHistory[histIdx]; caretEnd(); }
+            } else if (e.key === "ArrowDown") {
+                e.preventDefault();
+                if (histIdx > 0) { histIdx--; inp().value = cmdHistory[histIdx]; }
+                else             { histIdx = -1; inp().value = ""; }
+            } else if (e.key === "Escape") {
+                cancelPending();
             }
             }
-
-            // ── Input handling ────────────────────────────────────────────────
-            function handleKey(e) {
-                if (e.key === "Enter") {
-                    submitInput();
-                } else if (e.key === "ArrowUp") {
-                    e.preventDefault();
-                    if (histIdx < cmdHistory.length - 1) {
-                        histIdx++;
-                        inp().value = cmdHistory[histIdx];
-                        caretToEnd();
+        }
+
+        function submitInput() {
+            var raw = inp().value; inp().value = ""; histIdx = -1;
+
+            // Empty Enter + pending file → run the file
+            if (raw.trim() === "" && pendingCode !== null) {
+                var code = pendingCode; cancelPending();
+                line("sys", "# Running loaded file…");
+                line("in",  "> (file)");
+                execCode(code);
+                return;
+            }
+            if (raw.trim() === "") return;
+
+            if (!cmdHistory.length || cmdHistory[0] !== raw) {
+                cmdHistory.unshift(raw);
+                if (cmdHistory.length > 500) cmdHistory.pop();
+            }
+            if (pendingCode !== null) cancelPending();
+            line("in", "> " + raw);
+
+            // Client-side dot commands
+            var cmd = raw.trim();
+            if      (cmd === ".help")    { showHelp();    return; }
+            else if (cmd === ".clear")   { clearOutput(); return; }
+            else if (cmd === ".reset")   { reconnect();   return; }
+            else if (cmd === ".history") { showHistory(); return; }
+            else if (cmd === ".docs")    { toggleDocs();  return; }
+
+            execCode(raw);
+        }
+
+        // ── File loading ─────────────────────────────────────────────────────
+        function loadFile(vpath) {
+            ao_module_agirun("Terminal/backend/readfile.agi", { path: vpath },
+                function(content) {
+                    if (typeof content === "string" && content.indexOf("__ERROR__") === 0) {
+                        line("err", "✕ " + content.replace("__ERROR__", "")); return;
                     }
                     }
-                } else if (e.key === "ArrowDown") {
-                    e.preventDefault();
-                    if (histIdx > 0) {
-                        histIdx--;
-                        inp().value = cmdHistory[histIdx];
-                    } else {
-                        histIdx = -1;
-                        inp().value = "";
+                    var codeLines = String(content).split("\n");
+                    var preview   = Math.min(codeLines.length, 20);
+                    var filename  = vpath.split("/").pop();
+
+                    line("dim",  "");
+                    line("file", "# ■ Loaded: " + filename + "  (" + codeLines.length + " line" + (codeLines.length !== 1 ? "s" : "") + ")");
+                    line("dim",  "# " + "─".repeat(48));
+                    for (var i = 0; i < preview; i++) {
+                        var n = String(i + 1); while (n.length < 3) n = " " + n;
+                        line("file", n + "  " + codeLines[i]);
                     }
                     }
-                } else if (e.key === "Escape") {
-                    cancelPending();
-                }
-            }
+                    if (codeLines.length > preview)
+                        line("dim", "     … and " + (codeLines.length - preview) + " more lines");
+                    line("dim",  "# " + "─".repeat(48));
 
 
-            function submitInput() {
-                var raw = inp().value;
-                inp().value = "";
-                histIdx = -1;
-
-                // Empty Enter with pending file → run it
-                if (raw.trim() === "" && pendingCode !== null) {
-                    var code = pendingCode;
-                    cancelPending();
-                    line("sys", "# Running loaded file…");
-                    line("in",  "> (file)");
-                    execCode(code);
-                    return;
-                }
-
-                if (raw.trim() === "") return;
-
-                // Record history (deduplicate consecutive)
-                if (cmdHistory.length === 0 || cmdHistory[0] !== raw) {
-                    cmdHistory.unshift(raw);
-                    if (cmdHistory.length > 500) cmdHistory.pop();
-                }
-
-                // Cancel any pending file if the user typed something new
-                if (pendingCode !== null) cancelPending();
-
-                line("in", "> " + raw);
-
-                // Built-in dot-commands (handled client-side)
-                var cmd = raw.trim();
-                if      (cmd === ".help")    { showHelp(); return; }
-                else if (cmd === ".clear")   { clearOutput(); return; }
-                else if (cmd === ".reset")   { reconnect(); return; }
-                else if (cmd === ".history") { showHistory(); return; }
-
-                execCode(raw);
-            }
-
-            // ── File loading ──────────────────────────────────────────────────
-            function loadFile(vpath) {
-                ao_module_agirun("Terminal/backend/readfile.agi", { path: vpath },
-                    function(content) {
-                        if (typeof content === "string" && content.indexOf("__ERROR__") === 0) {
-                            line("err", "✕ " + content.replace("__ERROR__", ""));
-                            return;
-                        }
-                        var codeLines = String(content).split("\n");
-                        var preview   = Math.min(codeLines.length, 20);
-                        var filename  = vpath.split("/").pop();
-
-                        line("dim", "");
-                        line("file", "# ■ Loaded: " + filename + "  (" + codeLines.length + " line" + (codeLines.length !== 1 ? "s" : "") + ")");
-                        line("dim",  "# " + "─".repeat(50));
-                        for (var i = 0; i < preview; i++) {
-                            var num = String(i + 1);
-                            while (num.length < 3) num = " " + num;
-                            line("file", num + "  " + codeLines[i]);
-                        }
-                        if (codeLines.length > preview) {
-                            line("dim", "     … and " + (codeLines.length - preview) + " more line" + (codeLines.length - preview !== 1 ? "s" : ""));
-                        }
-                        line("dim",  "# " + "─".repeat(50));
-
-                        pendingCode = content;
-                        document.getElementById("pendingBanner").style.display = "block";
-                        focusInput();
-                    },
-                    function() {
-                        line("err", "✕ Failed to read file: " + vpath);
+                    pendingCode = content;
+                    document.getElementById("pendingBanner").style.display = "block";
+                    focusInput();
+                },
+                function() { line("err", "✕ Failed to read: " + vpath); }
+            );
+        }
+
+        function cancelPending() {
+            pendingCode = null;
+            document.getElementById("pendingBanner").style.display = "none";
+        }
+
+        // ── Built-in commands ────────────────────────────────────────────────
+        function showHelp() {
+            line("sys", "# ── Commands ───────────────────────────────────────────");
+            line("sys", "#  .help     show this help");
+            line("sys", "#  .clear    clear output");
+            line("sys", "#  .reset    reconnect / new session");
+            line("sys", "#  .history  show command history");
+            line("sys", "#  .docs     toggle API reference panel");
+            line("sys", "# ");
+            line("sys", "# ── Keys ────────────────────────────────────────────");
+            line("sys", "#  Enter     run (or confirm loaded file)");
+            line("sys", "#  ↑ / ↓     history navigation");
+            line("sys", "#  Escape    cancel loaded file");
+            line("sys", "# ");
+            line("sys", "# ── Tips ────────────────────────────────────────────");
+            line("sys", "#  var x = 10; x * 2    → variables persist");
+            line("sys", "#  requirelib(\"filelib\") → then use filelib.*");
+            line("sys", "#  sendResp() / echo()  → output is captured");
+            line("sys", "#  Open .agi files from the file manager to load them here.");
+        }
+
+        function showHistory() {
+            if (!cmdHistory.length) { line("sys", "# (no history yet)"); return; }
+            cmdHistory.slice().reverse().forEach(function(c, i) { line("sys", "# " + (i+1) + "  " + c); });
+        }
+
+        function clearOutput() { document.getElementById("termOutput").innerHTML = ""; }
+
+        function reconnect() {
+            if (ws) { ws.close(); ws = null; }
+            sessionReady = false; cancelPending(); clearOutput();
+            setTimeout(connect, 150);
+        }
+
+        // ── UI helpers ───────────────────────────────────────────────────────
+        function line(type, text) {
+            var out = document.getElementById("termOutput");
+            var d   = document.createElement("div");
+            var map = { in:"tl-in", ok:"tl-ok", err:"tl-err", log:"tl-log", sys:"tl-sys", file:"tl-file", dim:"tl-dim" };
+            d.className = "tl " + (map[type] || "tl-dim");
+            d.textContent = text;
+            out.appendChild(d);
+            out.scrollTop = out.scrollHeight;
+        }
+        function inp()     { return document.getElementById("termInput"); }
+        function focusInput() { inp().focus(); }
+        function caretEnd()   { var el = inp(), v = el.value.length; el.setSelectionRange(v,v); }
+        function setConn(state, label) {
+            document.getElementById("connDot").className = "conn-dot " + state;
+            document.getElementById("connLabel").textContent = label;
+        }
+
+        // ══ DOCS PANEL ═══════════════════════════════════════════════════════
+
+        var docsData     = null;
+        var activeSection = "all";
+
+        function toggleDocs() {
+            var panel  = document.getElementById("docsPanel");
+            var btn    = document.getElementById("btnDocs");
+            var isOpen = panel.classList.contains("open");
+            if (isOpen) {
+                panel.classList.remove("open");
+                btn.classList.remove("active");
+            } else {
+                panel.classList.add("open");
+                btn.classList.add("active");
+                if (!docsData) loadDocs();
+            }
+            focusInput();
+        }
+
+        function loadDocs() {
+            ao_module_agirun("Terminal/backend/getdocs.agi", {},
+                function(data) {
+                    if (!data || data.error) {
+                        document.getElementById("docsList").innerHTML =
+                            "<div class='docs-empty'>Failed to load documentation.</div>";
+                        return;
                     }
                     }
-                );
-            }
-
-            function cancelPending() {
-                pendingCode = null;
-                document.getElementById("pendingBanner").style.display = "none";
-            }
-
-            // ── Built-in commands ─────────────────────────────────────────────
-            function showHelp() {
-                line("sys", "# ── Terminal Commands ──────────────────────────────");
-                line("sys", "#  .help       show this help");
-                line("sys", "#  .clear      clear all output");
-                line("sys", "#  .reset      reconnect and start a fresh session");
-                line("sys", "#  .history    print command history");
-                line("sys", "# ");
-                line("sys", "# ── Key Bindings ───────────────────────────────");
-                line("sys", "#  Enter       execute input (or run loaded file)");
-                line("sys", "#  ↑ / ↓       navigate command history");
-                line("sys", "#  Escape      cancel loaded file");
-                line("sys", "# ");
-                line("sys", "# ── Tips ─────────────────────────────────────");
-                line("sys", "#  Variables persist across commands within a session.");
-                line("sys", "#  requirelib(\"filelib\") works exactly as in .agi scripts.");
-                line("sys", "#  sendResp() / echo() output is captured and returned.");
-                line("sys", "#  Open .agi files from the file manager to load them here.");
-            }
-
-            function showHistory() {
-                if (cmdHistory.length === 0) {
-                    line("sys", "# (no history yet)");
-                    return;
+                    docsData = data;
+                    buildDocsTabs();
+                    renderDocsList("", "all");
+                },
+                function() {
+                    document.getElementById("docsList").innerHTML =
+                        "<div class='docs-empty'>Could not reach documentation backend.</div>";
                 }
                 }
-                cmdHistory.slice().reverse().forEach(function(cmd, i) {
-                    line("sys", "# " + String(i + 1) + "  " + cmd);
-                });
+            );
+        }
+
+        function buildDocsTabs() {
+            var el = document.getElementById("docsTabs");
+            el.innerHTML = "";
+
+            function mkTab(id, label, loadCmd) {
+                var b = document.createElement("button");
+                b.className = "docs-tab" + (id === "all" ? " active" : "");
+                b.id = "dtab-" + id;
+                b.textContent = label;
+                b.onclick = function() { selectSection(id); };
+
+                // Double-click on a library tab inserts its requirelib() call
+                if (loadCmd) {
+                    b.title = "Double-click to insert: " + loadCmd;
+                    b.ondblclick = function(e) {
+                        e.stopPropagation();
+                        insertToInput(loadCmd);
+                    };
+                }
+                el.appendChild(b);
             }
             }
 
 
-            function clearOutput() {
-                document.getElementById("termOutput").innerHTML = "";
-            }
+            mkTab("all", "All");
+            docsData.sections.forEach(function(s) { mkTab(s.id, s.name, s.load || null); });
+        }
 
 
-            function reconnect() {
-                if (ws) { ws.close(); ws = null; }
-                sessionReady = false;
-                cancelPending();
-                clearOutput();
-                setTimeout(connect, 100);
-            }
+        function selectSection(id) {
+            activeSection = id;
+            document.querySelectorAll(".docs-tab").forEach(function(t) { t.classList.remove("active"); });
+            var active = document.getElementById("dtab-" + id);
+            if (active) active.classList.add("active");
+            renderDocsList(document.getElementById("docsSearch").value, id);
+        }
 
 
-            // ── UI helpers ────────────────────────────────────────────────────
-            function line(type, text) {
-                var out  = document.getElementById("termOutput");
-                var div  = document.createElement("div");
-                var map  = { in:"tl-in", ok:"tl-ok", err:"tl-err",
-                             log:"tl-log", sys:"tl-sys", file:"tl-file", dim:"tl-dim" };
-                div.className = "tl " + (map[type] || "tl-dim");
-                div.textContent = text;
-                out.appendChild(div);
-                out.scrollTop = out.scrollHeight;
-            }
+        function filterDocs(q) { renderDocsList(q, activeSection); }
 
 
-            function inp() { return document.getElementById("termInput"); }
+        function renderDocsList(query, sectionId) {
+            query = (query || "").toLowerCase().trim();
+            var listEl = document.getElementById("docsList");
+            listEl.innerHTML = "";
 
 
-            function focusInput() { inp().focus(); }
+            var sections = (sectionId === "all")
+                ? docsData.sections
+                : docsData.sections.filter(function(s) { return s.id === sectionId; });
 
 
-            function caretToEnd() {
-                var el = inp();
-                var v  = el.value.length;
-                el.setSelectionRange(v, v);
-            }
+            var total = 0;
 
 
-            function setConn(state, label) {
-                var dot = document.getElementById("connDot");
-                dot.className = "conn-dot " + state;
-                document.getElementById("connLabel").textContent = label;
-            }
+            sections.forEach(function(section) {
+                var fns = (section.functions || []).filter(function(f) {
+                    return !query || (f.name + f.sig + f.desc).toLowerCase().indexOf(query) !== -1;
+                });
+                if (!fns.length) return;
+
+                // Section label only in "all" view or when searching
+                if (sectionId === "all" || query) {
+                    var lbl = document.createElement("div");
+                    lbl.className = "docs-section-label";
+                    lbl.textContent = section.name + (section.load ? "  —  " + section.load : "");
+                    listEl.appendChild(lbl);
+                }
 
 
-            // ── Boot ──────────────────────────────────────────────────────────
-            // Collect any files passed from the file manager (URL hash)
-            (function() {
-                var inputFiles = ao_module_loadInputFiles();
-                inputFiles.forEach(function(vpath) {
+                fns.forEach(function(fn) {
+                    listEl.appendChild(buildFnCard(fn, total));
+                    total++;
+                });
+            });
+
+            if (!total) {
+                listEl.innerHTML = "<div class='docs-empty'>No results for \"" + escHtml(query) + "\"</div>";
+            }
+        }
+
+        function buildFnCard(fn, idx) {
+            var card = document.createElement("div");
+            card.className = "docs-fn";
+
+            // Signature row
+            var row = document.createElement("div");
+            row.className = "docs-fn-row";
+
+            var sig = document.createElement("span");
+            sig.className = "docs-fn-sig";
+            sig.textContent = fn.sig || fn.name;
+            sig.title = "Click to insert into input";
+            sig.onclick = function() { insertToInput(fn.sig || fn.name); };
+
+            var ins = document.createElement("button");
+            ins.className = "docs-ins-btn";
+            ins.textContent = "↵ Insert";
+            ins.onclick = function() { insertToInput(fn.sig || fn.name); };
+
+            row.appendChild(sig);
+            row.appendChild(ins);
+            card.appendChild(row);
+
+            // Description
+            if (fn.desc) {
+                var desc = document.createElement("div");
+                desc.className = "docs-fn-desc";
+                desc.textContent = fn.desc;
+                card.appendChild(desc);
+            }
+
+            // Return type
+            if (fn.ret) {
+                var ret = document.createElement("div");
+                ret.className = "docs-fn-ret";
+                ret.textContent = "→ " + fn.ret;
+                card.appendChild(ret);
+            }
+
+            // Expandable example
+            if (fn.example) {
+                var exId    = "ex" + idx;
+                var toggle  = document.createElement("span");
+                toggle.className  = "docs-ex-toggle";
+                toggle.innerHTML  = "<span id='exarrow" + exId + "'>►</span> Example";
+                toggle.onclick    = function() { toggleExample(exId); };
+                card.appendChild(toggle);
+
+                var block = document.createElement("div");
+                block.className = "docs-ex-block";
+                block.id        = exId;
+                block.textContent = fn.example;
+
+                var runBtn = document.createElement("button");
+                runBtn.className = "docs-ex-run";
+                runBtn.textContent = "► Run this example";
+                var exCode = fn.example;
+                runBtn.onclick = function() {
+                    // Echo first line in terminal and exec the full example
+                    var firstLine = exCode.split("\n")[0];
+                    var suffix    = exCode.split("\n").length > 1 ? "…" : "";
+                    line("in", "> " + firstLine + suffix);
+                    execCode(exCode);
+                };
+                block.appendChild(runBtn);
+                card.appendChild(block);
+            }
+
+            return card;
+        }
+
+        function toggleExample(id) {
+            var block  = document.getElementById(id);
+            var arrow  = document.getElementById("exarrow" + id);
+            var isOpen = block.style.display === "block";
+            block.style.display = isOpen ? "none" : "block";
+            if (arrow) arrow.textContent = isOpen ? "►" : "▼";
+        }
+
+        function insertToInput(sig) {
+            var el = inp();
+            el.value = sig;
+            el.focus();
+            // If signature ends with (), place cursor just inside the parens
+            if (sig.endsWith(")")) {
+                var lp = sig.lastIndexOf("(");
+                if (lp !== -1) el.setSelectionRange(lp + 1, sig.length - 1);
+            } else {
+                caretEnd();
+            }
+        }
+
+        // ── Utils ────────────────────────────────────────────────────────────
+        function escHtml(s) {
+            return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
+        }
+
+        // ══ BOOT ══════════════════════════════════════════════════════════════
+        $(document).ready(function() {
+            // Collect any .agi / .js files passed via URL hash from the file manager
+            let filelist = ao_module_loadInputFiles();
+            if (filelist != null){
+                filelist.forEach(function(vpath) {
                     vpath = vpath.trim();
                     vpath = vpath.trim();
-                    if (vpath !== "" && (vpath.match(/\.agi$/) || vpath.match(/\.js$/))) {
+                    if (vpath && (vpath.match(/\.agi$/) || vpath.match(/\.js$/))){
                         pendingFiles.push(vpath);
                         pendingFiles.push(vpath);
                     }
                     }
                 });
                 });
-            })();
-
+            }
+        
+            // Auto-connect
             connect();
             connect();
+        });
         </script>
         </script>
     </body>
     </body>
 </html>
 </html>