@deepseek-ai/dsh-api-terminal-controller 0.1.6-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +93 -0
- package/README.zh.md +93 -0
- package/lib/client.js +677 -0
- package/lib/index.js +844 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1131 -0
- package/lib/typert.remote-client.d.ts +49 -0
- package/lib/typert.remote-client.js +529 -0
- package/lib/types/client/close-requests.d.ts +31 -0
- package/lib/types/client/close-requests.js +75 -0
- package/lib/types/client/index.d.ts +88 -0
- package/lib/types/client/index.js +157 -0
- package/lib/types/client/model.d.ts +119 -0
- package/lib/types/client/model.js +323 -0
- package/lib/types/client/shell-preference.d.ts +11 -0
- package/lib/types/client/shell-preference.js +26 -0
- package/lib/types/index.d.ts +135 -0
- package/lib/types/index.js +371 -0
- package/lib/types/shells.d.ts +21 -0
- package/lib/types/shells.js +54 -0
- package/lib/types/stream.d.ts +29 -0
- package/lib/types/stream.js +78 -0
- package/lib/types/terminal.d.ts +61 -0
- package/lib/types/terminal.js +168 -0
- package/lib/types/types.d.ts +69 -0
- package/lib/types/types.js +2 -0
- package/package.json +93 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the browser preference.
|
|
3
|
+
* @returns the last selected shell path, or null when storage is unavailable.
|
|
4
|
+
*/
|
|
5
|
+
export declare function preferredShell(): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Remember the selected shell without making storage a startup dependency.
|
|
8
|
+
* @param path - verified executable path offered by the Host.
|
|
9
|
+
*/
|
|
10
|
+
export declare function rememberShell(path: string): void;
|
|
11
|
+
//# sourceMappingURL=shell-preference.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Browser-local shell preference; Host discovery decides whether the saved path is usable. */
|
|
2
|
+
const KEY = 'dsh.terminal.shell';
|
|
3
|
+
/**
|
|
4
|
+
* Read the browser preference.
|
|
5
|
+
* @returns the last selected shell path, or null when storage is unavailable.
|
|
6
|
+
*/
|
|
7
|
+
export function preferredShell() {
|
|
8
|
+
try {
|
|
9
|
+
return typeof localStorage === 'undefined' ? null : localStorage.getItem(KEY);
|
|
10
|
+
}
|
|
11
|
+
catch (_storageUnavailable) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Remember the selected shell without making storage a startup dependency.
|
|
17
|
+
* @param path - verified executable path offered by the Host.
|
|
18
|
+
*/
|
|
19
|
+
export function rememberShell(path) {
|
|
20
|
+
try {
|
|
21
|
+
if (typeof localStorage !== 'undefined')
|
|
22
|
+
localStorage.setItem(KEY, path);
|
|
23
|
+
}
|
|
24
|
+
catch (_storageUnavailable) { /* Private browsing or quota failure leaves this launch usable. */ }
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=shell-preference.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Session-scoped browser terminals over the composed subprocess and sandbox providers. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import z from '@deepseek-ai/schemastery';
|
|
4
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
5
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
6
|
+
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
7
|
+
import type { TerminalShell, TerminalAttachmentId, TerminalCreateRequest, TerminalEnvironment, TerminalFrame, WebTerminalId, WebTerminalInfo } from './types.ts';
|
|
8
|
+
export type * from './types.ts';
|
|
9
|
+
declare module '@deepseek-ai/cordis' {
|
|
10
|
+
interface Context {
|
|
11
|
+
/** Interactive user terminals, separate from the Agent terminal tool registry. */
|
|
12
|
+
terminalController: TerminalController;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Deployment limits and an optional shell profile. */
|
|
16
|
+
export interface Config {
|
|
17
|
+
/** Explicit shell profile; omission uses the execution environment's default shell. */
|
|
18
|
+
readonly shell?: {
|
|
19
|
+
/** Executable path or PATH name, verified by the subprocess provider. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** User-visible profile name. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Arguments passed to the interactive shell. */
|
|
24
|
+
args: string[];
|
|
25
|
+
} | undefined;
|
|
26
|
+
/** Executable names or paths checked for the new-terminal shell selector. */
|
|
27
|
+
readonly shellCandidates: string[];
|
|
28
|
+
/** Maximum retained terminals and pending allocations per Session. */
|
|
29
|
+
readonly maxTerminals: number;
|
|
30
|
+
/** Maximum terminal width in columns. */
|
|
31
|
+
readonly maxCols: number;
|
|
32
|
+
/** Maximum terminal height in rows. */
|
|
33
|
+
readonly maxRows: number;
|
|
34
|
+
/** Screen history rows retained for reconnecting clients. */
|
|
35
|
+
readonly scrollback: number;
|
|
36
|
+
/** Maximum queued UTF-8 frame bytes per output follower before disconnection. */
|
|
37
|
+
readonly maxBufferedBytes: number;
|
|
38
|
+
/** Maximum UTF-8 bytes in one input request. */
|
|
39
|
+
readonly maxInputBytes: number;
|
|
40
|
+
/** Provider process-termination grace period in milliseconds. */
|
|
41
|
+
readonly disposeGraceMs: number;
|
|
42
|
+
}
|
|
43
|
+
/** Typed Remote control of transient Session-owned terminal processes. */
|
|
44
|
+
export declare class TerminalController extends TypertRemoteService {
|
|
45
|
+
private readonly config;
|
|
46
|
+
static inject: string[];
|
|
47
|
+
static Config: z<Config>;
|
|
48
|
+
private readonly owners;
|
|
49
|
+
private readonly lifetime;
|
|
50
|
+
/**
|
|
51
|
+
* @param ctx - Host context carrying typed Remote and execution providers.
|
|
52
|
+
* @param config - validated terminal limits and optional shell profile.
|
|
53
|
+
*/
|
|
54
|
+
constructor(ctx: Context, config: Config);
|
|
55
|
+
/**
|
|
56
|
+
* Read the Session working directory and terminal limits without resolving a shell.
|
|
57
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
58
|
+
* @param signal - request cancellation.
|
|
59
|
+
* @returns the Session workspace directory and terminal limits.
|
|
60
|
+
*/
|
|
61
|
+
environment(agent: Agent, signal: AbortSignal): TerminalEnvironment;
|
|
62
|
+
/**
|
|
63
|
+
* Discover installed shells in the Session's execution environment.
|
|
64
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
65
|
+
* @param signal - request cancellation.
|
|
66
|
+
* @returns verified profiles, with the configured or system default first.
|
|
67
|
+
*/
|
|
68
|
+
shells(agent: Agent, signal: AbortSignal): Promise<TerminalShell[]>;
|
|
69
|
+
/**
|
|
70
|
+
* List retained terminals without resolving or activating an Agent.
|
|
71
|
+
* @param sessionId - displayed Session identity, including offline history.
|
|
72
|
+
* @returns terminals retained for this Host lifetime.
|
|
73
|
+
*/
|
|
74
|
+
list(sessionId: SessionId): WebTerminalInfo[];
|
|
75
|
+
/**
|
|
76
|
+
* Allocate an interactive shell once for a caller-generated identity.
|
|
77
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
78
|
+
* @param request - initial dimensions and idempotency identity.
|
|
79
|
+
* @param signal - allocation cancellation; committed terminals survive disconnection.
|
|
80
|
+
* @returns the existing or newly committed terminal.
|
|
81
|
+
*/
|
|
82
|
+
create(agent: Agent, request: TerminalCreateRequest, signal: AbortSignal): Promise<WebTerminalInfo>;
|
|
83
|
+
/**
|
|
84
|
+
* Attach to a terminal without binding its process lifetime to the transport.
|
|
85
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
86
|
+
* @param id - terminal identity.
|
|
87
|
+
* @param attachmentId - new exclusive input attachment.
|
|
88
|
+
* @param signal - physical stream cancellation.
|
|
89
|
+
* @returns screen recovery followed by output and metadata changes.
|
|
90
|
+
*/
|
|
91
|
+
follow(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, signal: AbortSignal): AsyncIterable<TerminalFrame>;
|
|
92
|
+
/**
|
|
93
|
+
* Deliver raw input, including Tab completion and control characters.
|
|
94
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
95
|
+
* @param id - terminal identity.
|
|
96
|
+
* @param attachmentId - current writable attachment.
|
|
97
|
+
* @param data - input bytes represented as UTF-8 text.
|
|
98
|
+
* @returns after provider input acceptance.
|
|
99
|
+
*/
|
|
100
|
+
write(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, data: string): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Update the dimensions of the PTY and recovery screen.
|
|
103
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
104
|
+
* @param id - terminal identity.
|
|
105
|
+
* @param attachmentId - current writable attachment.
|
|
106
|
+
* @param cols - column count.
|
|
107
|
+
* @param rows - row count.
|
|
108
|
+
* @returns after the resize completes.
|
|
109
|
+
*/
|
|
110
|
+
resize(agent: Agent, id: WebTerminalId, attachmentId: TerminalAttachmentId, cols: number, rows: number): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Rename a terminal without changing its shell.
|
|
113
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
114
|
+
* @param id - terminal identity.
|
|
115
|
+
* @param title - nonempty display title, at most 120 characters.
|
|
116
|
+
*/
|
|
117
|
+
rename(agent: Agent, id: WebTerminalId, title: string): void;
|
|
118
|
+
/**
|
|
119
|
+
* Close an identity to future creation and kill its process range; repeated closes succeed.
|
|
120
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
121
|
+
* @param id - terminal identity.
|
|
122
|
+
* @returns after provider cleanup succeeds. A failure retains the terminal for retry.
|
|
123
|
+
*/
|
|
124
|
+
close(agent: Agent, id: WebTerminalId): Promise<void>;
|
|
125
|
+
private owner;
|
|
126
|
+
private disposeOwner;
|
|
127
|
+
private terminal;
|
|
128
|
+
private requireOpen;
|
|
129
|
+
private dimensions;
|
|
130
|
+
private execution;
|
|
131
|
+
private spawn;
|
|
132
|
+
}
|
|
133
|
+
/** Browser terminal service plugin. */
|
|
134
|
+
export default TerminalController;
|
|
135
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import z from '@deepseek-ai/schemastery';
|
|
36
|
+
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
37
|
+
import { discoverShells, resolveShell } from "./shells.js";
|
|
38
|
+
import { BrowserTerminal } from "./terminal.js";
|
|
39
|
+
/** Typed Remote control of transient Session-owned terminal processes. */
|
|
40
|
+
let TerminalController = (() => {
|
|
41
|
+
let _classSuper = TypertRemoteService;
|
|
42
|
+
let _instanceExtraInitializers = [];
|
|
43
|
+
let _environment_decorators;
|
|
44
|
+
let _shells_decorators;
|
|
45
|
+
let _list_decorators;
|
|
46
|
+
let _create_decorators;
|
|
47
|
+
let _follow_decorators;
|
|
48
|
+
let _write_decorators;
|
|
49
|
+
let _resize_decorators;
|
|
50
|
+
let _rename_decorators;
|
|
51
|
+
let _close_decorators;
|
|
52
|
+
return class TerminalController extends _classSuper {
|
|
53
|
+
static {
|
|
54
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
55
|
+
_environment_decorators = [Remote];
|
|
56
|
+
_shells_decorators = [Remote];
|
|
57
|
+
_list_decorators = [Remote];
|
|
58
|
+
_create_decorators = [Remote];
|
|
59
|
+
_follow_decorators = [Remote({ mode: 'stream' })];
|
|
60
|
+
_write_decorators = [Remote];
|
|
61
|
+
_resize_decorators = [Remote];
|
|
62
|
+
_rename_decorators = [Remote];
|
|
63
|
+
_close_decorators = [Remote];
|
|
64
|
+
__esDecorate(this, null, _environment_decorators, { kind: "method", name: "environment", static: false, private: false, access: { has: obj => "environment" in obj, get: obj => obj.environment }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
65
|
+
__esDecorate(this, null, _shells_decorators, { kind: "method", name: "shells", static: false, private: false, access: { has: obj => "shells" in obj, get: obj => obj.shells }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
66
|
+
__esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
67
|
+
__esDecorate(this, null, _create_decorators, { kind: "method", name: "create", static: false, private: false, access: { has: obj => "create" in obj, get: obj => obj.create }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
68
|
+
__esDecorate(this, null, _follow_decorators, { kind: "method", name: "follow", static: false, private: false, access: { has: obj => "follow" in obj, get: obj => obj.follow }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
69
|
+
__esDecorate(this, null, _write_decorators, { kind: "method", name: "write", static: false, private: false, access: { has: obj => "write" in obj, get: obj => obj.write }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
70
|
+
__esDecorate(this, null, _resize_decorators, { kind: "method", name: "resize", static: false, private: false, access: { has: obj => "resize" in obj, get: obj => obj.resize }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
71
|
+
__esDecorate(this, null, _rename_decorators, { kind: "method", name: "rename", static: false, private: false, access: { has: obj => "rename" in obj, get: obj => obj.rename }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
72
|
+
__esDecorate(this, null, _close_decorators, { kind: "method", name: "close", static: false, private: false, access: { has: obj => "close" in obj, get: obj => obj.close }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
73
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
74
|
+
}
|
|
75
|
+
config = __runInitializers(this, _instanceExtraInitializers);
|
|
76
|
+
static inject = ['subprocess', 'sandboxPolicy', 'sessionProjections', 'typert'];
|
|
77
|
+
static Config = z.object({
|
|
78
|
+
shell: z.union([z.object({
|
|
79
|
+
path: z.string().required(), name: z.string().required(), args: z.array(z.string()).default([]),
|
|
80
|
+
}), z.const(undefined)]),
|
|
81
|
+
shellCandidates: z.array(z.string().min(1)).default(['zsh', 'bash', 'fish', 'pwsh', 'powershell', 'cmd']),
|
|
82
|
+
maxTerminals: z.number().step(1).min(1).default(8),
|
|
83
|
+
maxCols: z.number().step(1).min(2).default(500),
|
|
84
|
+
maxRows: z.number().step(1).min(1).default(200),
|
|
85
|
+
scrollback: z.number().step(1).min(0).default(1000),
|
|
86
|
+
maxBufferedBytes: z.number().step(1).min(1024).default(2 * 1024 * 1024),
|
|
87
|
+
maxInputBytes: z.number().step(1).min(1).default(64 * 1024),
|
|
88
|
+
disposeGraceMs: z.number().step(1).min(1).default(1000),
|
|
89
|
+
});
|
|
90
|
+
owners = new Map();
|
|
91
|
+
lifetime = new AbortController();
|
|
92
|
+
/**
|
|
93
|
+
* @param ctx - Host context carrying typed Remote and execution providers.
|
|
94
|
+
* @param config - validated terminal limits and optional shell profile.
|
|
95
|
+
*/
|
|
96
|
+
constructor(ctx, config) {
|
|
97
|
+
super(ctx, 'terminalController', { namespace: 'terminal' });
|
|
98
|
+
this.config = config;
|
|
99
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
100
|
+
if (eventName !== 'session/event')
|
|
101
|
+
return;
|
|
102
|
+
const [session, event] = args;
|
|
103
|
+
if (event.type !== 'sandbox/mode')
|
|
104
|
+
return;
|
|
105
|
+
const owner = this.owners.get(session.id);
|
|
106
|
+
if (owner === undefined || owner.terminals.size + owner.pending.size + owner.allocations.size === 0)
|
|
107
|
+
return;
|
|
108
|
+
const current = ctx.sessionProjections.stateOf(session, 'sandboxMode') ?? ctx.sandboxPolicy.defaultMode;
|
|
109
|
+
if (event.data.mode !== current)
|
|
110
|
+
throw new Error('Close browser terminals before changing the Session sandbox mode');
|
|
111
|
+
}, { global: true });
|
|
112
|
+
ctx.effect(() => async () => {
|
|
113
|
+
this.lifetime.abort(new Error('Terminal controller disposed'));
|
|
114
|
+
const results = await Promise.allSettled([...this.owners].map(([id, owner]) => this.disposeOwner(id, owner)));
|
|
115
|
+
const errors = results.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
116
|
+
if (errors.length > 0)
|
|
117
|
+
throw new AggregateError(errors, 'Browser terminal cleanup failed');
|
|
118
|
+
}, 'terminal-controller.processes');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Read the Session working directory and terminal limits without resolving a shell.
|
|
122
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
123
|
+
* @param signal - request cancellation.
|
|
124
|
+
* @returns the Session workspace directory and terminal limits.
|
|
125
|
+
*/
|
|
126
|
+
environment(agent, signal) {
|
|
127
|
+
signal.throwIfAborted();
|
|
128
|
+
const { sandboxPolicy } = this.execution(agent);
|
|
129
|
+
return { cwd: sandboxPolicy.resolve({ session: agent.session }).workspaceRoot,
|
|
130
|
+
maxInputBytes: this.config.maxInputBytes, maxCols: this.config.maxCols,
|
|
131
|
+
maxRows: this.config.maxRows, scrollback: this.config.scrollback };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Discover installed shells in the Session's execution environment.
|
|
135
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
136
|
+
* @param signal - request cancellation.
|
|
137
|
+
* @returns verified profiles, with the configured or system default first.
|
|
138
|
+
*/
|
|
139
|
+
shells(agent, signal) {
|
|
140
|
+
signal.throwIfAborted();
|
|
141
|
+
return discoverShells(this.execution(agent).subprocess, this.config.shell, this.config.shellCandidates, signal);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* List retained terminals without resolving or activating an Agent.
|
|
145
|
+
* @param sessionId - displayed Session identity, including offline history.
|
|
146
|
+
* @returns terminals retained for this Host lifetime.
|
|
147
|
+
*/
|
|
148
|
+
list(sessionId) {
|
|
149
|
+
const owner = this.owners.get(sessionId);
|
|
150
|
+
if (owner === undefined)
|
|
151
|
+
return [];
|
|
152
|
+
return [...owner.terminals.values(), ...owner.allocations.values()].map(terminal => terminal.info);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Allocate an interactive shell once for a caller-generated identity.
|
|
156
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
157
|
+
* @param request - initial dimensions and idempotency identity.
|
|
158
|
+
* @param signal - allocation cancellation; committed terminals survive disconnection.
|
|
159
|
+
* @returns the existing or newly committed terminal.
|
|
160
|
+
*/
|
|
161
|
+
async create(agent, request, signal) {
|
|
162
|
+
this.lifetime.signal.throwIfAborted();
|
|
163
|
+
if (!/^[\w-]{1,128}$/u.test(request.id))
|
|
164
|
+
throw new Error('Invalid terminal identity');
|
|
165
|
+
this.dimensions(request.cols, request.rows);
|
|
166
|
+
const owner = this.owner(agent);
|
|
167
|
+
owner.lifetime.signal.throwIfAborted();
|
|
168
|
+
this.requireOpen(owner, request.id);
|
|
169
|
+
const existing = owner.terminals.get(request.id);
|
|
170
|
+
if (existing !== undefined)
|
|
171
|
+
return existing.info;
|
|
172
|
+
const pending = owner.pending.get(request.id);
|
|
173
|
+
if (pending !== undefined) {
|
|
174
|
+
const terminal = await pending;
|
|
175
|
+
this.requireOpen(owner, request.id);
|
|
176
|
+
return terminal.info;
|
|
177
|
+
}
|
|
178
|
+
if (owner.allocations.has(request.id))
|
|
179
|
+
throw new Error('Close the failed terminal allocation before creating it again');
|
|
180
|
+
if (new Set([...owner.terminals.keys(), ...owner.pending.keys(), ...owner.allocations.keys()]).size >= this.config.maxTerminals)
|
|
181
|
+
throw new RemoteError('terminal/limit-reached', 'Session terminal limit reached', { limit: this.config.maxTerminals });
|
|
182
|
+
const allocation = this.spawn(agent, owner, request, AbortSignal.any([signal, this.lifetime.signal, owner.lifetime.signal]));
|
|
183
|
+
owner.pending.set(request.id, allocation);
|
|
184
|
+
try {
|
|
185
|
+
const terminal = await allocation;
|
|
186
|
+
owner.terminals.set(request.id, terminal);
|
|
187
|
+
owner.allocations.delete(request.id);
|
|
188
|
+
this.requireOpen(owner, request.id);
|
|
189
|
+
return terminal.info;
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
owner.pending.delete(request.id);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Attach to a terminal without binding its process lifetime to the transport.
|
|
197
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
198
|
+
* @param id - terminal identity.
|
|
199
|
+
* @param attachmentId - new exclusive input attachment.
|
|
200
|
+
* @param signal - physical stream cancellation.
|
|
201
|
+
* @returns screen recovery followed by output and metadata changes.
|
|
202
|
+
*/
|
|
203
|
+
follow(agent, id, attachmentId, signal) {
|
|
204
|
+
if (!/^[\w-]{1,128}$/u.test(attachmentId))
|
|
205
|
+
throw new Error('Invalid terminal attachment identity');
|
|
206
|
+
return this.terminal(agent, id).follow(attachmentId, signal);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Deliver raw input, including Tab completion and control characters.
|
|
210
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
211
|
+
* @param id - terminal identity.
|
|
212
|
+
* @param attachmentId - current writable attachment.
|
|
213
|
+
* @param data - input bytes represented as UTF-8 text.
|
|
214
|
+
* @returns after provider input acceptance.
|
|
215
|
+
*/
|
|
216
|
+
async write(agent, id, attachmentId, data) {
|
|
217
|
+
if (Buffer.byteLength(data, 'utf8') > this.config.maxInputBytes)
|
|
218
|
+
throw new Error('Terminal input exceeds the configured limit');
|
|
219
|
+
await this.terminal(agent, id).write(attachmentId, data);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Update the dimensions of the PTY and recovery screen.
|
|
223
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
224
|
+
* @param id - terminal identity.
|
|
225
|
+
* @param attachmentId - current writable attachment.
|
|
226
|
+
* @param cols - column count.
|
|
227
|
+
* @param rows - row count.
|
|
228
|
+
* @returns after the resize completes.
|
|
229
|
+
*/
|
|
230
|
+
async resize(agent, id, attachmentId, cols, rows) {
|
|
231
|
+
this.dimensions(cols, rows);
|
|
232
|
+
await this.terminal(agent, id).resize(attachmentId, cols, rows);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Rename a terminal without changing its shell.
|
|
236
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
237
|
+
* @param id - terminal identity.
|
|
238
|
+
* @param title - nonempty display title, at most 120 characters.
|
|
239
|
+
*/
|
|
240
|
+
rename(agent, id, title) {
|
|
241
|
+
if (title.trim().length === 0 || title.length > 120)
|
|
242
|
+
throw new Error('Terminal title must contain 1–120 characters');
|
|
243
|
+
this.terminal(agent, id).rename(title.trim());
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Close an identity to future creation and kill its process range; repeated closes succeed.
|
|
247
|
+
* @param agent - Session owner supplied by the Gateway.
|
|
248
|
+
* @param id - terminal identity.
|
|
249
|
+
* @returns after provider cleanup succeeds. A failure retains the terminal for retry.
|
|
250
|
+
*/
|
|
251
|
+
async close(agent, id) {
|
|
252
|
+
const owner = this.owner(agent);
|
|
253
|
+
owner.closedIds.add(id);
|
|
254
|
+
// create publishes the allocation before this wait settles; close owns it even if create then rejects.
|
|
255
|
+
await owner.pending.get(id)?.catch(() => { });
|
|
256
|
+
const terminal = owner.terminals.get(id);
|
|
257
|
+
if (terminal !== undefined) {
|
|
258
|
+
await terminal.close();
|
|
259
|
+
owner.terminals.delete(id);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const allocation = owner.allocations.get(id);
|
|
263
|
+
if (allocation === undefined)
|
|
264
|
+
return;
|
|
265
|
+
await allocation.handle.terminate();
|
|
266
|
+
owner.allocations.delete(id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
owner(agent) {
|
|
270
|
+
let owner = this.owners.get(agent.id);
|
|
271
|
+
if (owner === undefined) {
|
|
272
|
+
owner = { terminals: new Map(), pending: new Map(), allocations: new Map(), closedIds: new Set(), lifetime: new AbortController() };
|
|
273
|
+
this.owners.set(agent.id, owner);
|
|
274
|
+
const owned = owner;
|
|
275
|
+
agent.ctx.effect(() => async () => { await this.disposeOwner(agent.id, owned); }, 'terminal-controller.owner');
|
|
276
|
+
}
|
|
277
|
+
return owner;
|
|
278
|
+
}
|
|
279
|
+
disposeOwner(id, owner) {
|
|
280
|
+
if (owner.cleanup !== undefined)
|
|
281
|
+
return owner.cleanup;
|
|
282
|
+
owner.lifetime.abort(new Error('Terminal Session owner disposed'));
|
|
283
|
+
owner.cleanup = (async () => {
|
|
284
|
+
await Promise.allSettled(owner.pending.values());
|
|
285
|
+
const results = await Promise.allSettled([
|
|
286
|
+
...[...owner.terminals.values()].map(terminal => terminal.close()),
|
|
287
|
+
...[...owner.allocations.values()].map(allocation => allocation.handle.terminate()),
|
|
288
|
+
]);
|
|
289
|
+
const errors = results.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
290
|
+
if (errors.length > 0)
|
|
291
|
+
throw new AggregateError(errors, 'Session terminal cleanup failed');
|
|
292
|
+
owner.terminals.clear();
|
|
293
|
+
owner.allocations.clear();
|
|
294
|
+
this.owners.delete(id);
|
|
295
|
+
})().catch((error) => { delete owner.cleanup; throw error; });
|
|
296
|
+
return owner.cleanup;
|
|
297
|
+
}
|
|
298
|
+
terminal(agent, id) {
|
|
299
|
+
const terminal = this.owners.get(agent.id)?.terminals.get(id);
|
|
300
|
+
if (terminal === undefined)
|
|
301
|
+
throw new Error('Terminal no longer exists in this Session');
|
|
302
|
+
return terminal;
|
|
303
|
+
}
|
|
304
|
+
requireOpen(owner, id) {
|
|
305
|
+
if (owner.closedIds.has(id))
|
|
306
|
+
throw new Error('Terminal was closed in this Session');
|
|
307
|
+
}
|
|
308
|
+
dimensions(cols, rows) {
|
|
309
|
+
if (!Number.isSafeInteger(cols) || cols < 2 || cols > this.config.maxCols
|
|
310
|
+
|| !Number.isSafeInteger(rows) || rows < 1 || rows > this.config.maxRows)
|
|
311
|
+
throw new Error('Terminal dimensions exceed the configured limits');
|
|
312
|
+
}
|
|
313
|
+
execution(agent) {
|
|
314
|
+
// The Agent context selects execution providers but does not inject consumer services.
|
|
315
|
+
const subprocess = agent.ctx.get('subprocess');
|
|
316
|
+
const sandboxPolicy = agent.ctx.get('sandboxPolicy');
|
|
317
|
+
if (subprocess === undefined || sandboxPolicy === undefined)
|
|
318
|
+
throw new Error('The Session execution environment requires subprocess and sandbox policy providers');
|
|
319
|
+
return { subprocess, sandboxPolicy };
|
|
320
|
+
}
|
|
321
|
+
async spawn(agent, owner, request, signal) {
|
|
322
|
+
const environment = this.environment(agent, signal);
|
|
323
|
+
const { subprocess, sandboxPolicy } = this.execution(agent);
|
|
324
|
+
const shell = request.shellPath === undefined
|
|
325
|
+
? await resolveShell(subprocess, this.config.shell, signal)
|
|
326
|
+
: (await this.shells(agent, signal)).find(candidate => candidate.path === request.shellPath);
|
|
327
|
+
if (shell === undefined)
|
|
328
|
+
throw new Error('Selected shell is not available in this execution environment');
|
|
329
|
+
const policy = sandboxPolicy.resolve({ session: agent.session });
|
|
330
|
+
let argv = [shell.path, ...shell.args];
|
|
331
|
+
if (policy.mode !== 'danger-full-access') {
|
|
332
|
+
const sandbox = agent.ctx.get('sandbox');
|
|
333
|
+
if (sandbox === undefined)
|
|
334
|
+
throw new Error('The Session sandbox mode requires an execution sandbox provider');
|
|
335
|
+
argv = (await sandbox.confine(argv, { ...policy, mode: policy.mode }, signal)).argv;
|
|
336
|
+
}
|
|
337
|
+
const handle = await subprocess.spawnTerminal({
|
|
338
|
+
argv, cwd: environment.cwd, cols: request.cols, rows: request.rows,
|
|
339
|
+
terminalType: 'xterm-256color', env: { DSH_SESSION_ID: agent.id },
|
|
340
|
+
graceMs: this.config.disposeGraceMs, signal,
|
|
341
|
+
});
|
|
342
|
+
const allocation = {
|
|
343
|
+
handle,
|
|
344
|
+
info: {
|
|
345
|
+
id: request.id, shell, title: shell.name, cwd: environment.cwd,
|
|
346
|
+
cols: request.cols, rows: request.rows, state: 'running', exitCode: null,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
owner.allocations.set(request.id, allocation);
|
|
350
|
+
try {
|
|
351
|
+
signal.throwIfAborted();
|
|
352
|
+
return new BrowserTerminal(handle, allocation.info, this.config.scrollback, this.config.maxBufferedBytes);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
allocation.info = { ...allocation.info, state: 'failed', error: error instanceof Error ? error.message : String(error) };
|
|
356
|
+
try {
|
|
357
|
+
await handle.terminate();
|
|
358
|
+
owner.allocations.delete(request.id);
|
|
359
|
+
}
|
|
360
|
+
catch (cleanupError) {
|
|
361
|
+
throw new AggregateError([error, cleanupError], 'Terminal allocation cleanup failed');
|
|
362
|
+
}
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
})();
|
|
368
|
+
export { TerminalController };
|
|
369
|
+
/** Browser terminal service plugin. */
|
|
370
|
+
export default TerminalController;
|
|
371
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Shell selection and executable verification use the target execution provider. */
|
|
2
|
+
import { type SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
|
|
3
|
+
import type { TerminalShell } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the configured shell or the execution environment's default shell.
|
|
6
|
+
* @param subprocess - target execution provider.
|
|
7
|
+
* @param configured - optional profile overriding the environment's default shell.
|
|
8
|
+
* @param signal - resolution cancellation.
|
|
9
|
+
* @returns one verified shell; a declared default that cannot resolve rejects.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveShell(subprocess: SubprocessRuntime, configured: TerminalShell | undefined, signal: AbortSignal): Promise<TerminalShell>;
|
|
12
|
+
/**
|
|
13
|
+
* List verified candidates after the configured or environment-default shell.
|
|
14
|
+
* @param subprocess - target execution provider.
|
|
15
|
+
* @param configured - optional default profile.
|
|
16
|
+
* @param candidates - executable names or paths permitted for shell selection.
|
|
17
|
+
* @param signal - discovery cancellation.
|
|
18
|
+
* @returns unique installed shells, with the default first; transport failures reject.
|
|
19
|
+
*/
|
|
20
|
+
export declare function discoverShells(subprocess: SubprocessRuntime, configured: TerminalShell | undefined, candidates: readonly string[], signal: AbortSignal): Promise<TerminalShell[]>;
|
|
21
|
+
//# sourceMappingURL=shells.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Shell selection and executable verification use the target execution provider. */
|
|
2
|
+
import { SubprocessExecutableNotFoundError } from '@deepseek-ai/dsh-subprocess';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the configured shell or the execution environment's default shell.
|
|
5
|
+
* @param subprocess - target execution provider.
|
|
6
|
+
* @param configured - optional profile overriding the environment's default shell.
|
|
7
|
+
* @param signal - resolution cancellation.
|
|
8
|
+
* @returns one verified shell; a declared default that cannot resolve rejects.
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveShell(subprocess, configured, signal) {
|
|
11
|
+
let shell = configured;
|
|
12
|
+
if (shell === undefined) {
|
|
13
|
+
const environment = await subprocess.terminalEnvironment(signal);
|
|
14
|
+
shell = profile(environment.defaultShell ?? (environment.platform === 'windows' ? 'cmd.exe' : '/bin/sh'));
|
|
15
|
+
}
|
|
16
|
+
const path = await subprocess.resolveExecutable(shell.path, undefined, signal);
|
|
17
|
+
return { ...shell, path };
|
|
18
|
+
}
|
|
19
|
+
function profile(path) {
|
|
20
|
+
const name = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1);
|
|
21
|
+
const kind = name.toLowerCase().replace(/\.exe$/u, '');
|
|
22
|
+
return { path, name, args: kind === 'cmd' ? [] : kind === 'pwsh' || kind === 'powershell' ? ['-NoLogo'] : ['-i'] };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* List verified candidates after the configured or environment-default shell.
|
|
26
|
+
* @param subprocess - target execution provider.
|
|
27
|
+
* @param configured - optional default profile.
|
|
28
|
+
* @param candidates - executable names or paths permitted for shell selection.
|
|
29
|
+
* @param signal - discovery cancellation.
|
|
30
|
+
* @returns unique installed shells, with the default first; transport failures reject.
|
|
31
|
+
*/
|
|
32
|
+
export async function discoverShells(subprocess, configured, candidates, signal) {
|
|
33
|
+
const preferred = await resolveShell(subprocess, configured, signal);
|
|
34
|
+
const found = await Promise.all(candidates.map(async (candidate) => {
|
|
35
|
+
try {
|
|
36
|
+
return await resolveShell(subprocess, profile(candidate), signal);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof SubprocessExecutableNotFoundError)
|
|
40
|
+
return undefined;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
const shells = new Map();
|
|
45
|
+
for (const shell of [preferred, ...found]) {
|
|
46
|
+
if (shell === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
const key = shell.path.includes('\\') ? shell.path.toLowerCase() : shell.path;
|
|
49
|
+
if (!shells.has(key))
|
|
50
|
+
shells.set(key, shell);
|
|
51
|
+
}
|
|
52
|
+
return [...shells.values()];
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=shells.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TerminalFrame } from './types.ts';
|
|
2
|
+
/** Slow followers fail explicitly; a later attachment recovers from the screen. */
|
|
3
|
+
export declare class TerminalFollower {
|
|
4
|
+
private readonly maxBytes;
|
|
5
|
+
private readonly queue;
|
|
6
|
+
private bytes;
|
|
7
|
+
private wake;
|
|
8
|
+
private closed;
|
|
9
|
+
private finished;
|
|
10
|
+
private failure;
|
|
11
|
+
/** @param maxBytes - maximum queued UTF-8 bytes for this follower. */
|
|
12
|
+
constructor(maxBytes: number);
|
|
13
|
+
/**
|
|
14
|
+
* Queue a frame or fail this follower when its byte limit is exceeded.
|
|
15
|
+
* @param frame - next ordered frame.
|
|
16
|
+
*/
|
|
17
|
+
push(frame: TerminalFrame): void;
|
|
18
|
+
/** Finish after delivering every queued frame, including the final exit state. */
|
|
19
|
+
finish(): void;
|
|
20
|
+
/** Stop this follower without stopping its terminal. */
|
|
21
|
+
close(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Drain until detached or failed.
|
|
24
|
+
* @param signal - Remote generation cancellation.
|
|
25
|
+
* @returns ordered terminal frames.
|
|
26
|
+
*/
|
|
27
|
+
read(signal: AbortSignal): AsyncIterable<TerminalFrame>;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=stream.d.ts.map
|