golem-kit 0.1.1 → 0.2.0
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/CHANGELOG.md +31 -0
- package/README.md +8 -5
- package/docs/agents.md +64 -0
- package/docs/app-backend.md +259 -0
- package/docs/architecture.md +93 -0
- package/docs/builder.md +15 -0
- package/docs/knowledge.md +35 -0
- package/docs/local-cli.md +19 -12
- package/docs/source-development.md +31 -0
- package/index.html +9 -0
- package/package.json +24 -5
- package/src/backend/accounts.ts +287 -0
- package/src/backend/app.ts +269 -0
- package/src/backend/files.ts +68 -0
- package/src/backend/http.ts +276 -0
- package/src/backend/index.ts +10 -0
- package/src/backend/jobs.ts +302 -0
- package/src/backend/jsonl.ts +87 -0
- package/src/backend/knowledge.ts +264 -0
- package/src/backend/model.ts +129 -0
- package/src/backend/rules.ts +53 -0
- package/src/backend/sqlite.ts +73 -0
- package/src/backend/views.ts +216 -0
- package/src/brain.ts +94 -0
- package/src/browser/adapters.ts +229 -53
- package/src/browser/ansi.ts +104 -0
- package/src/browser/app.d.ts +5 -2
- package/src/browser/app.tsx +167 -39
- package/src/browser/groups.tsx +29 -0
- package/src/browser/main.tsx +1 -0
- package/src/browser/panekeys.ts +34 -0
- package/src/browser/sources.tsx +113 -0
- package/src/browser/styles.css +36 -0
- package/src/browser/terminal.tsx +89 -0
- package/src/browser-build.ts +20 -7
- package/src/chat.ts +74 -0
- package/src/cli.ts +85 -13
- package/src/client.ts +205 -0
- package/src/config.ts +139 -5
- package/src/dev-server.ts +336 -39
- package/src/entry.mjs +19 -0
- package/src/eslint.mjs +55 -0
- package/src/operations.ts +169 -0
- package/src/runtime/assistant.ts +141 -0
- package/src/runtime/discovery.ts +13 -7
- package/src/runtime/harness/agent-status.js +388 -0
- package/src/runtime/harness/claude-tmux.js +573 -0
- package/src/runtime/harness/codex-notify.js +95 -0
- package/src/runtime/harness/codex-tmux.js +292 -0
- package/src/runtime/harness/fake.js +430 -0
- package/src/runtime/harness/package.json +1 -0
- package/src/runtime/harness/port.js +208 -0
- package/src/runtime/harness/tmux-session.js +556 -0
- package/src/runtime/harness/tmux.js +285 -0
- package/src/runtime/harness/turnend-hook.js +105 -0
- package/src/runtime/session.ts +171 -34
- package/src/runtime/tmux.ts +173 -0
- package/src/runtime/tool-names.ts +19 -0
- package/src/source-mode.ts +56 -0
- package/vite.config.ts +2 -4
- package/src/runtime/codex.ts +0 -119
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Vendored from bridge-commander 5bf87e4b harness/codex-tmux.js (zero-dependency). Local patches are marked 'golem:'.
|
|
2
|
+
'use strict';
|
|
3
|
+
// codex-tmux — the OpenAI Codex CLI implementation of the harness port, over tmux.
|
|
4
|
+
//
|
|
5
|
+
// HarnessRef: { harness: 'codex', session: 'bc-<id>', window?, cwd, resumeId? }
|
|
6
|
+
// session/window — same tmux addressing as every tmux adapter (tmux-session.js):
|
|
7
|
+
// session-granular refs own a whole `bc-*` session, window-granular
|
|
8
|
+
// refs live as a named window inside a shared one (workers inside
|
|
9
|
+
// their lieutenant's session).
|
|
10
|
+
// resumeId — the codex THREAD-ID (a uuid). Unlike claude there is no
|
|
11
|
+
// --session-id flag: codex assigns the id itself, so the ref is
|
|
12
|
+
// born WITHOUT resumeId and adopts it from the first turn-end
|
|
13
|
+
// (the notify relay records it to <stateDir>/<key>.session-id and
|
|
14
|
+
// POSTs it to the server, which writes it back into the ref).
|
|
15
|
+
// `codex resume <thread-id>` continues the SAME thread (verified
|
|
16
|
+
// 0.144.1 — see smoke-codex.js), so refs survive death/resume.
|
|
17
|
+
//
|
|
18
|
+
// Session/window/pane plumbing is shared with the other tmux adapters — see
|
|
19
|
+
// tmux-session.js. This module owns only what is codex-specific: launch line,
|
|
20
|
+
// screen signatures, the notify relay wiring, and resume.
|
|
21
|
+
//
|
|
22
|
+
// Verified launch template (codex 0.144.1):
|
|
23
|
+
// codex --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust \
|
|
24
|
+
// -c notify='["node","<relay>","<stateDir>","<key>","<url>"]'
|
|
25
|
+
// - --dangerously-bypass-approvals-and-sandbox is codex's analog of claude's
|
|
26
|
+
// --dangerously-skip-permissions (YOLO mode: no sandbox, no approval
|
|
27
|
+
// prompts — port rule #4, full autonomy at launch).
|
|
28
|
+
// - --dangerously-bypass-hook-trust suppresses the "Hooks need review"
|
|
29
|
+
// picker that a global ~/.codex/hooks.json otherwise raises at launch —
|
|
30
|
+
// without it spawn hangs on that screen.
|
|
31
|
+
// - -c notify=[...] wires the turn-end relay (codex-notify.js): codex runs
|
|
32
|
+
// it at every turn boundary with the payload JSON appended as the LAST
|
|
33
|
+
// argv. One mechanism gives BOTH turn-end detection AND the thread-id for
|
|
34
|
+
// resume — nothing is written into the worktree (port rule #5 for free).
|
|
35
|
+
// - the prompt is NEVER passed on the command line — codex launches bare,
|
|
36
|
+
// and once launch-settle confirms the composer is up, the prompt is typed
|
|
37
|
+
// into it via the same verified-submit machinery send() uses (t.submit).
|
|
38
|
+
// A prompt riding in argv would sit in that process's command line for
|
|
39
|
+
// the life of the session — visible to `ps`/`pgrep -f`, and a broad
|
|
40
|
+
// pattern-kill run BY that very agent (matching its own argv) could
|
|
41
|
+
// freeze or kill itself. The prompt file in stateDir stays the source of
|
|
42
|
+
// truth; only the delivery mechanism changed.
|
|
43
|
+
// - a fresh cwd shows codex's directory-trust prompt even with the bypass
|
|
44
|
+
// flags ("Do you trust the contents of this directory?", "Yes, continue"
|
|
45
|
+
// preselected — Enter accepts); launch-settle auto-accepts it, exactly
|
|
46
|
+
// like claude's folder trust.
|
|
47
|
+
//
|
|
48
|
+
// Turn boundaries: the notify relay appends the SAME event shape the claude
|
|
49
|
+
// Stop hook emits to <stateDir>/<key>.turnend.jsonl, so onTurnEnd() is the
|
|
50
|
+
// shared tail and the server's /api/turn-end needs nothing codex-specific.
|
|
51
|
+
|
|
52
|
+
const fs = require('node:fs');
|
|
53
|
+
const path = require('node:path');
|
|
54
|
+
const t = require('./tmux.js');
|
|
55
|
+
const s = require('./tmux-session.js');
|
|
56
|
+
const { codexStatus, SLASH_COMMANDS, helpText, formatStatus } = require('./agent-status.js');
|
|
57
|
+
|
|
58
|
+
const NOTIFY_SCRIPT = path.join(__dirname, 'codex-notify.js');
|
|
59
|
+
const TRUST_RE = /Do you trust the contents of this directory|Yes, continue/;
|
|
60
|
+
|
|
61
|
+
// UI_READY_RE matches signatures only the codex main UI renders: the intro
|
|
62
|
+
// box (">_ OpenAI Codex (vX.Y.Z)"), the YOLO-mode permissions line, or the
|
|
63
|
+
// composer prompt glyph '›' at a line start. The directory-trust screen shows
|
|
64
|
+
// none of these as a line of its own — and trustRe is checked first anyway.
|
|
65
|
+
const UI_READY_RE = /OpenAI Codex \(v|YOLO mode|\n›/;
|
|
66
|
+
const SETTLE = { trustRe: TRUST_RE, readyRe: UI_READY_RE, label: 'codex' };
|
|
67
|
+
|
|
68
|
+
// golem: permissionFlags(profile) — 'bypass' (default) is the builder's YOLO launch; 'readonly' is
|
|
69
|
+
// the chat window's: read-only sandbox and never a prompt (a prompt in a headless pane hangs the
|
|
70
|
+
// chat forever; `-a never` returns the refusal to the model instead).
|
|
71
|
+
const PERMISSION_FLAGS = {
|
|
72
|
+
bypass: '--dangerously-bypass-approvals-and-sandbox',
|
|
73
|
+
readonly: '--sandbox read-only --ask-for-approval never',
|
|
74
|
+
};
|
|
75
|
+
function permissionFlags(profile) {
|
|
76
|
+
const flags = PERMISSION_FLAGS[profile || 'bypass'];
|
|
77
|
+
if (!flags) throw new Error(`unknown permission profile: ${profile}`);
|
|
78
|
+
return flags;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The permission + notify flags every codex launch (spawn AND resume) carries.
|
|
82
|
+
function launchFlags(stateDir, key, callbackUrl, permissions) {
|
|
83
|
+
const notify = ['node', NOTIFY_SCRIPT, stateDir, key].concat(callbackUrl ? [callbackUrl] : []);
|
|
84
|
+
return `${permissionFlags(permissions)} --dangerously-bypass-hook-trust `
|
|
85
|
+
+ `-c ${s.shellQuote('notify=' + JSON.stringify(notify))}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// spawn(cwd, prompt, opts?) -> HarnessRef
|
|
89
|
+
// opts: { session?, window?, stateDir?, callbackUrl?, extraArgs?: string[], installHooks?: boolean }
|
|
90
|
+
// Same opts contract as claude-tmux.js. installHooks is accepted and ignored:
|
|
91
|
+
// codex has no settings-file hook — the notify relay rides the launch line, so
|
|
92
|
+
// there is nothing to install (or clobber) in any cwd.
|
|
93
|
+
// The returned ref carries NO resumeId: codex assigns the thread-id itself and
|
|
94
|
+
// the first notify delivers it (the server adopts it from that turn-end).
|
|
95
|
+
async function spawn(cwd, prompt, opts = {}) {
|
|
96
|
+
const cwdAbs = path.resolve(cwd);
|
|
97
|
+
if (!fs.existsSync(cwdAbs)) throw new Error(`spawn cwd does not exist: ${cwdAbs}`);
|
|
98
|
+
const { session, window } = await s.claimPaneNames(opts);
|
|
99
|
+
const stateDir = s.stateDirOf(opts);
|
|
100
|
+
const key = s.stateKey(session, window);
|
|
101
|
+
|
|
102
|
+
const promptFile = path.join(stateDir, `${key}.prompt`);
|
|
103
|
+
fs.writeFileSync(promptFile, prompt);
|
|
104
|
+
// Recorded so resume() can replay them — a worker pinned to a model by its
|
|
105
|
+
// playbook must not come back on the default one (tmux-session.js).
|
|
106
|
+
s.recordSpawnArgs(stateDir, key, opts);
|
|
107
|
+
|
|
108
|
+
await s.createPane(session, window, cwdAbs);
|
|
109
|
+
try {
|
|
110
|
+
const extra = (opts.extraArgs || []).map(s.shellQuote).join(' ');
|
|
111
|
+
const launchCmd = s.envPrefix(opts) /* golem */ + 'codex '
|
|
112
|
+
+ launchFlags(stateDir, key, opts.callbackUrl || process.env.BC_TURNEND_URL || '', opts.permissions)
|
|
113
|
+
+ (extra ? ' ' + extra : '');
|
|
114
|
+
await s.launchAndSettle(s.paneTarget(session, window), launchCmd, SETTLE);
|
|
115
|
+
await deliverPrompt(s.paneTarget(session, window), prompt);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
await s.killPane(session, window);
|
|
118
|
+
try { fs.unlinkSync(promptFile); } catch { /* best-effort */ }
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ref = { harness: 'codex', session, cwd: cwdAbs };
|
|
123
|
+
if (window) ref.window = window;
|
|
124
|
+
return ref;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// deliverPrompt(target, prompt) — type the brief into the just-settled
|
|
128
|
+
// composer with verified submission (t.submit — same mechanism send() uses:
|
|
129
|
+
// type once, retry only Enter, never retype). Runs once, right after
|
|
130
|
+
// launchAndSettle confirms the main UI is up, so the brief never rides in
|
|
131
|
+
// argv (see the file-header note on why that matters).
|
|
132
|
+
async function deliverPrompt(target, prompt) {
|
|
133
|
+
const verdict = await t.submit(target, prompt, {
|
|
134
|
+
retries: Number(process.env.BC_SEND_RETRIES || 3),
|
|
135
|
+
enterSleep: Number(process.env.BC_SEND_SLEEP_MS || 400),
|
|
136
|
+
});
|
|
137
|
+
if (verdict === 'pending' || verdict === 'send-failed') {
|
|
138
|
+
// The pane rides on the failure (same reason as claude-tmux): a launch that
|
|
139
|
+
// settles and then will not take the brief is diagnosed from the screen
|
|
140
|
+
// underneath, and only the harness can see it.
|
|
141
|
+
let tail = '';
|
|
142
|
+
try { tail = (await t.capture(target, 20)) || ''; } catch (e) { tail = ''; }
|
|
143
|
+
throw new Error((verdict === 'pending'
|
|
144
|
+
? 'brief not submitted at spawn (Enter swallowed; text left in composer)'
|
|
145
|
+
: 'brief not sent at spawn (tmux send failed)') + '; pane tail:\n' + tail);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// send(ref, text) — type into the session with verified submission.
|
|
150
|
+
// Enter is retried, never the text. Throws when the submit provably failed.
|
|
151
|
+
// (The '›' composer glyph is in tmux.js PROMPT_GLYPHS so a cleared codex
|
|
152
|
+
// composer reads 'empty' — the positive ack that the submit landed.)
|
|
153
|
+
async function send(ref, text) {
|
|
154
|
+
const name = s.stateKey(ref.session, ref.window);
|
|
155
|
+
if (!(await alive(ref))) throw new Error(`session ${name} is not alive`);
|
|
156
|
+
const verdict = await t.submit(s.paneTarget(ref.session, ref.window), text, {
|
|
157
|
+
retries: Number(process.env.BC_SEND_RETRIES || 3),
|
|
158
|
+
enterSleep: Number(process.env.BC_SEND_SLEEP_MS || 400),
|
|
159
|
+
});
|
|
160
|
+
if (verdict === 'pending') {
|
|
161
|
+
throw new Error(`text not submitted to ${name} (Enter swallowed; text left in composer)`);
|
|
162
|
+
}
|
|
163
|
+
if (verdict === 'send-failed') {
|
|
164
|
+
throw new Error(`text not sent to ${name} (tmux send failed)`);
|
|
165
|
+
}
|
|
166
|
+
// 'empty' = confirmed; 'unknown' = pane unreadable, assume sent (lenient —
|
|
167
|
+
// an unreadable pane must not turn a normal send into a false error).
|
|
168
|
+
await t.sleep(1000); // let the turn spin up so an immediate capture sees it working
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// alive(ref) — the ref's session (and window, for window-granular refs) exists
|
|
172
|
+
// AND its pane is still running the agent — same rule as claude: a pane
|
|
173
|
+
// sitting back at a bare shell means codex exited (pane_current_command reads
|
|
174
|
+
// 'codex' while it runs, verified 0.144.1).
|
|
175
|
+
async function alive(ref) {
|
|
176
|
+
// STRICT: this answer is what the board drops worker records on, so a tmux it
|
|
177
|
+
// could not read must throw rather than pass for "the pane is gone".
|
|
178
|
+
if (!(await s.paneExists(ref.session, ref.window, { strict: true }))) return false;
|
|
179
|
+
const cmd = await s.paneCommand(s.paneTarget(ref.session, ref.window), { strict: true });
|
|
180
|
+
return cmd !== null && !s.SHELLS.has(cmd);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// resumable(ref, opts?) -> bool — would resume(ref) restore memory? True when
|
|
184
|
+
// a thread-id is recoverable: ref.resumeId, or the relay-recorded session-id
|
|
185
|
+
// file in the state dir. Introspection only — the server uses it to pick
|
|
186
|
+
// resume vs relaunch-with-charter.
|
|
187
|
+
async function resumable(ref, opts = {}) {
|
|
188
|
+
if (ref.resumeId) return true;
|
|
189
|
+
try {
|
|
190
|
+
return !!fs.readFileSync(path.join(s.stateDirOf(opts), `${s.stateKey(ref.session, ref.window)}.session-id`), 'utf8').trim();
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// resume(ref) -> HarnessRef — reincarnate a dead session with memory when possible.
|
|
197
|
+
// Prefers the relay-recorded thread-id (ground truth, refreshed every turn)
|
|
198
|
+
// over ref.resumeId, kills any leftover dead pane, relaunches
|
|
199
|
+
// `codex resume <thread-id>` with the same bypass + notify flags in a fresh
|
|
200
|
+
// pane under the same name. Resuming continues the SAME thread-id (verified),
|
|
201
|
+
// so the ref stays valid across death/resume cycles — and were codex ever to
|
|
202
|
+
// fork, the next notify would deliver the new id and the server would adopt
|
|
203
|
+
// it. Without any id: fresh `codex` launch (memory lost).
|
|
204
|
+
async function resume(ref, opts = {}) {
|
|
205
|
+
if (await alive(ref)) return { ...ref };
|
|
206
|
+
const stateDir = s.stateDirOf(opts);
|
|
207
|
+
const key = s.stateKey(ref.session, ref.window);
|
|
208
|
+
let resumeId = ref.resumeId;
|
|
209
|
+
// golem: every conversation of an app shares one tmux name, so the recorded id is whichever
|
|
210
|
+
// agent ran there last; a ref that knows its own id wins over the file.
|
|
211
|
+
if (!resumeId) {
|
|
212
|
+
try {
|
|
213
|
+
const rec = fs.readFileSync(path.join(stateDir, `${key}.session-id`), 'utf8').trim();
|
|
214
|
+
if (rec) resumeId = rec;
|
|
215
|
+
} catch {
|
|
216
|
+
// no recorded id — fall back to the ref's
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
await s.killPane(ref.session, ref.window); // clear any dead pane still holding the name
|
|
220
|
+
|
|
221
|
+
await s.createPane(ref.session, ref.window, ref.cwd);
|
|
222
|
+
try {
|
|
223
|
+
// The spawn's extra flags are replayed, not rebuilt: --model and friends
|
|
224
|
+
// came from the card's playbook and a resume that drops them is a worker
|
|
225
|
+
// quietly moved to another model. opts.extraArgs, when given, wins.
|
|
226
|
+
const rec = s.recordedSpawnArgs(stateDir, key);
|
|
227
|
+
const extra = (opts.extraArgs || rec.args).map(s.shellQuote).join(' ');
|
|
228
|
+
const launchCmd = s.envPrefix(opts) /* golem */ + (resumeId ? `codex resume ${resumeId} ` : 'codex ')
|
|
229
|
+
+ launchFlags(stateDir, key, opts.callbackUrl || process.env.BC_TURNEND_URL || '', opts.permissions || rec.permissions)
|
|
230
|
+
+ (extra ? ' ' + extra : '');
|
|
231
|
+
await s.launchAndSettle(s.paneTarget(ref.session, ref.window), launchCmd, SETTLE);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
await s.killPane(ref.session, ref.window);
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
const out = { harness: 'codex', session: ref.session, cwd: ref.cwd };
|
|
237
|
+
if (resumeId) out.resumeId = resumeId;
|
|
238
|
+
if (ref.window) out.window = ref.window;
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// kill(ref) — end the agent's pane for good. Idempotent: killing a dead or
|
|
243
|
+
// missing one is a no-op. Session-granular refs take the whole session;
|
|
244
|
+
// window-granular refs take ONLY their window. Harness state files are left
|
|
245
|
+
// behind on purpose — the thread-id and turn-end log are cheap, and a later
|
|
246
|
+
// resume(ref) can still reincarnate the conversation if the kill turns out
|
|
247
|
+
// premature.
|
|
248
|
+
async function kill(ref) {
|
|
249
|
+
await s.killPane(ref.session, ref.window);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---------- slash commands + status (OPTIONAL capability verbs — port.js) ----------
|
|
253
|
+
// status(ref, opts?) reads the rollout log codex already writes
|
|
254
|
+
// (~/.codex/sessions/YYYY/MM/DD/rollout-*-<threadId>.jsonl — agent-status.js).
|
|
255
|
+
// The thread-id comes from the relay-recorded session-id file first and
|
|
256
|
+
// ref.resumeId second — the same order resume() uses, and the reason status
|
|
257
|
+
// works for a ref that never adopted a resumeId. With no rollout on disk
|
|
258
|
+
// status is null, never a throw.
|
|
259
|
+
function commands() {
|
|
260
|
+
return SLASH_COMMANDS.map((c) => ({ ...c }));
|
|
261
|
+
}
|
|
262
|
+
async function status(ref, opts = {}) {
|
|
263
|
+
return codexStatus(ref, { ...opts, stateDir: s.stateDirOf(opts) });
|
|
264
|
+
}
|
|
265
|
+
// No /autocompact here: codex has no such slash command (its binary only
|
|
266
|
+
// carries the model_auto_compact_token_limit CONFIG scope — verified 0.144.x).
|
|
267
|
+
async function runCommand(ref, command, opts = {}) {
|
|
268
|
+
const line = String(command || '').trim();
|
|
269
|
+
const name = line.split(/\s+/)[0];
|
|
270
|
+
const key = s.stateKey(ref.session, ref.window);
|
|
271
|
+
if (name === '/help') return helpText(commands());
|
|
272
|
+
if (name === '/status') {
|
|
273
|
+
const st = await status(ref, opts);
|
|
274
|
+
if (!st) throw new Error('no status for ' + key + ' — rollout log not found (thread-id not adopted yet?)');
|
|
275
|
+
return formatStatus(st);
|
|
276
|
+
}
|
|
277
|
+
if (name === '/compact') {
|
|
278
|
+
await send(ref, line); // verified submit; codex's own /compact runs in-session
|
|
279
|
+
return '"' + line + '" submitted to ' + key + ' — the session runs it in-place';
|
|
280
|
+
}
|
|
281
|
+
throw new Error('unknown command ' + name + ' (see /help)');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// onTurnEnd / openPane / paneSnapshot / paneInput / adoptWindow — the shared
|
|
285
|
+
// implementations verbatim (tmux-session.js): codex-notify.js writes the same
|
|
286
|
+
// turnend.jsonl shape the claude Stop hook does, pane viewing is pure
|
|
287
|
+
// capture-pane, pane input is pure send-keys, and adoption is pure
|
|
288
|
+
// rename-window.
|
|
289
|
+
const { onTurnEnd, openPane, paneSnapshot, paneInput, adoptWindow } = s;
|
|
290
|
+
|
|
291
|
+
module.exports = { spawn, send, alive, resumable, resume, kill, onTurnEnd, permissionFlags,
|
|
292
|
+
openPane, paneSnapshot, paneInput, commands, runCommand, status, adoptWindow };
|