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 +2 -2
- package/package.json +1 -1
- package/src/browser/chatgpt-web-adapter.js +20 -10
- package/src/cli/main.js +2 -2
- package/src/cli/prompt-input.js +128 -4
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ wtagent "检查这个项目并修复测试"
|
|
|
63
63
|
wtagent -C ./my-project "创建一个网站"
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
之后直接在终端中继续对话。WTAgent 会把任务交给 GPT Web
|
|
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
|
@@ -813,23 +813,33 @@ export class ChatGPTWebAdapter {
|
|
|
813
813
|
}
|
|
814
814
|
|
|
815
815
|
async #assistantText(message) {
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
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
|
|
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
|
-
|
|
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.
|
|
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")
|
package/src/cli/prompt-input.js
CHANGED
|
@@ -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:
|
|
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 =
|
|
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
|
|
96
|
-
|
|
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
|
|