u-foo 2.3.26 → 2.3.27
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/agent/activityStatePublisher.js +19 -4
- package/src/agent/activityStateWriter.js +29 -13
- package/src/agent/activityTracker.js +147 -0
- package/src/agent/internalRunner.js +26 -9
- package/src/agent/launchEnvironment.js +64 -0
- package/src/agent/launcher.js +2 -12
- package/src/chat/index.js +1 -0
- package/src/daemon/ops.js +13 -2
- package/src/daemon/status.js +2 -0
package/package.json
CHANGED
|
@@ -7,6 +7,10 @@ const { writeActivityState } = require("./activityStateWriter");
|
|
|
7
7
|
* Encapsulates the "write to disk + broadcast event" pattern used by
|
|
8
8
|
* ptyRunner, launcher, notifier, and internalRunner.
|
|
9
9
|
*
|
|
10
|
+
* Dedupe key is `state|detail` so that within the same canonical state
|
|
11
|
+
* (e.g. `working`) callers can publish detail transitions like
|
|
12
|
+
* `thinking` → `tool bash` and have them propagate to the dashboard.
|
|
13
|
+
*
|
|
10
14
|
* @param {object} options
|
|
11
15
|
* @param {string} options.agentsFile - Path to all-agents.json
|
|
12
16
|
* @param {string} options.subscriber - Subscriber ID (e.g. "claude-code:abc123")
|
|
@@ -22,16 +26,23 @@ function createActivityStatePublisher(options = {}) {
|
|
|
22
26
|
} = options;
|
|
23
27
|
|
|
24
28
|
let lastState = "";
|
|
29
|
+
let lastDetail = "";
|
|
25
30
|
|
|
26
31
|
function publish(state, extra = {}, publishOptions = {}) {
|
|
27
|
-
|
|
32
|
+
const detail = typeof extra.detail === "string" ? extra.detail : "";
|
|
33
|
+
if (state === lastState && detail === lastDetail) return false;
|
|
28
34
|
const since = extra.since || undefined;
|
|
29
35
|
const effectiveForce = typeof publishOptions.force === "boolean"
|
|
30
36
|
? publishOptions.force
|
|
31
37
|
: force;
|
|
32
|
-
const changed = writeActivityState(agentsFile, subscriber, state, {
|
|
38
|
+
const changed = writeActivityState(agentsFile, subscriber, state, {
|
|
39
|
+
since,
|
|
40
|
+
force: effectiveForce,
|
|
41
|
+
detail,
|
|
42
|
+
});
|
|
33
43
|
if (!changed) return false;
|
|
34
44
|
lastState = state;
|
|
45
|
+
lastDetail = detail;
|
|
35
46
|
// Write to bus events directory for daemon bridge to pick up.
|
|
36
47
|
// Writes directly to events dir to avoid queueing into subscriber pending files.
|
|
37
48
|
try {
|
|
@@ -51,7 +62,7 @@ function createActivityStatePublisher(options = {}) {
|
|
|
51
62
|
subscriber,
|
|
52
63
|
state,
|
|
53
64
|
previous: extra.previous || "",
|
|
54
|
-
...
|
|
65
|
+
...detail ? { detail } : {},
|
|
55
66
|
},
|
|
56
67
|
};
|
|
57
68
|
fs.appendFileSync(eventFile, JSON.stringify(entry) + "\n");
|
|
@@ -65,7 +76,11 @@ function createActivityStatePublisher(options = {}) {
|
|
|
65
76
|
return lastState;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
function getLastDetail() {
|
|
80
|
+
return lastDetail;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { publish, getLastState, getLastDetail };
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
module.exports = { createActivityStatePublisher };
|
|
@@ -4,14 +4,18 @@ const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnost
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Centralized helper for writing activity_state to all-agents.json.
|
|
7
|
-
* Used by both ptyRunner and notifier to avoid duplicated read-modify-write logic.
|
|
8
7
|
*
|
|
9
|
-
* -
|
|
8
|
+
* - Writes when `state` OR `detail` changes (state change refreshes `activity_since`,
|
|
9
|
+
* detail-only change keeps the existing `activity_since` so the "busy duration"
|
|
10
|
+
* shown in dashboards stays continuous across e.g. `working · thinking` →
|
|
11
|
+
* `working · tool bash`).
|
|
10
12
|
* - Respects priority: won't overwrite working/waiting_input/blocked with idle
|
|
11
|
-
* unless explicitly requested via `force` option.
|
|
13
|
+
* unless explicitly requested via `force` option. Detail-only updates within
|
|
14
|
+
* the same state are always allowed.
|
|
12
15
|
*/
|
|
13
16
|
function writeActivityState(agentsFilePath, subscriber, state, options = {}) {
|
|
14
17
|
const { since, force = false } = options;
|
|
18
|
+
const detail = typeof options.detail === "string" ? options.detail : "";
|
|
15
19
|
try {
|
|
16
20
|
if (!agentsFilePath || !fs.existsSync(agentsFilePath)) return false;
|
|
17
21
|
const data = readJSON(agentsFilePath, null);
|
|
@@ -26,21 +30,33 @@ function writeActivityState(agentsFilePath, subscriber, state, options = {}) {
|
|
|
26
30
|
return false;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
const
|
|
33
|
+
const agent = data.agents[subscriber];
|
|
34
|
+
const currentState = agent.activity_state;
|
|
35
|
+
const currentDetail = typeof agent.activity_detail === "string" ? agent.activity_detail : "";
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
if (current === state) return false;
|
|
37
|
+
if (currentState === state && currentDetail === detail) return false;
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
if (
|
|
40
|
+
currentState !== state
|
|
41
|
+
&& !force
|
|
42
|
+
&& (currentState === "working" || currentState === "waiting_input" || currentState === "blocked")
|
|
43
|
+
) {
|
|
37
44
|
if (state === "idle" || state === "ready") return false;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
agent.activity_state = state;
|
|
48
|
+
if (detail) {
|
|
49
|
+
agent.activity_detail = detail;
|
|
50
|
+
} else {
|
|
51
|
+
delete agent.activity_detail;
|
|
52
|
+
}
|
|
53
|
+
if (currentState !== state) {
|
|
54
|
+
agent.activity_since = since
|
|
55
|
+
? new Date(since).toISOString()
|
|
56
|
+
: new Date().toISOString();
|
|
57
|
+
} else if (!agent.activity_since) {
|
|
58
|
+
agent.activity_since = new Date().toISOString();
|
|
59
|
+
}
|
|
44
60
|
writeJSON(agentsFilePath, data);
|
|
45
61
|
return true;
|
|
46
62
|
} catch {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generic agent activity tracker.
|
|
5
|
+
*
|
|
6
|
+
* Maps high-level lifecycle hooks and normalized provider stream events to the
|
|
7
|
+
* canonical activity model (`starting`/`ready`/`working`/`idle`/`waiting_input`/`blocked`)
|
|
8
|
+
* plus a short detail string (e.g. `thinking`, `tool bash`). Publishes through
|
|
9
|
+
* an injected publisher so the same tracker can be reused by internal,
|
|
10
|
+
* internal-pty, or future runner shapes.
|
|
11
|
+
*
|
|
12
|
+
* Design contract:
|
|
13
|
+
* - The tracker never reads PTY text or guesses state from prose. All state
|
|
14
|
+
* transitions come from explicit hook calls (`notify*`, `request*`,
|
|
15
|
+
* `mark*`) or from normalized provider events.
|
|
16
|
+
* - Detail is best-effort: callers can override it on `notifyTurnStart` /
|
|
17
|
+
* `markIdle` if they want a custom phrasing.
|
|
18
|
+
* - The tracker stays passive about `waiting_input` and `blocked`. They are
|
|
19
|
+
* intentionally exposed as explicit methods (`requestUserInput`,
|
|
20
|
+
* `markBlocked`) and have no auto-trigger from the provider stream — those
|
|
21
|
+
* states should only fire from structured runtime signals.
|
|
22
|
+
*/
|
|
23
|
+
function createActivityTracker({ publisher } = {}) {
|
|
24
|
+
if (!publisher || typeof publisher.publish !== "function") {
|
|
25
|
+
throw new Error("createActivityTracker requires a publisher with publish()");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let currentState = "";
|
|
29
|
+
let currentDetail = "";
|
|
30
|
+
let currentTurnId = "";
|
|
31
|
+
|
|
32
|
+
function emit(state, detail = "", publishOptions = {}) {
|
|
33
|
+
const normalizedDetail = String(detail || "");
|
|
34
|
+
if (state === currentState && normalizedDetail === currentDetail) return false;
|
|
35
|
+
const previous = currentState;
|
|
36
|
+
const ok = publisher.publish(state, {
|
|
37
|
+
detail: normalizedDetail,
|
|
38
|
+
previous,
|
|
39
|
+
}, publishOptions);
|
|
40
|
+
if (!ok) return false;
|
|
41
|
+
currentState = state;
|
|
42
|
+
currentDetail = normalizedDetail;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compactToolName(name) {
|
|
47
|
+
const trimmed = String(name || "").trim();
|
|
48
|
+
if (!trimmed) return "tool";
|
|
49
|
+
if (trimmed.length <= 32) return trimmed;
|
|
50
|
+
return `${trimmed.slice(0, 29)}...`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function notifyStarting(detail = "runner") {
|
|
54
|
+
emit("starting", detail);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function notifyReady(detail = "") {
|
|
58
|
+
emit("ready", detail);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function notifyTurnStart(detail = "thinking") {
|
|
62
|
+
emit("working", detail);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function markIdle(detail = "") {
|
|
66
|
+
emit("idle", detail, { force: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requestUserInput(reason = "") {
|
|
70
|
+
emit("waiting_input", reason);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function clearUserInput(detail = "") {
|
|
74
|
+
if (currentState !== "waiting_input") return;
|
|
75
|
+
emit("idle", detail, { force: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function markBlocked(reason = "") {
|
|
79
|
+
emit("blocked", reason);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function onProviderEvent(event = {}) {
|
|
83
|
+
const type = event && typeof event === "object" ? String(event.type || "") : "";
|
|
84
|
+
if (!type) return;
|
|
85
|
+
|
|
86
|
+
if (type === "thread_started") {
|
|
87
|
+
// Provider acknowledged the thread; wait for the first turn to flip to working.
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (type === "turn_started") {
|
|
92
|
+
currentTurnId = String(event.turnId || event.turn_id || "");
|
|
93
|
+
emit("working", "thinking");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (type === "text_delta") {
|
|
98
|
+
// First text delta on a turn that didn't emit turn_started still counts as working.
|
|
99
|
+
if (currentState !== "working") {
|
|
100
|
+
emit("working", "thinking");
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (type === "tool_call") {
|
|
106
|
+
emit("working", `tool ${compactToolName(event.name)}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (type === "tool_result") {
|
|
111
|
+
// Keep `tool <name>` until the next event (text_delta or another tool_call)
|
|
112
|
+
// shifts the detail. This avoids a flap back to `thinking` for a single frame.
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (type === "turn_completed") {
|
|
117
|
+
currentTurnId = "";
|
|
118
|
+
emit("idle", "", { force: true });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (type === "turn_failed") {
|
|
123
|
+
currentTurnId = "";
|
|
124
|
+
// Default policy: drop back to idle. Callers that want `blocked` semantics
|
|
125
|
+
// for unrecoverable failures should call markBlocked() explicitly.
|
|
126
|
+
emit("idle", "", { force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getState() {
|
|
131
|
+
return { state: currentState, detail: currentDetail, turnId: currentTurnId };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
notifyStarting,
|
|
136
|
+
notifyReady,
|
|
137
|
+
notifyTurnStart,
|
|
138
|
+
markIdle,
|
|
139
|
+
requestUserInput,
|
|
140
|
+
clearUserInput,
|
|
141
|
+
markBlocked,
|
|
142
|
+
onProviderEvent,
|
|
143
|
+
getState,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { createActivityTracker };
|
|
@@ -5,6 +5,7 @@ const { spawnSync } = require("child_process");
|
|
|
5
5
|
const EventBus = require("../bus");
|
|
6
6
|
const { readJSON, writeJSON } = require("../bus/utils");
|
|
7
7
|
const { createActivityStatePublisher } = require("./activityStatePublisher");
|
|
8
|
+
const { createActivityTracker } = require("./activityTracker");
|
|
8
9
|
const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
|
|
9
10
|
const { createCodexThreadProvider } = require("./codexThreadProvider");
|
|
10
11
|
const { createClaudeThreadProvider } = require("./claudeThreadProvider");
|
|
@@ -305,7 +306,8 @@ async function handleEvent(
|
|
|
305
306
|
busSender,
|
|
306
307
|
extraArgs = [],
|
|
307
308
|
threadRuntime = null,
|
|
308
|
-
bootstrapText = ""
|
|
309
|
+
bootstrapText = "",
|
|
310
|
+
tracker = null
|
|
309
311
|
) {
|
|
310
312
|
if (!evt || !evt.data || !evt.data.message) return;
|
|
311
313
|
const memoryPrefix = buildMemoryPrefix(projectRoot);
|
|
@@ -333,6 +335,7 @@ async function handleEvent(
|
|
|
333
335
|
emitStreamDelta,
|
|
334
336
|
streamToPublisher,
|
|
335
337
|
threadRuntime,
|
|
338
|
+
tracker,
|
|
336
339
|
});
|
|
337
340
|
return;
|
|
338
341
|
}
|
|
@@ -379,14 +382,21 @@ async function handleThreadedEvent({
|
|
|
379
382
|
emitStreamDelta,
|
|
380
383
|
streamToPublisher = true,
|
|
381
384
|
threadRuntime,
|
|
385
|
+
tracker = null,
|
|
382
386
|
}) {
|
|
383
387
|
try {
|
|
384
388
|
const plainReplyParts = [];
|
|
389
|
+
if (tracker && typeof tracker.notifyTurnStart === "function") {
|
|
390
|
+
tracker.notifyTurnStart();
|
|
391
|
+
}
|
|
385
392
|
for await (const event of threadRuntime.thread.runStreamed(prompt, {})) {
|
|
386
393
|
if (!event || typeof event !== "object") continue;
|
|
387
394
|
if (typeof threadRuntime.syncProviderSessionId === "function") {
|
|
388
395
|
threadRuntime.syncProviderSessionId();
|
|
389
396
|
}
|
|
397
|
+
if (tracker && typeof tracker.onProviderEvent === "function") {
|
|
398
|
+
tracker.onProviderEvent(event);
|
|
399
|
+
}
|
|
390
400
|
if (event.type === "text_delta" && event.delta) {
|
|
391
401
|
if (streamToPublisher) {
|
|
392
402
|
emitStreamDelta(event.delta);
|
|
@@ -420,6 +430,9 @@ async function handleThreadedEvent({
|
|
|
420
430
|
if (threadRuntime && typeof threadRuntime.rebuildThread === "function") {
|
|
421
431
|
await threadRuntime.rebuildThread();
|
|
422
432
|
}
|
|
433
|
+
if (tracker && typeof tracker.markIdle === "function") {
|
|
434
|
+
tracker.markIdle();
|
|
435
|
+
}
|
|
423
436
|
const errorText = `[internal:${agentType}] error: ${err && err.message ? err.message : "unknown error"}`;
|
|
424
437
|
// eslint-disable-next-line no-console
|
|
425
438
|
console.error(errorText);
|
|
@@ -757,10 +770,7 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
757
770
|
const activityPublisher = createActivityStatePublisher({
|
|
758
771
|
agentsFile, subscriber, projectRoot,
|
|
759
772
|
});
|
|
760
|
-
|
|
761
|
-
function setActivityState(state) {
|
|
762
|
-
activityPublisher.publish(state);
|
|
763
|
-
}
|
|
773
|
+
const activityTracker = createActivityTracker({ publisher: activityPublisher });
|
|
764
774
|
|
|
765
775
|
function getInteractiveSession(publisher) {
|
|
766
776
|
const key = String(publisher || "unknown");
|
|
@@ -774,7 +784,12 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
774
784
|
return session;
|
|
775
785
|
}
|
|
776
786
|
|
|
777
|
-
|
|
787
|
+
activityTracker.notifyStarting("runner");
|
|
788
|
+
if (threadRuntime && threadRuntime.enabled) {
|
|
789
|
+
activityTracker.notifyReady(provider || "");
|
|
790
|
+
} else {
|
|
791
|
+
activityTracker.notifyReady("");
|
|
792
|
+
}
|
|
778
793
|
|
|
779
794
|
// 心跳更新函数
|
|
780
795
|
const updateHeartbeat = () => {
|
|
@@ -835,7 +850,7 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
835
850
|
}
|
|
836
851
|
|
|
837
852
|
if (runnableEvents.length > 0) {
|
|
838
|
-
|
|
853
|
+
activityTracker.notifyTurnStart("thinking");
|
|
839
854
|
}
|
|
840
855
|
|
|
841
856
|
for (const evt of runnableEvents) {
|
|
@@ -851,7 +866,8 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
851
866
|
busSender,
|
|
852
867
|
bootstrap.extraArgs,
|
|
853
868
|
threadRuntime,
|
|
854
|
-
bootstrap.promptText
|
|
869
|
+
bootstrap.promptText,
|
|
870
|
+
activityTracker
|
|
855
871
|
);
|
|
856
872
|
if (evt.__agentViewRaw) {
|
|
857
873
|
getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
|
|
@@ -861,7 +877,7 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
861
877
|
updateHeartbeat();
|
|
862
878
|
lastHeartbeat = now;
|
|
863
879
|
if (runnableEvents.length > 0) {
|
|
864
|
-
|
|
880
|
+
activityTracker.markIdle();
|
|
865
881
|
}
|
|
866
882
|
await busSender.flush();
|
|
867
883
|
}
|
|
@@ -880,6 +896,7 @@ module.exports = {
|
|
|
880
896
|
runInternalRunner,
|
|
881
897
|
createBusSender,
|
|
882
898
|
handleEvent,
|
|
899
|
+
handleThreadedEvent,
|
|
883
900
|
createThreadRuntime,
|
|
884
901
|
getCodexThreadMode,
|
|
885
902
|
getWorkerThreadToolMode,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect the launch environment for a ufoo agent.
|
|
5
|
+
*
|
|
6
|
+
* Output is the canonical `mode` plus an optional `terminalApp` hint when the
|
|
7
|
+
* mode is `terminal` (used by the inject layer to pick AppleScript flavor).
|
|
8
|
+
*
|
|
9
|
+
* Detection rules, evaluated top-to-bottom (first match wins):
|
|
10
|
+
*
|
|
11
|
+
* 0. `UFOO_LAUNCH_MODE` is a non-`auto` canonical value → trust it (short-circuit).
|
|
12
|
+
* 1. `TMUX_PANE` is set → tmux.
|
|
13
|
+
* 2. `UFOO_HOST_SESSION_ID` is set → host.
|
|
14
|
+
* 3. `TERM_PROGRAM === "Apple_Terminal"` → terminal / Apple_Terminal.
|
|
15
|
+
* 4. `TERM_PROGRAM === "iTerm.app"` or `ITERM_SESSION_ID` set → terminal / iterm2.
|
|
16
|
+
* 5. Fallback → terminal (no app hint).
|
|
17
|
+
*
|
|
18
|
+
* Host is detected by env var alone here; callers that want to override host
|
|
19
|
+
* with native-terminal evidence (e.g. daemon ops auto-resolution) layer that
|
|
20
|
+
* on top of the detector's output.
|
|
21
|
+
* Internal / internal-pty modes are always set explicitly by the daemon when
|
|
22
|
+
* spawning worker processes (see ptyRunner / daemon ops), so they fall through
|
|
23
|
+
* the explicit short-circuit and never need to be auto-detected here.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const CANONICAL_LAUNCH_MODES = Object.freeze([
|
|
27
|
+
"terminal",
|
|
28
|
+
"tmux",
|
|
29
|
+
"host",
|
|
30
|
+
"internal",
|
|
31
|
+
"internal-pty",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const CANONICAL_LAUNCH_MODE_SET = new Set(CANONICAL_LAUNCH_MODES);
|
|
35
|
+
|
|
36
|
+
function detectLaunchEnvironment(env = process.env) {
|
|
37
|
+
const explicit = String(env.UFOO_LAUNCH_MODE || "").trim();
|
|
38
|
+
if (explicit && explicit !== "auto" && CANONICAL_LAUNCH_MODE_SET.has(explicit)) {
|
|
39
|
+
return { mode: explicit, terminalApp: "", source: "explicit" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (env.TMUX_PANE) {
|
|
43
|
+
return { mode: "tmux", terminalApp: "", source: "auto" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (env.UFOO_HOST_SESSION_ID) {
|
|
47
|
+
return { mode: "host", terminalApp: "", source: "auto" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const termProgram = String(env.TERM_PROGRAM || "").trim();
|
|
51
|
+
if (termProgram === "Apple_Terminal") {
|
|
52
|
+
return { mode: "terminal", terminalApp: "Apple_Terminal", source: "auto" };
|
|
53
|
+
}
|
|
54
|
+
if (termProgram === "iTerm.app" || env.ITERM_SESSION_ID) {
|
|
55
|
+
return { mode: "terminal", terminalApp: "iterm2", source: "auto" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { mode: "terminal", terminalApp: "", source: "auto" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
detectLaunchEnvironment,
|
|
63
|
+
CANONICAL_LAUNCH_MODES,
|
|
64
|
+
};
|
package/src/agent/launcher.js
CHANGED
|
@@ -10,6 +10,7 @@ const { showBanner } = require("../utils/banner");
|
|
|
10
10
|
const AgentNotifier = require("./notifier");
|
|
11
11
|
const { ActivityDetector } = require("./activityDetector");
|
|
12
12
|
const { createActivityStatePublisher } = require("./activityStatePublisher");
|
|
13
|
+
const { detectLaunchEnvironment } = require("./launchEnvironment");
|
|
13
14
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
14
15
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
15
16
|
const { probeHostCapabilities } = require("../terminal/adapters/hostAdapter");
|
|
@@ -191,18 +192,7 @@ function findPreviousSession(cwd, agentType, tty, tmuxPane) {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
function resolveLaunchMode() {
|
|
194
|
-
|
|
195
|
-
if (explicit) return explicit;
|
|
196
|
-
if (process.env.UFOO_HOST_SESSION_ID) return "host";
|
|
197
|
-
// Deprecated: HORIZON_SESSION_ID fallback (remove after migration)
|
|
198
|
-
if (process.env.HORIZON_SESSION_ID) {
|
|
199
|
-
if (process.env.UFOO_DEBUG) {
|
|
200
|
-
console.error("[launcher] HORIZON_SESSION_ID is deprecated, use UFOO_HOST_SESSION_ID");
|
|
201
|
-
}
|
|
202
|
-
return "host";
|
|
203
|
-
}
|
|
204
|
-
if (process.env.TMUX_PANE) return "tmux";
|
|
205
|
-
return "terminal";
|
|
195
|
+
return detectLaunchEnvironment().mode;
|
|
206
196
|
}
|
|
207
197
|
|
|
208
198
|
function shouldShowLaunchBanner(agentType = "") {
|
package/src/chat/index.js
CHANGED
|
@@ -1316,6 +1316,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1316
1316
|
...currentMeta,
|
|
1317
1317
|
activity_state: diskState,
|
|
1318
1318
|
activity_since: currentMeta.activity_since || diskMeta.activity_since || "",
|
|
1319
|
+
activity_detail: currentMeta.activity_detail || diskMeta.activity_detail || "",
|
|
1319
1320
|
});
|
|
1320
1321
|
}
|
|
1321
1322
|
}
|
package/src/daemon/ops.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
createSession: createHostSession,
|
|
16
16
|
} = require("../terminal/adapters/hostAdapter");
|
|
17
17
|
const { resolveDefaultManualBootstrap } = require("../agent/defaultBootstrap");
|
|
18
|
+
const { detectLaunchEnvironment } = require("../agent/launchEnvironment");
|
|
18
19
|
|
|
19
20
|
function normalizeLaunchAgent(agent = "") {
|
|
20
21
|
const value = String(agent || "").trim().toLowerCase();
|
|
@@ -167,9 +168,19 @@ function resolveConfiguredLaunchMode(configuredMode = "", options = {}) {
|
|
|
167
168
|
if (mode === "internal" || mode === "internal-pty" || mode === "tmux" || mode === "terminal" || mode === "host") {
|
|
168
169
|
return mode;
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
// Auto mode: defer to the unified detector. Daemon ops differs from the
|
|
172
|
+
// launcher in two ways: host context can also arrive via `options`, and
|
|
173
|
+
// when running under a native terminal app we want to override a stale
|
|
174
|
+
// env-level UFOO_HOST_SESSION_ID. Both are handled here, on top of the
|
|
175
|
+
// detector's output.
|
|
176
|
+
const detected = detectLaunchEnvironment().mode;
|
|
177
|
+
if (detected === "tmux") return "tmux";
|
|
172
178
|
const nativeTerminalApp = resolveNativeTerminalApp(options);
|
|
179
|
+
if (detected === "host") {
|
|
180
|
+
if (nativeTerminalApp) return "terminal";
|
|
181
|
+
return "host";
|
|
182
|
+
}
|
|
183
|
+
const hostContext = resolveHostLaunchContext(options);
|
|
173
184
|
if (hostContext.hostDaemonSock && !nativeTerminalApp) return "host";
|
|
174
185
|
return "terminal";
|
|
175
186
|
}
|
package/src/daemon/status.js
CHANGED
|
@@ -155,6 +155,7 @@ function buildStatus(projectRoot, options = {}) {
|
|
|
155
155
|
const tty = meta?.tty || "";
|
|
156
156
|
const activity_state = meta?.activity_state || "";
|
|
157
157
|
const activity_since = meta?.activity_since || "";
|
|
158
|
+
const activity_detail = meta?.activity_detail || "";
|
|
158
159
|
return {
|
|
159
160
|
id,
|
|
160
161
|
nickname,
|
|
@@ -166,6 +167,7 @@ function buildStatus(projectRoot, options = {}) {
|
|
|
166
167
|
tty,
|
|
167
168
|
activity_state,
|
|
168
169
|
activity_since,
|
|
170
|
+
activity_detail,
|
|
169
171
|
host_inject_sock: meta?.host_inject_sock || "",
|
|
170
172
|
host_daemon_sock: meta?.host_daemon_sock || "",
|
|
171
173
|
host_name: meta?.host_name || "",
|