cofluxd 2.10.0 → 2.12.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/coflux.mjs +10 -6
- package/cofluxd.mjs +31 -12
- package/executor/host.js +5 -0
- package/executor/runner.js +199 -33
- package/package.json +1 -1
- package/skills/coflux/SKILL.md +17 -4
package/coflux.mjs
CHANGED
|
@@ -517,11 +517,13 @@ function renderExecutorTimeout(timeoutSec, runId, phase) {
|
|
|
517
517
|
return `等待超时(${timeoutSec}s):executor 任务 ${runId} 仍是 ${phase},已请求取消。可加大 --timeout 后重发`;
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
/** Submit. A transport failure retries with the same submissionId; a refusal is reported verbatim.
|
|
521
|
-
|
|
520
|
+
/** Submit. A transport failure retries with the same submissionId; a refusal is reported verbatim.
|
|
521
|
+
* `title` names the run on the desktop's card (plan 20260929-executor-pip); empty = the daemon
|
|
522
|
+
* falls back to the prompt's first line. */
|
|
523
|
+
async function executorSubmit(prompt, write, title) {
|
|
522
524
|
const submission = submissionId();
|
|
523
525
|
for (let attempt = 0; ; attempt += 1) {
|
|
524
|
-
const result = await agentPostResult({ action: "executor.submit", submissionId: submission, prompt, write });
|
|
526
|
+
const result = await agentPostResult({ action: "executor.submit", submissionId: submission, prompt, write, title });
|
|
525
527
|
if (result.ok) {
|
|
526
528
|
const runId = String(result.value?.runId ?? "");
|
|
527
529
|
if (!runId) die("daemon 没有返回 runId(版本太旧?)");
|
|
@@ -533,15 +535,16 @@ async function executorSubmit(prompt, write) {
|
|
|
533
535
|
}
|
|
534
536
|
|
|
535
537
|
async function cmdExecutor(values) {
|
|
536
|
-
if (positionals[1] !== "run") die(`executor 的子命令只有 run:coflux executor run --prompt="<任务>" [--write]`);
|
|
538
|
+
if (positionals[1] !== "run") die(`executor 的子命令只有 run:coflux executor run --prompt="<任务>" [--title="<标题>"] [--write]`);
|
|
537
539
|
const prompt = String(values.prompt ?? "").trim();
|
|
538
540
|
if (!prompt) {
|
|
539
541
|
die(`executor run 需要 --prompt="<任务>"(一句把边界说清的任务描述,例如 --prompt="把 crates/worker 的 clippy 警告清掉")`);
|
|
540
542
|
}
|
|
541
543
|
const write = Boolean(values.write);
|
|
544
|
+
const title = String(values.title ?? "").trim();
|
|
542
545
|
const timeoutSec = executorTimeoutSecs(values.timeout);
|
|
543
546
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
544
|
-
const runId = await executorSubmit(prompt, write);
|
|
547
|
+
const runId = await executorSubmit(prompt, write, title);
|
|
545
548
|
for (;;) {
|
|
546
549
|
// The first poll does not sleep: a rejection (write lock taken, model not configured) has to
|
|
547
550
|
// surface immediately instead of costing the caller a whole poll interval.
|
|
@@ -587,13 +590,14 @@ const HELP = `coflux —— 账号与终端操作
|
|
|
587
590
|
coflux notify "<一句话>" 发送站内通知;服务器保存后确认送达
|
|
588
591
|
coflux progress "<一句话>" 播报进度:显示在工作区卡片上,被下一条覆盖(不打扰用户)
|
|
589
592
|
coflux ports 列出本工作区的监听端口及可直接打开的预览 URL
|
|
590
|
-
coflux executor run --prompt="<任务>" [--write] [--timeout <秒>]
|
|
593
|
+
coflux executor run --prompt="<任务>" [--title="<标题>"] [--write] [--timeout <秒>]
|
|
591
594
|
把一个边界清楚的子任务甩给内置的轻量 executor(由本机 Coflux.app
|
|
592
595
|
执行),阻塞到跑完并打印它的最终回复与改动文件。一次性:没有会话、
|
|
593
596
|
不续聊,要改就再发一次。入参只有任务描述与读写模式——模型由用户在
|
|
594
597
|
Coflux.app 里全局配一次。默认只读;--write 才允许改文件(同一工作区
|
|
595
598
|
同时只允许一个写任务)。它被内核级沙箱锁在本工作区目录内,**不联网**
|
|
596
599
|
(先把依赖装好再甩),也**不会 git commit**(改动由你自己 review 提交)
|
|
600
|
+
--title 给这次运行起个短标题:用户在本终端上会看到一张进度小卡
|
|
597
601
|
只有装了 Coflux.app 的这台机器能用
|
|
598
602
|
coflux workspace 一行 JSON 报出「我在哪」:workspaceId(cwd 所在的有效工作区,本地命令
|
|
599
603
|
都落在它上面)、path、owningWorkspaceId(本终端此刻归属哪个工作区)、
|
package/cofluxd.mjs
CHANGED
|
@@ -28,9 +28,15 @@ import { executorRuntime, plistXml, ptydPlistXml, ptydSystemdUnit, systemdUnit }
|
|
|
28
28
|
const DEFAULT_SERVER = "wss://api.coflux.dev/daemon";
|
|
29
29
|
|
|
30
30
|
const REPO = "myWsq/coflux";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// Release downloads route by version, never by failure (plan 20260930-r2-download-mirror): the mirror
|
|
32
|
+
// (R2 at dl.coflux.dev) holds only the latest stable release and its `latest.json` pointer; every other
|
|
33
|
+
// version, and every prerelease, comes from the GitHub Releases archive. No mirror-then-GitHub fallback:
|
|
34
|
+
// a mirror outage must stay visible.
|
|
35
|
+
const RELEASE_MIRROR_BASE = (
|
|
36
|
+
process.env.COFLUX_RELEASE_DOWNLOAD_BASE || "https://dl.coflux.dev/releases"
|
|
37
|
+
).replace(/\/+$/, "");
|
|
38
|
+
const RELEASE_ARCHIVE_BASE = (
|
|
39
|
+
process.env.COFLUX_RELEASE_ARCHIVE_BASE || `https://github.com/${REPO}/releases/download`
|
|
34
40
|
).replace(/\/+$/, "");
|
|
35
41
|
const MAX_RELEASE_METADATA_BYTES = 1024 * 1024;
|
|
36
42
|
const HOME = process.env.COFLUX_HOME || join(homedir(), ".coflux");
|
|
@@ -258,16 +264,18 @@ function persistCliReleaseFloor(version) {
|
|
|
258
264
|
}
|
|
259
265
|
}
|
|
260
266
|
|
|
261
|
-
//
|
|
262
|
-
async function
|
|
267
|
+
// The latest stable tag named by the mirror's `latest.json`, or null when it cannot be read or is invalid.
|
|
268
|
+
async function readMirrorLatestTag() {
|
|
263
269
|
try {
|
|
264
270
|
const body = await fetchBounded(
|
|
265
|
-
`${
|
|
271
|
+
`${RELEASE_MIRROR_BASE}/latest.json`,
|
|
266
272
|
MAX_RELEASE_METADATA_BYTES,
|
|
267
|
-
"
|
|
273
|
+
"release 下载镜像 latest.json",
|
|
268
274
|
);
|
|
269
|
-
const
|
|
270
|
-
|
|
275
|
+
const tag = JSON.parse(body.toString("utf8"))?.version;
|
|
276
|
+
if (typeof tag !== "string") return null;
|
|
277
|
+
assertReleaseVersion(tag);
|
|
278
|
+
return tag;
|
|
271
279
|
} catch {
|
|
272
280
|
return null;
|
|
273
281
|
}
|
|
@@ -322,13 +330,23 @@ async function ensureBinaries({ version, binDir, skipIfPresent }) {
|
|
|
322
330
|
}
|
|
323
331
|
const target = rustTarget();
|
|
324
332
|
let releaseVersion;
|
|
333
|
+
let releaseBase;
|
|
325
334
|
if (!version || version === "latest") {
|
|
326
|
-
const tag = await
|
|
327
|
-
if (!tag)
|
|
335
|
+
const tag = await readMirrorLatestTag();
|
|
336
|
+
if (!tag) {
|
|
337
|
+
die(
|
|
338
|
+
`无法从下载镜像读取最新版本(${RELEASE_MIRROR_BASE}/latest.json),拒绝无版本身份的下载。\n` +
|
|
339
|
+
" 用 --version vX.Y.Z 指定版本即可从 GitHub Releases 安装。",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
328
342
|
releaseVersion = tag;
|
|
343
|
+
releaseBase = RELEASE_MIRROR_BASE;
|
|
329
344
|
console.log(`最新版本: ${releaseVersion}`);
|
|
330
345
|
} else {
|
|
331
346
|
releaseVersion = version;
|
|
347
|
+
// Only the tag the mirror's pointer names lives on the mirror; any other version (older, newer or a
|
|
348
|
+
// prerelease), and any version asked for while the pointer is unreadable, comes from the archive.
|
|
349
|
+
releaseBase = (await readMirrorLatestTag()) === releaseVersion ? RELEASE_MIRROR_BASE : RELEASE_ARCHIVE_BASE;
|
|
332
350
|
}
|
|
333
351
|
try {
|
|
334
352
|
assertReleaseVersion(releaseVersion);
|
|
@@ -348,7 +366,8 @@ async function ensureBinaries({ version, binDir, skipIfPresent }) {
|
|
|
348
366
|
die(error instanceof Error ? error.message : String(error));
|
|
349
367
|
}
|
|
350
368
|
|
|
351
|
-
const base = `${
|
|
369
|
+
const base = `${releaseBase}/${releaseVersion}`;
|
|
370
|
+
console.log(`下载源: ${base}`);
|
|
352
371
|
const stageDir = fs.mkdtempSync(join(BIN_DIR, ".coflux-release-install-"));
|
|
353
372
|
let failure;
|
|
354
373
|
try {
|
package/executor/host.js
CHANGED
|
@@ -632,6 +632,8 @@ var ExecutorManager = class {
|
|
|
632
632
|
const run = this.live.get(runId);
|
|
633
633
|
if (run) run.settled = true;
|
|
634
634
|
this.apply(this.table.finish(runId, outcome));
|
|
635
|
+
} else if (message.type === "transcript") {
|
|
636
|
+
if (this.live.has(runId)) this.deps.sendTranscript?.(runId, message.fragment);
|
|
635
637
|
}
|
|
636
638
|
}
|
|
637
639
|
/**
|
|
@@ -683,6 +685,9 @@ function createExecutorHostCore(options) {
|
|
|
683
685
|
};
|
|
684
686
|
},
|
|
685
687
|
sendReport: (report) => options.send({ type: "report", ...report }),
|
|
688
|
+
// The daemon assigns the seq and buffers per run; the same frame rides JSONL for the daemon's
|
|
689
|
+
// own host and the device channel for Coflux.app.
|
|
690
|
+
sendTranscript: (runId, fragment) => options.send({ type: "transcript", runId, fragment }),
|
|
686
691
|
log: options.log
|
|
687
692
|
});
|
|
688
693
|
return {
|
package/executor/runner.js
CHANGED
|
@@ -82,6 +82,181 @@ var EXECUTOR_RUNNER_EXIT = {
|
|
|
82
82
|
aborted: 11
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
// ../executor/src/transcript.ts
|
|
86
|
+
var TOOL_OUTPUT_HEAD_CHARS = 4 * 1024;
|
|
87
|
+
var TOOL_OUTPUT_TAIL_CHARS = 2 * 1024;
|
|
88
|
+
var REDACTED = "***";
|
|
89
|
+
var CREDENTIAL_ENV_NAMES = [
|
|
90
|
+
"ANTHROPIC_API_KEY",
|
|
91
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
92
|
+
"OPENAI_API_KEY",
|
|
93
|
+
"GEMINI_API_KEY",
|
|
94
|
+
"GOOGLE_API_KEY",
|
|
95
|
+
"OPENROUTER_API_KEY",
|
|
96
|
+
"GROQ_API_KEY",
|
|
97
|
+
"MISTRAL_API_KEY",
|
|
98
|
+
"XAI_API_KEY",
|
|
99
|
+
"DEEPSEEK_API_KEY"
|
|
100
|
+
];
|
|
101
|
+
function capToolOutput(text) {
|
|
102
|
+
if (text.length <= TOOL_OUTPUT_HEAD_CHARS + TOOL_OUTPUT_TAIL_CHARS) return text;
|
|
103
|
+
const head = text.slice(0, TOOL_OUTPUT_HEAD_CHARS);
|
|
104
|
+
const tail = text.slice(text.length - TOOL_OUTPUT_TAIL_CHARS);
|
|
105
|
+
const omitted = Buffer.byteLength(text.slice(TOOL_OUTPUT_HEAD_CHARS, text.length - TOOL_OUTPUT_TAIL_CHARS));
|
|
106
|
+
return `${head}
|
|
107
|
+
\u2026 ${omitted} bytes omitted \u2026
|
|
108
|
+
${tail}`;
|
|
109
|
+
}
|
|
110
|
+
function salientArgument(toolName, args) {
|
|
111
|
+
const input = args ?? {};
|
|
112
|
+
const pick = (...keys) => {
|
|
113
|
+
for (const key of keys) {
|
|
114
|
+
const value = input[key];
|
|
115
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
116
|
+
}
|
|
117
|
+
return "";
|
|
118
|
+
};
|
|
119
|
+
switch (toolName) {
|
|
120
|
+
case "bash":
|
|
121
|
+
return pick("command", "cmd");
|
|
122
|
+
case "read":
|
|
123
|
+
case "write":
|
|
124
|
+
case "edit":
|
|
125
|
+
case "ls":
|
|
126
|
+
return pick("path", "file_path", "filePath", "directory");
|
|
127
|
+
case "grep":
|
|
128
|
+
case "find": {
|
|
129
|
+
const pattern = pick("pattern", "query", "regex", "glob");
|
|
130
|
+
const path = pick("path", "directory");
|
|
131
|
+
return path ? `${pattern} ${path}`.trim() : pattern;
|
|
132
|
+
}
|
|
133
|
+
default: {
|
|
134
|
+
const direct = pick("command", "path", "pattern", "query");
|
|
135
|
+
if (direct) return direct;
|
|
136
|
+
try {
|
|
137
|
+
const json = JSON.stringify(args);
|
|
138
|
+
return json && json !== "{}" && json !== "null" ? json : "";
|
|
139
|
+
} catch {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function toolResultText(result) {
|
|
146
|
+
if (result === void 0 || result === null) return "";
|
|
147
|
+
if (typeof result === "string") return result;
|
|
148
|
+
if (typeof result !== "object") return String(result);
|
|
149
|
+
const record = result;
|
|
150
|
+
if (Array.isArray(record.content)) {
|
|
151
|
+
const parts = [];
|
|
152
|
+
for (const item of record.content) {
|
|
153
|
+
if (typeof item === "string") parts.push(item);
|
|
154
|
+
else if (item && typeof item === "object" && typeof item.text === "string") {
|
|
155
|
+
parts.push(item.text);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (parts.length > 0) return parts.join("\n");
|
|
159
|
+
}
|
|
160
|
+
for (const key of ["output", "text", "stdout", "message", "error"]) {
|
|
161
|
+
if (typeof record[key] === "string") return record[key];
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
return JSON.stringify(result);
|
|
165
|
+
} catch {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function redact(text, secrets) {
|
|
170
|
+
let out = text;
|
|
171
|
+
for (const secret of secrets) {
|
|
172
|
+
if (!secret) continue;
|
|
173
|
+
out = out.split(secret).join(REDACTED);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function toolEnvironment(base, extra, secrets) {
|
|
178
|
+
const env = {};
|
|
179
|
+
const stripped = new Set(CREDENTIAL_ENV_NAMES);
|
|
180
|
+
for (const [name, value] of Object.entries(base ?? {})) {
|
|
181
|
+
if (value === void 0 || stripped.has(name)) continue;
|
|
182
|
+
if (secrets.some((secret) => secret && value.includes(secret))) continue;
|
|
183
|
+
env[name] = value;
|
|
184
|
+
}
|
|
185
|
+
for (const [name, value] of Object.entries(extra)) env[name] = value;
|
|
186
|
+
return env;
|
|
187
|
+
}
|
|
188
|
+
function createTranscriptRecorder(options) {
|
|
189
|
+
const now = options.now ?? (() => Date.now());
|
|
190
|
+
const secrets = options.secrets.filter(Boolean);
|
|
191
|
+
const emit = (fragment) => {
|
|
192
|
+
const clean = Object.fromEntries(
|
|
193
|
+
Object.entries(fragment).map(([key, value]) => [key, typeof value === "string" ? redact(value, secrets) : value])
|
|
194
|
+
);
|
|
195
|
+
options.emit(clean);
|
|
196
|
+
};
|
|
197
|
+
let streaming = "";
|
|
198
|
+
let lastComplete = "";
|
|
199
|
+
let stopReason = "";
|
|
200
|
+
let errorMessage = "";
|
|
201
|
+
const open = /* @__PURE__ */ new Map();
|
|
202
|
+
return {
|
|
203
|
+
onEvent(event) {
|
|
204
|
+
switch (event.type) {
|
|
205
|
+
case "message_start": {
|
|
206
|
+
if (event.message?.role === "assistant") streaming = "";
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "message_update": {
|
|
210
|
+
const inner = event.assistantMessageEvent;
|
|
211
|
+
if (inner?.type === "text_delta" && inner.delta) streaming += inner.delta;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "message_end": {
|
|
215
|
+
const message = event.message ?? {};
|
|
216
|
+
if (message.stopReason) stopReason = message.stopReason;
|
|
217
|
+
if (message.errorMessage) errorMessage = message.errorMessage;
|
|
218
|
+
if (message.role !== void 0 && message.role !== "assistant") break;
|
|
219
|
+
const text = streaming.trim();
|
|
220
|
+
streaming = "";
|
|
221
|
+
if (!text) break;
|
|
222
|
+
lastComplete = text;
|
|
223
|
+
emit({ kind: "assistant", text, at: now() });
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "tool_execution_start": {
|
|
227
|
+
const tool = event.toolName || "tool";
|
|
228
|
+
const record = { tool, argument: salientArgument(tool, event.args), at: now() };
|
|
229
|
+
open.set(event.toolCallId || `${tool}-${open.size}`, record);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case "tool_execution_end": {
|
|
233
|
+
const tool = event.toolName || "tool";
|
|
234
|
+
const key = event.toolCallId || "";
|
|
235
|
+
const started = open.get(key);
|
|
236
|
+
open.delete(key);
|
|
237
|
+
emit({
|
|
238
|
+
kind: "tool",
|
|
239
|
+
tool,
|
|
240
|
+
argument: started?.argument ?? salientArgument(tool, void 0),
|
|
241
|
+
output: capToolOutput(toolResultText(event.result)),
|
|
242
|
+
failed: event.isError === true,
|
|
243
|
+
at: now()
|
|
244
|
+
});
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
default:
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
error(text) {
|
|
252
|
+
if (!text) return;
|
|
253
|
+
emit({ kind: "error", text, at: now() });
|
|
254
|
+
},
|
|
255
|
+
lastAssistantText: () => streaming.trim() ? streaming.trim() : lastComplete,
|
|
256
|
+
lastStop: () => ({ reason: stopReason, errorMessage })
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
85
260
|
// ../executor/src/runner.ts
|
|
86
261
|
var parentPort = process.parentPort;
|
|
87
262
|
function send(message) {
|
|
@@ -93,10 +268,10 @@ function onHostMessage(listener) {
|
|
|
93
268
|
else process.on("message", (value) => listener(value));
|
|
94
269
|
}
|
|
95
270
|
var transcriptSeq = 0;
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
send({ type: "transcript", seq: ++transcriptSeq, kind, text });
|
|
271
|
+
function sendFragment(fragment) {
|
|
272
|
+
send({ type: "transcript", seq: ++transcriptSeq, fragment });
|
|
99
273
|
}
|
|
274
|
+
var recorder = createTranscriptRecorder({ emit: sendFragment, secrets: [] });
|
|
100
275
|
var liveGroups = /* @__PURE__ */ new Set();
|
|
101
276
|
function killGroup(pgid, signal) {
|
|
102
277
|
try {
|
|
@@ -119,17 +294,16 @@ function sandboxedBashOperations(start) {
|
|
|
119
294
|
cwd: start.workspaceRoot,
|
|
120
295
|
detached: true,
|
|
121
296
|
// Its own process group, so the whole group can be killed.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
297
|
+
// The credential is never handed down to a tool process: the well-known credential
|
|
298
|
+
// variables are stripped by name, and anything carrying the key's value by value, so a tool
|
|
299
|
+
// that dumps its environment cannot echo it into the transcript. TMPDIR is the scratch dir,
|
|
300
|
+
// not the system /tmp: that holds other tasks and shared sockets, and the profile does not
|
|
301
|
+
// allow it anyway.
|
|
302
|
+
env: toolEnvironment(
|
|
303
|
+
options.env,
|
|
304
|
+
{ TMPDIR: start.scratchDir, COFLUX_EXECUTOR_RUN_ID: start.runId },
|
|
305
|
+
[start.apiKey]
|
|
306
|
+
),
|
|
133
307
|
stdio: ["ignore", "pipe", "pipe"]
|
|
134
308
|
});
|
|
135
309
|
const pgid = child.pid;
|
|
@@ -234,7 +408,7 @@ async function run(start) {
|
|
|
234
408
|
writable: start.write
|
|
235
409
|
});
|
|
236
410
|
if (verdict) {
|
|
237
|
-
|
|
411
|
+
recorder.error(`\u5DF2\u62E6\u4E0B ${event.toolName}\uFF1A${verdict.reason}`);
|
|
238
412
|
return { block: true, reason: verdict.reason };
|
|
239
413
|
}
|
|
240
414
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
@@ -286,23 +460,12 @@ async function run(start) {
|
|
|
286
460
|
resourceLoader
|
|
287
461
|
});
|
|
288
462
|
send({ type: "running" });
|
|
289
|
-
let lastAssistantText = "";
|
|
290
|
-
let lastStopReason = "";
|
|
291
|
-
let lastErrorMessage = "";
|
|
292
463
|
session.subscribe((event) => {
|
|
293
|
-
if (event.type === "
|
|
294
|
-
const inner = event.assistantMessageEvent;
|
|
295
|
-
if (inner?.type === "text_delta" && inner.delta) lastAssistantText += inner.delta;
|
|
296
|
-
} else if (event.type === "tool_execution_start") {
|
|
464
|
+
if (event.type === "tool_execution_start") {
|
|
297
465
|
const name = event.toolName ?? "tool";
|
|
298
466
|
send({ type: "progress", note: `\u6B63\u5728\u6267\u884C ${name}` });
|
|
299
|
-
transcript("tool", `\u2192 ${name}`);
|
|
300
|
-
} else if (event.type === "message_end" || event.type === "turn_end") {
|
|
301
|
-
const message = event.message ?? {};
|
|
302
|
-
if (message.stopReason) lastStopReason = message.stopReason;
|
|
303
|
-
if (message.errorMessage) lastErrorMessage = message.errorMessage;
|
|
304
|
-
if (lastAssistantText) transcript("assistant", lastAssistantText);
|
|
305
467
|
}
|
|
468
|
+
recorder.onEvent(event);
|
|
306
469
|
});
|
|
307
470
|
const timeout = setTimeout(() => void session.abort(), start.timeoutMs);
|
|
308
471
|
try {
|
|
@@ -311,12 +474,14 @@ async function run(start) {
|
|
|
311
474
|
clearTimeout(timeout);
|
|
312
475
|
}
|
|
313
476
|
await stopAllGroups();
|
|
477
|
+
const { reason: lastStopReason, errorMessage: lastErrorMessage } = recorder.lastStop();
|
|
478
|
+
const lastAssistantText = recorder.lastAssistantText();
|
|
314
479
|
if (lastStopReason === "error") {
|
|
315
|
-
|
|
480
|
+
recorder.error(lastErrorMessage || "\u6A21\u578B\u8C03\u7528\u5931\u8D25");
|
|
316
481
|
send({
|
|
317
482
|
type: "done",
|
|
318
483
|
outcome: "model_error",
|
|
319
|
-
summary: lastAssistantText
|
|
484
|
+
summary: lastAssistantText,
|
|
320
485
|
changedFiles: [...changedFiles],
|
|
321
486
|
error: lastErrorMessage || "\u6A21\u578B\u8C03\u7528\u5931\u8D25\uFF0C\u4E14\u672A\u7ED9\u51FA\u539F\u56E0"
|
|
322
487
|
});
|
|
@@ -326,7 +491,7 @@ async function run(start) {
|
|
|
326
491
|
send({
|
|
327
492
|
type: "done",
|
|
328
493
|
outcome: "cancelled",
|
|
329
|
-
summary: lastAssistantText
|
|
494
|
+
summary: lastAssistantText,
|
|
330
495
|
changedFiles: [...changedFiles],
|
|
331
496
|
error: lastErrorMessage || "\u4EFB\u52A1\u88AB\u4E2D\u65AD"
|
|
332
497
|
});
|
|
@@ -345,7 +510,7 @@ async function run(start) {
|
|
|
345
510
|
send({
|
|
346
511
|
type: "done",
|
|
347
512
|
outcome: "succeeded",
|
|
348
|
-
summary: lastAssistantText
|
|
513
|
+
summary: lastAssistantText,
|
|
349
514
|
changedFiles: [...changedFiles]
|
|
350
515
|
});
|
|
351
516
|
}
|
|
@@ -361,11 +526,12 @@ onHostMessage((message) => {
|
|
|
361
526
|
}
|
|
362
527
|
if (message?.type !== "start") return;
|
|
363
528
|
if (!message.scratchDir) message.scratchDir = mkdtempSync(`${tmpdir()}/coflux-executor-`);
|
|
529
|
+
recorder = createTranscriptRecorder({ emit: sendFragment, secrets: [message.apiKey] });
|
|
364
530
|
run(message).catch(async (error) => {
|
|
365
531
|
if (aborting) return;
|
|
366
532
|
await stopAllGroups();
|
|
367
533
|
const text = error instanceof Error ? error.message : String(error);
|
|
368
|
-
|
|
534
|
+
recorder.error(text);
|
|
369
535
|
send({
|
|
370
536
|
type: "done",
|
|
371
537
|
outcome: text.includes("\u6A21\u578B") || text.includes("model") ? "model_error" : "tool_failed",
|
package/package.json
CHANGED
package/skills/coflux/SKILL.md
CHANGED
|
@@ -422,14 +422,20 @@ server, use it to get the URL and tell the user directly; they click it and nobo
|
|
|
422
422
|
### Hand a bounded sub-task to the executor
|
|
423
423
|
|
|
424
424
|
```sh
|
|
425
|
-
coflux executor run --prompt="Fix every clippy warning in crates/worker" --write
|
|
426
|
-
coflux executor run --prompt="Find why the relay reconnect test flakes and report back"
|
|
425
|
+
coflux executor run --title="Fix clippy in worker" --prompt="Fix every clippy warning in crates/worker" --write
|
|
426
|
+
coflux executor run --title="Why does the relay test flake" --prompt="Find why the relay reconnect test flakes and report back"
|
|
427
427
|
```
|
|
428
428
|
|
|
429
429
|
The executor is a small agent built into coflux. Give it one self-contained job and it works in
|
|
430
430
|
**the workspace your cwd is in** while you keep your own context for the main thread. The command
|
|
431
431
|
blocks until the job ends, then prints the executor's final report and the files it changed.
|
|
432
432
|
|
|
433
|
+
**Always pass `--title`**: a few words naming the job. While the run is going, the user sees a
|
|
434
|
+
small card on this terminal (on every desktop of the account) with that title, the run's live
|
|
435
|
+
progress and a stop button; they can expand it to read everything the executor did. Without a
|
|
436
|
+
title the card shows the prompt's first line, which is rarely a good name. The card is read-only
|
|
437
|
+
and disappears when the run ends — the result comes back to you, not to the user.
|
|
438
|
+
|
|
433
439
|
Reach for it when a job is mechanical, bounded and verbose — chasing a failing test suite, a
|
|
434
440
|
repetitive refactor across many files, a search that would cost you many tool calls. Keep the work
|
|
435
441
|
yourself when it needs the conversation's context, the user's judgment, or decisions the prompt
|
|
@@ -459,8 +465,9 @@ Its boundaries, enforced by that kernel sandbox — count on them, and tell it w
|
|
|
459
465
|
refused outright rather than queued; read-only runs may go in parallel up to a small cap.
|
|
460
466
|
|
|
461
467
|
A run ends if the host it is on goes away — the user quits the desktop app, signs out, or stops the
|
|
462
|
-
machine's terminals — and you get a definite failure, never a hang.
|
|
463
|
-
|
|
468
|
+
machine's terminals — and you get a definite failure, never a hang. The user can also stop it from
|
|
469
|
+
the card, which comes back to you as `cancelled`. `--timeout <seconds>` caps how long you wait; the
|
|
470
|
+
default is 30 minutes and a timeout cancels the run before failing. The model
|
|
464
471
|
comes from the user's account-wide executor settings; if they have not configured one, the command
|
|
465
472
|
says so in one line — relay that to the user instead of retrying.
|
|
466
473
|
|
|
@@ -528,6 +535,12 @@ this machine, so they are there whether or not a desktop is open.
|
|
|
528
535
|
the page exposed them, the element's selector, DOM path and key computed styles, and the paths of
|
|
529
536
|
its images. It always names the workspace it resolved: an empty list in the wrong workspace means
|
|
530
537
|
you moved (`coflux workspace`). Add `--json` for structured output.
|
|
538
|
+
- One annotation is not always one element. When the user selected several elements together, it
|
|
539
|
+
lists each one with its own component chain and context under "Element 1 of n": the comment
|
|
540
|
+
applies to all of them, so change them consistently and resolve the annotation once. When the user
|
|
541
|
+
dragged a **region**, it gives the region's size and offset, the *container* (the innermost
|
|
542
|
+
element holding it — usually the component to edit) and the components inside the region; the
|
|
543
|
+
comment is about that area as a whole, and the screenshot shows exactly the region.
|
|
531
544
|
- Find the code from the most specific lead: a source location first, then the component names,
|
|
532
545
|
then text and selectors (`rg` for them). Treat the computed styles as the current state, not as
|
|
533
546
|
the target: map colors, sizes and spacing onto the project's design system (its tokens, theme and
|