Skip to content

Core Concepts ​

App (bundle) ​

An app = an entry file + optional additional sources/resources + metadata. Internally it is called a bundle. Apps have a lifecycle: In Development → Under Review → Published → Installed. The tabs in the app manager map to these stages.

Entry file and metadata header ​

Every app has one entry file (e.g. main.js / main.ts) whose top // ==UserScript== comment block declares the metadata: the name (@name), where it runs (@match), and which built-ins it uses (@require). See "Metadata Header & Directives" for the full list.

  • The entry must be a .js / .mjs / .ts / .tsx / .jsx file; .vue files can only be imported, never used as the entry
  • Plugin builds only honour the entry file's directives. A // @require in a non-entry file's header only applies when you run <that file> directly in the terminal; the editor shows a Warning on such lines, and other files cannot borrow it
  • When importing from local files, the script named main / index / entry is picked as the entry by default (whether or not it has a metadata header), falling back to the first script. You can change it in the import dialog, and later switch it from the IDE's entry dropdown or with entryFile set <path> in the terminal
  • After switching the entry, the app's name, match scope and built-ins all come from the new entry's header. If the new entry has no metadata header, the next compile registers it on <all_urls> with no built-ins. Deleting the entry file automatically picks the currently open script or the first script as the new entry. In both cases the registered artifact only changes after you click "Compile"
  • npm dependency versions are read only from the package.json at the workspace root, wherever the entry file lives

App name and @name ​

The project name and the entry's @name stay in sync both ways: edit @name and save to rename the project; rename the project in the app list (or let the AI name it from your first prompt) and the entry's @name is rewritten to match. Deleting @name does not clear the project name — compiles keep using the project name.

Two app shapes ​

Every app, plain JS included, is bundled into a single artifact by the in-browser compiler before it is injected. The table below is only the label shown at import time; it does not change the compile pipeline or how directives behave:

ShapeDetectionCompilation
Plain JS appSingle .js entry, no ESM importsCompiled and bundled in the browser, then injected
Compiled source appContains .vue / .ts / .tsx / .jsx / .mjs or ESM importsCompiled and bundled in the browser, then injected

Both shapes share the same metadata header. Wire local modules and npm packages with standard import; keep @require for built-in runtime libraries only (a local file fails the compile).

Built-in libraries ​

The extension runtime provides eight libraries directly, so they add nothing to your app's size. Enable each with @require <id> (except LFRuntime, which is always available):

LibraryGlobalPurpose
runtimeLFRuntimeApp context: metadata, hot reload, resources, styles (always available)
domLFDOMDOM operations, mounting, locators, semantic snapshots
storeLFStoreApp-scoped, cross-site key/value storage
httpLFHttpBackground-proxied HTTP / streaming / WebSocket
modelLFModelPluginX AI gateway (chat / streaming / images)
userLFUserPlugin login state and current user info
networkLFNetworkInspect requests/responses of the current tab
agentLFAgentSpawn AI agent instances running in detached IDE sessions (plugincmd handlers only)

Execution environment ​

Saved apps are injected as chrome.userScripts USER_SCRIPT-world scripts: isolated from page JS but sharing the DOM, and injected into the top-level page only (never into iframes). During development, liveDebug injections set LFRuntime.liveDebug to true, but that unlocks no extra capabilities.

The workspace bridge (LFRuntime.readFile / writeFile / execTerminalCommand) can only be called directly from temporary terminal run scripts (run / run -c / run --tab / run --target background). In app source — liveDebug, enabled, installed or background apps alike — a direct call fails the compile. Register a command with LFRuntime.cli.register({ workspace: ['read' | 'write' | 'exec'] }) and use ctx.workspace.readFile / writeFile / exec inside its handler; the lease only lasts for that plugincmd call.