Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions contract.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"source": {
"repository": "https://github.com/1lck/Lithe-IDEA",
"directory": "shared/contracts/plugins/v1",
"revision": "04b1fb848f8f593d51e998b0cc0f487bc8c56f40"
"revision": "f64d262c48f1ff5ad1778b301138596493a09ad5"
},
"files": [
{
"path": "README.md",
"sha256": "02f8480592dba210e6e5d78d2eb14d9cf6d4dbd4ee8eef0339982f67eecf9696"
"sha256": "4cfe0f5f6cb15980bf9db4b19fe5ed80082c417132ccad04dee5959d7efdef72"
},
{
"path": "fixtures/backend-workspace.plugin.json",
Expand Down Expand Up @@ -124,7 +124,7 @@
},
{
"path": "fixtures/function-protocol.json",
"sha256": "e880024b108b2d11c5c2b93c39ea642460989eaf530bc1f85254f404780dfc3b"
"sha256": "4341a2145d495431f4017ae2aed95214c512c14c5efb866c5a1178912c66006b"
},
{
"path": "fixtures/grammars/http.monarch.json",
Expand Down Expand Up @@ -226,6 +226,10 @@
"path": "fixtures/settings-v2.plugin.json",
"sha256": "83d4c7faf7bfbb9c474639bfc0ef42dae8cd7d370d388f8527589f2afed12981"
},
{
"path": "fixtures/terminal-backend.plugin.json",
"sha256": "664669aa36b8b3af464495048f76cb1154c38407c816e7997447f372c139fff1"
},
{
"path": "fixtures/tool-definition.plugin.json",
"sha256": "06b94a1886edcaa8ade3ae1462d01aa6e54e7ec3104ae28d0b796f2a303fd90a"
Expand All @@ -252,7 +256,7 @@
},
{
"path": "plugin-manifest.schema.json",
"sha256": "a308c44479fd2f1f3a51f54f75831f1052ec02aa18e7f294187946bd30ea3a6c"
"sha256": "fba04d8c2c053cec877ab0419e1236810a75368fb3536b271b6a6301ba750339"
},
{
"path": "proposals/toolchains/README.md",
Expand Down
102 changes: 102 additions & 0 deletions contract/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ Rules for publishing across levels and capability versions:
| `credentials.store` | the backend uses the [credentials host service](#host-services-protocol-version-2); refused without a `backend` |
| `workspace.metadata` | the backend uses the [workspace host service](#workspace-contexts-and-capability-discovery-protocol-version-3); refused without `backend` 2 |
| `editor.actions` | the backend registers independent editor actions; requires `backend` 2 and `documents.read`, grants only `editor/registerActions`, not document edits or domain execution |
| `terminal.create` | the backend creates and manages its own [native terminals](#plugin-owned-native-terminals-function-protocol-3), including bounded display history for reopen; requires `backend` 2 |

| Activation event | Meaning |
| --- | --- |
Expand Down Expand Up @@ -2972,3 +2973,104 @@ Schema 1 loads the complete node set on refresh. Forms, lazy child data sources,
multi-instance selectors, global function commands, native renderer factories
and Windows rendering remain follow-up work under #1209; this subset does not
claim completion of that whole issue.

### Plugin-owned native terminals (function protocol 3)

[`terminal-backend.plugin.json`](fixtures/terminal-backend.plugin.json) is the
minimal manifest. `terminal.create` grants `lithe/host/terminal/create`, `show`, `close`, `reopen`,
`dispose`, and `onExit`. It requires `backend:2`; discover the individual
methods through `getCapabilities` before using them. macOS implements this
service. Windows has no handler and does not advertise its methods. The
plugin's web page calls its backend through `functions.invoke`; the backend
uses these host requests with the invocation's workspace context. There is
no direct web `terminal.*` bridge and no new protocol version.

| Method (prefix `lithe/host/terminal/`) | Params, in addition to `context` | Result |
| --- | --- | --- |
| `create` | `executable`, optional `arguments`, `title`, `cwd`, `environment`, `show`, `retainedContextBytes` | `{terminalID,generation:1}` |
| `show` | `terminalID` | `null` |
| `close` | `terminalID`, optional `retainedContextBytes` | `{retainedContextBytes}` after bounded native cleanup |
| `reopen` | `terminalID`, optional `show` | `{terminalID,generation,restoredContextBytes}` |
| `dispose` | `terminalID` | `null` after closing and forgetting the handle |
| `onExit` | `terminalID` | `{subscriptionID}` |

`executable` is an absolute path or a bare command resolved by the native
terminal's PATH lookup; relative paths containing separators are refused.
`arguments` is an argv array, never shell text. `cwd` is a `{rootID,path}`
workspace directory resource (an empty path selects the folder root); without
it, the workspace root is used. The native adapter rejects a canonical cwd
outside that workspace. `environment` maps variable names to string overrides
or `null` removals. Process-ownership scope variables cannot be overridden.
`show` defaults to true; it selects the terminal in its own window without
raising a background window. With `show:false`, creation and reopening preserve
the current terminal selection and do not reveal the terminal panel or request
input focus; the native tab is mounted when the plugin explicitly calls `show`.
Unknown options fail before any side effect.

Creation and reopening ask for execution consent with the program, argv and
working directory, all available in a scrollable, selectable native text view,
then revalidate context and cancellation before launching
through the existing PTY terminal module. The response waits for user choice
without a machine-operation deadline, but cancellation/session/context closure
still ends it. Native launch and process cleanup retain their own bounds.
The host never types a launch into an existing shell or reads other terminals.
There is no `sendText`, user-terminal enumeration, web emulator, or Docker
special case. First release is local authority only; a remote context fails
with `plugin.host.authorityUnsupported` instead of starting anything locally.

Each handle belongs to one plugin backend session and one context generation.
A handle from another plugin, context or replacement backend is `notFound`.
At most eight handles per plugin are retained, including closed handles;
`dispose` releases one. Limits are 256 arguments, 16 KiB per argument or
environment value, 64 environment entries, 64 KiB combined launch data, and
128 UTF-8 bytes in a nonempty single-line title. `busy` refuses concurrent
close/reopen/dispose on the same handle. Reopening a running terminal is
`preconditionFailed`; showing a closed terminal asks the plugin to reopen it.

#### Close, retained context and reopen

`close` ends the process tree and removes the native tab, keeping the handle's
launch configuration and a plain-text suffix of its displayed terminal output.
`retainedContextBytes` is a nonnegative UTF-8 byte budget: default **16384**,
maximum **262144**, and **0** disables history. It can be chosen at create or
changed at close; over-limit values are refused, never silently expanded.
The result reports actual retained bytes, which can be smaller to avoid cutting
a Unicode scalar. The history is a rendering snapshot, not input events, raw
escape sequences, a resumable process image, or shell state. Secrets already
printed or echoed by a program can be part of that display; choose 0 when no
history should be retained.

`reopen` uses the saved executable, argv, cwd and environment in a **new** PTY
process. It feeds retained plain text to the display before spawning, never to
stdin. Shell variables, unsubmitted input, process state and automatic command
replay are not restored. Natural exit and user tab close are also reopenable;
the snapshot is bounded by the same budget. It keeps `terminalID` and increments
`generation`; late exit callbacks of previous generations are ignored. These
handles/history exist only in memory, not across app/backend restart or
workspace close. No state is written to the app bundle or plugin package.

Natural exit and user tab close run the same bounded native cleanup before
retaining the budgeted snapshot and releasing the complete native session and
render buffer. The handle is `busy` during cleanup; its exit notification is
sent after cleanup, so a subscriber can reopen immediately. A zero-byte budget
retains no display text on any close or exit path.

`dispose` closes any live process and permanently forgets the launch settings
and history. Ending the backend session, disabling/updating the plugin or
closing/replacing its workspace does the same for only its owned terminals;
user-created terminals are unaffected. Closing a page alone does not close a
completed terminal creation. Cancellation before launch creates nothing;
cancellation or context loss during launch closes the newly created process.
Cancelled creation/reopen is not safe to retry automatically.

`onExit` supports one live subscription per handle. Its event is
`{type:"exit",terminalID,generation,exitCode,reason}`, with an integer or null
exit code and `reason:"exited"|"closed"`. Each generation publishes one outcome.
Subscribing after exit delivers its recorded outcome once; the subscription
continues across reopen. Disposing it stops notifications and keeps the
terminal open. A disposed terminal has no further events.

The executable and argv remain plugin-owned semantics. A container plugin can
launch the user's Docker CLI with an explicit engine and container ID; another
tool can launch its interactive client through exactly the same service.
Neither requires a fake Build/Run provider.
10 changes: 9 additions & 1 deletion contract/v1/fixtures/function-protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,15 @@
},
{"name": "editor actions are a discoverable independent host service", "method": "lithe/host/editor/registerActions", "version": 3, "hostServices": ["lithe/host/editor/registerActions"], "params": {"context": {"contextID": "w1", "generation": 1}, "id": "actions", "function": "actions.collect", "selector": {"fileExtensions": ["http"]}}, "expected": {"forward": "editor"}},
{"name": "document reads do not grant editor action contribution", "method": "lithe/host/editor/registerActions", "version": 3, "hostServices": ["lithe/host/documents/getSnapshot"], "params": {}, "expected": {"code": "plugin.host.permissionDenied", "rpcCode": -32010}},
{"name": "protocol 2 does not expose independent editor actions", "method": "lithe/host/editor/registerActions", "version": 2, "hostServices": ["lithe/host/editor/registerActions"], "params": {}, "expected": {"code": "plugin.host.unavailable", "rpcCode": -32601}}
{"name": "protocol 2 does not expose independent editor actions", "method": "lithe/host/editor/registerActions", "version": 2, "hostServices": ["lithe/host/editor/registerActions"], "params": {}, "expected": {"code": "plugin.host.unavailable", "rpcCode": -32601}},
{"name": "terminal create is an individually granted protocol 3 method", "version": 3, "hostServices": ["lithe/host/terminal/create"], "method": "lithe/host/terminal/create", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "executable": "example-tool"}, "expected": {"forward": "terminal"}},
{"name": "terminal show is an individually granted protocol 3 method", "version": 3, "hostServices": ["lithe/host/terminal/show"], "method": "lithe/host/terminal/show", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "terminalID": "owned"}, "expected": {"forward": "terminal"}},
{"name": "terminal close is an individually granted protocol 3 method", "version": 3, "hostServices": ["lithe/host/terminal/close"], "method": "lithe/host/terminal/close", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "terminalID": "owned"}, "expected": {"forward": "terminal"}},
{"name": "terminal onExit is an individually granted protocol 3 method", "version": 3, "hostServices": ["lithe/host/terminal/onExit"], "method": "lithe/host/terminal/onExit", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "terminalID": "owned"}, "expected": {"forward": "terminal"}},
{"name": "terminal aggregate grant cannot authorize terminal creation", "version": 3, "hostServices": ["terminal"], "method": "lithe/host/terminal/create", "params": {}, "expected": {"code": "plugin.host.permissionDenied", "rpcCode": -32010}},
{"name": "an older protocol cannot create a native terminal", "version": 2, "hostServices": ["lithe/host/terminal/create"], "method": "lithe/host/terminal/create", "params": {}, "expected": {"code": "plugin.host.unavailable", "rpcCode": -32601}},
{"name": "terminal reopen is granted on protocol 3", "version": 3, "hostServices": ["lithe/host/terminal/reopen"], "method": "lithe/host/terminal/reopen", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "terminalID": "owned"}, "expected": {"forward": "terminal"}},
{"name": "terminal dispose is granted on protocol 3", "version": 3, "hostServices": ["lithe/host/terminal/dispose"], "method": "lithe/host/terminal/dispose", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "terminalID": "owned"}, "expected": {"forward": "terminal"}}
],
"invocationContexts": [
{
Expand Down
35 changes: 35 additions & 0 deletions contract/v1/fixtures/terminal-backend.plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"schemaVersion": 2,
"id": "dev.lithe.fixture.terminalbackend",
"displayName": "Native Terminal Backend",
"version": "1.0.0",
"pluginAPIVersion": 3,
"requires": {
"backend": 2,
"serverProcess": 1
},
"vendor": {
"id": "org.example",
"displayName": "Example",
"signatureRequirement": "publisherPackage"
},
"permissions": [
"backend.launch",
"workspace.metadata",
"terminal.create"
],
"activationEvents": [
"onWorkspaceOpen"
],
"languages": [],
"backend": {
"launch": {
"kind": "packaged",
"path": "bin/backend"
},
"arguments": [
"--state",
"${stateDir}"
]
}
}
3 changes: 2 additions & 1 deletion contract/v1/plugin-manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"preview.open",
"toolchains.provide",
"browser.open",
"window.messages"] }
"window.messages",
"terminal.create"] }
},
"activationEvents": {
"type": "array",
Expand Down
Loading
Loading