@rynx-ai/runtime 0.1.11-beta.5 → 0.1.11-beta.6

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.
@@ -1,5 +1,14 @@
1
1
  import { TerminalRegistry } from "../terminal/registry.js";
2
2
  import { toWireError } from "./protocol.js";
3
+ const TRAEX_STARTUP_WATCH_MS = 20_000;
4
+ const TRAEX_STARTUP_POLL_MS = 100;
5
+ const TRAEX_PROMPT_RETRY_MS = 500;
6
+ function normalizeTraexPane(pane) {
7
+ return pane.toLowerCase().replace(/\s+/g, " ").trim();
8
+ }
9
+ function isTerminalProtocolResponse(input) {
10
+ return /^(?:\x1b\[(?:(?:\?|>)[0-9;]*c|[0-9;]*(?:n|R|t)|[IO]))+$/.test(input);
11
+ }
3
12
  export class RunnerSession {
4
13
  transport;
5
14
  executor;
@@ -48,8 +57,17 @@ export class RunnerSession {
48
57
  });
49
58
  return;
50
59
  case "term.input":
51
- this.cancelTraexStartupWatcher(this.attachmentThreadIds.get(msg.attachId));
52
- this.attachments.get(msg.attachId)?.write(Buffer.from(msg.dataB64, "base64").toString("utf8"));
60
+ {
61
+ const localThreadId = this.attachmentThreadIds.get(msg.attachId);
62
+ const input = Buffer.from(msg.dataB64, "base64").toString("utf8");
63
+ // xterm sends device/focus reports through the same onData channel as
64
+ // keystrokes. They must reach the TUI without pretending the user has
65
+ // taken over startup prompt handling.
66
+ if (!isTerminalProtocolResponse(input)) {
67
+ this.cancelTraexStartupWatcher(localThreadId);
68
+ }
69
+ this.attachments.get(msg.attachId)?.write(input);
70
+ }
53
71
  return;
54
72
  case "term.resize":
55
73
  this.attachments.get(msg.attachId)?.resize(msg.cols, msg.rows);
@@ -215,8 +233,8 @@ export class RunnerSession {
215
233
  try {
216
234
  const input = msg.input ?? msg.text;
217
235
  const outcome = (await provider.injectMessage?.(msg.localThreadId, input)) ?? "notLive";
218
- if (outcome === "injected")
219
- this.cancelTraexStartupWatcher(msg.localThreadId);
236
+ // App-server injection is independent of the Terminal TUI startup, so it
237
+ // must not cancel prompt handling for the pane that is still starting.
220
238
  this.transport.send({ t: "injected", reqId: msg.reqId, localThreadId: msg.localThreadId, outcome });
221
239
  }
222
240
  catch (error) {
@@ -255,7 +273,6 @@ export class RunnerSession {
255
273
  if (!spec)
256
274
  return;
257
275
  const terminalId = `${localThreadId}-main`;
258
- const coldStart = !this.terminals.has(terminalId);
259
276
  const term = this.terminals.getOrCreate(terminalId, {
260
277
  cwd: spec.cwd,
261
278
  command: spec.command,
@@ -264,7 +281,7 @@ export class RunnerSession {
264
281
  rows: rows ?? 40,
265
282
  ...(spec.env ? { env: spec.env } : {}),
266
283
  });
267
- if (coldStart && spec.skipTraexStartupPrompts) {
284
+ if (spec.skipTraexStartupPrompts) {
268
285
  const watcher = Symbol(localThreadId);
269
286
  this.traexStartupWatchers.set(localThreadId, watcher);
270
287
  void this.skipTraexStartupPrompts(localThreadId, term, watcher);
@@ -279,46 +296,48 @@ export class RunnerSession {
279
296
  const prompts = [
280
297
  {
281
298
  id: "welcome",
282
- matches: (pane) => pane.includes("Welcome to TRAE CLI") && pane.includes("Press enter to continue"),
299
+ matches: (pane) => pane.includes("welcome to trae cli") && pane.includes("press enter to continue"),
283
300
  dismiss: () => terminal.sendEnter(),
284
301
  },
285
302
  {
286
303
  id: "migration",
287
- matches: (pane) => pane.includes("Legacy TRAE CLI data detected") &&
288
- pane.includes("Would you like to migrate detected data to the new TRAE CLI storage?") &&
289
- pane.includes("Select what to import:"),
304
+ matches: (pane) => pane.includes("legacy trae cli data detected") &&
305
+ pane.includes("select what to import") &&
306
+ (pane.includes("skip for now") || pane.includes("don't ask again")),
290
307
  dismiss: () => terminal.interrupt(),
291
308
  },
292
309
  {
293
310
  id: "hooks",
294
- matches: (pane) => pane.includes("Hooks need review") &&
295
- pane.includes("hooks are new or changed.") &&
296
- pane.includes("Trust all and continue") &&
297
- pane.includes("Continue without trusting"),
311
+ matches: (pane) => pane.includes("hooks need review") &&
312
+ pane.includes("trust all and continue") &&
313
+ pane.includes("continue without trusting"),
298
314
  dismiss: () => terminal.interrupt(),
299
315
  },
300
316
  ];
301
- const dismissed = new Set();
302
- const deadline = Date.now() + 20_000;
317
+ let activePromptId;
318
+ let lastDismissedAt = 0;
319
+ const deadline = Date.now() + TRAEX_STARTUP_WATCH_MS;
303
320
  const terminalId = `${localThreadId}-main`;
304
321
  while (!this.shuttingDown &&
305
322
  Date.now() < deadline &&
306
323
  this.traexStartupWatchers.get(localThreadId) === watcher &&
307
324
  this.terminals.get(terminalId) === terminal) {
308
- const pane = terminal.capturePane();
309
- // Once the normal composer is visible, startup is complete and there is
310
- // no reason to keep polling the tmux pane for the remainder of the window.
311
- if (pane.includes("TRAE CLI Next"))
312
- break;
325
+ // Do not treat a composer frame as completion: Traex can render it before
326
+ // the startup modals arrive. The bounded deadline stops this watcher.
327
+ const pane = normalizeTraexPane(terminal.capturePane());
313
328
  const prompt = prompts.find((candidate) => candidate.matches(pane));
314
- if (prompt && !dismissed.has(prompt.id)) {
329
+ const now = Date.now();
330
+ if (!prompt) {
331
+ activePromptId = undefined;
332
+ }
333
+ else if (prompt.id !== activePromptId ||
334
+ now - lastDismissedAt >= TRAEX_PROMPT_RETRY_MS) {
315
335
  prompt.dismiss();
316
- dismissed.add(prompt.id);
317
- if (dismissed.size === prompts.length)
318
- break;
336
+ activePromptId = prompt.id;
337
+ lastDismissedAt = now;
319
338
  }
320
339
  await new Promise((resolve) => {
321
- const timer = setTimeout(resolve, 100);
340
+ const timer = setTimeout(resolve, TRAEX_STARTUP_POLL_MS);
322
341
  timer.unref();
323
342
  });
324
343
  }
@@ -146,7 +146,9 @@ export declare class TmuxTerminal {
146
146
  paste(text: string, bufferName?: string): void;
147
147
  /**
148
148
  * Attach a client. `role: "read-only"` passes tmux `-r` so the viewer cannot
149
- * type (defense-in-depth on top of the WS bridge dropping input frames).
149
+ * type and `ignore-size` so even its initial PTY dimensions cannot resize the
150
+ * owner's pane (defense-in-depth on top of the WS bridge dropping input and
151
+ * resize frames).
150
152
  */
151
153
  attach(role: "owner" | "read-only", dims?: {
152
154
  cols?: number;
@@ -319,14 +319,17 @@ export class TmuxTerminal {
319
319
  }
320
320
  /**
321
321
  * Attach a client. `role: "read-only"` passes tmux `-r` so the viewer cannot
322
- * type (defense-in-depth on top of the WS bridge dropping input frames).
322
+ * type and `ignore-size` so even its initial PTY dimensions cannot resize the
323
+ * owner's pane (defense-in-depth on top of the WS bridge dropping input and
324
+ * resize frames).
323
325
  */
324
326
  async attach(role, dims) {
325
327
  this.start();
326
328
  const spawn = this.injectedSpawn ?? (await loadPtySpawn());
327
- const args = [...this.base(), "attach-session", "-t", TMUX_TARGET];
329
+ const args = [...this.base(), "attach-session"];
328
330
  if (role === "read-only")
329
- args.push("-r");
331
+ args.push("-r", "-f", "ignore-size");
332
+ args.push("-t", TMUX_TARGET);
330
333
  const proc = spawn(this.tmuxBin, args, {
331
334
  name: "xterm-256color",
332
335
  cols: dims?.cols ?? this.cols,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/runtime",
3
- "version": "0.1.11-beta.5",
3
+ "version": "0.1.11-beta.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "node-pty": "^1.0.0",
28
28
  "ws": "^8.21.0",
29
- "@rynx-ai/core": "0.1.11-beta.5"
29
+ "@rynx-ai/core": "0.1.11-beta.6"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/ws": "^8.18.1"