@co0ontty/wand 0.2.1 → 0.3.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/README.md +25 -5
- package/dist/acp-protocol.d.ts +67 -0
- package/dist/acp-protocol.js +291 -0
- package/dist/claude-pty-bridge.d.ts +139 -0
- package/dist/claude-pty-bridge.js +649 -0
- package/dist/claude-stream-adapter.d.ts +35 -0
- package/dist/claude-stream-adapter.js +153 -0
- package/dist/claude-structured-runner.d.ts +27 -0
- package/dist/claude-structured-runner.js +106 -0
- package/dist/config.js +2 -2
- package/dist/message-parser.js +12 -66
- package/dist/message-queue.d.ts +57 -0
- package/dist/message-queue.js +127 -0
- package/dist/process-manager.d.ts +32 -25
- package/dist/process-manager.js +503 -780
- package/dist/server.js +366 -51
- package/dist/session-lifecycle.d.ts +81 -0
- package/dist/session-lifecycle.js +176 -0
- package/dist/storage.js +12 -1
- package/dist/types.d.ts +105 -5
- package/dist/web-ui/content/scripts.js +2307 -658
- package/dist/web-ui/content/styles.css +5284 -2771
- package/dist/web-ui/index.js +8 -5
- package/dist/web-ui/scripts.js +8 -1
- package/package.json +2 -9
- package/dist/web-ui/utils.d.ts +0 -4
- package/dist/web-ui/utils.js +0 -12
- package/dist/web-ui.d.ts +0 -1
- package/dist/web-ui.js +0 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
function normalizeToolInput(block) {
|
|
2
|
+
if (typeof block._partialJson === "string" && block._partialJson.length > 0) {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = JSON.parse(block._partialJson);
|
|
5
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
6
|
+
return parsed;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Keep original input if partial JSON is incomplete.
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (block.input && typeof block.input === "object" && !Array.isArray(block.input)) {
|
|
14
|
+
return block.input;
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
function appendAssistantBlock(target, block) {
|
|
19
|
+
if (!block.type) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (block.type === "text") {
|
|
23
|
+
target.push({ type: "text", text: typeof block.text === "string" ? block.text : "" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (block.type === "thinking") {
|
|
27
|
+
target.push({ type: "thinking", thinking: typeof block.thinking === "string" ? block.thinking : "" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (block.type === "tool_use") {
|
|
31
|
+
target.push({
|
|
32
|
+
type: "tool_use",
|
|
33
|
+
id: typeof block.id === "string" ? block.id : "",
|
|
34
|
+
name: typeof block.name === "string" ? block.name : "",
|
|
35
|
+
description: typeof block.description === "string" ? block.description : undefined,
|
|
36
|
+
input: normalizeToolInput({ input: block.input, _partialJson: block._partialJson }),
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (block.type === "tool_result") {
|
|
41
|
+
const content = typeof block.content === "string"
|
|
42
|
+
? block.content
|
|
43
|
+
: Array.isArray(block.content)
|
|
44
|
+
? block.content
|
|
45
|
+
: "";
|
|
46
|
+
target.push({
|
|
47
|
+
type: "tool_result",
|
|
48
|
+
tool_use_id: typeof block.tool_use_id === "string" ? block.tool_use_id : "",
|
|
49
|
+
content,
|
|
50
|
+
is_error: block.is_error === true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function buildUsage(event) {
|
|
55
|
+
if (!event.usage && event.total_cost_usd === undefined) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
inputTokens: typeof event.usage?.input_tokens === "number" ? event.usage.input_tokens : undefined,
|
|
60
|
+
outputTokens: typeof event.usage?.output_tokens === "number" ? event.usage.output_tokens : undefined,
|
|
61
|
+
cacheReadInputTokens: typeof event.usage?.cache_read_input_tokens === "number"
|
|
62
|
+
? event.usage.cache_read_input_tokens
|
|
63
|
+
: undefined,
|
|
64
|
+
cacheCreationInputTokens: typeof event.usage?.cache_creation_input_tokens === "number"
|
|
65
|
+
? event.usage.cache_creation_input_tokens
|
|
66
|
+
: undefined,
|
|
67
|
+
totalCostUsd: typeof event.total_cost_usd === "number" ? event.total_cost_usd : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function updateClaudeStreamState(state, event) {
|
|
71
|
+
if (typeof event.session_id === "string" && event.session_id.length > 0) {
|
|
72
|
+
state.sessionId = event.session_id;
|
|
73
|
+
}
|
|
74
|
+
switch (event.type) {
|
|
75
|
+
case "assistant": {
|
|
76
|
+
const content = Array.isArray(event.message?.content) ? event.message?.content : [];
|
|
77
|
+
for (const block of content) {
|
|
78
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
79
|
+
appendAssistantBlock(state.blocks, block);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "content_block_start": {
|
|
85
|
+
if (event.content_block && typeof event.content_block === "object") {
|
|
86
|
+
appendAssistantBlock(state.blocks, event.content_block);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "content_block_delta": {
|
|
91
|
+
const lastBlock = state.blocks[state.blocks.length - 1];
|
|
92
|
+
if (!lastBlock || !event.delta) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (lastBlock.type === "text" && event.delta.type === "text_delta" && typeof event.delta.text === "string") {
|
|
96
|
+
lastBlock.text += event.delta.text;
|
|
97
|
+
}
|
|
98
|
+
else if (lastBlock.type === "thinking" &&
|
|
99
|
+
event.delta.type === "thinking_delta" &&
|
|
100
|
+
typeof event.delta.thinking === "string") {
|
|
101
|
+
lastBlock.thinking += event.delta.thinking;
|
|
102
|
+
}
|
|
103
|
+
else if (lastBlock.type === "tool_use" &&
|
|
104
|
+
typeof event.delta.partial_json === "string") {
|
|
105
|
+
const nextPartial = (lastBlock._partialJson ?? "") + event.delta.partial_json;
|
|
106
|
+
lastBlock._partialJson = nextPartial;
|
|
107
|
+
lastBlock.input = normalizeToolInput({
|
|
108
|
+
input: lastBlock.input,
|
|
109
|
+
_partialJson: nextPartial,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "user": {
|
|
115
|
+
const content = Array.isArray(event.message?.content) ? event.message?.content : [];
|
|
116
|
+
for (const block of content) {
|
|
117
|
+
if (block &&
|
|
118
|
+
typeof block === "object" &&
|
|
119
|
+
"type" in block &&
|
|
120
|
+
block.type === "tool_result") {
|
|
121
|
+
appendAssistantBlock(state.blocks, block);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "result": {
|
|
127
|
+
const usage = buildUsage(event);
|
|
128
|
+
if (usage) {
|
|
129
|
+
state.usage = usage;
|
|
130
|
+
}
|
|
131
|
+
if (typeof event.result === "string") {
|
|
132
|
+
const hasAssistantText = state.blocks.some((block) => block.type === "text");
|
|
133
|
+
if (!hasAssistantText && event.result.trim().length > 0) {
|
|
134
|
+
state.blocks.push({ type: "text", text: event.result });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (event.result && typeof event.result === "object") {
|
|
138
|
+
const resultContent = event.result.content;
|
|
139
|
+
if (Array.isArray(resultContent)) {
|
|
140
|
+
for (const block of resultContent) {
|
|
141
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
142
|
+
appendAssistantBlock(state.blocks, block);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
return state;
|
|
153
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ChildProcess } from "node:child_process";
|
|
2
|
+
import type { ContentBlock, ConversationTurn } from "./types.js";
|
|
3
|
+
export interface ClaudeStructuredRunnerCallbacks {
|
|
4
|
+
onOutput: (text: string) => void;
|
|
5
|
+
onBlocks: (blocks: ContentBlock[], usage?: ConversationTurn["usage"]) => void;
|
|
6
|
+
onSessionId: (sessionId: string) => void;
|
|
7
|
+
onClose: (exitCode: number | null, state: ClaudeStructuredRunnerState) => void;
|
|
8
|
+
onError: (error: Error) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface ClaudeStructuredRunnerState {
|
|
11
|
+
stdoutBuffer: string;
|
|
12
|
+
assistantBlocks: ContentBlock[];
|
|
13
|
+
usage?: ConversationTurn["usage"];
|
|
14
|
+
sessionId: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface StartClaudeStructuredRunnerOptions {
|
|
17
|
+
command: string;
|
|
18
|
+
cwd: string;
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
20
|
+
callbacks: ClaudeStructuredRunnerCallbacks;
|
|
21
|
+
}
|
|
22
|
+
export interface ClaudeStructuredRunnerHandle {
|
|
23
|
+
child: ChildProcess;
|
|
24
|
+
getState: () => ClaudeStructuredRunnerState;
|
|
25
|
+
kill: () => void;
|
|
26
|
+
}
|
|
27
|
+
export declare function startClaudeStructuredRunner(options: StartClaudeStructuredRunnerOptions): ClaudeStructuredRunnerHandle;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { updateClaudeStreamState } from "./claude-stream-adapter.js";
|
|
3
|
+
function toContentBlocks(blocks) {
|
|
4
|
+
return blocks.map((block) => {
|
|
5
|
+
switch (block.type) {
|
|
6
|
+
case "text":
|
|
7
|
+
return { type: "text", text: typeof block.text === "string" ? block.text : "" };
|
|
8
|
+
case "thinking":
|
|
9
|
+
return { type: "thinking", thinking: typeof block.thinking === "string" ? block.thinking : "" };
|
|
10
|
+
case "tool_use":
|
|
11
|
+
return {
|
|
12
|
+
type: "tool_use",
|
|
13
|
+
id: typeof block.id === "string" ? block.id : "",
|
|
14
|
+
name: typeof block.name === "string" ? block.name : "",
|
|
15
|
+
description: typeof block.description === "string" ? block.description : undefined,
|
|
16
|
+
input: block.input && typeof block.input === "object" && !Array.isArray(block.input)
|
|
17
|
+
? block.input
|
|
18
|
+
: {},
|
|
19
|
+
};
|
|
20
|
+
case "tool_result":
|
|
21
|
+
return {
|
|
22
|
+
type: "tool_result",
|
|
23
|
+
tool_use_id: typeof block.tool_use_id === "string" ? block.tool_use_id : "",
|
|
24
|
+
content: typeof block.content === "string"
|
|
25
|
+
? block.content
|
|
26
|
+
: Array.isArray(block.content)
|
|
27
|
+
? block.content
|
|
28
|
+
: "",
|
|
29
|
+
is_error: block.is_error === true,
|
|
30
|
+
};
|
|
31
|
+
default:
|
|
32
|
+
return { type: "text", text: JSON.stringify(block) };
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function startClaudeStructuredRunner(options) {
|
|
37
|
+
const state = {
|
|
38
|
+
stdoutBuffer: "",
|
|
39
|
+
assistantBlocks: [],
|
|
40
|
+
usage: undefined,
|
|
41
|
+
sessionId: null,
|
|
42
|
+
};
|
|
43
|
+
const child = spawn(options.command, [], {
|
|
44
|
+
cwd: options.cwd,
|
|
45
|
+
env: options.env,
|
|
46
|
+
shell: true,
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
});
|
|
49
|
+
child.stdout?.on("data", (chunk) => {
|
|
50
|
+
state.stdoutBuffer += chunk.toString();
|
|
51
|
+
const lines = state.stdoutBuffer.split("\n");
|
|
52
|
+
state.stdoutBuffer = lines.pop() || "";
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (!trimmed) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const event = JSON.parse(trimmed);
|
|
60
|
+
const nextState = updateClaudeStreamState({
|
|
61
|
+
blocks: state.assistantBlocks,
|
|
62
|
+
usage: state.usage,
|
|
63
|
+
sessionId: state.sessionId,
|
|
64
|
+
}, event);
|
|
65
|
+
state.assistantBlocks = nextState.blocks.map((block) => ({ ...block }));
|
|
66
|
+
state.usage = nextState.usage;
|
|
67
|
+
if (nextState.sessionId && nextState.sessionId !== state.sessionId) {
|
|
68
|
+
state.sessionId = nextState.sessionId;
|
|
69
|
+
options.callbacks.onSessionId(nextState.sessionId);
|
|
70
|
+
}
|
|
71
|
+
options.callbacks.onBlocks(state.assistantBlocks, state.usage);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
options.callbacks.onOutput(trimmed + "\n");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
child.stderr?.on("data", (chunk) => {
|
|
79
|
+
options.callbacks.onOutput(chunk.toString());
|
|
80
|
+
});
|
|
81
|
+
child.on("close", (code) => {
|
|
82
|
+
options.callbacks.onClose(code, {
|
|
83
|
+
stdoutBuffer: state.stdoutBuffer,
|
|
84
|
+
assistantBlocks: [...state.assistantBlocks],
|
|
85
|
+
usage: state.usage,
|
|
86
|
+
sessionId: state.sessionId,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
child.on("error", (error) => {
|
|
90
|
+
options.callbacks.onError(error);
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
child,
|
|
94
|
+
getState: () => ({
|
|
95
|
+
stdoutBuffer: state.stdoutBuffer,
|
|
96
|
+
assistantBlocks: [...state.assistantBlocks],
|
|
97
|
+
usage: state.usage,
|
|
98
|
+
sessionId: state.sessionId,
|
|
99
|
+
}),
|
|
100
|
+
kill: () => {
|
|
101
|
+
if (!child.killed) {
|
|
102
|
+
child.kill();
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -5,7 +5,7 @@ import process from "node:process";
|
|
|
5
5
|
const DEFAULT_CONFIG_DIR = ".wand";
|
|
6
6
|
const DEFAULT_CONFIG_FILE = "config.json";
|
|
7
7
|
export const defaultConfig = () => ({
|
|
8
|
-
host: "
|
|
8
|
+
host: "127.0.0.1",
|
|
9
9
|
port: 8443,
|
|
10
10
|
https: true,
|
|
11
11
|
password: "change-me",
|
|
@@ -102,7 +102,7 @@ function mergeWithDefaults(input) {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
export function isExecutionMode(value) {
|
|
105
|
-
return value === "auto-edit" || value === "default" || value === "full-access" || value === "native" || value === "managed";
|
|
105
|
+
return value === "assist" || value === "agent" || value === "agent-max" || value === "auto-edit" || value === "default" || value === "full-access" || value === "native" || value === "managed";
|
|
106
106
|
}
|
|
107
107
|
function normalizePresetCommand(command) {
|
|
108
108
|
const trimmed = command.trim();
|
package/dist/message-parser.js
CHANGED
|
@@ -31,7 +31,6 @@ function stripAnsi(text) {
|
|
|
31
31
|
i++;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
// Other escape sequences: skip the next character
|
|
35
34
|
continue;
|
|
36
35
|
}
|
|
37
36
|
// Skip control characters except \n, \r, \t
|
|
@@ -41,10 +40,10 @@ function stripAnsi(text) {
|
|
|
41
40
|
}
|
|
42
41
|
return stripped;
|
|
43
42
|
}
|
|
44
|
-
/**
|
|
43
|
+
/** Lines considered as UI noise (pass in trimmed) */
|
|
45
44
|
function isNoiseLine(line) {
|
|
46
|
-
if (
|
|
47
|
-
return
|
|
45
|
+
if (line.length === 0)
|
|
46
|
+
return false;
|
|
48
47
|
if (line.startsWith("────"))
|
|
49
48
|
return true;
|
|
50
49
|
if (line === "❯")
|
|
@@ -55,104 +54,55 @@ function isNoiseLine(line) {
|
|
|
55
54
|
return true;
|
|
56
55
|
if (/^Sonnet\b/.test(line))
|
|
57
56
|
return true;
|
|
58
|
-
if (line.startsWith("~/"))
|
|
59
|
-
return true;
|
|
60
|
-
if (line.includes("● high"))
|
|
61
|
-
return true;
|
|
62
57
|
if (line.includes("Failed to install Anthropic"))
|
|
63
58
|
return true;
|
|
64
59
|
if (line.includes("Claude Code has switched"))
|
|
65
60
|
return true;
|
|
66
|
-
if (line.includes("Fluttering"))
|
|
67
|
-
return true;
|
|
68
61
|
if (line.includes("? for shortcuts"))
|
|
69
62
|
return true;
|
|
70
|
-
if (line.startsWith("0;") || line.startsWith("9;"))
|
|
71
|
-
return true;
|
|
72
63
|
if (line.includes("Claude is waiting"))
|
|
73
64
|
return true;
|
|
74
|
-
if (/[✢✳✶✻✽]/.test(line))
|
|
75
|
-
return true;
|
|
76
|
-
if (/^[▐▝▘]/.test(line))
|
|
77
|
-
return true;
|
|
78
|
-
const singleCharNoise = ["lu", "ue", "tr", "ti", "g", "n", "i…", "…", "uts", "lt", "rg", "·"];
|
|
79
|
-
if (singleCharNoise.includes(line) && line.length < 4)
|
|
80
|
-
return true;
|
|
81
|
-
if (line.startsWith("✽F") || line.startsWith("✻F"))
|
|
82
|
-
return true;
|
|
83
65
|
if (line.includes("[wand]"))
|
|
84
66
|
return true;
|
|
85
|
-
if (line.
|
|
86
|
-
return true;
|
|
87
|
-
if (line.includes("acceptedit"))
|
|
88
|
-
return true;
|
|
89
|
-
if (line.includes("shift+tab"))
|
|
90
|
-
return true;
|
|
91
|
-
if (line.includes("tabtocycle"))
|
|
67
|
+
if (line.startsWith("0;") || line.startsWith("9;"))
|
|
92
68
|
return true;
|
|
93
69
|
if (line.includes("ctrl+g"))
|
|
94
70
|
return true;
|
|
95
71
|
if (line.includes("/effort"))
|
|
96
72
|
return true;
|
|
97
|
-
if (line.includes("Haiku"))
|
|
98
|
-
return true;
|
|
99
|
-
if (line.includes("to cycle"))
|
|
100
|
-
return true;
|
|
101
|
-
if (/\bhigh\s*·/.test(line) || /\bmedium\s*·/.test(line) || /\blow\s*·/.test(line))
|
|
102
|
-
return true;
|
|
103
|
-
if (line.includes("npm WARN") || line.includes("npm notice"))
|
|
104
|
-
return true;
|
|
105
73
|
if (/^Using .* for .* session/.test(line))
|
|
106
74
|
return true;
|
|
107
|
-
if (line.includes("Permissions") && line.includes("mode"))
|
|
108
|
-
return true;
|
|
109
75
|
if (line.startsWith("Press ") && line.includes(" for"))
|
|
110
76
|
return true;
|
|
111
77
|
if (line.startsWith("type ") && line.includes(" to "))
|
|
112
78
|
return true;
|
|
113
|
-
if (line.length < 3 && !/^[a-zA-Z]{3}$/.test(line))
|
|
114
|
-
return true;
|
|
115
79
|
return false;
|
|
116
80
|
}
|
|
117
|
-
/** Filter assistant content line */
|
|
118
81
|
function isAssistantContent(line) {
|
|
119
|
-
if (line.includes("⏺"))
|
|
120
|
-
return true;
|
|
121
|
-
if (line.length < 8)
|
|
122
|
-
return false;
|
|
123
|
-
if (/[✢✳✶✻✽]/.test(line))
|
|
124
|
-
return false;
|
|
125
|
-
if (/^[▐▝▘]/.test(line))
|
|
126
|
-
return false;
|
|
127
82
|
if (line.startsWith("❯"))
|
|
128
83
|
return false;
|
|
129
84
|
if (line.includes("esctointerrupt"))
|
|
130
85
|
return false;
|
|
131
|
-
if (line.startsWith("?for") || line.startsWith("? for"))
|
|
132
|
-
return false;
|
|
133
86
|
return true;
|
|
134
87
|
}
|
|
135
88
|
export function parseMessages(output) {
|
|
136
89
|
const messages = [];
|
|
137
90
|
if (!output)
|
|
138
91
|
return messages;
|
|
139
|
-
// Strip ANSI and normalize
|
|
140
92
|
const stripped = stripAnsi(output).replace(/\r/g, "\n");
|
|
141
|
-
const lines = stripped.split("\n")
|
|
142
|
-
|
|
143
|
-
const cleaned = lines.filter((line) => !isNoiseLine(line));
|
|
93
|
+
const lines = stripped.split("\n");
|
|
94
|
+
const cleaned = lines.filter((line) => !isNoiseLine(line.trim()));
|
|
144
95
|
if (!cleaned.length)
|
|
145
96
|
return messages;
|
|
146
97
|
const turns = [];
|
|
147
98
|
let currentUserText = null;
|
|
148
99
|
let currentAssistantLines = [];
|
|
149
|
-
for (const
|
|
100
|
+
for (const rawLine of cleaned) {
|
|
101
|
+
const line = rawLine.trim();
|
|
150
102
|
if (line.startsWith("❯")) {
|
|
151
103
|
const afterPrompt = line.replace(/^❯\s*/, "").trim();
|
|
152
|
-
// Skip prompt suggestions
|
|
153
104
|
if (afterPrompt.startsWith("Try"))
|
|
154
105
|
continue;
|
|
155
|
-
// Finalize previous turn
|
|
156
106
|
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
157
107
|
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
158
108
|
currentAssistantLines = [];
|
|
@@ -161,7 +111,6 @@ export function parseMessages(output) {
|
|
|
161
111
|
currentUserText = afterPrompt;
|
|
162
112
|
}
|
|
163
113
|
else {
|
|
164
|
-
// Standalone ❯ — finalize and reset
|
|
165
114
|
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
166
115
|
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
167
116
|
currentAssistantLines = [];
|
|
@@ -170,20 +119,17 @@ export function parseMessages(output) {
|
|
|
170
119
|
}
|
|
171
120
|
}
|
|
172
121
|
else if (currentUserText !== null && isAssistantContent(line)) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (cleanLine)
|
|
176
|
-
currentAssistantLines.push(cleanLine);
|
|
122
|
+
const contentLine = rawLine.startsWith("⏺") ? rawLine.slice(1) : rawLine;
|
|
123
|
+
currentAssistantLines.push(contentLine);
|
|
177
124
|
}
|
|
178
125
|
}
|
|
179
|
-
// Finalize last turn
|
|
180
126
|
if (currentUserText !== null && currentAssistantLines.length > 0) {
|
|
181
127
|
turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
|
|
182
128
|
}
|
|
183
|
-
// Convert to messages
|
|
184
129
|
for (const turn of turns) {
|
|
185
130
|
messages.push({ role: "user", content: turn.user });
|
|
186
|
-
|
|
131
|
+
const content = turn.assistantLines.join("\n").replace(/[ \t]+\n/g, "\n").replace(/[\n\s]+$/, "");
|
|
132
|
+
messages.push({ role: "assistant", content });
|
|
187
133
|
}
|
|
188
134
|
return messages;
|
|
189
135
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Queue for managing user inputs
|
|
3
|
+
* Inspired by Happy's MessageQueue2 implementation
|
|
4
|
+
*/
|
|
5
|
+
import type { QueuedMessage, AutonomyPolicy, ApprovalPolicy, EscalationScope } from "./types.js";
|
|
6
|
+
export interface MessageQueueOptions {
|
|
7
|
+
autonomyPolicy?: AutonomyPolicy;
|
|
8
|
+
approvalPolicy?: ApprovalPolicy;
|
|
9
|
+
allowedScopes?: EscalationScope[];
|
|
10
|
+
}
|
|
11
|
+
export declare class MessageQueue {
|
|
12
|
+
private queue;
|
|
13
|
+
private processing;
|
|
14
|
+
private onMessageCallback;
|
|
15
|
+
private lastMessageId;
|
|
16
|
+
/**
|
|
17
|
+
* Add a message to the queue
|
|
18
|
+
*/
|
|
19
|
+
enqueue(content: string, options?: MessageQueueOptions): QueuedMessage;
|
|
20
|
+
/**
|
|
21
|
+
* Add a high-priority message (like /compact, /clear)
|
|
22
|
+
*/
|
|
23
|
+
enqueuePriority(content: string, options?: MessageQueueOptions): QueuedMessage;
|
|
24
|
+
/**
|
|
25
|
+
* Clear the queue and add a new message
|
|
26
|
+
* Used for /compact and /clear commands
|
|
27
|
+
*/
|
|
28
|
+
clearAndEnqueue(content: string, options?: MessageQueueOptions): QueuedMessage;
|
|
29
|
+
/**
|
|
30
|
+
* Set the message handler
|
|
31
|
+
*/
|
|
32
|
+
onMessage(handler: (message: QueuedMessage) => Promise<void>): void;
|
|
33
|
+
/**
|
|
34
|
+
* Process the next message in the queue
|
|
35
|
+
*/
|
|
36
|
+
private processNext;
|
|
37
|
+
/**
|
|
38
|
+
* Get the current queue length
|
|
39
|
+
*/
|
|
40
|
+
get length(): number;
|
|
41
|
+
/**
|
|
42
|
+
* Check if the queue is empty
|
|
43
|
+
*/
|
|
44
|
+
get isEmpty(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Check if a message is being processed
|
|
47
|
+
*/
|
|
48
|
+
get isProcessing(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Clear all pending messages
|
|
51
|
+
*/
|
|
52
|
+
clear(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Get all pending messages (for debugging)
|
|
55
|
+
*/
|
|
56
|
+
getPending(): QueuedMessage[];
|
|
57
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Queue for managing user inputs
|
|
3
|
+
* Inspired by Happy's MessageQueue2 implementation
|
|
4
|
+
*/
|
|
5
|
+
export class MessageQueue {
|
|
6
|
+
queue = [];
|
|
7
|
+
processing = false;
|
|
8
|
+
onMessageCallback = null;
|
|
9
|
+
lastMessageId = 0;
|
|
10
|
+
/**
|
|
11
|
+
* Add a message to the queue
|
|
12
|
+
*/
|
|
13
|
+
enqueue(content, options = {}) {
|
|
14
|
+
const message = {
|
|
15
|
+
id: `msg-${++this.lastMessageId}-${Date.now()}`,
|
|
16
|
+
content,
|
|
17
|
+
priority: 1,
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
metadata: {
|
|
20
|
+
autonomyPolicy: options.autonomyPolicy,
|
|
21
|
+
approvalPolicy: options.approvalPolicy,
|
|
22
|
+
allowedScopes: options.allowedScopes,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
this.queue.push(message);
|
|
26
|
+
this.processNext();
|
|
27
|
+
return message;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Add a high-priority message (like /compact, /clear)
|
|
31
|
+
*/
|
|
32
|
+
enqueuePriority(content, options = {}) {
|
|
33
|
+
const message = {
|
|
34
|
+
id: `msg-priority-${++this.lastMessageId}-${Date.now()}`,
|
|
35
|
+
content,
|
|
36
|
+
priority: 10,
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
isolate: true,
|
|
39
|
+
metadata: {
|
|
40
|
+
autonomyPolicy: options.autonomyPolicy,
|
|
41
|
+
approvalPolicy: options.approvalPolicy,
|
|
42
|
+
allowedScopes: options.allowedScopes,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
// Insert at the beginning of the queue
|
|
46
|
+
this.queue.unshift(message);
|
|
47
|
+
this.processNext();
|
|
48
|
+
return message;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Clear the queue and add a new message
|
|
52
|
+
* Used for /compact and /clear commands
|
|
53
|
+
*/
|
|
54
|
+
clearAndEnqueue(content, options = {}) {
|
|
55
|
+
const clearedCount = this.queue.length;
|
|
56
|
+
this.queue = [];
|
|
57
|
+
if (clearedCount > 0) {
|
|
58
|
+
console.error(`[MessageQueue] Cleared ${clearedCount} pending messages`);
|
|
59
|
+
}
|
|
60
|
+
return this.enqueuePriority(content, options);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Set the message handler
|
|
64
|
+
*/
|
|
65
|
+
onMessage(handler) {
|
|
66
|
+
this.onMessageCallback = handler;
|
|
67
|
+
this.processNext();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Process the next message in the queue
|
|
71
|
+
*/
|
|
72
|
+
async processNext() {
|
|
73
|
+
if (this.processing || this.queue.length === 0 || !this.onMessageCallback) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.processing = true;
|
|
77
|
+
// Sort by priority (higher first), then by timestamp
|
|
78
|
+
this.queue.sort((a, b) => {
|
|
79
|
+
if (a.priority !== b.priority) {
|
|
80
|
+
return b.priority - a.priority;
|
|
81
|
+
}
|
|
82
|
+
return a.timestamp - b.timestamp;
|
|
83
|
+
});
|
|
84
|
+
const message = this.queue.shift();
|
|
85
|
+
try {
|
|
86
|
+
await this.onMessageCallback(message);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error(`[MessageQueue] Error processing message ${message.id}:`, error);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
this.processing = false;
|
|
93
|
+
// Process next message
|
|
94
|
+
this.processNext();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get the current queue length
|
|
99
|
+
*/
|
|
100
|
+
get length() {
|
|
101
|
+
return this.queue.length;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if the queue is empty
|
|
105
|
+
*/
|
|
106
|
+
get isEmpty() {
|
|
107
|
+
return this.queue.length === 0;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if a message is being processed
|
|
111
|
+
*/
|
|
112
|
+
get isProcessing() {
|
|
113
|
+
return this.processing;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Clear all pending messages
|
|
117
|
+
*/
|
|
118
|
+
clear() {
|
|
119
|
+
this.queue = [];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get all pending messages (for debugging)
|
|
123
|
+
*/
|
|
124
|
+
getPending() {
|
|
125
|
+
return [...this.queue];
|
|
126
|
+
}
|
|
127
|
+
}
|