@agentuity/coder-tui 2.0.9 → 2.0.11

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/src/remote-tui.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Remote TUI — Native Pi Coding Agent Renderer for Remote Sessions
3
3
  *
4
- * Creates a real AgentSession + InteractiveMode backed by a remote sandbox
4
+ * Creates a real AgentSessionRuntime + InteractiveMode backed by a remote sandbox
5
5
  * via Hub WebSocket, with the coder extension loaded for Hub UI (footer,
6
6
  * /hub overlay, commands, titlebar).
7
7
  *
8
8
  * Architecture:
9
9
  * Remote TUI → Hub WebSocket (controller) → Sandbox (Pi RPC mode)
10
10
  * - User input → agent.prompt() (monkey-patched) → RPC `prompt` → Hub → sandbox
11
- * - Sandbox Pi → AgentEvent stream → Hub broadcast → Agent.emit() → InteractiveMode renders natively
11
+ * - Sandbox Pi → AgentEvent stream → Hub broadcast → AgentSession event handling → InteractiveMode renders natively
12
12
  * - Hub UI → coder extension (loaded via DefaultResourceLoader) provides footer, /hub, commands
13
13
  *
14
14
  * The local Agent never calls an LLM. Its prompt/steer/abort are monkey-patched
@@ -22,14 +22,16 @@
22
22
  *
23
23
  * IMPORTANT: Initialization order matters!
24
24
  * 1. Create RemoteSession (no connection yet)
25
- * 2. Create AgentSession, patch Agent/Session methods
25
+ * 2. Create AgentSessionRuntime, patch Agent/Session methods
26
26
  * 3. Register ALL event handlers on RemoteSession
27
27
  * 4. THEN connect — so hydration + replay events are captured
28
28
  */
29
29
 
30
30
  import {
31
- createAgentSession,
32
- DefaultResourceLoader,
31
+ createAgentSessionFromServices,
32
+ createAgentSessionRuntime,
33
+ createAgentSessionServices,
34
+ getAgentDir,
33
35
  InteractiveMode,
34
36
  SessionManager,
35
37
  } from '@mariozechner/pi-coding-agent';
@@ -49,6 +51,11 @@ import { RemoteSession } from './remote-session.ts';
49
51
  import type { RpcEvent } from './remote-session.ts';
50
52
  import { agentuityCoderHub } from './index.ts';
51
53
  import { handleRemoteUiRequest, REMOTE_FIRE_AND_FORGET_UI_METHODS } from './remote-ui-handler.ts';
54
+ import {
55
+ dispatchRemoteSessionEvent,
56
+ installRemoteRuntimeOperationGuards,
57
+ REMOTE_RUNTIME_OPERATION_MESSAGE,
58
+ } from './remote-runtime.ts';
52
59
 
53
60
  const DEBUG = !!process.env['AGENTUITY_DEBUG'];
54
61
 
@@ -60,7 +67,7 @@ function log(msg: string): void {
60
67
  * Run the native Pi TUI connected to a remote sandbox session.
61
68
  *
62
69
  * This is the entry point for `agentuity coder start --remote <sessionId>`.
63
- * Creates an AgentSession with the coder extension loaded (Hub UI), then
70
+ * Creates an AgentSessionRuntime with the coder extension loaded (Hub UI), then
64
71
  * monkey-patches the Agent for remote-backed execution.
65
72
  */
66
73
  export async function runRemoteTui(options: {
@@ -90,33 +97,74 @@ export async function runRemoteTui(options: {
90
97
  let hydrationStreamingDetected = false;
91
98
  let sessionResumeSeen = false;
92
99
 
93
- // ── 2. Create AgentSession with coder extension loaded ──
100
+ // ── 2. Create AgentSessionRuntime with coder extension loaded ──
94
101
  // The extension provides Hub UI (footer, /hub overlay, commands, titlebar).
95
102
  // AGENTUITY_CODER_NATIVE_REMOTE=1 tells it to skip legacy event rendering.
96
103
  const cwd = process.cwd();
97
- const resourceLoader = new DefaultResourceLoader({
98
- cwd,
99
- noExtensions: true, // Skip file-system extension discovery
100
- extensionFactories: [agentuityCoderHub], // Load coder extension directly
101
- });
102
- await resourceLoader.reload();
104
+ const runtime = await createAgentSessionRuntime(
105
+ async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
106
+ const services = await createAgentSessionServices({
107
+ cwd,
108
+ agentDir,
109
+ resourceLoaderOptions: {
110
+ noExtensions: true, // Skip file-system extension discovery
111
+ extensionFactories: [agentuityCoderHub], // Load coder extension directly
112
+ },
113
+ });
114
+
115
+ return {
116
+ ...(await createAgentSessionFromServices({
117
+ services,
118
+ sessionManager,
119
+ sessionStartEvent,
120
+ tools: [], // No local tools — sandbox has all the tools
121
+ })),
122
+ services,
123
+ diagnostics: services.diagnostics,
124
+ };
125
+ },
126
+ {
127
+ cwd,
128
+ agentDir: getAgentDir(),
129
+ sessionManager: SessionManager.inMemory(),
130
+ }
131
+ );
132
+ const session = runtime.session;
133
+ const agent: any = session.agent;
134
+ let runtimeDisposed = false;
135
+ log('AgentSessionRuntime created');
103
136
 
104
- const { session } = await createAgentSession({
105
- sessionManager: SessionManager.inMemory(),
106
- tools: [], // No local tools — sandbox has all the tools
107
- resourceLoader,
108
- });
109
- log('AgentSession created');
137
+ function notifyBlockedRuntimeOperation(operation: string): void {
138
+ const message = `${REMOTE_RUNTIME_OPERATION_MESSAGE} (${operation})`;
139
+ const ctx = getNativeRemoteExtensionContext();
140
+ if (ctx?.hasUI) {
141
+ ctx.ui.notify(message, 'warning');
142
+ ctx.ui.setStatus('remote_runtime', operation);
143
+ return;
144
+ }
145
+ log(message);
146
+ }
147
+
148
+ installRemoteRuntimeOperationGuards(runtime as any, (operation) =>
149
+ notifyBlockedRuntimeOperation(operation)
150
+ );
151
+
152
+ async function disposeRuntime(): Promise<void> {
153
+ if (runtimeDisposed) return;
154
+ runtimeDisposed = true;
155
+ await runtime.dispose();
156
+ }
110
157
 
111
158
  // NOTE: Do NOT call session.bindExtensions() here.
112
159
  // InteractiveMode.initExtensions() calls it with the proper uiContext.
113
160
  // Calling it early fires session_start twice, duplicating extension init.
114
-
115
- // Access the Agent instance (typed as `any` for monkey-patching)
116
- const agent: any = session.agent;
117
161
  let lifecycleState = remote.getLifecycleState();
118
162
  let lifecycleOwnsWorkingMessage = false;
119
163
 
164
+ function emitSessionEvent(event: RpcEvent): void {
165
+ dispatchRemoteSessionEvent(session as any, event);
166
+ }
167
+
120
168
  function applyLifecycleUi(state: RemoteLifecycleState): void {
121
169
  const ctx = getNativeRemoteExtensionContext();
122
170
  if (!ctx?.hasUI) return;
@@ -174,7 +222,7 @@ export async function runRemoteTui(options: {
174
222
 
175
223
  // Emit synthetic agent_start so InteractiveMode shows "working" immediately
176
224
  syntheticAgentStartEmitted = true;
177
- agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() });
225
+ emitSessionEvent({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() } as any);
178
226
 
179
227
  // Send RPC command to sandbox
180
228
  remote.prompt(text);
@@ -268,7 +316,7 @@ export async function runRemoteTui(options: {
268
316
  //
269
317
  // Events that arrive before InteractiveMode is initialized are buffered
270
318
  // and flushed after init (InteractiveMode registers listeners during init,
271
- // so agent.emit() before that fires into the void).
319
+ // so session event dispatch before that fires into the void).
272
320
  let interactiveModeReady = false;
273
321
  let eventBuffer: RpcEvent[] = [];
274
322
  let seenMessageStart = false;
@@ -315,7 +363,7 @@ export async function runRemoteTui(options: {
315
363
  }
316
364
 
317
365
  for (const event of syntheticEvents) {
318
- agent.emit(event);
366
+ emitSessionEvent(event);
319
367
  }
320
368
  }
321
369
 
@@ -507,10 +555,14 @@ export async function runRemoteTui(options: {
507
555
  ) {
508
556
  log(`Live ${rpcEvent.type} without prior message_start — injecting synthetics`);
509
557
  if (!hadSeenAgentStart) {
510
- agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() } as any);
558
+ emitSessionEvent({
559
+ type: 'agent_start',
560
+ agentName: 'lead',
561
+ timestamp: Date.now(),
562
+ } as any);
511
563
  seenAgentStart = true;
512
564
  }
513
- agent.emit({
565
+ emitSessionEvent({
514
566
  type: 'message_start',
515
567
  message: {
516
568
  role: 'assistant',
@@ -535,7 +587,7 @@ export async function runRemoteTui(options: {
535
587
  }
536
588
 
537
589
  // Emit to subscribers — InteractiveMode.handleEvent processes this
538
- agent.emit(rpcEvent);
590
+ emitSessionEvent(rpcEvent);
539
591
  if (injectedSyntheticMessageStart && rpcEvent.type === 'message_end') {
540
592
  seenMessageStart = false;
541
593
  seenAgentStart = hadSeenAgentStart;
@@ -787,7 +839,7 @@ export async function runRemoteTui(options: {
787
839
 
788
840
  // ── 9. Start InteractiveMode — full native Pi TUI ──
789
841
  log('Creating InteractiveMode');
790
- const interactive = new InteractiveMode(session);
842
+ const interactive = new InteractiveMode(runtime);
791
843
  log('InteractiveMode created, calling init...');
792
844
  await interactive.init();
793
845
 
@@ -803,8 +855,8 @@ export async function runRemoteTui(options: {
803
855
  // the streaming indicator right away, before any buffered events flush.
804
856
  // This prevents the blank screen gap between connect and first event.
805
857
  log('Hydration detected streaming — emitting immediate synthetics');
806
- agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() } as any);
807
- agent.emit({
858
+ emitSessionEvent({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() } as any);
859
+ emitSessionEvent({
808
860
  type: 'message_start',
809
861
  message: {
810
862
  role: 'assistant',
@@ -836,7 +888,7 @@ export async function runRemoteTui(options: {
836
888
  if (eventBuffer.length > 0) {
837
889
  log(`Flushing ${eventBuffer.length} events: ${eventBuffer.map((e) => e.type).join(', ')}`);
838
890
  for (const buffered of eventBuffer) {
839
- agent.emit(buffered);
891
+ emitSessionEvent(buffered);
840
892
  if (buffered.type === 'agent_end') {
841
893
  resolveRunningPrompt();
842
894
  }
@@ -851,6 +903,11 @@ export async function runRemoteTui(options: {
851
903
  remote.close();
852
904
  setNativeRemoteExtensionContext(null);
853
905
  interactive.stop();
906
+ void disposeRuntime().catch((err) => {
907
+ log(
908
+ `Runtime dispose error during cleanup: ${err instanceof Error ? err.message : String(err)}`
909
+ );
910
+ });
854
911
  };
855
912
  process.on('SIGINT', cleanup);
856
913
  process.on('SIGTERM', cleanup);
@@ -863,6 +920,9 @@ export async function runRemoteTui(options: {
863
920
  } finally {
864
921
  remote.close();
865
922
  setNativeRemoteExtensionContext(null);
923
+ await disposeRuntime().catch((err) => {
924
+ log(`Runtime dispose error: ${err instanceof Error ? err.message : String(err)}`);
925
+ });
866
926
  log('Remote TUI exited');
867
927
  }
868
928