u-foo 2.3.10 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.10",
3
+ "version": "2.3.11",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -45,6 +45,8 @@
45
45
  "bench:global-switch": "node scripts/global-chat-switch-benchmark.js"
46
46
  },
47
47
  "dependencies": {
48
+ "@anthropic-ai/claude-agent-sdk": "^0.2.138",
49
+ "@openai/codex-sdk": "^0.130.0",
48
50
  "blessed": "^0.1.81",
49
51
  "chalk": "^4.1.2",
50
52
  "commander": "^13.1.0",
@@ -3,9 +3,10 @@
3
3
  const {
4
4
  createClaudeEventState,
5
5
  normalizeClaudeEvent,
6
+ normalizeClaudeMessage,
7
+ normalizeClaudeUsage,
6
8
  } = require("./claudeEventTranslator");
7
9
  const { redactUfooEvent } = require("../providerapi/redactor");
8
- const { sendUpstreamRequest } = require("./upstreamTransport");
9
10
 
10
11
  const CACHE_CONTROL = Object.freeze({ type: "ephemeral" });
11
12
 
@@ -26,6 +27,25 @@ function resolveAnthropicSdk() {
26
27
  }
27
28
  }
28
29
 
30
+ async function resolveClaudeAgentSdk() {
31
+ try {
32
+ return await import("@anthropic-ai/claude-agent-sdk");
33
+ } catch (err) {
34
+ const error = new Error("Claude Agent SDK mode requires @anthropic-ai/claude-agent-sdk");
35
+ error.code = "CLAUDE_AGENT_SDK_UNAVAILABLE";
36
+ error.cause = err;
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ function resolveClaudeAgentQuery(sdk) {
42
+ const query = sdk && (sdk.query || (sdk.default && sdk.default.query));
43
+ if (typeof query !== "function") {
44
+ throw new Error("Claude Agent SDK module does not export query");
45
+ }
46
+ return query;
47
+ }
48
+
29
49
  async function defaultClaudeAuthProvider() {
30
50
  const apiKey = String(process.env.ANTHROPIC_API_KEY || "").trim();
31
51
  if (!apiKey) {
@@ -61,43 +81,12 @@ function defaultClaudeStreamFactory({ client, request }) {
61
81
  });
62
82
  }
63
83
 
64
- async function* defaultClaudeTransportStreamFactory({ request, auth = {}, model = "", attempt = 0 }) {
65
- const runtime = {
66
- provider: "claude",
67
- transport: "anthropic-messages",
68
- model: String(model || request.model || "").trim(),
69
- baseUrl: String(auth.baseUrl || process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com/v1").trim(),
70
- auth,
71
- credentialSource: auth.apiKey ? "thread-auth" : "thread-headers",
72
- };
73
- const result = await sendUpstreamRequest({
74
- runtime,
75
- request,
76
- timeoutMs: Number.isFinite(Number(request.timeout_ms)) ? Number(request.timeout_ms) : 120000,
84
+ function defaultClaudeAgentStreamFactory({ sdk, input, options }) {
85
+ const query = resolveClaudeAgentQuery(sdk);
86
+ return query({
87
+ prompt: String(input || ""),
88
+ options,
77
89
  });
78
- if (!result.ok) {
79
- const err = new Error(result.error || "Claude upstream request failed");
80
- err.code = "CLAUDE_UPSTREAM_FAILED";
81
- err.attempt = attempt;
82
- throw err;
83
- }
84
-
85
- yield {
86
- type: "message_start",
87
- message: {
88
- id: `msg-${Date.now().toString(36)}`,
89
- usage: result.usage || undefined,
90
- },
91
- };
92
- yield {
93
- type: "content_block_delta",
94
- index: 0,
95
- delta: {
96
- type: "text_delta",
97
- text: String(result.output || ""),
98
- },
99
- };
100
- yield { type: "message_stop" };
101
90
  }
102
91
 
103
92
  function normalizeToolDefinition(tool = {}) {
@@ -217,6 +206,74 @@ function createAssistantMessageFromState(state) {
217
206
  };
218
207
  }
219
208
 
209
+ function extraArgsToObject(extraArgs = []) {
210
+ const result = {};
211
+ if (!Array.isArray(extraArgs)) return result;
212
+ for (let i = 0; i < extraArgs.length; i += 1) {
213
+ const raw = String(extraArgs[i] || "");
214
+ if (!raw.startsWith("--")) continue;
215
+ const key = raw.replace(/^--+/, "");
216
+ if (!key) continue;
217
+ const next = i + 1 < extraArgs.length ? String(extraArgs[i + 1] || "") : "";
218
+ if (!next || next.startsWith("--")) {
219
+ result[key] = null;
220
+ continue;
221
+ }
222
+ result[key] = next;
223
+ i += 1;
224
+ }
225
+ return result;
226
+ }
227
+
228
+ function buildClaudeAgentOptions({
229
+ model = "",
230
+ cwd = "",
231
+ threadId = "",
232
+ extraArgs = [],
233
+ agentOptions = {},
234
+ opts = {},
235
+ } = {}) {
236
+ const options = {
237
+ ...agentOptions,
238
+ ...(opts && typeof opts.agentOptions === "object" ? opts.agentOptions : {}),
239
+ };
240
+ if (model && options.model === undefined) options.model = model;
241
+ if (cwd && options.cwd === undefined) options.cwd = cwd;
242
+ if (options.includePartialMessages === undefined) options.includePartialMessages = true;
243
+ if (options.extraArgs === undefined) {
244
+ const converted = extraArgsToObject(extraArgs);
245
+ if (Object.keys(converted).length > 0) options.extraArgs = converted;
246
+ }
247
+ if (threadId && !options.resume && !options.continue && !options.sessionId) {
248
+ options.resume = threadId;
249
+ }
250
+ if (opts && opts.abortController && options.abortController === undefined) {
251
+ options.abortController = opts.abortController;
252
+ }
253
+ if (opts && opts.signal && options.abortController === undefined) {
254
+ const controller = new AbortController();
255
+ opts.signal.addEventListener("abort", () => controller.abort(opts.signal.reason), { once: true });
256
+ options.abortController = controller;
257
+ }
258
+ return options;
259
+ }
260
+
261
+ function messageTextFromContent(content = []) {
262
+ if (!Array.isArray(content)) return "";
263
+ return content
264
+ .map((block) => (block && block.type === "text" ? String(block.text || "") : ""))
265
+ .join("");
266
+ }
267
+
268
+ function resultErrorMessage(message = {}) {
269
+ if (Array.isArray(message.errors) && message.errors.length > 0) {
270
+ return message.errors.map((item) => String(item || "")).filter(Boolean).join("\n");
271
+ }
272
+ if (typeof message.result === "string" && message.result) return message.result;
273
+ if (message.subtype) return String(message.subtype);
274
+ return "Claude Agent SDK query failed";
275
+ }
276
+
220
277
  function shouldRetryClaudeStream(err, attempt) {
221
278
  if (attempt >= 1) return false;
222
279
  const code = String((err && err.code) || "").trim().toUpperCase();
@@ -228,23 +285,132 @@ function shouldRetryClaudeStream(err, attempt) {
228
285
  class ClaudeApiThread {
229
286
  constructor({
230
287
  model = "",
288
+ cwd = "",
289
+ extraArgs = [],
231
290
  authProvider = defaultClaudeAuthProvider,
232
291
  clientFactory = defaultClaudeClientFactory,
233
- streamFactory = defaultClaudeStreamFactory,
292
+ streamFactory = defaultClaudeAgentStreamFactory,
234
293
  sdk,
294
+ agentOptions = {},
235
295
  maxTokens = 4096,
236
296
  } = {}) {
237
297
  this.id = "";
238
298
  this.model = model;
299
+ this.cwd = cwd;
300
+ this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
239
301
  this.authProvider = authProvider;
240
302
  this.clientFactory = clientFactory;
241
303
  this.streamFactory = streamFactory;
242
304
  this.sdk = sdk;
305
+ this.agentOptions = { ...agentOptions };
243
306
  this.maxTokens = maxTokens;
244
307
  this.messages = [];
245
308
  }
246
309
 
247
310
  async *runStreamed(input, opts = {}) {
311
+ if (this.streamFactory === defaultClaudeAgentStreamFactory) {
312
+ yield* this.runAgentSdkStreamed(input, opts);
313
+ return;
314
+ }
315
+ yield* this.runMessagesStreamed(input, opts);
316
+ }
317
+
318
+ async *runAgentSdkStreamed(input, opts = {}) {
319
+ if (!this.sdk) {
320
+ this.sdk = await resolveClaudeAgentSdk();
321
+ }
322
+ const options = buildClaudeAgentOptions({
323
+ model: this.model,
324
+ cwd: this.cwd,
325
+ threadId: this.id,
326
+ extraArgs: this.extraArgs,
327
+ agentOptions: this.agentOptions,
328
+ opts,
329
+ });
330
+ const stream = await this.streamFactory({
331
+ sdk: this.sdk,
332
+ input,
333
+ options,
334
+ model: this.model,
335
+ cwd: this.cwd,
336
+ threadId: this.id,
337
+ opts,
338
+ });
339
+ const state = createClaudeEventState({ threadId: this.id });
340
+ let threadStarted = false;
341
+ let sawStreamEvents = false;
342
+ let sawText = false;
343
+ let sawTurnCompleted = false;
344
+
345
+ const emitThreadStarted = function* emitThreadStarted(self, sessionId = "") {
346
+ const nextId = String(sessionId || self.id || "").trim();
347
+ if (nextId) self.id = nextId;
348
+ if (!threadStarted && self.id) {
349
+ threadStarted = true;
350
+ yield redactUfooEvent({ type: "thread_started", threadId: self.id });
351
+ }
352
+ };
353
+
354
+ for await (const message of stream) {
355
+ if (!message || typeof message !== "object") continue;
356
+ const sessionId = String(message.session_id || message.sessionId || "").trim();
357
+ for (const event of emitThreadStarted(this, sessionId)) yield event;
358
+
359
+ if (message.type === "stream_event") {
360
+ sawStreamEvents = true;
361
+ const events = normalizeClaudeEvent(message.event || {}, state);
362
+ for (const event of events) {
363
+ if (!event || typeof event !== "object") continue;
364
+ if (event.type === "text_delta" && event.delta) sawText = true;
365
+ if (event.type === "turn_completed") sawTurnCompleted = true;
366
+ yield redactUfooEvent(event);
367
+ }
368
+ continue;
369
+ }
370
+
371
+ if (message.type === "assistant") {
372
+ if (sawStreamEvents) continue;
373
+ const events = normalizeClaudeMessage(message.message || {});
374
+ for (const event of events) {
375
+ if (!event || typeof event !== "object") continue;
376
+ if (event.type === "text_delta" && event.delta) sawText = true;
377
+ if (event.type === "turn_completed") sawTurnCompleted = true;
378
+ yield redactUfooEvent(event);
379
+ }
380
+ continue;
381
+ }
382
+
383
+ if (message.type === "result") {
384
+ if (message.is_error) {
385
+ yield redactUfooEvent({
386
+ type: "turn_failed",
387
+ turnId: state.turnId || message.uuid || "",
388
+ error: resultErrorMessage(message),
389
+ });
390
+ continue;
391
+ }
392
+ if (!sawText && typeof message.result === "string" && message.result) {
393
+ sawText = true;
394
+ yield redactUfooEvent({
395
+ type: "text_delta",
396
+ delta: message.result,
397
+ itemType: "text",
398
+ });
399
+ }
400
+ if (!sawTurnCompleted) {
401
+ sawTurnCompleted = true;
402
+ yield redactUfooEvent({
403
+ type: "turn_completed",
404
+ turnId: state.turnId || message.uuid || "",
405
+ usage: normalizeClaudeUsage(message.usage || null),
406
+ stopReason: String(message.stop_reason || ""),
407
+ });
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ async *runMessagesStreamed(input, opts = {}) {
248
414
  if (!this.id) this.id = createThreadId();
249
415
  const userMessage = normalizeMessageInput(input);
250
416
  const requestMessages = this.messages.slice();
@@ -332,12 +498,15 @@ module.exports = {
332
498
  ClaudeApiThread,
333
499
  ClaudeThreadProvider,
334
500
  createClaudeThreadProvider,
501
+ buildClaudeAgentOptions,
335
502
  defaultClaudeAuthProvider,
503
+ defaultClaudeAgentStreamFactory,
336
504
  defaultClaudeClientFactory,
337
505
  defaultClaudeStreamFactory,
338
- defaultClaudeTransportStreamFactory,
506
+ extraArgsToObject,
339
507
  normalizeMessageInput,
340
508
  normalizeToolDefinition,
509
+ resolveClaudeAgentSdk,
341
510
  resolveAnthropicSdk,
342
511
  withCacheControlOnLastBlock,
343
512
  };
@@ -1,80 +1,79 @@
1
1
  const { normalizeCodexEvent } = require("./codexEventTranslator");
2
2
  const { redactUfooEvent } = require("../providerapi/redactor");
3
- const { sendUpstreamPrompt } = require("./upstreamTransport");
4
3
 
5
- function resolveCodexSdk() {
4
+ async function resolveCodexSdk() {
6
5
  try {
7
- // Optional dependency during Phase 1a seam work.
8
- // eslint-disable-next-line global-require, import/no-extraneous-dependencies
9
- return require("@openai/codex-sdk");
6
+ return await import("@openai/codex-sdk");
10
7
  } catch (err) {
11
- const error = new Error("Codex SDK seam enabled but @openai/codex-sdk is not installed");
8
+ const error = new Error("Codex SDK mode requires @openai/codex-sdk");
12
9
  error.code = "CODEX_SDK_UNAVAILABLE";
13
10
  error.cause = err;
14
11
  throw error;
15
12
  }
16
13
  }
17
14
 
18
- function defaultCodexStreamFactory({
19
- sdk,
20
- model,
21
- cwd,
22
- extraArgs = [],
23
- threadId = "",
24
- input,
25
- opts = {},
26
- }) {
27
- if (!sdk || typeof sdk.runStreamed !== "function") {
28
- throw new Error("Codex SDK seam requires runStreamed support");
15
+ function resolveCodexConstructor(sdk) {
16
+ const Codex = sdk && (sdk.Codex || (sdk.default && sdk.default.Codex) || sdk.default);
17
+ if (typeof Codex !== "function") {
18
+ throw new Error("Codex SDK module does not export Codex");
29
19
  }
30
- const { history, ...sdkOpts } = opts;
31
- void history;
32
- return sdk.runStreamed({
33
- model,
34
- cwd,
35
- extraArgs,
36
- threadId,
37
- input,
38
- ...sdkOpts,
39
- });
20
+ return Codex;
40
21
  }
41
22
 
42
- function createThreadId() {
43
- return `codex-thread-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
23
+ function buildCodexOptions({ codexOptions = {} } = {}) {
24
+ return { ...codexOptions };
44
25
  }
45
26
 
46
- async function* defaultCodexTransportStreamFactory({
47
- model,
48
- cwd,
49
- threadId = "",
27
+ function buildThreadOptions({
28
+ model = "",
29
+ cwd = "",
30
+ threadOptions = {},
31
+ } = {}) {
32
+ const options = { ...threadOptions };
33
+ if (model && options.model === undefined) {
34
+ options.model = model;
35
+ }
36
+ if (cwd && options.workingDirectory === undefined) {
37
+ options.workingDirectory = cwd;
38
+ }
39
+ if (options.skipGitRepoCheck === undefined) {
40
+ options.skipGitRepoCheck = true;
41
+ }
42
+ if (options.sandboxMode === undefined) {
43
+ options.sandboxMode = "workspace-write";
44
+ }
45
+ return options;
46
+ }
47
+
48
+ function buildTurnOptions(opts = {}) {
49
+ const {
50
+ history,
51
+ tools,
52
+ timeoutMs,
53
+ ...turnOptions
54
+ } = opts || {};
55
+ void history;
56
+ void tools;
57
+ void timeoutMs;
58
+ return turnOptions;
59
+ }
60
+
61
+ async function* defaultCodexStreamFactory({
62
+ thread,
50
63
  input,
51
64
  opts = {},
52
65
  }) {
53
- const nextThreadId = String(threadId || "").trim() || createThreadId();
54
- const result = await sendUpstreamPrompt({
55
- projectRoot: cwd,
56
- provider: "codex",
57
- model,
58
- prompt: String(input || ""),
59
- messages: Array.isArray(opts.history) ? opts.history : [],
60
- timeoutMs: Number.isFinite(Number(opts.timeoutMs)) ? Number(opts.timeoutMs) : 120000,
61
- });
62
- if (!result.ok) {
63
- const err = new Error(result.error || "Codex upstream request failed");
64
- err.code = result.errorCode || "CODEX_UPSTREAM_FAILED";
65
- throw err;
66
- }
67
-
68
- yield { type: "thread.started", thread_id: nextThreadId };
69
- yield {
70
- type: "item.completed",
71
- item: { type: "message", text: String(result.output || "") },
72
- };
73
- yield {
74
- type: "turn.completed",
75
- turn_id: `turn-${Date.now().toString(36)}`,
76
- usage: result.usage || null,
77
- };
66
+ if (!thread || typeof thread.runStreamed !== "function") {
67
+ throw new Error("Codex SDK thread requires runStreamed support");
68
+ }
69
+ const streamed = await thread.runStreamed(String(input || ""), buildTurnOptions(opts));
70
+ const events = streamed && streamed.events ? streamed.events : streamed;
71
+ if (!events || typeof events[Symbol.asyncIterator] !== "function") {
72
+ throw new Error("Codex SDK runStreamed did not return an async event stream");
73
+ }
74
+ for await (const event of events) {
75
+ yield event;
76
+ }
78
77
  }
79
78
 
80
79
  class CodexSdkThread {
@@ -84,26 +83,67 @@ class CodexSdkThread {
84
83
  extraArgs = [],
85
84
  streamFactory = defaultCodexStreamFactory,
86
85
  sdk,
86
+ codexClient,
87
+ sdkThread,
88
+ threadId = "",
87
89
  tools = [],
90
+ codexOptions = {},
91
+ threadOptions = {},
88
92
  } = {}) {
89
- this.id = "";
93
+ this.id = String(threadId || "").trim();
90
94
  this.model = model;
91
95
  this.cwd = cwd;
92
96
  this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
93
97
  this.streamFactory = streamFactory;
94
98
  this.sdk = sdk;
99
+ this.codexClient = codexClient || null;
100
+ this.sdkThread = sdkThread || null;
95
101
  this.tools = Array.isArray(tools) ? tools.slice() : [];
102
+ this.codexOptions = buildCodexOptions({ codexOptions });
103
+ this.threadOptions = { ...threadOptions };
96
104
  this.messages = [];
97
105
  }
98
106
 
107
+ async getCodexClient() {
108
+ if (this.codexClient) return this.codexClient;
109
+ if (!this.sdk) {
110
+ this.sdk = await resolveCodexSdk();
111
+ }
112
+ const Codex = resolveCodexConstructor(this.sdk);
113
+ this.codexClient = new Codex(this.codexOptions);
114
+ return this.codexClient;
115
+ }
116
+
117
+ async getSdkThread() {
118
+ if (this.sdkThread) return this.sdkThread;
119
+ const client = await this.getCodexClient();
120
+ const options = buildThreadOptions({
121
+ model: this.model,
122
+ cwd: this.cwd,
123
+ threadOptions: this.threadOptions,
124
+ });
125
+ this.sdkThread = this.id && typeof client.resumeThread === "function"
126
+ ? client.resumeThread(this.id, options)
127
+ : client.startThread(options);
128
+ if (this.sdkThread && this.sdkThread.id) {
129
+ this.id = this.sdkThread.id;
130
+ }
131
+ return this.sdkThread;
132
+ }
133
+
99
134
  async *runStreamed(input, opts = {}) {
100
135
  const mergedOpts = { ...opts };
101
136
  if (this.tools.length > 0 && !Array.isArray(mergedOpts.tools)) {
102
137
  mergedOpts.tools = this.tools.slice();
103
138
  }
104
139
  mergedOpts.history = this.messages.slice();
140
+ const sdkThread = this.streamFactory === defaultCodexStreamFactory
141
+ ? await this.getSdkThread()
142
+ : this.sdkThread;
105
143
  const stream = this.streamFactory({
106
144
  sdk: this.sdk,
145
+ thread: sdkThread,
146
+ codexClient: this.codexClient,
107
147
  model: this.model,
108
148
  cwd: this.cwd,
109
149
  extraArgs: this.extraArgs,
@@ -123,6 +163,9 @@ class CodexSdkThread {
123
163
  }
124
164
  yield redactUfooEvent(normalized);
125
165
  }
166
+ if (sdkThread && sdkThread.id) {
167
+ this.id = sdkThread.id;
168
+ }
126
169
  this.messages.push({ role: "user", content: String(input || "") });
127
170
  this.messages.push({ role: "assistant", content: outputText });
128
171
  }
@@ -139,14 +182,20 @@ class CodexThreadProvider {
139
182
  extraArgs = [],
140
183
  streamFactory = defaultCodexStreamFactory,
141
184
  sdk,
185
+ codexClient,
142
186
  tools = [],
187
+ codexOptions = {},
188
+ threadOptions = {},
143
189
  } = {}) {
144
190
  this.model = model;
145
191
  this.cwd = cwd;
146
192
  this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
147
193
  this.streamFactory = streamFactory;
148
- this.sdk = sdk || (streamFactory === defaultCodexStreamFactory ? resolveCodexSdk() : null);
194
+ this.sdk = sdk || null;
195
+ this.codexClient = codexClient || null;
149
196
  this.tools = Array.isArray(tools) ? tools.slice() : [];
197
+ this.codexOptions = buildCodexOptions({ codexOptions });
198
+ this.threadOptions = { ...threadOptions };
150
199
  }
151
200
 
152
201
  startThread() {
@@ -156,14 +205,26 @@ class CodexThreadProvider {
156
205
  extraArgs: this.extraArgs,
157
206
  streamFactory: this.streamFactory,
158
207
  sdk: this.sdk,
208
+ codexClient: this.codexClient,
159
209
  tools: this.tools,
210
+ codexOptions: this.codexOptions,
211
+ threadOptions: this.threadOptions,
160
212
  });
161
213
  }
162
214
 
163
215
  resumeThread(threadId = "") {
164
- const thread = this.startThread();
165
- thread.id = String(threadId || "").trim();
166
- return thread;
216
+ return new CodexSdkThread({
217
+ model: this.model,
218
+ cwd: this.cwd,
219
+ extraArgs: this.extraArgs,
220
+ streamFactory: this.streamFactory,
221
+ sdk: this.sdk,
222
+ codexClient: this.codexClient,
223
+ threadId,
224
+ tools: this.tools,
225
+ codexOptions: this.codexOptions,
226
+ threadOptions: this.threadOptions,
227
+ });
167
228
  }
168
229
  }
169
230
 
@@ -175,7 +236,7 @@ module.exports = {
175
236
  CodexSdkThread,
176
237
  CodexThreadProvider,
177
238
  createCodexThreadProvider,
239
+ buildThreadOptions,
178
240
  defaultCodexStreamFactory,
179
- defaultCodexTransportStreamFactory,
180
241
  resolveCodexSdk,
181
242
  };
@@ -7,14 +7,8 @@ const { runCliAgent } = require("./cliRunner");
7
7
  const { normalizeCliOutput } = require("./normalizeOutput");
8
8
  const { createActivityStatePublisher } = require("./activityStatePublisher");
9
9
  const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
10
- const {
11
- createCodexThreadProvider,
12
- defaultCodexTransportStreamFactory,
13
- } = require("./codexThreadProvider");
14
- const {
15
- createClaudeThreadProvider,
16
- defaultClaudeTransportStreamFactory,
17
- } = require("./claudeThreadProvider");
10
+ const { createCodexThreadProvider } = require("./codexThreadProvider");
11
+ const { createClaudeThreadProvider } = require("./claudeThreadProvider");
18
12
  const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
19
13
  const { buildUpstreamAuthFromCredential } = require("./credentials");
20
14
  const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
@@ -131,6 +125,73 @@ function drainQueue(queueFile) {
131
125
  return content.split(/\r?\n/).filter(Boolean);
132
126
  }
133
127
 
128
+ function parseAgentViewRawInput(message) {
129
+ if (typeof message !== "string" || !message.trim()) return null;
130
+ try {
131
+ const parsed = JSON.parse(message);
132
+ if (parsed && parsed.raw === true && typeof parsed.data === "string") {
133
+ return parsed.data;
134
+ }
135
+ } catch {
136
+ // Not a raw agent-view envelope.
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function createInteractiveInputSession({ write = () => {} } = {}) {
142
+ let buffer = "";
143
+
144
+ function writePrompt() {
145
+ write("> ");
146
+ }
147
+
148
+ function handleRaw(data) {
149
+ const submissions = [];
150
+ const text = String(data || "");
151
+ for (const char of text) {
152
+ if (char === "\r" || char === "\n") {
153
+ const submitted = buffer.trim();
154
+ buffer = "";
155
+ write("\r\n");
156
+ if (submitted) submissions.push(submitted);
157
+ else writePrompt();
158
+ continue;
159
+ }
160
+
161
+ if (char === "\u0003") {
162
+ buffer = "";
163
+ write("^C\r\n");
164
+ writePrompt();
165
+ continue;
166
+ }
167
+
168
+ if (char === "\u007f" || char === "\b") {
169
+ if (buffer.length > 0) {
170
+ buffer = buffer.slice(0, -1);
171
+ write("\b \b");
172
+ }
173
+ continue;
174
+ }
175
+
176
+ if (char >= " " && char !== "\u007f") {
177
+ buffer += char;
178
+ write(char);
179
+ }
180
+ }
181
+ return submissions;
182
+ }
183
+
184
+ return {
185
+ handleRaw,
186
+ writePrompt,
187
+ writeResponsePrompt: () => {
188
+ write("\r\n");
189
+ writePrompt();
190
+ },
191
+ getBuffer: () => buffer,
192
+ };
193
+ }
194
+
134
195
  async function handleEvent(
135
196
  projectRoot,
136
197
  agentType,
@@ -259,6 +320,9 @@ async function handleThreadedEvent({
259
320
  const plainReplyParts = [];
260
321
  for await (const event of threadRuntime.thread.runStreamed(prompt, {})) {
261
322
  if (!event || typeof event !== "object") continue;
323
+ if (typeof threadRuntime.syncProviderSessionId === "function") {
324
+ threadRuntime.syncProviderSessionId();
325
+ }
262
326
  if (event.type === "text_delta" && event.delta) {
263
327
  if (streamToPublisher) {
264
328
  emitStreamDelta(event.delta);
@@ -269,6 +333,9 @@ async function handleThreadedEvent({
269
333
  throw new Error(event.error || `thread turn failed for ${agentType}`);
270
334
  }
271
335
  }
336
+ if (typeof threadRuntime.syncProviderSessionId === "function") {
337
+ threadRuntime.syncProviderSessionId();
338
+ }
272
339
 
273
340
  if (streamToPublisher) {
274
341
  busSender.enqueue(
@@ -420,15 +487,47 @@ function buildClaudeAuthProvider(projectRoot) {
420
487
  };
421
488
  }
422
489
 
423
- function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "" }) {
490
+ function persistProviderSessionId(projectRoot, subscriber, providerSessionId) {
491
+ const id = String(providerSessionId || "").trim();
492
+ if (!projectRoot || !subscriber || !id) return false;
493
+ try {
494
+ const agentsFile = getUfooPaths(projectRoot).agentsFile;
495
+ const parsed = fs.existsSync(agentsFile)
496
+ ? JSON.parse(fs.readFileSync(agentsFile, "utf8"))
497
+ : {};
498
+ if (!parsed.agents || typeof parsed.agents !== "object") return false;
499
+ if (!parsed.agents[subscriber] || typeof parsed.agents[subscriber] !== "object") return false;
500
+ if (parsed.agents[subscriber].provider_session_id === id) return false;
501
+ parsed.agents[subscriber].provider_session_id = id;
502
+ parsed.agents[subscriber].provider_session_updated_at = new Date().toISOString();
503
+ fs.writeFileSync(agentsFile, `${JSON.stringify(parsed, null, 2)}\n`);
504
+ return true;
505
+ } catch {
506
+ return false;
507
+ }
508
+ }
509
+
510
+ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "", providerSessionId = "" }) {
424
511
  const disabledRuntime = {
425
512
  enabled: false,
426
513
  thread: null,
427
514
  toolRuntime: { enabled: false, mode: "disabled", tools: [] },
428
515
  close: async () => {},
429
516
  rebuildThread: async () => {},
517
+ syncProviderSessionId: () => false,
430
518
  };
431
519
 
520
+ const initialProviderSessionId = String(providerSessionId || "").trim();
521
+ let savedProviderSessionId = initialProviderSessionId;
522
+
523
+ function rememberProviderSessionId(thread) {
524
+ const id = String(thread && thread.id ? thread.id : "").trim();
525
+ if (!id || id === savedProviderSessionId) return false;
526
+ const changed = persistProviderSessionId(projectRoot, subscriber, id);
527
+ if (changed) savedProviderSessionId = id;
528
+ return changed;
529
+ }
530
+
432
531
  if (provider === "codex-cli") {
433
532
  if (getCodexThreadMode(projectRoot) !== "api") {
434
533
  return disabledRuntime;
@@ -444,9 +543,10 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
444
543
  cwd: projectRoot,
445
544
  extraArgs,
446
545
  tools: toolRuntime.tools,
447
- streamFactory: defaultCodexTransportStreamFactory,
448
546
  });
449
- let thread = providerInstance.startThread();
547
+ let thread = initialProviderSessionId
548
+ ? providerInstance.resumeThread(initialProviderSessionId)
549
+ : providerInstance.startThread();
450
550
 
451
551
  return {
452
552
  enabled: true,
@@ -454,6 +554,9 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
454
554
  get thread() {
455
555
  return thread;
456
556
  },
557
+ syncProviderSessionId() {
558
+ return rememberProviderSessionId(thread);
559
+ },
457
560
  async rebuildThread() {
458
561
  if (thread && typeof thread.close === "function") {
459
562
  await thread.close();
@@ -463,9 +566,10 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
463
566
  cwd: projectRoot,
464
567
  extraArgs,
465
568
  tools: toolRuntime.tools,
466
- streamFactory: defaultCodexTransportStreamFactory,
467
569
  });
468
- thread = providerInstance.startThread();
570
+ thread = savedProviderSessionId
571
+ ? providerInstance.resumeThread(savedProviderSessionId)
572
+ : providerInstance.startThread();
469
573
  },
470
574
  async close() {
471
575
  if (thread && typeof thread.close === "function") {
@@ -482,33 +586,40 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
482
586
  if (getClaudeThreadMode() !== "api") {
483
587
  return disabledRuntime;
484
588
  }
485
- if (typeof createClaudeThreadProvider !== "function" || typeof resolveClaudeUpstreamCredentials !== "function") {
589
+ if (typeof createClaudeThreadProvider !== "function") {
486
590
  return disabledRuntime;
487
591
  }
488
592
 
489
593
  try {
490
594
  let providerInstance = createClaudeThreadProvider({
491
595
  model,
492
- authProvider: buildClaudeAuthProvider(projectRoot),
493
- streamFactory: defaultClaudeTransportStreamFactory,
596
+ cwd: projectRoot,
597
+ extraArgs,
494
598
  });
495
- let thread = providerInstance.startThread();
599
+ let thread = initialProviderSessionId
600
+ ? providerInstance.resumeThread(initialProviderSessionId)
601
+ : providerInstance.startThread();
496
602
 
497
603
  return {
498
604
  enabled: true,
499
605
  get thread() {
500
606
  return thread;
501
607
  },
608
+ syncProviderSessionId() {
609
+ return rememberProviderSessionId(thread);
610
+ },
502
611
  async rebuildThread() {
503
612
  if (thread && typeof thread.close === "function") {
504
613
  await thread.close();
505
614
  }
506
615
  providerInstance = createClaudeThreadProvider({
507
616
  model,
508
- authProvider: buildClaudeAuthProvider(projectRoot),
509
- streamFactory: defaultClaudeTransportStreamFactory,
617
+ cwd: projectRoot,
618
+ extraArgs,
510
619
  });
511
- thread = providerInstance.startThread();
620
+ thread = savedProviderSessionId
621
+ ? providerInstance.resumeThread(savedProviderSessionId)
622
+ : providerInstance.startThread();
512
623
  },
513
624
  async close() {
514
625
  if (thread && typeof thread.close === "function") {
@@ -538,12 +649,14 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
538
649
  const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
539
650
  const model = process.env.UFOO_AGENT_MODEL || "";
540
651
  const busSender = createBusSender(projectRoot, subscriber);
652
+ const interactiveSessions = new Map();
541
653
  const threadRuntime = createThreadRuntime({
542
654
  projectRoot,
543
655
  provider,
544
656
  model,
545
657
  extraArgs,
546
658
  subscriber,
659
+ providerSessionId: process.env.UFOO_PROVIDER_SESSION_ID || "",
547
660
  });
548
661
 
549
662
  // Session state management for CLI continuity
@@ -586,6 +699,18 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
586
699
  activityPublisher.publish(state);
587
700
  }
588
701
 
702
+ function getInteractiveSession(publisher) {
703
+ const key = String(publisher || "unknown");
704
+ if (interactiveSessions.has(key)) return interactiveSessions.get(key);
705
+ const session = createInteractiveInputSession({
706
+ write: (delta) => {
707
+ busSender.enqueue(key, JSON.stringify({ stream: true, delta: String(delta || "") }));
708
+ },
709
+ });
710
+ interactiveSessions.set(key, session);
711
+ return session;
712
+ }
713
+
589
714
  setActivityState("ready");
590
715
 
591
716
  // 心跳更新函数
@@ -615,7 +740,6 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
615
740
  try {
616
741
  const lines = drainQueue(queueFile);
617
742
  if (lines.length > 0) {
618
- setActivityState("working");
619
743
  const events = [];
620
744
  for (const line of lines) {
621
745
  try {
@@ -625,7 +749,33 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
625
749
  }
626
750
  }
627
751
 
752
+ const runnableEvents = [];
628
753
  for (const evt of events) {
754
+ const rawInput = parseAgentViewRawInput(evt && evt.data ? evt.data.message : "");
755
+ if (rawInput === null) {
756
+ runnableEvents.push(evt);
757
+ continue;
758
+ }
759
+
760
+ const session = getInteractiveSession(evt.publisher || "unknown");
761
+ const submissions = session.handleRaw(rawInput);
762
+ for (const message of submissions) {
763
+ runnableEvents.push({
764
+ ...evt,
765
+ __agentViewRaw: true,
766
+ data: {
767
+ ...(evt.data || {}),
768
+ message,
769
+ },
770
+ });
771
+ }
772
+ }
773
+
774
+ if (runnableEvents.length > 0) {
775
+ setActivityState("working");
776
+ }
777
+
778
+ for (const evt of runnableEvents) {
629
779
  // eslint-disable-next-line no-await-in-loop
630
780
  await handleEvent(
631
781
  projectRoot,
@@ -640,6 +790,9 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
640
790
  extraArgs,
641
791
  threadRuntime
642
792
  );
793
+ if (evt.__agentViewRaw) {
794
+ getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
795
+ }
643
796
  }
644
797
 
645
798
  // Persist CLI session state after processing (only if changed and for claude)
@@ -659,7 +812,10 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
659
812
  // 处理消息后更新心跳
660
813
  updateHeartbeat();
661
814
  lastHeartbeat = now;
662
- setActivityState("idle");
815
+ if (runnableEvents.length > 0) {
816
+ setActivityState("idle");
817
+ }
818
+ await busSender.flush();
663
819
  }
664
820
  } finally {
665
821
  processing = false;
@@ -684,4 +840,7 @@ module.exports = {
684
840
  getClaudeThreadMode,
685
841
  buildClaudeAuthProvider,
686
842
  shouldFallbackToLegacyThreadProvider,
843
+ parseAgentViewRawInput,
844
+ createInteractiveInputSession,
845
+ persistProviderSessionId,
687
846
  };
@@ -116,11 +116,6 @@ function computeInjectedSubmitDelayMs(agentType, text) {
116
116
  return delayMs;
117
117
  }
118
118
 
119
- function buildPrompt(text, marker) {
120
- if (!marker) return text;
121
- return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
122
- }
123
-
124
119
  function resolveCommand(agentType, extraArgs = []) {
125
120
  const normalizedAgent = String(agentType || "").trim().toLowerCase();
126
121
  const extra = Array.isArray(extraArgs) ? extraArgs : [];
@@ -172,9 +167,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
172
167
  UFOO_INTERNAL_PTY: "1",
173
168
  };
174
169
 
170
+ const idleMs = Number.parseInt(process.env.UFOO_INTERNAL_PTY_IDLE_MS || "", 10) || 30000;
175
171
  const eventBus = new EventBus(projectRoot);
176
172
  const activityDetector = new ActivityDetector(agentType, {
177
173
  mode: "internal-pty",
174
+ quietWindowMs: idleMs,
178
175
  });
179
176
  const agentsFilePath = getUfooPaths(projectRoot).agentsFile;
180
177
 
@@ -184,15 +181,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
184
181
  let ptyReady = false;
185
182
  let readyTimer = null;
186
183
  let currentPublisher = "";
187
- let currentMarker = "";
188
184
  let pendingOutput = [];
189
185
  let outputBuffer = "";
190
186
  let flushTimer = null;
191
187
  let idleTimer = null;
192
188
  let watchdogTimer = null;
193
- let suppressEcho = false;
194
- let echoMarker = "";
195
- let suppressTimer = null;
196
189
  let ptyProcess = null;
197
190
  let restartCount = 0;
198
191
  let lastSpawnTime = 0;
@@ -205,14 +198,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
205
198
  const injectServer = setupInjectServer();
206
199
  initScreenBuffer(80, 24);
207
200
  const maxChunk = 2000;
208
- const idleMs = 30000;
209
201
  const watchdogMs = 120000;
210
202
  const maxQueue = 200;
211
203
  let sendQueue = Promise.resolve();
212
204
  const streamPublisherCache = new Map();
213
205
  const DROP_LINE_PATTERNS = [
214
- /__UFOO_DONE_/,
215
- /请在完成后输出以下标记/,
216
206
  /context left/i,
217
207
  /esc to interrupt/i,
218
208
  /for shortcuts/i,
@@ -660,8 +650,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
660
650
  detail: snap.detail,
661
651
  });
662
652
  // Quiet-window detector may classify IDLE sooner than stream fallback timer.
663
- // Release queue only when no explicit marker is being awaited.
664
- if (newState === "idle" && busy && !currentMarker && !suppressEcho) {
653
+ if (newState === "idle" && busy) {
665
654
  if (idleTimer) {
666
655
  clearTimeout(idleTimer);
667
656
  idleTimer = null;
@@ -696,49 +685,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
696
685
  if (!clean) return;
697
686
  outputBuffer += clean;
698
687
  activityDetector.processOutput(clean);
699
- if (suppressEcho) {
700
- if (echoMarker && outputBuffer.includes(echoMarker)) {
701
- const idx = outputBuffer.indexOf(echoMarker);
702
- outputBuffer = outputBuffer.slice(idx + echoMarker.length);
703
- outputBuffer = outputBuffer.replace(/^\n+/, "");
704
- suppressEcho = false;
705
- currentMarker = echoMarker;
706
- echoMarker = "";
707
- if (suppressTimer) {
708
- clearTimeout(suppressTimer);
709
- suppressTimer = null;
710
- }
711
- } else {
712
- return;
713
- }
714
- }
715
- if (currentMarker) {
716
- const idx = outputBuffer.indexOf(currentMarker);
717
- if (idx !== -1) {
718
- const before = outputBuffer.slice(0, idx);
719
- outputBuffer = "";
720
- if (before) {
721
- deliverChunk(before);
722
- }
723
- if (currentPublisher) {
724
- completePublisherResponse("marker");
725
- }
726
- currentMarker = "";
727
- busy = false;
728
- activityDetector.markIdle();
729
- currentPublisher = "";
730
- if (watchdogTimer) {
731
- clearTimeout(watchdogTimer);
732
- watchdogTimer = null;
733
- }
734
- if (idleTimer) {
735
- clearTimeout(idleTimer);
736
- idleTimer = null;
737
- }
738
- processQueue();
739
- return;
740
- }
741
- }
742
688
  scheduleFlush();
743
689
  // Ready detection: during TUI startup, reset the quiet timer on each output.
744
690
  // Once output stops for READY_QUIET_MS, the TUI is considered initialized.
@@ -805,7 +751,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
805
751
  busy = false;
806
752
  activityDetector.markIdle();
807
753
  currentPublisher = "";
808
- currentMarker = "";
809
754
 
810
755
  // If stop() was called, let the runner exit
811
756
  if (!running) return;
@@ -940,11 +885,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
940
885
  busy = true;
941
886
  activityDetector.markWorking();
942
887
  currentPublisher = next.publisher;
943
- currentMarker = next.marker || "";
944
- if (suppressTimer) {
945
- clearTimeout(suppressTimer);
946
- suppressTimer = null;
947
- }
948
888
  flushPending();
949
889
  if (next.text) {
950
890
  if (next.raw) {
@@ -952,30 +892,16 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
952
892
  } else {
953
893
  // Write text first, then send Enter separately.
954
894
  // Codex Ink TUI requires text and submit key as separate writes.
955
- // IMPORTANT: Defer marker detection until after Enter is sent,
956
- // because the prompt echo (TextInput display) contains the marker text.
957
- const prompt = buildPrompt(next.text, currentMarker);
958
- const savedMarker = currentMarker;
959
- suppressEcho = true;
960
- echoMarker = savedMarker;
961
- currentMarker = ""; // Disable marker detection during prompt echo & formatted display
962
- ptyProcess.write(prompt);
895
+ ptyProcess.write(next.text);
963
896
  setTimeout(() => {
964
897
  if (ptyProcess && ptyAlive) {
898
+ // Drop the local TUI input echo from any forwarded stream output.
965
899
  outputBuffer = "";
966
900
  const isClaude = agentType === "claude-code";
967
901
  if (isClaude) {
968
902
  // Claude Code: send CR directly without ESC.
969
903
  // ESC before CR is interpreted as Alt+Enter (newline).
970
904
  ptyProcess.write("\r");
971
- suppressTimer = setTimeout(() => {
972
- suppressTimer = null;
973
- if (!suppressEcho) return;
974
- suppressEcho = false;
975
- echoMarker = "";
976
- currentMarker = savedMarker;
977
- outputBuffer = "";
978
- }, 1500);
979
905
  } else {
980
906
  // Codex/others: ESC dismisses autocomplete, then CR submits.
981
907
  ptyProcess.write("\x1b");
@@ -983,14 +909,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
983
909
  if (ptyProcess && ptyAlive) {
984
910
  ptyProcess.write("\r");
985
911
  }
986
- suppressTimer = setTimeout(() => {
987
- suppressTimer = null;
988
- if (!suppressEcho) return;
989
- suppressEcho = false;
990
- echoMarker = "";
991
- currentMarker = savedMarker;
992
- outputBuffer = "";
993
- }, 1500);
994
912
  }, 100);
995
913
  }
996
914
  }
@@ -1001,13 +919,12 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1001
919
  watchdogTimer = setTimeout(() => {
1002
920
  watchdogTimer = null;
1003
921
  if (!busy) return;
1004
- const timeoutNote = `[internal-pty] marker timeout; restarting PTY`;
922
+ const timeoutNote = `[internal-pty] task timeout; restarting PTY`;
1005
923
  if (currentPublisher) {
1006
924
  completePublisherResponse("timeout", timeoutNote);
1007
925
  }
1008
926
  logNote(timeoutNote);
1009
- restartPty("marker timeout");
1010
- currentMarker = "";
927
+ restartPty("task timeout");
1011
928
  busy = false;
1012
929
  activityDetector.markIdle();
1013
930
  currentPublisher = "";
@@ -1058,11 +975,10 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1058
975
  if (messageQueue.length >= maxQueue) {
1059
976
  messageQueue.shift();
1060
977
  }
1061
- const marker = raw ? "" : `__UFOO_DONE_${Date.now()}_${Math.random().toString(16).slice(2)}__`;
1062
978
  const publisher = typeof evt.publisher === "object" && evt.publisher
1063
979
  ? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
1064
980
  : (evt.publisher || "unknown");
1065
- messageQueue.push({ publisher, raw, text, marker });
981
+ messageQueue.push({ publisher, raw, text });
1066
982
  }
1067
983
  }
1068
984
  processQueue();
@@ -126,7 +126,10 @@ function createAgentViewController(options = {}) {
126
126
 
127
127
  agentInputSuppressUntil = now() + 300;
128
128
  agentViewUsesBus = Boolean(options.useBus);
129
- if (!agentViewUsesBus) {
129
+ if (agentViewUsesBus) {
130
+ const label = getAgentLabel(agentId);
131
+ processStdout.write(`ufoo internal · ${label}\r\n\r\n> `);
132
+ } else {
130
133
  const sockPath = getInjectSockPath(agentId);
131
134
  connectAgentOutput(sockPath);
132
135
  connectAgentInput(sockPath);
@@ -57,6 +57,11 @@ function createDashboardKeyController(options = {}) {
57
57
  return Boolean(caps && caps.supportsSocketProtocol);
58
58
  }
59
59
 
60
+ function supportsInternalQueue(agentId) {
61
+ const caps = getAgentCapabilities(agentId);
62
+ return Boolean(caps && caps.supportsInternalQueueLoop);
63
+ }
64
+
60
65
  function withAgentInputFocus() {
61
66
  state.focusMode = "input";
62
67
  state.agentOutputSuppressed = false;
@@ -73,7 +78,7 @@ function createDashboardKeyController(options = {}) {
73
78
 
74
79
  function switchAgentView(agentId) {
75
80
  withAgentInputFocus();
76
- enterAgentView(agentId);
81
+ enterAgentView(agentId, { useBus: supportsInternalQueue(agentId) && !supportsSocket(agentId) });
77
82
  }
78
83
 
79
84
  function exitAgentDashboardToInput() {
@@ -154,7 +159,7 @@ function createDashboardKeyController(options = {}) {
154
159
  } else {
155
160
  withAgentInputFocus();
156
161
  state.selectedAgentIndex = nextIndex + 1;
157
- enterAgentView(nextAgent);
162
+ enterAgentView(nextAgent, { useBus: supportsInternalQueue(nextAgent) && !supportsSocket(nextAgent) });
158
163
  }
159
164
  } else {
160
165
  exitAgentView();
@@ -511,6 +516,16 @@ function createDashboardKeyController(options = {}) {
511
516
  enterAgentView(agentId);
512
517
  return true;
513
518
  }
519
+
520
+ if (supportsInternalQueue(agentId)) {
521
+ clearTargetAgent();
522
+ state.focusMode = "input";
523
+ state.dashboardView = "agents";
524
+ state.selectedAgentIndex = -1;
525
+ setScreenGrabKeys(false);
526
+ enterAgentView(agentId, { useBus: true });
527
+ return true;
528
+ }
514
529
  }
515
530
 
516
531
  exitDashboardMode(false);