wtagent 0.1.0-alpha.4 → 0.1.0-alpha.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.
package/README.md CHANGED
@@ -63,7 +63,7 @@ wtagent "检查这个项目并修复测试"
63
63
  wtagent -C ./my-project "创建一个网站"
64
64
  ```
65
65
 
66
- 之后直接在终端中继续对话。WTAgent 会把任务交给 GPT Web,并在本地执行文件操作和结构化本地命令。使用 `↑` / `↓` 浏览本次 CLI 会话的历史输入。
66
+ 之后直接在终端中继续对话。WTAgent 会把任务交给 GPT Web,并在本地执行文件操作和结构化本地命令。可以一次粘贴多行内容,粘贴后按 Enter 作为一条消息发送;使用 `↑` / `↓` 浏览本次 CLI 会话的历史输入。
67
67
 
68
68
  按 `Ctrl+C` 或 `Ctrl+D` 可退出并关闭专用 Chrome。若上一次异常退出留下了 Chrome,WTAgent 会在验证其 CDP 身份后复用并接管它。
69
69
 
@@ -130,7 +130,7 @@ wtagent "inspect this project and fix the tests"
130
130
  wtagent -C ./my-project "build a website"
131
131
  ```
132
132
 
133
- Continue chatting in the terminal. WTAgent sends tasks to GPT Web and executes file operations and structured local commands. Use `↑` / `↓` to browse input history from the current CLI session.
133
+ Continue chatting in the terminal. WTAgent sends tasks to GPT Web and executes file operations and structured local commands. You can paste multiple lines and then press Enter to send them as one message. Use `↑` / `↓` to browse input history from the current CLI session.
134
134
 
135
135
  Press `Ctrl+C` or `Ctrl+D` to exit and close the dedicated Chrome. If an abnormal exit leaves Chrome running, WTAgent verifies and adopts that CDP instance on the next start.
136
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtagent",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.6",
4
4
  "description": "Turn your own web AI session into a local tool-using agent.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -813,23 +813,33 @@ export class ChatGPTWebAdapter {
813
813
  }
814
814
 
815
815
  async #assistantText(message) {
816
- // ChatGPT may render the model's XML inside one of several code blocks,
817
- // alongside rich cards (weather, charts, etc.) or other markdown. Search
818
- // every code block for the envelope first; if none contains it, fall back
819
- // to the full message text so the protocol layer can extract the envelope
820
- // from wherever it landed.
816
+ // Read the whole assistant turn first. A long XML response can contain
817
+ // Markdown fences inside CDATA; ChatGPT then splits the rendered response
818
+ // into many <pre><code> nodes and the first node contains the opening tag
819
+ // but not the closing tag. The parent innerText keeps the complete envelope
820
+ // and avoids one CDP round trip per nested code block on every poll.
821
+ const fullText = await message.innerText().catch(() => "");
822
+ if (fullText.includes("<agent_response")) {
823
+ return fullText;
824
+ }
825
+
826
+ // Rare fallback for alternate renderers where the parent text omits code
827
+ // contents: prefer a complete code-block envelope, but retain a partial
828
+ // one so streaming progress remains visible until its closing tag arrives.
821
829
  const codeBlocks = message.locator("pre code");
822
830
  const codeBlockCount = await codeBlocks.count();
831
+ let partialEnvelope = "";
823
832
  for (let index = 0; index < codeBlockCount; index += 1) {
824
833
  const code = await codeBlocks.nth(index).innerText().catch(() => "");
825
- if (code.includes("<agent_response")) {
834
+ if (hasCompleteAgentEnvelope(code)) {
826
835
  return code;
827
836
  }
837
+ if (!partialEnvelope && code.includes("<agent_response")) {
838
+ partialEnvelope = code;
839
+ }
828
840
  }
829
-
830
- const fullText = await message.innerText().catch(() => "");
831
- if (fullText.includes("<agent_response")) {
832
- return fullText;
841
+ if (partialEnvelope) {
842
+ return partialEnvelope;
833
843
  }
834
844
 
835
845
  const markdown = message.locator(".markdown");
package/src/cli/main.js CHANGED
@@ -486,7 +486,7 @@ function printChatBanner(projectRoot) {
486
486
  const RESET = "\x1b[0m";
487
487
  console.log("");
488
488
  console.log(`${CYAN}WTAgent${RESET} ${DIM}· GPT Web · ${projectRoot}${RESET}`);
489
- console.log(`${DIM}Enter sends · ↑/↓ history · "exit", Ctrl+C, or Ctrl+D quits${RESET}`);
489
+ console.log(`${DIM}Enter sends · multiline paste · ↑/↓ history · "exit", Ctrl+C, or Ctrl+D quits${RESET}`);
490
490
  console.log("");
491
491
  }
492
492
 
@@ -611,7 +611,7 @@ async function runExport(sessionId, options) {
611
611
  const program = new Command()
612
612
  .name("wtagent")
613
613
  .description("Turn your web AI session into a local tool-using agent.")
614
- .version("0.1.0-alpha.4")
614
+ .version("0.1.0-alpha.6")
615
615
  .option("--home <path>", "Application data directory")
616
616
  .option("--profile-dir <path>", "Dedicated Chrome profile directory")
617
617
  .option("--chrome-path <path>", "Chrome/Chromium executable")
@@ -1,5 +1,16 @@
1
1
  import { input, select } from "@inquirer/prompts";
2
2
  import { createInterface } from "node:readline/promises";
3
+ import { Transform } from "node:stream";
4
+ import { StringDecoder } from "node:string_decoder";
5
+
6
+ const BRACKETED_PASTE_START = "\u001b[200~";
7
+ const BRACKETED_PASTE_END = "\u001b[201~";
8
+ const ENABLE_BRACKETED_PASTE = "\u001b[?2004h";
9
+ const DISABLE_BRACKETED_PASTE = "\u001b[?2004l";
10
+ // readline treats CR/LF as submit. U+2028 is a non-submitting, semantic line
11
+ // separator that can live in readline's editable buffer and history; decode it
12
+ // back to LF before the value leaves this module.
13
+ const PASTED_LINE_SEPARATOR = "\u2028";
3
14
 
4
15
  const CLEAN_EXIT_ERRORS = new Set([
5
16
  "AbortPromptError",
@@ -13,6 +24,110 @@ const EXIT_COMMANDS = new Set([
13
24
  "/quit",
14
25
  ]);
15
26
 
27
+ function longestMarkerPrefixAtEnd(text, marker) {
28
+ const limit = Math.min(text.length, marker.length - 1);
29
+ for (let length = limit; length > 0; length -= 1) {
30
+ if (text.endsWith(marker.slice(0, length))) {
31
+ return length;
32
+ }
33
+ }
34
+ return 0;
35
+ }
36
+
37
+ function encodePastedLines(text) {
38
+ return text.replace(/\r\n|\r|\n/g, PASTED_LINE_SEPARATOR);
39
+ }
40
+
41
+ function decodePastedLines(text) {
42
+ return String(text ?? "").replaceAll(PASTED_LINE_SEPARATOR, "\n");
43
+ }
44
+
45
+ function looksLikeUnbracketedMultilinePaste(text) {
46
+ const normalized = text.replace(/\r\n|\r/g, "\n");
47
+ const firstNewline = normalized.indexOf("\n");
48
+ if (firstNewline < 0) {
49
+ return false;
50
+ }
51
+ // A newline followed by more text, or two or more newlines in one raw data
52
+ // event, cannot be a single Enter key. Treat it as an unmarked paste. A
53
+ // normal test/pipe write such as "message\n" remains a submit.
54
+ return firstNewline < normalized.length - 1
55
+ || normalized.indexOf("\n", firstNewline + 1) >= 0;
56
+ }
57
+
58
+ class PasteAwareInput extends Transform {
59
+ constructor(source) {
60
+ super();
61
+ this.decoder = new StringDecoder("utf8");
62
+ this.pending = "";
63
+ this.inBracketedPaste = false;
64
+ this.isTTY = source.isTTY;
65
+ this.columns = source.columns;
66
+ this.setRawMode = typeof source.setRawMode === "function"
67
+ ? (enabled) => source.setRawMode(enabled)
68
+ : undefined;
69
+ }
70
+
71
+ _transform(chunk, _encoding, callback) {
72
+ try {
73
+ this.#consume(this.decoder.write(chunk), false);
74
+ callback();
75
+ } catch (error) {
76
+ callback(error);
77
+ }
78
+ }
79
+
80
+ _flush(callback) {
81
+ try {
82
+ this.#consume(this.decoder.end(), true);
83
+ callback();
84
+ } catch (error) {
85
+ callback(error);
86
+ }
87
+ }
88
+
89
+ #consume(text, flush) {
90
+ this.pending += text;
91
+ let output = "";
92
+
93
+ for (;;) {
94
+ const marker = this.inBracketedPaste
95
+ ? BRACKETED_PASTE_END
96
+ : BRACKETED_PASTE_START;
97
+ const markerIndex = this.pending.indexOf(marker);
98
+ if (markerIndex >= 0) {
99
+ const before = this.pending.slice(0, markerIndex);
100
+ output += this.inBracketedPaste
101
+ ? encodePastedLines(before)
102
+ : this.#outsidePaste(before);
103
+ this.pending = this.pending.slice(markerIndex + marker.length);
104
+ this.inBracketedPaste = !this.inBracketedPaste;
105
+ continue;
106
+ }
107
+
108
+ const retained = flush
109
+ ? 0
110
+ : longestMarkerPrefixAtEnd(this.pending, marker);
111
+ const ready = this.pending.slice(0, this.pending.length - retained);
112
+ output += this.inBracketedPaste
113
+ ? encodePastedLines(ready)
114
+ : this.#outsidePaste(ready);
115
+ this.pending = this.pending.slice(this.pending.length - retained);
116
+ break;
117
+ }
118
+
119
+ if (output) {
120
+ this.push(output);
121
+ }
122
+ }
123
+
124
+ #outsidePaste(text) {
125
+ return looksLikeUnbracketedMultilinePaste(text)
126
+ ? encodePastedLines(text)
127
+ : text;
128
+ }
129
+ }
130
+
16
131
  export function classifyChatInput(value) {
17
132
  const text = String(value ?? "").trim();
18
133
  if (!text) {
@@ -69,14 +184,18 @@ export class ShellChatInput {
69
184
  return null;
70
185
  }
71
186
 
187
+ const pasteInput = new PasteAwareInput(this.inputStream);
188
+ this.inputStream.pipe(pasteInput);
189
+ this.outputStream.write(ENABLE_BRACKETED_PASTE);
190
+
72
191
  const readline = createInterface({
73
- input: this.inputStream,
192
+ input: pasteInput,
74
193
  output: this.outputStream,
75
194
  terminal: true,
76
195
  historySize: this.historySize,
77
196
  removeHistoryDuplicates: true,
78
197
  });
79
- readline.history = [...this.history];
198
+ readline.history = this.history.map(encodePastedLines);
80
199
 
81
200
  const controller = new AbortController();
82
201
  let closed = false;
@@ -92,8 +211,10 @@ export class ShellChatInput {
92
211
  const answer = await readline.question(prompt, {
93
212
  signal: controller.signal,
94
213
  });
95
- this.history = readline.history.slice(0, this.historySize);
96
- return answer;
214
+ this.history = readline.history
215
+ .slice(0, this.historySize)
216
+ .map(decodePastedLines);
217
+ return decodePastedLines(answer);
97
218
  } catch (error) {
98
219
  if (closed || error?.name === "AbortError") {
99
220
  return null;
@@ -103,6 +224,9 @@ export class ShellChatInput {
103
224
  readline.removeListener("close", onClose);
104
225
  readline.removeListener("SIGINT", onInterrupt);
105
226
  readline.close();
227
+ this.outputStream.write(DISABLE_BRACKETED_PASTE);
228
+ this.inputStream.unpipe(pasteInput);
229
+ pasteInput.destroy();
106
230
  }
107
231
  }
108
232