u-foo 2.5.1 → 2.5.3
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 +1 -1
- package/src/agents/internal/internalRunner.js +57 -78
- package/src/agents/launch/launcher.js +3 -0
- package/src/agents/launch/notifier.js +38 -112
- package/src/agents/launch/ptyRunner.js +59 -49
- package/src/agents/launch/ptyWrapper.js +32 -1
- package/src/app/cli/busCoreCommands.js +19 -1
- package/src/app/cli/run.js +1 -53
- package/src/code/agent.js +41 -140
- package/src/coordination/bus/deliveryQueue.js +308 -0
- package/src/coordination/bus/index.js +3 -27
- package/src/coordination/bus/message.js +35 -15
- package/src/coordination/bus/queue.js +23 -7
- package/src/runtime/daemon/deliveryScheduler.js +205 -0
- package/src/runtime/daemon/index.js +27 -38
- package/src/runtime/daemon/ops.js +1 -5
- package/src/runtime/daemon/reportControlBus.js +20 -43
- package/templates/groups/build-ultra.json +1 -1
- package/SKILLS/brandkit/SKILL.md +0 -798
- package/SKILLS/brutalist-skill/SKILL.md +0 -92
- package/SKILLS/gpt-tasteskill/SKILL.md +0 -74
- package/SKILLS/image-to-code-skill/SKILL.md +0 -1228
- package/SKILLS/imagegen-frontend-mobile/SKILL.md +0 -1465
- package/SKILLS/imagegen-frontend-web/SKILL.md +0 -987
- package/SKILLS/llms.txt +0 -13
- package/SKILLS/minimalist-skill/SKILL.md +0 -85
- package/SKILLS/output-skill/SKILL.md +0 -49
- package/SKILLS/redesign-skill/SKILL.md +0 -178
- package/SKILLS/soft-skill/SKILL.md +0 -98
- package/SKILLS/stitch-skill/DESIGN.md +0 -121
- package/SKILLS/stitch-skill/SKILL.md +0 -184
- package/SKILLS/taste-skill/SKILL.md +0 -1206
- package/SKILLS/taste-skill-v1/SKILL.md +0 -226
- package/src/coordination/bus/daemon.js +0 -535
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ const { redactToolCallPayload, redactSecrets } = require("../../runtime/privacy/
|
|
|
16
16
|
const { buildCachedMemoryPrefix } = require("../../coordination/memory");
|
|
17
17
|
const { normalizePublisher, shouldForwardStreamToPublisher } = require("../launch/publisherRouting");
|
|
18
18
|
const { appendAgentRegistryDiagnostic } = require("../../coordination/state/agentRegistryDiagnostics");
|
|
19
|
+
const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
|
|
19
20
|
const {
|
|
20
21
|
buildDefaultStartupBootstrapPrompt,
|
|
21
22
|
isValueForCodexOption,
|
|
@@ -216,37 +217,15 @@ function shouldStreamReplyToPublisher(projectRoot, publisher, evt = {}) {
|
|
|
216
217
|
return shouldForwardStreamToPublisher(projectRoot, publisher);
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
content = fs.readFileSync(processingFile, "utf8");
|
|
227
|
-
readOk = true;
|
|
228
|
-
} catch {
|
|
229
|
-
try {
|
|
230
|
-
if (fs.existsSync(processingFile)) {
|
|
231
|
-
fs.renameSync(processingFile, queueFile);
|
|
232
|
-
}
|
|
233
|
-
} catch {
|
|
234
|
-
// ignore rollback errors
|
|
235
|
-
}
|
|
236
|
-
return [];
|
|
237
|
-
} finally {
|
|
238
|
-
if (readOk) {
|
|
239
|
-
try {
|
|
240
|
-
if (fs.existsSync(processingFile)) {
|
|
241
|
-
fs.rmSync(processingFile, { force: true });
|
|
242
|
-
}
|
|
243
|
-
} catch {
|
|
244
|
-
// ignore cleanup errors
|
|
245
|
-
}
|
|
246
|
-
}
|
|
220
|
+
function claimQueuedEvents(queueFile) {
|
|
221
|
+
const queue = new DeliveryQueue(queueFile);
|
|
222
|
+
const claims = [];
|
|
223
|
+
while (true) {
|
|
224
|
+
const claim = queue.claimNext();
|
|
225
|
+
if (!claim) break;
|
|
226
|
+
claims.push({ ...claim, queue });
|
|
247
227
|
}
|
|
248
|
-
|
|
249
|
-
return content.split(/\r?\n/).filter(Boolean);
|
|
228
|
+
return claims;
|
|
250
229
|
}
|
|
251
230
|
|
|
252
231
|
function parseAgentViewRawInput(message) {
|
|
@@ -835,67 +814,67 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
835
814
|
if (!processing) {
|
|
836
815
|
processing = true;
|
|
837
816
|
try {
|
|
838
|
-
const
|
|
839
|
-
if (
|
|
840
|
-
|
|
841
|
-
for (const line of lines) {
|
|
842
|
-
try {
|
|
843
|
-
events.push(JSON.parse(line));
|
|
844
|
-
} catch {
|
|
845
|
-
// ignore malformed line
|
|
846
|
-
}
|
|
847
|
-
}
|
|
817
|
+
const claims = claimQueuedEvents(queueFile);
|
|
818
|
+
if (claims.length > 0) {
|
|
819
|
+
let handledAny = false;
|
|
848
820
|
|
|
849
|
-
const
|
|
850
|
-
|
|
821
|
+
for (const claim of claims) {
|
|
822
|
+
const evt = claim.event;
|
|
823
|
+
const runnableEvents = [];
|
|
851
824
|
const rawInput = parseAgentViewRawInput(evt && evt.data ? evt.data.message : "");
|
|
852
825
|
if (rawInput === null) {
|
|
853
826
|
runnableEvents.push(evt);
|
|
854
|
-
|
|
827
|
+
} else {
|
|
828
|
+
const session = getInteractiveSession(evt.publisher || "unknown");
|
|
829
|
+
const submissions = session.handleRaw(rawInput);
|
|
830
|
+
for (const message of submissions) {
|
|
831
|
+
runnableEvents.push({
|
|
832
|
+
...evt,
|
|
833
|
+
__agentViewRaw: true,
|
|
834
|
+
data: {
|
|
835
|
+
...(evt.data || {}),
|
|
836
|
+
message,
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
}
|
|
855
840
|
}
|
|
856
841
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
for (const message of submissions) {
|
|
860
|
-
runnableEvents.push({
|
|
861
|
-
...evt,
|
|
862
|
-
__agentViewRaw: true,
|
|
863
|
-
data: {
|
|
864
|
-
...(evt.data || {}),
|
|
865
|
-
message,
|
|
866
|
-
},
|
|
867
|
-
});
|
|
842
|
+
if (runnableEvents.length > 0) {
|
|
843
|
+
activityTracker.notifyTurnStart("thinking");
|
|
868
844
|
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (runnableEvents.length > 0) {
|
|
872
|
-
activityTracker.notifyTurnStart("thinking");
|
|
873
|
-
}
|
|
874
845
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
846
|
+
try {
|
|
847
|
+
for (const runnableEvent of runnableEvents) {
|
|
848
|
+
// eslint-disable-next-line no-await-in-loop
|
|
849
|
+
await handleEvent(
|
|
850
|
+
projectRoot,
|
|
851
|
+
parsedAgentType,
|
|
852
|
+
provider,
|
|
853
|
+
model,
|
|
854
|
+
subscriber,
|
|
855
|
+
nickname,
|
|
856
|
+
runnableEvent,
|
|
857
|
+
busSender,
|
|
858
|
+
bootstrap.extraArgs,
|
|
859
|
+
threadRuntime,
|
|
860
|
+
bootstrap.promptText,
|
|
861
|
+
activityTracker
|
|
862
|
+
);
|
|
863
|
+
if (runnableEvent.__agentViewRaw) {
|
|
864
|
+
getInteractiveSession(runnableEvent.publisher || "unknown").writeResponsePrompt();
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
claim.queue.completeClaim(claim);
|
|
868
|
+
if (runnableEvents.length > 0) handledAny = true;
|
|
869
|
+
} catch (err) {
|
|
870
|
+
claim.queue.restoreClaim(claim);
|
|
871
|
+
throw err;
|
|
893
872
|
}
|
|
894
873
|
}
|
|
895
874
|
// 处理消息后更新心跳
|
|
896
875
|
updateHeartbeat();
|
|
897
876
|
lastHeartbeat = now;
|
|
898
|
-
if (
|
|
877
|
+
if (handledAny) {
|
|
899
878
|
activityTracker.markIdle();
|
|
900
879
|
}
|
|
901
880
|
await busSender.flush();
|
|
@@ -662,6 +662,9 @@ class AgentLauncher {
|
|
|
662
662
|
// Enable Claude Code SDK session state events for precise idle/busy detection
|
|
663
663
|
...(this.agentType === "claude-code" ? { CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1" } : {}),
|
|
664
664
|
},
|
|
665
|
+
titlePrefix: process.env.UFOO_LAUNCH_MODE === "host"
|
|
666
|
+
? (this._originalNickname || process.env.UFOO_NICKNAME || "")
|
|
667
|
+
: "",
|
|
665
668
|
// 未来扩展:ioAdapter: new TerminalIOAdapter()
|
|
666
669
|
});
|
|
667
670
|
|
|
@@ -9,8 +9,8 @@ const { shakeTerminalByTty } = require("../../coordination/bus/shake");
|
|
|
9
9
|
const { isITerm2 } = require("../../runtime/terminal/detect");
|
|
10
10
|
const iterm2 = require("../../runtime/terminal/iterm2");
|
|
11
11
|
const { createActivityStatePublisher } = require("../activity/activityStatePublisher");
|
|
12
|
-
const { INJECTION_MODES, getInjectionModeFromEvent } = require("../../coordination/bus/messageMeta");
|
|
13
12
|
const { buildPromptInjectionText } = require("../../coordination/bus/promptEnvelope");
|
|
13
|
+
const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Agent 消息通知监听器
|
|
@@ -41,6 +41,7 @@ class AgentNotifier {
|
|
|
41
41
|
safeSub,
|
|
42
42
|
"pending.jsonl"
|
|
43
43
|
);
|
|
44
|
+
this.deliveryQueue = new DeliveryQueue(this.queueFile);
|
|
44
45
|
this.agentsFile = paths.agentsFile;
|
|
45
46
|
|
|
46
47
|
// 初始化 injector
|
|
@@ -90,7 +91,9 @@ class AgentNotifier {
|
|
|
90
91
|
setTitle(nickname) {
|
|
91
92
|
if (!nickname) return;
|
|
92
93
|
if (!process.stdout || !process.stdout.isTTY) return;
|
|
93
|
-
process.
|
|
94
|
+
if (process.env.UFOO_LAUNCH_MODE !== "host") {
|
|
95
|
+
process.stdout.write(`\x1b]0;${nickname}\x07`);
|
|
96
|
+
}
|
|
94
97
|
if (isITerm2()) {
|
|
95
98
|
iterm2.setBadge(nickname);
|
|
96
99
|
iterm2.setCwd(this.projectRoot);
|
|
@@ -187,45 +190,6 @@ class AgentNotifier {
|
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
drainPending() {
|
|
191
|
-
if (!fs.existsSync(this.queueFile)) return [];
|
|
192
|
-
const processingFile = `${this.queueFile}.processing.${process.pid}.${Date.now()}`;
|
|
193
|
-
let content = "";
|
|
194
|
-
let readOk = false;
|
|
195
|
-
try {
|
|
196
|
-
fs.renameSync(this.queueFile, processingFile);
|
|
197
|
-
content = fs.readFileSync(processingFile, "utf8");
|
|
198
|
-
readOk = true;
|
|
199
|
-
} catch {
|
|
200
|
-
try {
|
|
201
|
-
if (fs.existsSync(processingFile)) {
|
|
202
|
-
fs.renameSync(processingFile, this.queueFile);
|
|
203
|
-
}
|
|
204
|
-
} catch {
|
|
205
|
-
// ignore rollback errors
|
|
206
|
-
}
|
|
207
|
-
return [];
|
|
208
|
-
} finally {
|
|
209
|
-
if (readOk) {
|
|
210
|
-
try {
|
|
211
|
-
if (fs.existsSync(processingFile)) {
|
|
212
|
-
fs.rmSync(processingFile, { force: true });
|
|
213
|
-
}
|
|
214
|
-
} catch {
|
|
215
|
-
// ignore cleanup errors
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (!content.trim()) return [];
|
|
220
|
-
return content.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
221
|
-
try {
|
|
222
|
-
return JSON.parse(line);
|
|
223
|
-
} catch {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
}).filter(Boolean);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
193
|
normalizePublisher(publisher) {
|
|
230
194
|
if (!publisher) return "";
|
|
231
195
|
if (typeof publisher === "string") return publisher;
|
|
@@ -272,57 +236,41 @@ class AgentNotifier {
|
|
|
272
236
|
return 0;
|
|
273
237
|
}
|
|
274
238
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const requeue = [];
|
|
278
|
-
let delivered = 0;
|
|
279
|
-
let consumedOne = false;
|
|
280
|
-
for (const evt of events) {
|
|
281
|
-
if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
const injectionMode = getInjectionModeFromEvent(evt, INJECTION_MODES.IMMEDIATE);
|
|
285
|
-
const activityState = this.getCurrentActivityState();
|
|
286
|
-
if (injectionMode === INJECTION_MODES.QUEUED && this.isBusyState(activityState)) {
|
|
287
|
-
requeue.push(evt);
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (consumedOne) {
|
|
291
|
-
requeue.push(evt);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const message = buildPromptInjectionText(evt, this.subscriber, this.getAgentsMap());
|
|
295
|
-
try {
|
|
296
|
-
// Inject the prompt-facing text into the terminal/tmux agent
|
|
297
|
-
// (Bus is the source of truth; inject is the delivery adapter.)
|
|
298
|
-
// eslint-disable-next-line no-await-in-loop
|
|
299
|
-
await this.injector.inject(this.subscriber, message);
|
|
300
|
-
delivered += 1;
|
|
301
|
-
consumedOne = true;
|
|
302
|
-
this.injectFailCount = 0;
|
|
303
|
-
this.updateActivityState("working");
|
|
304
|
-
// eslint-disable-next-line no-await-in-loop
|
|
305
|
-
await this.emitDelivery(evt, "ok");
|
|
306
|
-
} catch (err) {
|
|
307
|
-
consumedOne = true;
|
|
308
|
-
this.injectFailCount += 1;
|
|
309
|
-
requeue.push(evt);
|
|
310
|
-
// eslint-disable-next-line no-await-in-loop
|
|
311
|
-
await this.emitDelivery(evt, "error", err.message || "inject failed");
|
|
312
|
-
}
|
|
239
|
+
if (this.isBusyState(this.getCurrentActivityState())) {
|
|
240
|
+
return 0;
|
|
313
241
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
242
|
+
|
|
243
|
+
const claim = this.deliveryQueue.claimNext();
|
|
244
|
+
if (!claim) return 0;
|
|
245
|
+
|
|
246
|
+
const evt = claim.event;
|
|
247
|
+
if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
|
|
248
|
+
this.deliveryQueue.completeClaim(claim);
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const activityState = this.getCurrentActivityState();
|
|
253
|
+
if (this.isBusyState(activityState)) {
|
|
254
|
+
this.deliveryQueue.restoreClaim(claim);
|
|
255
|
+
return 0;
|
|
321
256
|
}
|
|
322
|
-
|
|
257
|
+
|
|
258
|
+
const message = buildPromptInjectionText(evt, this.subscriber, this.getAgentsMap());
|
|
259
|
+
try {
|
|
260
|
+
// Inject the prompt-facing text into the terminal/tmux agent.
|
|
261
|
+
await this.injector.inject(this.subscriber, message);
|
|
262
|
+
this.deliveryQueue.completeClaim(claim);
|
|
263
|
+
this.injectFailCount = 0;
|
|
264
|
+
this.updateActivityState("working");
|
|
265
|
+
await this.emitDelivery(evt, "ok");
|
|
323
266
|
this.lastWorkingAt = Date.now();
|
|
267
|
+
return 1;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
this.injectFailCount += 1;
|
|
270
|
+
this.deliveryQueue.restoreClaim(claim);
|
|
271
|
+
await this.emitDelivery(evt, "error", err.message || "inject failed");
|
|
272
|
+
return 0;
|
|
324
273
|
}
|
|
325
|
-
return delivered;
|
|
326
274
|
}
|
|
327
275
|
|
|
328
276
|
/**
|
|
@@ -367,32 +315,10 @@ class AgentNotifier {
|
|
|
367
315
|
if (currentCount > this.lastCount) {
|
|
368
316
|
const newCount = currentCount - this.lastCount;
|
|
369
317
|
this.notify(newCount);
|
|
370
|
-
|
|
371
|
-
// 自动触发终端输入(非阻塞)
|
|
372
|
-
this.autoTriggerInput().catch(() => {
|
|
373
|
-
// 忽略触发失败
|
|
374
|
-
});
|
|
375
318
|
}
|
|
376
319
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
if (this.isUfooCodeSubscriber()) {
|
|
380
|
-
if (this.lastUbusWakeCount !== currentCount) {
|
|
381
|
-
try {
|
|
382
|
-
await this.autoTriggerInput();
|
|
383
|
-
this.lastUbusWakeCount = currentCount;
|
|
384
|
-
} catch {
|
|
385
|
-
// ignore delivery errors
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
try {
|
|
390
|
-
await this.deliverPending();
|
|
391
|
-
} catch {
|
|
392
|
-
// ignore delivery errors
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
320
|
+
// Delivery is owned by the project daemon scheduler. The notifier keeps
|
|
321
|
+
// terminal notifications, title badges, heartbeat, and activity fallback.
|
|
396
322
|
if (currentCount <= 0) {
|
|
397
323
|
this.lastUbusWakeCount = -1;
|
|
398
324
|
}
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
resolveDefaultManualBootstrap,
|
|
18
18
|
} = require("../prompts/defaultBootstrap");
|
|
19
19
|
const { buildPromptInjectionText } = require("../../coordination/bus/promptEnvelope");
|
|
20
|
+
const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
|
|
20
21
|
|
|
21
22
|
function sleep(ms) {
|
|
22
23
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -40,37 +41,15 @@ function safeSubscriber(subscriber) {
|
|
|
40
41
|
return subscriber.replace(/:/g, "_");
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
content = fs.readFileSync(processingFile, "utf8");
|
|
51
|
-
readOk = true;
|
|
52
|
-
} catch {
|
|
53
|
-
try {
|
|
54
|
-
if (fs.existsSync(processingFile)) {
|
|
55
|
-
fs.renameSync(processingFile, queueFile);
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
// ignore rollback errors
|
|
59
|
-
}
|
|
60
|
-
return [];
|
|
61
|
-
} finally {
|
|
62
|
-
if (readOk) {
|
|
63
|
-
try {
|
|
64
|
-
if (fs.existsSync(processingFile)) {
|
|
65
|
-
fs.rmSync(processingFile, { force: true });
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
// ignore cleanup errors
|
|
69
|
-
}
|
|
70
|
-
}
|
|
44
|
+
function claimQueuedEvents(queueFile) {
|
|
45
|
+
const queue = new DeliveryQueue(queueFile);
|
|
46
|
+
const claims = [];
|
|
47
|
+
while (true) {
|
|
48
|
+
const claim = queue.claimNext();
|
|
49
|
+
if (!claim) break;
|
|
50
|
+
claims.push({ ...claim, queue });
|
|
71
51
|
}
|
|
72
|
-
|
|
73
|
-
return content.split(/\r?\n/).filter(Boolean);
|
|
52
|
+
return claims;
|
|
74
53
|
}
|
|
75
54
|
|
|
76
55
|
function stripAnsi(text) {
|
|
@@ -79,6 +58,14 @@ function stripAnsi(text) {
|
|
|
79
58
|
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
80
59
|
}
|
|
81
60
|
|
|
61
|
+
function rewriteTitleOscPrefix(text, prefix) {
|
|
62
|
+
if (!prefix || !text) return text;
|
|
63
|
+
return String(text).replace(/\x1b\]([012]);([^\x07\x1b]*)(\x07|\x1b\\)/g, (match, code, title, terminator) => {
|
|
64
|
+
if (!title || title.startsWith(`${prefix}:`)) return match;
|
|
65
|
+
return `\x1b]${code};${prefix}:${title}${terminator}`;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
82
69
|
function parseInputMessage(message) {
|
|
83
70
|
if (!message) return { raw: false, text: "" };
|
|
84
71
|
if (parseStreamEnvelope(message)) return null;
|
|
@@ -440,8 +427,29 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
440
427
|
}
|
|
441
428
|
}
|
|
442
429
|
|
|
443
|
-
|
|
430
|
+
const titlePrefix = process.env.UFOO_LAUNCH_MODE === "host"
|
|
431
|
+
? String(process.env.UFOO_NICKNAME || "").trim()
|
|
432
|
+
: "";
|
|
433
|
+
let titleOscBuffer = "";
|
|
434
|
+
|
|
435
|
+
function prependTitlePrefix(data) {
|
|
436
|
+
if (!titlePrefix) return Buffer.from(data || "").toString("utf8");
|
|
444
437
|
const text = Buffer.from(data || "").toString("utf8");
|
|
438
|
+
const combined = titleOscBuffer + text;
|
|
439
|
+
titleOscBuffer = "";
|
|
440
|
+
const trailingOscStart = combined.lastIndexOf("\x1b]");
|
|
441
|
+
if (trailingOscStart >= 0) {
|
|
442
|
+
const trailing = combined.slice(trailingOscStart);
|
|
443
|
+
if (!/(?:\x07|\x1b\\)/.test(trailing)) {
|
|
444
|
+
titleOscBuffer = trailing;
|
|
445
|
+
return rewriteTitleOscPrefix(combined.slice(0, trailingOscStart), titlePrefix);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return rewriteTitleOscPrefix(combined, titlePrefix);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function broadcastOutput(data) {
|
|
452
|
+
const text = prependTitlePrefix(data);
|
|
445
453
|
if (!text) return;
|
|
446
454
|
if (process.stdout && process.stdout.isTTY && typeof process.stdout.write === "function") {
|
|
447
455
|
try {
|
|
@@ -1044,28 +1052,29 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
1044
1052
|
lastHeartbeat = now;
|
|
1045
1053
|
}
|
|
1046
1054
|
|
|
1047
|
-
const
|
|
1048
|
-
if (
|
|
1055
|
+
const claims = claimQueuedEvents(queueFile);
|
|
1056
|
+
if (claims.length > 0) {
|
|
1049
1057
|
const agents = readAgentsMap(agentsFilePath);
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1058
|
+
for (const claim of claims) {
|
|
1059
|
+
const evt = claim.event;
|
|
1052
1060
|
try {
|
|
1053
|
-
|
|
1061
|
+
const input = buildPtyInputFromEvent(evt, subscriber, agents);
|
|
1062
|
+
if (!input) {
|
|
1063
|
+
claim.queue.completeClaim(claim);
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const { raw, text } = input;
|
|
1067
|
+
if (messageQueue.length >= maxQueue) {
|
|
1068
|
+
messageQueue.shift();
|
|
1069
|
+
}
|
|
1070
|
+
const publisher = typeof evt.publisher === "object" && evt.publisher
|
|
1071
|
+
? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
|
|
1072
|
+
: (evt.publisher || "unknown");
|
|
1073
|
+
messageQueue.push({ publisher, raw, text });
|
|
1074
|
+
claim.queue.completeClaim(claim);
|
|
1054
1075
|
} catch {
|
|
1055
|
-
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
for (const evt of events) {
|
|
1059
|
-
const input = buildPtyInputFromEvent(evt, subscriber, agents);
|
|
1060
|
-
if (!input) continue;
|
|
1061
|
-
const { raw, text } = input;
|
|
1062
|
-
if (messageQueue.length >= maxQueue) {
|
|
1063
|
-
messageQueue.shift();
|
|
1076
|
+
claim.queue.restoreClaim(claim);
|
|
1064
1077
|
}
|
|
1065
|
-
const publisher = typeof evt.publisher === "object" && evt.publisher
|
|
1066
|
-
? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
|
|
1067
|
-
: (evt.publisher || "unknown");
|
|
1068
|
-
messageQueue.push({ publisher, raw, text });
|
|
1069
1078
|
}
|
|
1070
1079
|
}
|
|
1071
1080
|
processQueue();
|
|
@@ -1078,6 +1087,7 @@ module.exports = {
|
|
|
1078
1087
|
appendStartupBootstrapArg,
|
|
1079
1088
|
buildPtyInputFromEvent,
|
|
1080
1089
|
parseInputMessage,
|
|
1090
|
+
rewriteTitleOscPrefix,
|
|
1081
1091
|
resolvePtyBootstrapArgs,
|
|
1082
1092
|
resolveCommand,
|
|
1083
1093
|
runPtyRunner,
|
|
@@ -41,6 +41,8 @@ class PtyWrapper {
|
|
|
41
41
|
|
|
42
42
|
// 可插拔的IO适配器(未来扩展)
|
|
43
43
|
this.ioAdapter = options.ioAdapter || null;
|
|
44
|
+
this.titlePrefix = String(options.titlePrefix || "").trim();
|
|
45
|
+
this._titleOscBuffer = "";
|
|
44
46
|
|
|
45
47
|
// 事件处理器引用(用于cleanup)
|
|
46
48
|
this._stdinHandler = null;
|
|
@@ -111,8 +113,10 @@ class PtyWrapper {
|
|
|
111
113
|
_attachDirectStreams(stdin, stdout) {
|
|
112
114
|
// PTY输出 -> stdout
|
|
113
115
|
this._ptyDataHandler = (data) => {
|
|
116
|
+
const output = this._prependTitlePrefix(data);
|
|
117
|
+
|
|
114
118
|
// 1. 输出到terminal
|
|
115
|
-
stdout.write(
|
|
119
|
+
stdout.write(output);
|
|
116
120
|
|
|
117
121
|
// 2. 可选:日志记录(JSONL格式)
|
|
118
122
|
if (this.logger) {
|
|
@@ -185,6 +189,33 @@ class PtyWrapper {
|
|
|
185
189
|
this.pty.onExit(this._ptyExitHandler);
|
|
186
190
|
}
|
|
187
191
|
|
|
192
|
+
_prependTitlePrefix(data) {
|
|
193
|
+
if (!this.titlePrefix) return data;
|
|
194
|
+
const text = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
|
|
195
|
+
const combined = this._titleOscBuffer + text;
|
|
196
|
+
this._titleOscBuffer = "";
|
|
197
|
+
|
|
198
|
+
const trailingOscStart = combined.lastIndexOf("\x1b]");
|
|
199
|
+
if (trailingOscStart >= 0) {
|
|
200
|
+
const trailing = combined.slice(trailingOscStart);
|
|
201
|
+
if (!/(?:\x07|\x1b\\)/.test(trailing)) {
|
|
202
|
+
this._titleOscBuffer = trailing;
|
|
203
|
+
return this._rewriteTitleOsc(combined.slice(0, trailingOscStart));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return this._rewriteTitleOsc(combined);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_rewriteTitleOsc(text) {
|
|
211
|
+
if (!this.titlePrefix || !text) return text;
|
|
212
|
+
const prefix = this.titlePrefix;
|
|
213
|
+
return text.replace(/\x1b\]([012]);([^\x07\x1b]*)(\x07|\x1b\\)/g, (match, code, title, terminator) => {
|
|
214
|
+
if (!title || title.startsWith(`${prefix}:`)) return match;
|
|
215
|
+
return `\x1b]${code};${prefix}:${title}${terminator}`;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
188
219
|
/**
|
|
189
220
|
* 写入数据到PTY(用于外部inject)
|
|
190
221
|
*
|
|
@@ -39,6 +39,18 @@ function parseSendArgs(cmdArgs = []) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function resolvePollSubscriber(cmdArgs = [], env = process.env) {
|
|
43
|
+
const positionals = cmdArgs.filter((arg) => typeof arg === "string" && !arg.startsWith("--"));
|
|
44
|
+
const subscriber = String(positionals[0] || env.UFOO_SUBSCRIBER_ID || "").trim();
|
|
45
|
+
if (!subscriber) {
|
|
46
|
+
throw new Error("poll requires [subscriber] or UFOO_SUBSCRIBER_ID");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
subscriber,
|
|
50
|
+
autoAck: cmdArgs.includes("--ack") || cmdArgs.includes("--auto-ack"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
|
|
43
55
|
switch (cmd) {
|
|
44
56
|
case "init":
|
|
@@ -76,6 +88,12 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
|
|
|
76
88
|
case "check":
|
|
77
89
|
await eventBus.check(cmdArgs[0]);
|
|
78
90
|
return {};
|
|
91
|
+
case "poll":
|
|
92
|
+
{
|
|
93
|
+
const parsed = resolvePollSubscriber(cmdArgs);
|
|
94
|
+
await eventBus.check(parsed.subscriber, parsed.autoAck);
|
|
95
|
+
}
|
|
96
|
+
return {};
|
|
79
97
|
case "ack":
|
|
80
98
|
await eventBus.ack(cmdArgs[0]);
|
|
81
99
|
return {};
|
|
@@ -99,4 +117,4 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
|
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
module.exports = { runBusCoreCommand };
|
|
120
|
+
module.exports = { runBusCoreCommand, resolvePollSubscriber };
|