@agentuity/coder 1.0.37
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 +57 -0
- package/dist/chain-preview.d.ts +55 -0
- package/dist/chain-preview.d.ts.map +1 -0
- package/dist/chain-preview.js +472 -0
- package/dist/chain-preview.js.map +1 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +402 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +22 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +99 -0
- package/dist/commands.js.map +1 -0
- package/dist/footer.d.ts +34 -0
- package/dist/footer.d.ts.map +1 -0
- package/dist/footer.js +249 -0
- package/dist/footer.js.map +1 -0
- package/dist/handlers.d.ts +24 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +83 -0
- package/dist/handlers.js.map +1 -0
- package/dist/hub-overlay.d.ts +107 -0
- package/dist/hub-overlay.d.ts.map +1 -0
- package/dist/hub-overlay.js +1794 -0
- package/dist/hub-overlay.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1585 -0
- package/dist/index.js.map +1 -0
- package/dist/output-viewer.d.ts +49 -0
- package/dist/output-viewer.d.ts.map +1 -0
- package/dist/output-viewer.js +389 -0
- package/dist/output-viewer.js.map +1 -0
- package/dist/overlay.d.ts +40 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +225 -0
- package/dist/overlay.js.map +1 -0
- package/dist/protocol.d.ts +118 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +3 -0
- package/dist/protocol.js.map +1 -0
- package/dist/remote-session.d.ts +113 -0
- package/dist/remote-session.d.ts.map +1 -0
- package/dist/remote-session.js +645 -0
- package/dist/remote-session.js.map +1 -0
- package/dist/remote-tui.d.ts +40 -0
- package/dist/remote-tui.d.ts.map +1 -0
- package/dist/remote-tui.js +606 -0
- package/dist/remote-tui.js.map +1 -0
- package/dist/renderers.d.ts +34 -0
- package/dist/renderers.d.ts.map +1 -0
- package/dist/renderers.js +669 -0
- package/dist/renderers.js.map +1 -0
- package/dist/review.d.ts +15 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +154 -0
- package/dist/review.js.map +1 -0
- package/dist/titlebar.d.ts +3 -0
- package/dist/titlebar.d.ts.map +1 -0
- package/dist/titlebar.js +59 -0
- package/dist/titlebar.js.map +1 -0
- package/dist/todo/index.d.ts +3 -0
- package/dist/todo/index.d.ts.map +1 -0
- package/dist/todo/index.js +3 -0
- package/dist/todo/index.js.map +1 -0
- package/dist/todo/store.d.ts +6 -0
- package/dist/todo/store.d.ts.map +1 -0
- package/dist/todo/store.js +43 -0
- package/dist/todo/store.js.map +1 -0
- package/dist/todo/types.d.ts +13 -0
- package/dist/todo/types.d.ts.map +1 -0
- package/dist/todo/types.js +2 -0
- package/dist/todo/types.js.map +1 -0
- package/package.json +44 -0
- package/src/chain-preview.ts +621 -0
- package/src/client.ts +515 -0
- package/src/commands.ts +132 -0
- package/src/footer.ts +305 -0
- package/src/handlers.ts +113 -0
- package/src/hub-overlay.ts +2324 -0
- package/src/index.ts +1907 -0
- package/src/output-viewer.ts +480 -0
- package/src/overlay.ts +294 -0
- package/src/protocol.ts +157 -0
- package/src/remote-session.ts +800 -0
- package/src/remote-tui.ts +707 -0
- package/src/renderers.ts +740 -0
- package/src/review.ts +201 -0
- package/src/titlebar.ts +63 -0
- package/src/todo/index.ts +2 -0
- package/src/todo/store.ts +49 -0
- package/src/todo/types.ts +14 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote TUI — Native Pi Coding Agent Renderer for Remote Sessions
|
|
3
|
+
*
|
|
4
|
+
* Creates a real AgentSession + InteractiveMode backed by a remote sandbox
|
|
5
|
+
* via Hub WebSocket, with the coder extension loaded for Hub UI (footer,
|
|
6
|
+
* /hub overlay, commands, titlebar).
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* Remote TUI → Hub WebSocket (controller) → Sandbox (Pi RPC mode)
|
|
10
|
+
* - User input → agent.prompt() (monkey-patched) → RPC `prompt` → Hub → sandbox
|
|
11
|
+
* - Sandbox Pi → AgentEvent stream → Hub broadcast → Agent.emit() → InteractiveMode renders natively
|
|
12
|
+
* - Hub UI → coder extension (loaded via DefaultResourceLoader) provides footer, /hub, commands
|
|
13
|
+
*
|
|
14
|
+
* The local Agent never calls an LLM. Its prompt/steer/abort are monkey-patched
|
|
15
|
+
* to send RPC commands. Its internal state is kept in sync with the remote agent
|
|
16
|
+
* by mirroring state updates from each received event.
|
|
17
|
+
*
|
|
18
|
+
* The coder extension sees AGENTUITY_CODER_REMOTE_SESSION (Hub UI mode) +
|
|
19
|
+
* AGENTUITY_CODER_NATIVE_REMOTE (skip legacy event rendering). It provides
|
|
20
|
+
* Hub connection, footer, /hub overlay, commands, titlebar — but does NOT
|
|
21
|
+
* intercept input or render events (this module handles both).
|
|
22
|
+
*
|
|
23
|
+
* IMPORTANT: Initialization order matters!
|
|
24
|
+
* 1. Create RemoteSession (no connection yet)
|
|
25
|
+
* 2. Create AgentSession, patch Agent/Session methods
|
|
26
|
+
* 3. Register ALL event handlers on RemoteSession
|
|
27
|
+
* 4. THEN connect — so hydration + replay events are captured
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Run the native Pi TUI connected to a remote sandbox session.
|
|
31
|
+
*
|
|
32
|
+
* This is the entry point for `agentuity coder start --remote <sessionId>`.
|
|
33
|
+
* Creates an AgentSession with the coder extension loaded (Hub UI), then
|
|
34
|
+
* monkey-patches the Agent for remote-backed execution.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runRemoteTui(options: {
|
|
37
|
+
hubWsUrl: string;
|
|
38
|
+
sessionId: string;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=remote-tui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-tui.d.ts","sourceRoot":"","sources":["../src/remote-tui.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAkBH;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8hBhB"}
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote TUI — Native Pi Coding Agent Renderer for Remote Sessions
|
|
3
|
+
*
|
|
4
|
+
* Creates a real AgentSession + InteractiveMode backed by a remote sandbox
|
|
5
|
+
* via Hub WebSocket, with the coder extension loaded for Hub UI (footer,
|
|
6
|
+
* /hub overlay, commands, titlebar).
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* Remote TUI → Hub WebSocket (controller) → Sandbox (Pi RPC mode)
|
|
10
|
+
* - User input → agent.prompt() (monkey-patched) → RPC `prompt` → Hub → sandbox
|
|
11
|
+
* - Sandbox Pi → AgentEvent stream → Hub broadcast → Agent.emit() → InteractiveMode renders natively
|
|
12
|
+
* - Hub UI → coder extension (loaded via DefaultResourceLoader) provides footer, /hub, commands
|
|
13
|
+
*
|
|
14
|
+
* The local Agent never calls an LLM. Its prompt/steer/abort are monkey-patched
|
|
15
|
+
* to send RPC commands. Its internal state is kept in sync with the remote agent
|
|
16
|
+
* by mirroring state updates from each received event.
|
|
17
|
+
*
|
|
18
|
+
* The coder extension sees AGENTUITY_CODER_REMOTE_SESSION (Hub UI mode) +
|
|
19
|
+
* AGENTUITY_CODER_NATIVE_REMOTE (skip legacy event rendering). It provides
|
|
20
|
+
* Hub connection, footer, /hub overlay, commands, titlebar — but does NOT
|
|
21
|
+
* intercept input or render events (this module handles both).
|
|
22
|
+
*
|
|
23
|
+
* IMPORTANT: Initialization order matters!
|
|
24
|
+
* 1. Create RemoteSession (no connection yet)
|
|
25
|
+
* 2. Create AgentSession, patch Agent/Session methods
|
|
26
|
+
* 3. Register ALL event handlers on RemoteSession
|
|
27
|
+
* 4. THEN connect — so hydration + replay events are captured
|
|
28
|
+
*/
|
|
29
|
+
import { createAgentSession, DefaultResourceLoader, InteractiveMode, SessionManager, } from '@mariozechner/pi-coding-agent';
|
|
30
|
+
import { RemoteSession } from "./remote-session.js";
|
|
31
|
+
import { agentuityCoderHub } from "./index.js";
|
|
32
|
+
const DEBUG = !!process.env['AGENTUITY_DEBUG'];
|
|
33
|
+
function log(msg) {
|
|
34
|
+
if (DEBUG)
|
|
35
|
+
console.error(`[remote-tui] ${msg}`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Run the native Pi TUI connected to a remote sandbox session.
|
|
39
|
+
*
|
|
40
|
+
* This is the entry point for `agentuity coder start --remote <sessionId>`.
|
|
41
|
+
* Creates an AgentSession with the coder extension loaded (Hub UI), then
|
|
42
|
+
* monkey-patches the Agent for remote-backed execution.
|
|
43
|
+
*/
|
|
44
|
+
export async function runRemoteTui(options) {
|
|
45
|
+
const { hubWsUrl, sessionId } = options;
|
|
46
|
+
log(`Starting remote TUI for session ${sessionId}`);
|
|
47
|
+
log(`Hub URL: ${hubWsUrl}`);
|
|
48
|
+
// Set env vars BEFORE loading the extension so it enters native remote mode
|
|
49
|
+
process.env.AGENTUITY_CODER_HUB_URL = hubWsUrl;
|
|
50
|
+
process.env.AGENTUITY_CODER_REMOTE_SESSION = sessionId;
|
|
51
|
+
process.env.AGENTUITY_CODER_NATIVE_REMOTE = '1';
|
|
52
|
+
// ── 1. Create RemoteSession (NOT connected yet) ──
|
|
53
|
+
// We register all handlers BEFORE connecting so that the hydration
|
|
54
|
+
// message from the Hub (sent immediately after init) is captured.
|
|
55
|
+
const remote = new RemoteSession(sessionId);
|
|
56
|
+
// TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
|
|
57
|
+
remote.apiKey = process.env.AGENTUITY_CODER_API_KEY || null;
|
|
58
|
+
let hydrationStreamingDetected = false;
|
|
59
|
+
// ── 2. Create AgentSession with coder extension loaded ──
|
|
60
|
+
// The extension provides Hub UI (footer, /hub overlay, commands, titlebar).
|
|
61
|
+
// AGENTUITY_CODER_NATIVE_REMOTE=1 tells it to skip legacy event rendering.
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
64
|
+
cwd,
|
|
65
|
+
noExtensions: true, // Skip file-system extension discovery
|
|
66
|
+
extensionFactories: [agentuityCoderHub], // Load coder extension directly
|
|
67
|
+
});
|
|
68
|
+
await resourceLoader.reload();
|
|
69
|
+
const { session } = await createAgentSession({
|
|
70
|
+
sessionManager: SessionManager.inMemory(),
|
|
71
|
+
tools: [], // No local tools — sandbox has all the tools
|
|
72
|
+
resourceLoader,
|
|
73
|
+
});
|
|
74
|
+
log('AgentSession created');
|
|
75
|
+
// NOTE: Do NOT call session.bindExtensions() here.
|
|
76
|
+
// InteractiveMode.initExtensions() calls it with the proper uiContext.
|
|
77
|
+
// Calling it early fires session_start twice, duplicating extension init.
|
|
78
|
+
// Access the Agent instance (typed as `any` for monkey-patching)
|
|
79
|
+
const agent = session.agent;
|
|
80
|
+
// ── 3. Patch Agent to be remote-backed ──
|
|
81
|
+
// Track the running prompt promise so InteractiveMode waits correctly
|
|
82
|
+
let runningPromptResolve = null;
|
|
83
|
+
let syntheticAgentStartEmitted = false;
|
|
84
|
+
// Override Agent.prompt — send RPC prompt command, block until agent_end
|
|
85
|
+
agent.prompt = async (input) => {
|
|
86
|
+
const text = extractPromptText(input);
|
|
87
|
+
log(`agent.prompt called, extracted text: ${text ? text.slice(0, 80) : '(empty)'}`);
|
|
88
|
+
if (!text)
|
|
89
|
+
return;
|
|
90
|
+
// Set streaming state — InteractiveMode checks this
|
|
91
|
+
agent._state.isStreaming = true;
|
|
92
|
+
agent._state.streamMessage = null;
|
|
93
|
+
agent._state.error = undefined;
|
|
94
|
+
// Create runningPrompt so waitForIdle() works
|
|
95
|
+
const runPromise = new Promise((resolve) => {
|
|
96
|
+
runningPromptResolve = resolve;
|
|
97
|
+
});
|
|
98
|
+
agent.runningPrompt = runPromise;
|
|
99
|
+
// Emit synthetic agent_start so InteractiveMode shows "working" immediately
|
|
100
|
+
syntheticAgentStartEmitted = true;
|
|
101
|
+
agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() });
|
|
102
|
+
// Send RPC command to sandbox
|
|
103
|
+
remote.prompt(text);
|
|
104
|
+
log(`Sent prompt: ${text.slice(0, 100)}`);
|
|
105
|
+
// Block until agent_end received from remote
|
|
106
|
+
await runPromise;
|
|
107
|
+
};
|
|
108
|
+
// Override Agent.steer — send RPC steer command
|
|
109
|
+
agent.steer = (m) => {
|
|
110
|
+
const text = extractMessageText(m);
|
|
111
|
+
if (text) {
|
|
112
|
+
remote.steer(text);
|
|
113
|
+
log(`Sent steer: ${text.slice(0, 100)}`);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// Override Agent.abort — send RPC abort command
|
|
117
|
+
agent.abort = () => {
|
|
118
|
+
remote.abort();
|
|
119
|
+
log('Sent abort');
|
|
120
|
+
resolveRunningPrompt();
|
|
121
|
+
};
|
|
122
|
+
// Override Agent.waitForIdle
|
|
123
|
+
agent.waitForIdle = () => {
|
|
124
|
+
return agent.runningPrompt ?? Promise.resolve();
|
|
125
|
+
};
|
|
126
|
+
// ── 4. Patch AgentSession methods ──
|
|
127
|
+
// session.prompt() does model/API key validation before calling agent.prompt().
|
|
128
|
+
// In remote mode, skip validation — the sandbox has the model/key.
|
|
129
|
+
// InteractiveMode calls session.prompt(text, { streamingBehavior: "steer" })
|
|
130
|
+
// when user types during streaming, and session.prompt(text, { streamingBehavior: "followUp" })
|
|
131
|
+
// for Alt+Enter follow-ups. Handle these by routing to steer/followUp commands.
|
|
132
|
+
session.prompt = async (text, options) => {
|
|
133
|
+
const behavior = options?.streamingBehavior;
|
|
134
|
+
log(`session.prompt called (behavior=${behavior ?? 'normal'}): ${text.slice(0, 80)}`);
|
|
135
|
+
// Extension commands (start with /) — let AgentSession handle them
|
|
136
|
+
// so extension-registered slash commands still work in remote mode
|
|
137
|
+
if (text.startsWith('/') && session._tryExecuteExtensionCommand) {
|
|
138
|
+
try {
|
|
139
|
+
const handled = await session._tryExecuteExtensionCommand(text);
|
|
140
|
+
if (handled) {
|
|
141
|
+
log(`Extension command handled: ${text}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
log(`Extension command error: ${err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (behavior === 'steer') {
|
|
150
|
+
remote.steer(text);
|
|
151
|
+
log(`Sent steer: ${text.slice(0, 80)}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (behavior === 'followUp') {
|
|
155
|
+
remote.sendCommand({ type: 'follow_up', message: text });
|
|
156
|
+
log(`Sent follow-up: ${text.slice(0, 80)}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Normal prompt — send to sandbox
|
|
160
|
+
await agent.prompt(text);
|
|
161
|
+
};
|
|
162
|
+
session.steer = async (text) => {
|
|
163
|
+
remote.steer(text);
|
|
164
|
+
};
|
|
165
|
+
session.followUp = async (text) => {
|
|
166
|
+
remote.sendCommand({ type: 'follow_up', message: text });
|
|
167
|
+
};
|
|
168
|
+
session.abort = async () => {
|
|
169
|
+
remote.abort();
|
|
170
|
+
resolveRunningPrompt();
|
|
171
|
+
};
|
|
172
|
+
// Disable auto-compaction (sandbox handles it)
|
|
173
|
+
session.setAutoCompactionEnabled(false);
|
|
174
|
+
session.setAutoRetryEnabled(false);
|
|
175
|
+
// ── 5. Wire up remote events → Agent event pipeline ──
|
|
176
|
+
// Only emit LIVE events (from broadcast) to Agent → InteractiveMode.
|
|
177
|
+
// Replay events (from Durable Stream) are historical — skip them.
|
|
178
|
+
// Hydration is handled separately via session_hydration → agent.replaceMessages().
|
|
179
|
+
//
|
|
180
|
+
// Events that arrive before InteractiveMode is initialized are buffered
|
|
181
|
+
// and flushed after init (InteractiveMode registers listeners during init,
|
|
182
|
+
// so agent.emit() before that fires into the void).
|
|
183
|
+
let interactiveModeReady = false;
|
|
184
|
+
let eventBuffer = [];
|
|
185
|
+
let seenMessageStart = false;
|
|
186
|
+
let seenAgentStart = false;
|
|
187
|
+
remote.onEvent((rpcEvent) => {
|
|
188
|
+
const source = rpcEvent._source ?? 'unknown';
|
|
189
|
+
log(`Event received: ${rpcEvent.type} (source=${source})`);
|
|
190
|
+
// session_hydration is handled separately below — skip it here
|
|
191
|
+
if (rpcEvent.type === 'session_hydration')
|
|
192
|
+
return;
|
|
193
|
+
// Skip duplicate agent_start if we already emitted a synthetic one
|
|
194
|
+
if (rpcEvent.type === 'agent_start' && syntheticAgentStartEmitted) {
|
|
195
|
+
syntheticAgentStartEmitted = false;
|
|
196
|
+
// Still update state from real event
|
|
197
|
+
updateAgentState(agent, rpcEvent);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Skip replay events — these are historical from Durable Stream
|
|
201
|
+
if (source === 'replay') {
|
|
202
|
+
log(`Skipping replay event: ${rpcEvent.type}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Track streaming lifecycle events so we can inject synthetics when
|
|
206
|
+
// we attach mid-stream (controller connected after agent already started).
|
|
207
|
+
if (rpcEvent.type === 'agent_start')
|
|
208
|
+
seenAgentStart = true;
|
|
209
|
+
if (rpcEvent.type === 'agent_end') {
|
|
210
|
+
seenAgentStart = false;
|
|
211
|
+
seenMessageStart = false;
|
|
212
|
+
}
|
|
213
|
+
if (rpcEvent.type === 'message_start')
|
|
214
|
+
seenMessageStart = true;
|
|
215
|
+
if (rpcEvent.type === 'message_end')
|
|
216
|
+
seenMessageStart = false;
|
|
217
|
+
// Update agent internal state (mirrors Agent._runLoop behavior)
|
|
218
|
+
updateAgentState(agent, rpcEvent);
|
|
219
|
+
// Buffer events until InteractiveMode is ready to receive them
|
|
220
|
+
if (!interactiveModeReady) {
|
|
221
|
+
eventBuffer.push(rpcEvent);
|
|
222
|
+
log(`Buffered event: ${rpcEvent.type} (InteractiveMode not ready)`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// Mid-stream attach guard: if we receive message_update or message_end
|
|
226
|
+
// without having seen message_start, inject synthetic agent_start +
|
|
227
|
+
// message_start so InteractiveMode sets up its streaming component.
|
|
228
|
+
// Without this, the events are silently dropped because
|
|
229
|
+
// InteractiveMode.streamingComponent is null.
|
|
230
|
+
// This happens when the controller connects mid-stream or after the
|
|
231
|
+
// agent finishes — the broadcast of agent_start/message_start occurred
|
|
232
|
+
// before the controller WebSocket was registered.
|
|
233
|
+
if ((rpcEvent.type === 'message_update' || rpcEvent.type === 'message_end') &&
|
|
234
|
+
!seenMessageStart) {
|
|
235
|
+
log(`Live ${rpcEvent.type} without prior message_start — injecting synthetics`);
|
|
236
|
+
if (!seenAgentStart) {
|
|
237
|
+
agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() });
|
|
238
|
+
seenAgentStart = true;
|
|
239
|
+
}
|
|
240
|
+
agent.emit({
|
|
241
|
+
type: 'message_start',
|
|
242
|
+
message: {
|
|
243
|
+
role: 'assistant',
|
|
244
|
+
content: [],
|
|
245
|
+
api: 'anthropic-messages',
|
|
246
|
+
provider: 'anthropic',
|
|
247
|
+
model: 'remote',
|
|
248
|
+
usage: {
|
|
249
|
+
input: 0,
|
|
250
|
+
output: 0,
|
|
251
|
+
cacheRead: 0,
|
|
252
|
+
cacheWrite: 0,
|
|
253
|
+
totalTokens: 0,
|
|
254
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
255
|
+
},
|
|
256
|
+
timestamp: Date.now(),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
seenMessageStart = true;
|
|
260
|
+
}
|
|
261
|
+
// Emit to subscribers — InteractiveMode.handleEvent processes this
|
|
262
|
+
agent.emit(rpcEvent);
|
|
263
|
+
// Resolve running prompt when agent finishes
|
|
264
|
+
if (rpcEvent.type === 'agent_end') {
|
|
265
|
+
resolveRunningPrompt();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
// ── 6. Wire up UI handlers for extension dialogs from sandbox ──
|
|
269
|
+
remote.setUiHandler(async (request) => {
|
|
270
|
+
// TODO: Bridge to InteractiveMode's extension UI context
|
|
271
|
+
log(`UI request: ${request.method} (no handler yet)`);
|
|
272
|
+
const fireAndForget = ['notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text'];
|
|
273
|
+
if (fireAndForget.includes(request.method))
|
|
274
|
+
return undefined;
|
|
275
|
+
return null;
|
|
276
|
+
});
|
|
277
|
+
// ── 7. Handle hydration (initial state from Hub) ──
|
|
278
|
+
// Hydration arrives as the message AFTER 'init' on the WebSocket.
|
|
279
|
+
// remote.connect() resolves on 'init', so hydration arrives in the next
|
|
280
|
+
// event loop tick. We use a promise to wait for it before creating
|
|
281
|
+
// InteractiveMode (which calls renderInitialMessages from SessionManager).
|
|
282
|
+
const sm = session.sessionManager;
|
|
283
|
+
let resolveHydration;
|
|
284
|
+
const hydrationReady = new Promise((resolve) => {
|
|
285
|
+
resolveHydration = resolve;
|
|
286
|
+
});
|
|
287
|
+
remote.onEvent((event) => {
|
|
288
|
+
if (event.type !== 'session_hydration')
|
|
289
|
+
return;
|
|
290
|
+
const entries = event.entries;
|
|
291
|
+
// Extract task text from hydration (Hub includes session.sandbox?.task)
|
|
292
|
+
const hydrationTask = event.task;
|
|
293
|
+
if (!entries?.length) {
|
|
294
|
+
log('Received session_hydration with no entries');
|
|
295
|
+
// Even with no entries, inject task as user message if available
|
|
296
|
+
if (hydrationTask) {
|
|
297
|
+
const taskMsg = {
|
|
298
|
+
role: 'user',
|
|
299
|
+
content: [{ type: 'text', text: hydrationTask }],
|
|
300
|
+
timestamp: Date.now(),
|
|
301
|
+
};
|
|
302
|
+
agent.replaceMessages([taskMsg]);
|
|
303
|
+
try {
|
|
304
|
+
sm.appendMessage(taskMsg);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
log(`SM append task error: ${err}`);
|
|
308
|
+
}
|
|
309
|
+
log('Injected task as user message (no entries)');
|
|
310
|
+
}
|
|
311
|
+
resolveHydration();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
log(`Hydrating ${entries.length} entries`);
|
|
315
|
+
const agentMessages = [];
|
|
316
|
+
// If we have a task and no user_prompt entry, inject the task as the first user message
|
|
317
|
+
const hasUserEntry = entries.some((e) => e.type === 'user_prompt' || e.role === 'user');
|
|
318
|
+
if (hydrationTask && !hasUserEntry) {
|
|
319
|
+
const taskMsg = {
|
|
320
|
+
role: 'user',
|
|
321
|
+
content: [{ type: 'text', text: hydrationTask }],
|
|
322
|
+
timestamp: Date.now(),
|
|
323
|
+
};
|
|
324
|
+
agentMessages.push(taskMsg);
|
|
325
|
+
try {
|
|
326
|
+
sm.appendMessage(taskMsg);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
log(`SM append task error: ${err}`);
|
|
330
|
+
}
|
|
331
|
+
log('Injected task as user message');
|
|
332
|
+
}
|
|
333
|
+
for (const entry of entries) {
|
|
334
|
+
const text = typeof entry.content === 'string'
|
|
335
|
+
? entry.content
|
|
336
|
+
: Array.isArray(entry.content)
|
|
337
|
+
? entry.content
|
|
338
|
+
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
339
|
+
.map((c) => c.text)
|
|
340
|
+
.join('\n')
|
|
341
|
+
: '';
|
|
342
|
+
if (!text)
|
|
343
|
+
continue;
|
|
344
|
+
// Hub conversation entries use type: 'message' for assistant, 'thinking' for thinking,
|
|
345
|
+
// 'task_result' for delegation results, 'turn' for turn markers, 'user_prompt' for user input.
|
|
346
|
+
// Only 'user_prompt' entries are user messages; everything else is assistant-side.
|
|
347
|
+
const isAssistant = entry.role === 'assistant' ||
|
|
348
|
+
entry.type === 'message' ||
|
|
349
|
+
entry.type === 'thinking' ||
|
|
350
|
+
entry.type === 'task_result' ||
|
|
351
|
+
entry.type === 'assistant';
|
|
352
|
+
if (isAssistant) {
|
|
353
|
+
const msg = {
|
|
354
|
+
role: 'assistant',
|
|
355
|
+
content: [{ type: 'text', text }],
|
|
356
|
+
api: 'anthropic-messages',
|
|
357
|
+
provider: 'anthropic',
|
|
358
|
+
model: 'remote',
|
|
359
|
+
usage: {
|
|
360
|
+
input: 0,
|
|
361
|
+
output: 0,
|
|
362
|
+
cacheRead: 0,
|
|
363
|
+
cacheWrite: 0,
|
|
364
|
+
totalTokens: 0,
|
|
365
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
366
|
+
},
|
|
367
|
+
stopReason: 'stop',
|
|
368
|
+
timestamp: entry.timestamp ?? Date.now(),
|
|
369
|
+
};
|
|
370
|
+
agentMessages.push(msg);
|
|
371
|
+
try {
|
|
372
|
+
sm.appendMessage(msg);
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
log(`SM append error: ${err}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
const msg = {
|
|
380
|
+
role: 'user',
|
|
381
|
+
content: [{ type: 'text', text }],
|
|
382
|
+
timestamp: entry.timestamp ?? Date.now(),
|
|
383
|
+
};
|
|
384
|
+
agentMessages.push(msg);
|
|
385
|
+
try {
|
|
386
|
+
sm.appendMessage(msg);
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
log(`SM append error: ${err}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (agentMessages.length > 0) {
|
|
394
|
+
agent.replaceMessages(agentMessages);
|
|
395
|
+
log(`Hydrated ${agentMessages.length} agent messages (+ session manager)`);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
log('Hydration: 0 messages after filtering (all entries had empty text?)');
|
|
399
|
+
}
|
|
400
|
+
// Restore streaming state from hydration — fixes first-connect miss
|
|
401
|
+
const streamingState = event.streamingState;
|
|
402
|
+
if (streamingState?.isStreaming) {
|
|
403
|
+
agent._state.isStreaming = true;
|
|
404
|
+
hydrationStreamingDetected = true;
|
|
405
|
+
// Create runningPrompt so InteractiveMode knows we're busy
|
|
406
|
+
if (!agent.runningPrompt) {
|
|
407
|
+
const runPromise = new Promise((resolve) => {
|
|
408
|
+
runningPromptResolve = resolve;
|
|
409
|
+
});
|
|
410
|
+
agent.runningPrompt = runPromise;
|
|
411
|
+
}
|
|
412
|
+
log(`Hydration: session is streaming with ${streamingState.activeTasks?.length ?? 0} active tasks`);
|
|
413
|
+
}
|
|
414
|
+
resolveHydration();
|
|
415
|
+
});
|
|
416
|
+
// ── 8. NOW connect to Hub ──
|
|
417
|
+
// All handlers are registered, so hydration + replay events will be captured.
|
|
418
|
+
log('Connecting to Hub (handlers registered)...');
|
|
419
|
+
await remote.connect(hubWsUrl);
|
|
420
|
+
log('Connected to Hub as controller');
|
|
421
|
+
// Wait for hydration message (arrives right after init), with a timeout
|
|
422
|
+
// in case this is the first connection and there's nothing to hydrate.
|
|
423
|
+
const HYDRATION_TIMEOUT_MS = 2000;
|
|
424
|
+
await Promise.race([
|
|
425
|
+
hydrationReady,
|
|
426
|
+
new Promise((resolve) => setTimeout(() => {
|
|
427
|
+
log('Hydration timeout — no session_hydration received');
|
|
428
|
+
resolve();
|
|
429
|
+
}, HYDRATION_TIMEOUT_MS)),
|
|
430
|
+
]);
|
|
431
|
+
const smEntries = sm.getEntries?.() ?? [];
|
|
432
|
+
log(`SessionManager has ${smEntries.length} entries after hydration`);
|
|
433
|
+
log(`Post-hydration: SM has ${smEntries.length} entries, leafId=${sm.getLeafId?.() ?? 'N/A'}`);
|
|
434
|
+
// ── 9. Start InteractiveMode — full native Pi TUI ──
|
|
435
|
+
log('Creating InteractiveMode');
|
|
436
|
+
const interactive = new InteractiveMode(session);
|
|
437
|
+
log('InteractiveMode created, calling init...');
|
|
438
|
+
await interactive.init();
|
|
439
|
+
// Flush buffered events now that InteractiveMode is listening.
|
|
440
|
+
// If the session was already streaming when we connected (mid-stream attach),
|
|
441
|
+
// InteractiveMode needs agent_start + message_start to set up its streaming
|
|
442
|
+
// components. Without these, message_update events are silently dropped
|
|
443
|
+
// because InteractiveMode.streamingComponent is null.
|
|
444
|
+
interactiveModeReady = true;
|
|
445
|
+
if (hydrationStreamingDetected) {
|
|
446
|
+
// Immediately emit agent_start + message_start so InteractiveMode shows
|
|
447
|
+
// the streaming indicator right away, before any buffered events flush.
|
|
448
|
+
// This prevents the blank screen gap between connect and first event.
|
|
449
|
+
log('Hydration detected streaming — emitting immediate synthetics');
|
|
450
|
+
agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() });
|
|
451
|
+
agent.emit({
|
|
452
|
+
type: 'message_start',
|
|
453
|
+
message: {
|
|
454
|
+
role: 'assistant',
|
|
455
|
+
content: [],
|
|
456
|
+
api: 'anthropic-messages',
|
|
457
|
+
provider: 'anthropic',
|
|
458
|
+
model: 'remote',
|
|
459
|
+
usage: {
|
|
460
|
+
input: 0,
|
|
461
|
+
output: 0,
|
|
462
|
+
cacheRead: 0,
|
|
463
|
+
cacheWrite: 0,
|
|
464
|
+
totalTokens: 0,
|
|
465
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
466
|
+
},
|
|
467
|
+
timestamp: Date.now(),
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
seenAgentStart = true;
|
|
471
|
+
seenMessageStart = true;
|
|
472
|
+
// Remove any agent_start/message_start from buffer since we already emitted them
|
|
473
|
+
eventBuffer = eventBuffer.filter((e) => e.type !== 'agent_start' && e.type !== 'message_start');
|
|
474
|
+
}
|
|
475
|
+
if (eventBuffer.length > 0) {
|
|
476
|
+
log(`Flushing ${eventBuffer.length} events: ${eventBuffer.map((e) => e.type).join(', ')}`);
|
|
477
|
+
for (const buffered of eventBuffer) {
|
|
478
|
+
agent.emit(buffered);
|
|
479
|
+
if (buffered.type === 'agent_end') {
|
|
480
|
+
resolveRunningPrompt();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
eventBuffer = [];
|
|
485
|
+
log('InteractiveMode initialized, calling run...');
|
|
486
|
+
// Handle clean shutdown
|
|
487
|
+
const cleanup = () => {
|
|
488
|
+
remote.close();
|
|
489
|
+
interactive.stop();
|
|
490
|
+
};
|
|
491
|
+
process.on('SIGINT', cleanup);
|
|
492
|
+
process.on('SIGTERM', cleanup);
|
|
493
|
+
try {
|
|
494
|
+
await interactive.run();
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
log(`InteractiveMode.run() threw: ${err instanceof Error ? err.stack : String(err)}`);
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
finally {
|
|
501
|
+
remote.close();
|
|
502
|
+
log('Remote TUI exited');
|
|
503
|
+
}
|
|
504
|
+
// ── Helper: resolve the running prompt promise ──
|
|
505
|
+
function resolveRunningPrompt() {
|
|
506
|
+
syntheticAgentStartEmitted = false;
|
|
507
|
+
agent._state.isStreaming = false;
|
|
508
|
+
agent._state.streamMessage = null;
|
|
509
|
+
agent._state.pendingToolCalls = new Set();
|
|
510
|
+
if (runningPromptResolve) {
|
|
511
|
+
runningPromptResolve();
|
|
512
|
+
runningPromptResolve = null;
|
|
513
|
+
agent.runningPrompt = undefined;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// ══════════════════════════════════════════════
|
|
518
|
+
// Agent state synchronization
|
|
519
|
+
// Mirrors Agent._runLoop state updates (agent.js lines 317-352)
|
|
520
|
+
// ══════════════════════════════════════════════
|
|
521
|
+
function updateAgentState(agent, event) {
|
|
522
|
+
const state = agent._state;
|
|
523
|
+
switch (event.type) {
|
|
524
|
+
case 'agent_start':
|
|
525
|
+
state.isStreaming = true;
|
|
526
|
+
break;
|
|
527
|
+
case 'agent_end':
|
|
528
|
+
state.isStreaming = false;
|
|
529
|
+
state.streamMessage = null;
|
|
530
|
+
state.pendingToolCalls = new Set();
|
|
531
|
+
break;
|
|
532
|
+
case 'message_start':
|
|
533
|
+
state.streamMessage = event.message;
|
|
534
|
+
state.isStreaming = true;
|
|
535
|
+
break;
|
|
536
|
+
case 'message_update':
|
|
537
|
+
state.streamMessage = event.message;
|
|
538
|
+
break;
|
|
539
|
+
case 'message_end':
|
|
540
|
+
state.streamMessage = null;
|
|
541
|
+
state.messages = [...state.messages, event.message];
|
|
542
|
+
break;
|
|
543
|
+
case 'tool_execution_start': {
|
|
544
|
+
const s = new Set(state.pendingToolCalls);
|
|
545
|
+
s.add(event.toolCallId);
|
|
546
|
+
state.pendingToolCalls = s;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
case 'tool_execution_end': {
|
|
550
|
+
const s = new Set(state.pendingToolCalls);
|
|
551
|
+
s.delete(event.toolCallId);
|
|
552
|
+
state.pendingToolCalls = s;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
case 'thinking_start':
|
|
556
|
+
state.isStreaming = true;
|
|
557
|
+
break;
|
|
558
|
+
case 'thinking_end':
|
|
559
|
+
break;
|
|
560
|
+
case 'tool_call':
|
|
561
|
+
state.isStreaming = true;
|
|
562
|
+
break;
|
|
563
|
+
case 'tool_result':
|
|
564
|
+
break;
|
|
565
|
+
case 'task_start':
|
|
566
|
+
state.isStreaming = true;
|
|
567
|
+
break;
|
|
568
|
+
case 'task_complete':
|
|
569
|
+
case 'task_error':
|
|
570
|
+
break;
|
|
571
|
+
case 'turn_end': {
|
|
572
|
+
const msg = event.message;
|
|
573
|
+
if (msg?.role === 'assistant' && msg?.errorMessage) {
|
|
574
|
+
state.error = msg.errorMessage;
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// ══════════════════════════════════════════════
|
|
581
|
+
// Text extraction helpers
|
|
582
|
+
// ══════════════════════════════════════════════
|
|
583
|
+
function extractPromptText(input) {
|
|
584
|
+
if (typeof input === 'string')
|
|
585
|
+
return input;
|
|
586
|
+
if (Array.isArray(input)) {
|
|
587
|
+
return input.map(extractMessageText).filter(Boolean).join('\n');
|
|
588
|
+
}
|
|
589
|
+
return extractMessageText(input);
|
|
590
|
+
}
|
|
591
|
+
function extractMessageText(msg) {
|
|
592
|
+
if (typeof msg === 'string')
|
|
593
|
+
return msg;
|
|
594
|
+
if (!msg || typeof msg !== 'object')
|
|
595
|
+
return '';
|
|
596
|
+
if (typeof msg.content === 'string')
|
|
597
|
+
return msg.content;
|
|
598
|
+
if (Array.isArray(msg.content)) {
|
|
599
|
+
return msg.content
|
|
600
|
+
.filter((c) => c?.type === 'text' && typeof c.text === 'string')
|
|
601
|
+
.map((c) => c.text)
|
|
602
|
+
.join('\n');
|
|
603
|
+
}
|
|
604
|
+
return '';
|
|
605
|
+
}
|
|
606
|
+
//# sourceMappingURL=remote-tui.js.map
|