TUIC SDK
The TUIC SDK provides window.tuic inside iframes hosted by TUICommander, enabling plugins and external pages to interact with the host app: open files, read content, launch terminals, copy to clipboard, receive theme updates, and more.
Two Injection Modes
1. Inline HTML Tabs (Plugins)
Plugins use html tabs — TUIC injects the SDK <script> directly into the iframe content. window.tuic is available immediately on load.
// Plugin panel — window.tuic is injected automatically
tuic.open("README.md"); // relative to active repo
tuic.open("/absolute/path/file.txt", { pinned: true });
tuic.edit("src/App.tsx", { line: 42 });
tuic.terminal(tuic.activeRepo());
See Plugin Authoring Guide for full plugin details.
2. URL Tabs (External Pages)
Tabs created with url load content from a remote server in an iframe. The parent cannot inject scripts into cross-origin iframes, so the page must opt in to the SDK via a postMessage handshake.
Handshake Protocol
┌─────────────┐ ┌─────────────┐
│ TUIC Host │ │ iframe URL │
│ (parent) │ │ (child) │
└──────┬──────┘ └──────┬──────┘
│ iframe onload │
│── tuic:sdk-init ────────────────────────>│
│── tuic:repo-changed ────────────────────>│
│── tuic:theme-changed ───────────────────>│
│ │ creates window.tuic
│ │ dispatches "tuic:ready"
│ │
│ (fallback path — async listeners) │
│<── tuic:sdk-request ──────────────────────│
│── tuic:sdk-init + repo + theme ─────────>│
│ │
│<── tuic:open, tuic:edit, ... ─────────────│ (on user action)
│── tuic:get-file-result ─────────────────>│ (async response)
│── tuic:host-message ────────────────────>│ (push from host)
Both paths are implemented in src/components/PluginPanel/PluginPanel.tsx. The version field carries TUIC_SDK_VERSION so the child can feature-detect.
Step 1: Child Page — Bootstrap Listener
Add this <script> in the <head> of your page, before any framework initialization. It must be synchronous so the listener is registered before the parent’s onload fires.
<script>
(function () {
window.addEventListener("message", function (e) {
if (!e.data || e.data.type !== "tuic:sdk-init") return;
window.tuic = {
version: "1.0",
open: function (path, opts) {
parent.postMessage({ type: "tuic:open", path: path, pinned: !!(opts && opts.pinned) }, "*");
},
edit: function (path, opts) {
parent.postMessage({ type: "tuic:edit", path: path, line: (opts && opts.line) || 0 }, "*");
},
terminal: function (repoPath) {
parent.postMessage({ type: "tuic:terminal", repoPath: repoPath }, "*");
}
};
window.dispatchEvent(new Event("tuic:ready"));
});
})();
</script>
Note: For URL-mode pages, only the basic methods (open, edit, terminal) are shown above. To use the full SDK (activeRepo, getFile, theme, etc.), copy the complete SDK from
src/components/PluginPanel/tuicSdk.tsor use the inline HTML mode.
Step 2: Child Page — React to SDK Availability
Use the tuic:ready event to update your UI (e.g., show an “Open in TUIC” button):
// Alpine.js example
Alpine.data("myApp", () => ({
_tuicReady: false,
get hasTuic() { return this._tuicReady; },
init() {
window.addEventListener("tuic:ready", () => { this._tuicReady = true; });
// If SDK was already initialized before Alpine mounted
if (window.tuic) this._tuicReady = true;
},
tuicOpen(filePath) {
if (window.tuic) window.tuic.open(filePath, { pinned: true });
}
}));
API Reference
Files
tuic.open(path, opts?)
Open a file in a TUIC tab.
| Param | Type | Description |
|---|---|---|
path | string | File path — relative (resolved against active repo) or absolute |
opts.pinned | boolean | Pin the tab (default: false) |
tuic.edit(path, opts?)
Open a file in the external editor.
| Param | Type | Description |
|---|---|---|
path | string | File path — relative or absolute |
opts.line | number | Line number to jump to (default: 0) |
tuic.getFile(path): Promise<string>
Read a file’s text content from the active repo.
| Param | Type | Description |
|---|---|---|
path | string | File path — relative or absolute |
Returns a Promise that resolves with the file content string, or rejects with an Error if the file is not found, the path escapes the repo root, or no active repo is set.
tuic.getFile("package.json")
.then(content => JSON.parse(content))
.catch(err => console.error("Cannot read:", err.message));
Path Resolution
All file methods (open, edit, getFile) accept both relative and absolute paths:
- Relative paths (e.g.,
"README.md","src/App.tsx") are resolved against the active repository root. - Absolute paths (e.g.,
"/Users/me/code/repo/file.ts") are matched against known repositories (longest prefix wins). - Path traversal (
../) that escapes the repo root is blocked and returns an error. ./prefixes are supported and normalized.
tuic.open("README.md"); // → /active/repo/README.md
tuic.open("src/../README.md"); // → /active/repo/README.md
tuic.open("/Users/me/repo/file.ts"); // → absolute, matched to repo
tuic.getFile("../../../etc/passwd"); // → rejected (traversal)
tuic:// Links
HTML <a> tags with tuic:// href are automatically intercepted:
<a href="tuic://open/README.md">View README</a>
<a href="tuic://edit/src/main.rs?line=42">Edit main.rs:42</a>
<a href="tuic://terminal?repo=/path/to/repo">Open terminal</a>
Link pathnames are treated as relative paths (the leading / from URL parsing is stripped).
Repository
tuic.activeRepo(): string | null
Returns the path of the currently active repository, or null if none is active.
var repo = tuic.activeRepo();
// "/Users/me/code/myproject" or null
tuic.onRepoChange(callback)
Register a listener that fires when the active repo changes.
| Param | Type | Description |
|---|---|---|
callback | (repoPath: string | null) => void | Called with the new active repo path |
tuic.offRepoChange(callback)
Unregister a previously registered repo-change listener.
tuic.terminal(repoPath)
Open a terminal in the given repository.
| Param | Type | Description |
|---|---|---|
repoPath | string | Repository root path (absolute) |
UI Feedback
tuic.toast(title, opts?)
Show a native toast notification in the host app.
| Param | Type | Description |
|---|---|---|
title | string | Toast title (required) |
opts.message | string | Optional body text |
opts.level | "info" | "warn" | "error" | Severity (default: "info") |
opts.sound | boolean | Play a notification sound (default: false). Each level has a distinct tone: info = soft blip, warn = double beep, error = descending sweep. |
tuic.toast("Import complete", { message: "42 items imported" });
tuic.toast("Rate limited", { message: "Try again in 30s", level: "warn", sound: true });
tuic.clipboard(text)
Copy text to the system clipboard. Works from sandboxed iframes (which cannot access navigator.clipboard directly).
| Param | Type | Description |
|---|---|---|
text | string | Text to copy |
Messaging
tuic.send(data)
Send structured data to the host. The host receives it via pluginRegistry.handlePanelMessage().
| Param | Type | Description |
|---|---|---|
data | any | JSON-serializable payload |
tuic.onMessage(callback)
Register a listener for messages pushed from the host.
| Param | Type | Description |
|---|---|---|
callback | (data: any) => void | Called with the message payload |
tuic.offMessage(callback)
Unregister a previously registered message listener.
Theme
tuic.theme: object | null
Read-only property containing the current theme as a key-value object. Keys are camelCase versions of CSS custom properties (e.g., --bg-primary → bgPrimary).
var theme = tuic.theme;
// { bgPrimary: "#1e1e2e", fgPrimary: "#cdd6f4", accent: "#89b4fa", ... }
tuic.onThemeChange(callback)
Register a listener that fires when the host theme changes.
| Param | Type | Description |
|---|---|---|
callback | (theme: object) => void | Called with the new theme object |
tuic.offThemeChange(callback)
Unregister a previously registered theme-change listener.
Version
tuic.version: string
The SDK version string (currently "1.0").
Testing the SDK
An interactive test page is included at docs/examples/sdk-test.html. It runs automatic verification of all SDK methods and provides buttons for interactive testing.
How to launch it
From an AI agent (Claude Code, etc.):
Use the TUIC MCP ui tool to open it as an inline HTML tab:
mcp__tuicommander__ui action=tab id="sdk-test" title="SDK Test Suite" html="<contents of docs/examples/sdk-test.html>" pinned=false focus=true
From a plugin:
Register a plugin that serves the HTML content as a panel tab. The SDK is injected automatically into inline HTML tabs.
From JavaScript (dev console or app code):
// Read the file and open as a tab
const html = await invoke("fs_read_file", { repoPath: "/path/to/tuicommander", file: "docs/examples/sdk-test.html" });
mdTabsStore.addHtml("sdk-test", "SDK Test Suite", html);
The test page verifies:
- SDK presence and version
activeRepo()return valueonRepoChangelistener registration- Theme delivery and
onThemeChange onMessagelistener registrationgetFile("README.md")reads file contentgetFile("../../../etc/passwd")is blocked by traversal guard
Interactive buttons test: open, edit, terminal, toast (all levels), clipboard, getFile, and send.
Timing Notes
The <script> bootstrap in the child page must be synchronous and in <head> to guarantee the message listener is registered before the parent’s iframe.onload fires tuic:sdk-init. If your page loads the bootstrap asynchronously (e.g., as an ES module), there is a race condition — the init message may arrive before the listener exists.
If you cannot guarantee synchronous loading, implement a retry: have the child send { type: "tuic:sdk-request" } to the parent on DOMContentLoaded (or whenever the listener is registered), and the parent will respond with tuic:sdk-init. This fallback is fully supported by the host.
Source Files
| File | Description |
|---|---|
src/components/PluginPanel/tuicSdk.ts | SDK script injected into iframes |
src/components/PluginPanel/PluginPanel.tsx | Host-side message handlers |
src/components/PluginPanel/resolveTuicPath.ts | Path resolution (relative + traversal guard) |
docs/examples/sdk-test.html | Interactive test/example page |