u-foo 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/bin/ufoo.js +15 -7
- package/modules/AGENTS.template.md +4 -102
- package/package.json +3 -2
- package/scripts/global-chat-switch-benchmark.js +406 -0
- package/src/agent/activityDetector.js +328 -0
- package/src/agent/activityStatePublisher.js +67 -0
- package/src/agent/activityStateWriter.js +40 -0
- package/src/agent/internalRunner.js +13 -0
- package/src/agent/launcher.js +47 -7
- package/src/agent/notifier.js +73 -4
- package/src/agent/ptyRunner.js +81 -34
- package/src/agent/ufooAgent.js +192 -6
- package/src/bus/message.js +1 -9
- package/src/bus/subscriber.js +2 -0
- package/src/bus/utils.js +10 -0
- package/src/chat/agentBar.js +21 -3
- package/src/chat/agentViewController.js +2 -0
- package/src/chat/chatLogController.js +28 -5
- package/src/chat/commandExecutor.js +127 -3
- package/src/chat/commands.js +8 -0
- package/src/chat/daemonConnection.js +77 -4
- package/src/chat/daemonCoordinator.js +36 -0
- package/src/chat/daemonMessageRouter.js +22 -0
- package/src/chat/daemonTransport.js +47 -5
- package/src/chat/daemonTransportDefaults.js +1 -0
- package/src/chat/dashboardKeyController.js +89 -1
- package/src/chat/dashboardView.js +312 -93
- package/src/chat/index.js +683 -41
- package/src/chat/inputHistoryController.js +33 -3
- package/src/chat/inputListenerController.js +22 -12
- package/src/chat/layout.js +12 -7
- package/src/chat/projectCloseController.js +119 -0
- package/src/chat/projectRuntimes.js +55 -0
- package/src/chat/statusLineController.js +52 -6
- package/src/chat/streamTracker.js +6 -0
- package/src/chat/transport.js +41 -5
- package/src/cli.js +167 -4
- package/src/daemon/index.js +54 -5
- package/src/daemon/ipcServer.js +6 -1
- package/src/daemon/ops.js +245 -35
- package/src/daemon/status.js +3 -1
- package/src/init/index.js +32 -3
- package/src/projects/projectId.js +29 -0
- package/src/projects/registry.js +279 -0
- package/src/ufoo/agentsStore.js +44 -0
|
@@ -2,16 +2,18 @@ const fs = require("fs");
|
|
|
2
2
|
|
|
3
3
|
function createInputHistoryController(options = {}) {
|
|
4
4
|
const {
|
|
5
|
-
inputHistoryFile,
|
|
6
|
-
historyDir,
|
|
5
|
+
inputHistoryFile: inputHistoryFileOption,
|
|
6
|
+
historyDir: historyDirOption,
|
|
7
7
|
setInputValue = () => {},
|
|
8
8
|
getInputValue = () => "",
|
|
9
9
|
fsMod = fs,
|
|
10
10
|
} = options;
|
|
11
11
|
|
|
12
|
-
if (!
|
|
12
|
+
if (!inputHistoryFileOption || !historyDirOption) {
|
|
13
13
|
throw new Error("createInputHistoryController requires inputHistoryFile and historyDir");
|
|
14
14
|
}
|
|
15
|
+
let inputHistoryFile = inputHistoryFileOption;
|
|
16
|
+
let historyDir = historyDirOption;
|
|
15
17
|
|
|
16
18
|
const inputHistory = [];
|
|
17
19
|
let historyIndex = 0;
|
|
@@ -24,6 +26,9 @@ function createInputHistoryController(options = {}) {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function loadInputHistory(limit = 2000) {
|
|
29
|
+
inputHistory.length = 0;
|
|
30
|
+
historyIndex = 0;
|
|
31
|
+
historyDraft = "";
|
|
27
32
|
try {
|
|
28
33
|
const raw = fsMod.readFileSync(inputHistoryFile, "utf8");
|
|
29
34
|
const lines = String(raw || "").trim().split(/\r?\n/).filter(Boolean);
|
|
@@ -85,6 +90,28 @@ function createInputHistoryController(options = {}) {
|
|
|
85
90
|
setIndexToEnd();
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
function setHistoryTarget(next = {}) {
|
|
94
|
+
if (!next.inputHistoryFile || !next.historyDir) {
|
|
95
|
+
throw new Error("setHistoryTarget requires inputHistoryFile and historyDir");
|
|
96
|
+
}
|
|
97
|
+
inputHistoryFile = next.inputHistoryFile;
|
|
98
|
+
historyDir = next.historyDir;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function restoreDraft(draft = "") {
|
|
102
|
+
const nextDraft = String(draft || "");
|
|
103
|
+
setInputValue(nextDraft);
|
|
104
|
+
historyIndex = inputHistory.length;
|
|
105
|
+
historyDraft = nextDraft;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getDraftForPersistence() {
|
|
109
|
+
if (historyIndex === inputHistory.length) {
|
|
110
|
+
return String(getInputValue() || "");
|
|
111
|
+
}
|
|
112
|
+
return String(historyDraft || "");
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
return {
|
|
89
116
|
loadInputHistory,
|
|
90
117
|
updateDraftFromInput,
|
|
@@ -92,6 +119,9 @@ function createInputHistoryController(options = {}) {
|
|
|
92
119
|
historyDown,
|
|
93
120
|
commitSubmittedText,
|
|
94
121
|
setIndexToEnd,
|
|
122
|
+
setHistoryTarget,
|
|
123
|
+
restoreDraft,
|
|
124
|
+
getDraftForPersistence,
|
|
95
125
|
getState: () => ({
|
|
96
126
|
history: [...inputHistory],
|
|
97
127
|
historyIndex,
|
|
@@ -204,23 +204,33 @@ function createInputListenerController(options = {}) {
|
|
|
204
204
|
|
|
205
205
|
if (keyName === "up" || keyName === "down") {
|
|
206
206
|
const innerWidth = getWrapWidth();
|
|
207
|
-
if (innerWidth
|
|
208
|
-
|
|
209
|
-
const value = (textarea && textarea.value) || "";
|
|
210
|
-
const { row, col } = getCursorRowCol(value, cursorPos, innerWidth);
|
|
211
|
-
if (getPreferredCol() === null) setPreferredCol(col);
|
|
212
|
-
const totalRows = countLines(value, innerWidth);
|
|
213
|
-
|
|
214
|
-
if (keyName === "down" && row >= totalRows - 1) {
|
|
207
|
+
if (innerWidth <= 0) {
|
|
208
|
+
if (keyName === "down") {
|
|
215
209
|
enterDashboardMode();
|
|
216
210
|
return;
|
|
217
211
|
}
|
|
212
|
+
ensureInputCursorVisible();
|
|
213
|
+
updateCursor(textarea);
|
|
214
|
+
render(textarea);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
218
217
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
const cursorPos = getCursorPos();
|
|
219
|
+
const value = (textarea && textarea.value) || "";
|
|
220
|
+
const { row, col } = getCursorRowCol(value, cursorPos, innerWidth);
|
|
221
|
+
if (getPreferredCol() === null) setPreferredCol(col);
|
|
222
|
+
const totalRows = countLines(value, innerWidth);
|
|
223
|
+
|
|
224
|
+
if (keyName === "down" && row >= totalRows - 1) {
|
|
225
|
+
enterDashboardMode();
|
|
226
|
+
return;
|
|
223
227
|
}
|
|
228
|
+
|
|
229
|
+
const targetRow = keyName === "up"
|
|
230
|
+
? Math.max(0, row - 1)
|
|
231
|
+
: Math.min(totalRows - 1, row + 1);
|
|
232
|
+
setCursorPos(getCursorPosForRowCol(value, targetRow, getPreferredCol(), innerWidth));
|
|
233
|
+
|
|
224
234
|
ensureInputCursorVisible();
|
|
225
235
|
updateCursor(textarea);
|
|
226
236
|
render(textarea);
|
package/src/chat/layout.js
CHANGED
|
@@ -2,8 +2,13 @@ function createChatLayout(options = {}) {
|
|
|
2
2
|
const {
|
|
3
3
|
blessed,
|
|
4
4
|
currentInputHeight = 4,
|
|
5
|
+
dashboardHeight = 1,
|
|
5
6
|
version = "unknown",
|
|
6
7
|
} = options;
|
|
8
|
+
const normalizedDashboardHeight = Number.isFinite(dashboardHeight) && dashboardHeight > 0
|
|
9
|
+
? Math.floor(dashboardHeight)
|
|
10
|
+
: 1;
|
|
11
|
+
const reservedBottomLines = Math.max(2, currentInputHeight + 1);
|
|
7
12
|
|
|
8
13
|
const screen = blessed.screen({
|
|
9
14
|
smartCSR: true,
|
|
@@ -33,7 +38,7 @@ function createChatLayout(options = {}) {
|
|
|
33
38
|
top: 0,
|
|
34
39
|
left: 0,
|
|
35
40
|
width: "100%",
|
|
36
|
-
height:
|
|
41
|
+
height: `100%-${reservedBottomLines}`, // Will be adjusted dynamically
|
|
37
42
|
tags: true,
|
|
38
43
|
scrollable: true,
|
|
39
44
|
alwaysScroll: true,
|
|
@@ -97,7 +102,7 @@ function createChatLayout(options = {}) {
|
|
|
97
102
|
bottom: 0,
|
|
98
103
|
left: 0,
|
|
99
104
|
width: "100%",
|
|
100
|
-
height:
|
|
105
|
+
height: normalizedDashboardHeight,
|
|
101
106
|
style: { fg: "gray" },
|
|
102
107
|
tags: true,
|
|
103
108
|
});
|
|
@@ -105,7 +110,7 @@ function createChatLayout(options = {}) {
|
|
|
105
110
|
// Bottom border line for input area (above dashboard)
|
|
106
111
|
const inputBottomLine = blessed.line({
|
|
107
112
|
parent: screen,
|
|
108
|
-
bottom:
|
|
113
|
+
bottom: normalizedDashboardHeight,
|
|
109
114
|
left: 1,
|
|
110
115
|
width: "100%-2",
|
|
111
116
|
orientation: "horizontal",
|
|
@@ -115,10 +120,10 @@ function createChatLayout(options = {}) {
|
|
|
115
120
|
// Prompt indicator
|
|
116
121
|
const promptBox = blessed.box({
|
|
117
122
|
parent: screen,
|
|
118
|
-
bottom:
|
|
123
|
+
bottom: normalizedDashboardHeight + 1,
|
|
119
124
|
left: 0,
|
|
120
125
|
width: 2,
|
|
121
|
-
height: currentInputHeight -
|
|
126
|
+
height: Math.max(1, currentInputHeight - normalizedDashboardHeight - 2),
|
|
122
127
|
content: ">",
|
|
123
128
|
style: { fg: "cyan" },
|
|
124
129
|
});
|
|
@@ -126,10 +131,10 @@ function createChatLayout(options = {}) {
|
|
|
126
131
|
// Input area without left/right border
|
|
127
132
|
const input = blessed.textarea({
|
|
128
133
|
parent: screen,
|
|
129
|
-
bottom:
|
|
134
|
+
bottom: normalizedDashboardHeight + 1,
|
|
130
135
|
left: 2,
|
|
131
136
|
width: "100%-2",
|
|
132
|
-
height: currentInputHeight -
|
|
137
|
+
height: Math.max(1, currentInputHeight - normalizedDashboardHeight - 2),
|
|
133
138
|
inputOnFocus: true,
|
|
134
139
|
keys: true,
|
|
135
140
|
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
function normalizeIndex(value, length) {
|
|
2
|
+
const parsed = Number(value);
|
|
3
|
+
const nextIndex = Number.isFinite(parsed) ? Math.trunc(parsed) : Number.NaN;
|
|
4
|
+
if (!Number.isFinite(nextIndex) || nextIndex < 0 || nextIndex >= length) {
|
|
5
|
+
return -1;
|
|
6
|
+
}
|
|
7
|
+
return nextIndex;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function defaultResolveProjectRoot(row = {}) {
|
|
11
|
+
return String((row && row.project_root) || "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createProjectCloseController(options = {}) {
|
|
15
|
+
const {
|
|
16
|
+
getProjects = () => [],
|
|
17
|
+
getActiveProjectRoot = () => "",
|
|
18
|
+
resolveProjectRoot = defaultResolveProjectRoot,
|
|
19
|
+
isRunning = () => false,
|
|
20
|
+
stopDaemon = () => false,
|
|
21
|
+
switchProject = async () => ({ ok: false, error: "project switching unavailable" }),
|
|
22
|
+
refreshProjects = () => {},
|
|
23
|
+
renderDashboard = () => {},
|
|
24
|
+
renderScreen = () => {},
|
|
25
|
+
logMessage = () => {},
|
|
26
|
+
escapeBlessed = (value) => String(value || ""),
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
let closingProject = false;
|
|
30
|
+
|
|
31
|
+
function pickFallbackProjectRoot(targetProjectRoot) {
|
|
32
|
+
const rows = Array.isArray(getProjects()) ? getProjects() : [];
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
const root = resolveProjectRoot(row);
|
|
35
|
+
if (!root || root === targetProjectRoot) continue;
|
|
36
|
+
return root;
|
|
37
|
+
}
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function requestCloseProject(index) {
|
|
42
|
+
if (closingProject) {
|
|
43
|
+
return { ok: false, error: "project close already in progress" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rows = Array.isArray(getProjects()) ? getProjects() : [];
|
|
47
|
+
const nextIndex = normalizeIndex(index, rows.length);
|
|
48
|
+
if (nextIndex < 0) {
|
|
49
|
+
return { ok: false, error: "project index out of range" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const target = rows[nextIndex] || {};
|
|
53
|
+
const projectRoot = resolveProjectRoot(target);
|
|
54
|
+
if (!projectRoot) {
|
|
55
|
+
return { ok: false, error: "project root unavailable" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const projectName = String(target.project_name || projectRoot);
|
|
59
|
+
const escapedName = escapeBlessed(projectName);
|
|
60
|
+
const activeProjectRoot = String(getActiveProjectRoot() || "");
|
|
61
|
+
|
|
62
|
+
closingProject = true;
|
|
63
|
+
try {
|
|
64
|
+
logMessage("status", `{white-fg}⚙{/white-fg} Closing project ${escapedName} daemon and agents...`);
|
|
65
|
+
|
|
66
|
+
let switchedTo = "";
|
|
67
|
+
if (activeProjectRoot === projectRoot) {
|
|
68
|
+
const fallbackRoot = pickFallbackProjectRoot(projectRoot);
|
|
69
|
+
if (!fallbackRoot) {
|
|
70
|
+
const error = "Cannot close current project; switch to another project first";
|
|
71
|
+
logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(error)}`);
|
|
72
|
+
return { ok: false, error };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const switched = await Promise.resolve(switchProject(fallbackRoot));
|
|
76
|
+
if (!switched || switched.ok !== true) {
|
|
77
|
+
const reason = String((switched && switched.error) || "switch failed");
|
|
78
|
+
logMessage("error", `{white-fg}✗{/white-fg} Failed to switch project before close: ${escapeBlessed(reason)}`);
|
|
79
|
+
return { ok: false, error: reason };
|
|
80
|
+
}
|
|
81
|
+
switchedTo = fallbackRoot;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const wasRunning = Boolean(isRunning(projectRoot));
|
|
85
|
+
stopDaemon(projectRoot);
|
|
86
|
+
|
|
87
|
+
refreshProjects();
|
|
88
|
+
renderDashboard();
|
|
89
|
+
renderScreen();
|
|
90
|
+
|
|
91
|
+
if (wasRunning) {
|
|
92
|
+
logMessage("status", `{white-fg}✓{/white-fg} Closed project ${escapedName} daemon and agents`);
|
|
93
|
+
} else {
|
|
94
|
+
logMessage("status", `{white-fg}✓{/white-fg} Project ${escapedName} daemon already stopped`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
project_root: projectRoot,
|
|
100
|
+
switched_to: switchedTo || undefined,
|
|
101
|
+
};
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const message = err && err.message ? err.message : String(err || "project close failed");
|
|
104
|
+
logMessage("error", `{white-fg}✗{/white-fg} Failed to close project: ${escapeBlessed(message)}`);
|
|
105
|
+
return { ok: false, error: message };
|
|
106
|
+
} finally {
|
|
107
|
+
closingProject = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
requestCloseProject,
|
|
113
|
+
pickFallbackProjectRoot,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
createProjectCloseController,
|
|
119
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function parseTimestampMs(value) {
|
|
2
|
+
const parsed = Date.parse(String(value || ""));
|
|
3
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function projectLabel(row = {}) {
|
|
7
|
+
return String(row.project_name || row.project_root || "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeInteractionMs(value) {
|
|
11
|
+
const num = Number(value);
|
|
12
|
+
if (!Number.isFinite(num) || num < 0) return 0;
|
|
13
|
+
return num;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function filterVisibleProjectRuntimes(rows = []) {
|
|
17
|
+
const sourceRows = Array.isArray(rows) ? rows : [];
|
|
18
|
+
return sourceRows.filter((row) => {
|
|
19
|
+
const status = String((row && row.status) || "").trim().toLowerCase();
|
|
20
|
+
return status !== "stopped";
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sortProjectRuntimes(options = {}) {
|
|
25
|
+
const {
|
|
26
|
+
rows = [],
|
|
27
|
+
activeProjectRoot = "",
|
|
28
|
+
resolveProjectRoot = (row) => String((row && row.project_root) || ""),
|
|
29
|
+
getInteractionMs = () => 0,
|
|
30
|
+
} = options;
|
|
31
|
+
const sourceRows = Array.isArray(rows) ? rows.slice() : [];
|
|
32
|
+
// Keep arg usage for backward compatibility with existing callers/tests.
|
|
33
|
+
void activeProjectRoot;
|
|
34
|
+
void resolveProjectRoot;
|
|
35
|
+
|
|
36
|
+
sourceRows.sort((a, b) => {
|
|
37
|
+
const bInteraction = normalizeInteractionMs(getInteractionMs(b));
|
|
38
|
+
const aInteraction = normalizeInteractionMs(getInteractionMs(a));
|
|
39
|
+
if (bInteraction !== aInteraction) return bInteraction - aInteraction;
|
|
40
|
+
|
|
41
|
+
const bSeen = parseTimestampMs(b && b.last_seen);
|
|
42
|
+
const aSeen = parseTimestampMs(a && a.last_seen);
|
|
43
|
+
if (bSeen !== aSeen) return bSeen - aSeen;
|
|
44
|
+
|
|
45
|
+
return projectLabel(a).localeCompare(projectLabel(b), "en", { sensitivity: "base" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return sourceRows;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
sortProjectRuntimes,
|
|
53
|
+
parseTimestampMs,
|
|
54
|
+
filterVisibleProjectRuntimes,
|
|
55
|
+
};
|
|
@@ -107,20 +107,66 @@ function createStatusLineController(options = {}) {
|
|
|
107
107
|
renderStatusLine();
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function
|
|
111
|
-
|
|
110
|
+
function normalizePendingItem(text, options = {}) {
|
|
111
|
+
const key = options && typeof options.key === "string"
|
|
112
|
+
? options.key.trim()
|
|
113
|
+
: "";
|
|
114
|
+
return {
|
|
115
|
+
text: text || "",
|
|
116
|
+
key,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function headPendingText() {
|
|
121
|
+
if (pendingStatusLines.length === 0) return "";
|
|
122
|
+
const item = pendingStatusLines[0];
|
|
123
|
+
return item && typeof item.text === "string" ? item.text : "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function queueStatusLine(text, options = {}) {
|
|
127
|
+
const item = normalizePendingItem(text, options);
|
|
128
|
+
if (item.key) {
|
|
129
|
+
const existingIndex = pendingStatusLines.findIndex((entry) => entry.key === item.key);
|
|
130
|
+
if (existingIndex >= 0) {
|
|
131
|
+
pendingStatusLines[existingIndex] = item;
|
|
132
|
+
if (existingIndex === 0) {
|
|
133
|
+
setPrimaryStatus(item.text, { pending: true });
|
|
134
|
+
renderScreen();
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pendingStatusLines.push(item);
|
|
112
141
|
if (pendingStatusLines.length === 1) {
|
|
113
|
-
setPrimaryStatus(
|
|
142
|
+
setPrimaryStatus(item.text, { pending: true });
|
|
114
143
|
renderScreen();
|
|
115
144
|
}
|
|
116
145
|
}
|
|
117
146
|
|
|
118
|
-
function resolveStatusLine(text) {
|
|
147
|
+
function resolveStatusLine(text, options = {}) {
|
|
148
|
+
const key = options && typeof options.key === "string"
|
|
149
|
+
? options.key.trim()
|
|
150
|
+
: "";
|
|
151
|
+
let removedHead = false;
|
|
152
|
+
|
|
119
153
|
if (pendingStatusLines.length > 0) {
|
|
120
|
-
|
|
154
|
+
if (key) {
|
|
155
|
+
const index = pendingStatusLines.findIndex((entry) => entry.key === key);
|
|
156
|
+
if (index >= 0) {
|
|
157
|
+
pendingStatusLines.splice(index, 1);
|
|
158
|
+
removedHead = index === 0;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
pendingStatusLines.shift();
|
|
162
|
+
removedHead = true;
|
|
163
|
+
}
|
|
121
164
|
}
|
|
165
|
+
|
|
122
166
|
if (pendingStatusLines.length > 0) {
|
|
123
|
-
|
|
167
|
+
if (removedHead || !primaryStatusPending) {
|
|
168
|
+
setPrimaryStatus(headPendingText(), { pending: true });
|
|
169
|
+
}
|
|
124
170
|
} else {
|
|
125
171
|
setPrimaryStatus(text || "", { pending: false });
|
|
126
172
|
}
|
|
@@ -136,11 +136,17 @@ function createStreamTracker(options = {}) {
|
|
|
136
136
|
return true;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
function discardAll() {
|
|
140
|
+
streamStates.clear();
|
|
141
|
+
pendingDeliveries.clear();
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
return {
|
|
140
145
|
beginStream,
|
|
141
146
|
appendStreamDelta,
|
|
142
147
|
finalizeStream,
|
|
143
148
|
hasStream,
|
|
149
|
+
discardAll,
|
|
144
150
|
markPendingDelivery,
|
|
145
151
|
getPendingState,
|
|
146
152
|
consumePendingDelivery,
|
package/src/chat/transport.js
CHANGED
|
@@ -3,10 +3,46 @@ const path = require("path");
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const { spawn, spawnSync } = require("child_process");
|
|
5
5
|
|
|
6
|
-
function connectSocket(sockPath) {
|
|
6
|
+
function connectSocket(sockPath, options = {}) {
|
|
7
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
8
|
+
? Math.trunc(options.timeoutMs)
|
|
9
|
+
: 0;
|
|
7
10
|
return new Promise((resolve, reject) => {
|
|
8
|
-
|
|
9
|
-
client.
|
|
11
|
+
let timeoutHandle = null;
|
|
12
|
+
const client = net.createConnection(sockPath, () => {
|
|
13
|
+
if (timeoutHandle) {
|
|
14
|
+
clearTimeout(timeoutHandle);
|
|
15
|
+
}
|
|
16
|
+
resolve(client);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const cleanup = () => {
|
|
20
|
+
if (timeoutHandle) {
|
|
21
|
+
clearTimeout(timeoutHandle);
|
|
22
|
+
timeoutHandle = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
client.on("error", (err) => {
|
|
27
|
+
cleanup();
|
|
28
|
+
reject(err);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (timeoutMs > 0) {
|
|
32
|
+
timeoutHandle = setTimeout(() => {
|
|
33
|
+
const err = new Error(`connect timeout after ${timeoutMs}ms`);
|
|
34
|
+
err.code = "ETIMEDOUT";
|
|
35
|
+
try {
|
|
36
|
+
client.destroy(err);
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
reject(err);
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
if (typeof timeoutHandle.unref === "function") {
|
|
43
|
+
timeoutHandle.unref();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
10
46
|
});
|
|
11
47
|
}
|
|
12
48
|
|
|
@@ -38,11 +74,11 @@ function stopDaemon(projectRoot) {
|
|
|
38
74
|
});
|
|
39
75
|
}
|
|
40
76
|
|
|
41
|
-
async function connectWithRetry(sockPath, retries, delayMs) {
|
|
77
|
+
async function connectWithRetry(sockPath, retries, delayMs, options = {}) {
|
|
42
78
|
for (let i = 0; i < retries; i += 1) {
|
|
43
79
|
try {
|
|
44
80
|
// eslint-disable-next-line no-await-in-loop
|
|
45
|
-
const client = await connectSocket(sockPath);
|
|
81
|
+
const client = await connectSocket(sockPath, options);
|
|
46
82
|
return client;
|
|
47
83
|
} catch {
|
|
48
84
|
// eslint-disable-next-line no-await-in-loop
|