@tea-agent/loop-agent 0.34.4 → 0.35.0-beta.1
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/AGENTS.md +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/dist/shared/operator/capabilities.js +0 -7
- package/dist/worker/cli.js +21 -27
- package/dist/worker/console/chat/chat-event-store.js +49 -1
- package/dist/worker/console/chat/pi-runtime.js +78 -1
- package/dist/worker/console/chat/routes.js +24 -1
- package/dist/worker/console/chat/shortcuts.js +208 -9
- package/dist/worker/console/chat/tool-preview.js +162 -5
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/index.js +1 -0
- package/dist/worker/console/open-browser.js +134 -0
- package/dist/worker/console/recovery-cta.js +1 -1
- package/dist/worker/console/server.js +1 -1
- package/dist/worker/console/static/assets/index-qpkysQYW.css +1 -0
- package/dist/worker/console/static/assets/index-y980PqtP.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-markdown-security.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +1 -3
- package/dist/worker/console/static-src/operator-chat/format.js +2 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +11 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +81 -3
- package/dist/workflows/dag/frontend-implementation-contract.js +49 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +5 -0
- package/dist/workflows/dag/init-hybrid.js +62 -4
- package/docs/architecture/evolution.md +1 -1
- package/docs/architecture/system-overview.md +1 -1
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +2 -2
- package/docs/templates/init-managed-agents.md +2 -2
- package/package.json +4 -1
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +2 -2
- package/skills/loop-agent/references/command-reference.md +2 -3
- package/dist/worker/console/static/assets/index-B6Qdbk8V.js +0 -29
- package/dist/worker/console/static/assets/index-Bt0NUxcQ.css +0 -1
package/AGENTS.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
4. 涉及测试纪律/验证声明/调试时继续读:`docs/governance/harness-methodology-*.md`。
|
|
39
39
|
5. 查看最近提交、相关 plan/progress/report;`git status --short --branch`;跑最小基线验证。
|
|
40
40
|
6. 后端/接口/pytest → `taskKind: "backend-test"`(不是 `--profile`);知识回写 `knowledge-sync`;图谱开荒 `knowledge-graph-bootstrap`。`--profile` 仅 `auto|minimal|standard|reviewed|supervised`。
|
|
41
|
-
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790
|
|
41
|
+
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;listen 成功后默认打开系统浏览器,`--no-open` 禁止)(`/inspect/` 只读);`observe serve` 已下线(`OBSERVE_SERVE_REMOVED` + exit 2);`observe snapshot` 仍可用。
|
|
42
42
|
8. 分支合并 → 先读 `docs/operations/branch-merge-guideline.md`。
|
|
43
43
|
|
|
44
44
|
## 会话协议
|
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.34.5] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 优化 Operator Chat 对话页体验,包括隐藏重复标题、升级 Markdown 渲染、改进工具调用展示,并合并了 `/` 命令面板
|
|
10
|
+
- 控制台启动后默认自动打开系统浏览器看板
|
|
11
|
+
|
|
12
|
+
### 改进
|
|
13
|
+
|
|
14
|
+
- 优化 Operator Chat 对话页(UI-01 至 UI-11):隐藏重复标题、补齐品牌空态与真实注入上下文侧栏、改进工具调用摘要与结果展示、修复遮罩 hover 形变、为截断会话标题提供 tooltip、升级 Markdown/GFM 渲染,并增加用户主动打开的调用记录入口与合并后的 `/` 命令面板
|
|
15
|
+
- 会话列表操作改为按需显示 Remix Icon,同时保留键盘 focus 与触屏可访问路径
|
|
16
|
+
- 输入区将图片上传收敛为图标按钮,并避免“全部工具 · N · 状态”歧义
|
|
17
|
+
- 控制台在服务监听成功后默认打开系统浏览器
|
|
18
|
+
|
|
5
19
|
## [0.34.4] - 2026-08-12
|
|
6
20
|
|
|
7
21
|
### 重点更新
|
|
8
22
|
|
|
23
|
+
- 裸 `agent-worker console` 收敛为唯一看板入口,监听成功后默认打开系统浏览器,并删除 `console serve`
|
|
9
24
|
- 本地连续交付改为按精确 Git tree 复用验证结果,避免提交、推送和分支合并之间机械重复全量测试,同时保持不同 tree、不同环境和远程 CI 的独立质量门槛
|
|
10
25
|
- 全面加固 Operator Chat 的 Turn 所有权、停止与异常恢复流程,解决浏览器与服务端状态错位时的重复提交、停止失效和错误提示问题
|
|
11
26
|
- Operator Chat 新增按仓库保存的 Skills 自动使用偏好,用户可控制技能是否进入模型自动发现范围,同时保留手动调用能力
|
|
@@ -20,6 +35,7 @@
|
|
|
20
35
|
|
|
21
36
|
### 改进
|
|
22
37
|
|
|
38
|
+
- 裸 `agent-worker console` 成为唯一 Console 启动入口:本地图形交互环境在服务监听成功后默认打开系统浏览器,`--no-open` 可明确禁止;删除 `console serve` 兼容子命令
|
|
23
39
|
- source→target 连续交付统一选择一个本地全量验证权威;默认 commit/push 不增加步骤,远程 CI 与发布环境不复用本地 receipt
|
|
24
40
|
- pre-push receipt 诊断和执行日志改用有界命令摘要,并严格校验 tree、环境、命令合同、有效期及 receipt 内容一致性
|
|
25
41
|
- Skills 运行时快照改为使用服务端资源标识和独立的自动调用状态,资源变更仅接受当前 inventory 中的合法条目
|
package/README.md
CHANGED
|
@@ -104,8 +104,8 @@ loop-agent dag report --run-id <run-id>
|
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
# 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
|
|
107
|
-
agent-worker console # 默认 repo=当前目录、port=8790
|
|
108
|
-
agent-worker console
|
|
107
|
+
agent-worker console # 默认 repo=当前目录、port=8790;本地图形环境默认打开浏览器
|
|
108
|
+
agent-worker console --no-open # 只启动服务,不打开浏览器
|
|
109
109
|
agent-worker console doctor --repo .
|
|
110
110
|
|
|
111
111
|
```
|
|
@@ -2299,13 +2299,6 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
2299
2299
|
source: "agent-worker",
|
|
2300
2300
|
exclusionReason: "Not exposed to Operator Chat in M5.",
|
|
2301
2301
|
},
|
|
2302
|
-
{
|
|
2303
|
-
command: "agent-worker console serve",
|
|
2304
|
-
coverage: "excluded",
|
|
2305
|
-
action: null,
|
|
2306
|
-
source: "agent-worker",
|
|
2307
|
-
exclusionReason: "Not exposed to Operator Chat in M5.",
|
|
2308
|
-
},
|
|
2309
2302
|
{
|
|
2310
2303
|
command: "agent-worker console doctor",
|
|
2311
2304
|
coverage: "excluded",
|
package/dist/worker/cli.js
CHANGED
|
@@ -712,37 +712,22 @@ export function buildAgentWorkerProgram() {
|
|
|
712
712
|
process.stdout.write(`${JSON.stringify(snapshot)}\n`);
|
|
713
713
|
});
|
|
714
714
|
consoleCmd
|
|
715
|
-
.description("Loop Operator Console (local); bare `agent-worker console` starts it directly (repo defaults to current working directory, port defaults to 8790)")
|
|
716
|
-
.enablePositionalOptions()
|
|
717
|
-
.passThroughOptions()
|
|
715
|
+
.description("Loop Operator Console (local); bare `agent-worker console` starts it directly (repo defaults to current working directory, port defaults to 8790). Opens the system browser after listen unless --no-open or a non-graphical environment.")
|
|
718
716
|
.option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
|
|
719
717
|
.option("--port <port>", "HTTP port", "8790")
|
|
720
718
|
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
721
719
|
.option("--debug", "Log request source/method/path/status to stderr")
|
|
722
720
|
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
723
721
|
.option("--default-timeout-ms <ms>", "LoopAgentClient hard timeout for Console operations (ms; default 14400000 / 4h; 0 disables)")
|
|
724
|
-
.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
defaultTimeoutMsRaw: options.defaultTimeoutMs,
|
|
734
|
-
});
|
|
735
|
-
});
|
|
736
|
-
consoleCmd
|
|
737
|
-
.command("serve")
|
|
738
|
-
.option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
|
|
739
|
-
.option("--port <port>", "HTTP port", "8790")
|
|
740
|
-
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
741
|
-
.option("--debug", "Log request source/method/path/status to stderr")
|
|
742
|
-
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
743
|
-
.option("--default-timeout-ms <ms>", "LoopAgentClient hard timeout for Console operations (ms; default 14400000 / 4h; 0 disables)")
|
|
744
|
-
.description("Start Loop Operator Console HTTP server (compat entry; equivalent to bare `agent-worker console`)")
|
|
745
|
-
.action(async (options) => {
|
|
722
|
+
.option("--no-open", "Do not open the system browser after the Console listens")
|
|
723
|
+
.action(async (options, command) => {
|
|
724
|
+
// Reject unknown tokens such as legacy `console serve` instead of starting HTTP.
|
|
725
|
+
const extras = command.args.filter((arg) => arg.length > 0);
|
|
726
|
+
if (extras.length > 0) {
|
|
727
|
+
process.stderr.write(`error: unknown command '${extras[0]}' for agent-worker console\n`);
|
|
728
|
+
process.exitCode = 1;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
746
731
|
await runConsoleServe({
|
|
747
732
|
repoRoot: path.resolve(options.repo),
|
|
748
733
|
host: options.host,
|
|
@@ -752,6 +737,8 @@ export function buildAgentWorkerProgram() {
|
|
|
752
737
|
? path.resolve(options.appData)
|
|
753
738
|
: undefined,
|
|
754
739
|
defaultTimeoutMsRaw: options.defaultTimeoutMs,
|
|
740
|
+
// Commander maps --no-open to open:false; default remains open:true.
|
|
741
|
+
noOpen: options.open === false,
|
|
755
742
|
});
|
|
756
743
|
});
|
|
757
744
|
consoleCmd
|
|
@@ -850,9 +837,10 @@ function parseOptionalTimeoutMs(raw) {
|
|
|
850
837
|
}
|
|
851
838
|
return value;
|
|
852
839
|
}
|
|
853
|
-
/**
|
|
840
|
+
/** Console HTTP entry for bare `agent-worker console` (listen first, then optional browser open). */
|
|
854
841
|
async function runConsoleServe(options) {
|
|
855
842
|
const { createConsoleServer } = await import("./console/server.js");
|
|
843
|
+
const { maybeOpenConsoleBrowser } = await import("./console/open-browser.js");
|
|
856
844
|
try {
|
|
857
845
|
const server = await createConsoleServer({
|
|
858
846
|
repoRoot: options.repoRoot,
|
|
@@ -862,7 +850,13 @@ async function runConsoleServe(options) {
|
|
|
862
850
|
appDataRoot: options.appDataRoot,
|
|
863
851
|
defaultTimeoutMs: parseOptionalTimeoutMs(options.defaultTimeoutMsRaw),
|
|
864
852
|
});
|
|
865
|
-
|
|
853
|
+
// Keep stdout bind URL contract (may include 0.0.0.0 / ::); browser URL is normalized separately.
|
|
854
|
+
process.stdout.write(`${server.url}
|
|
855
|
+
`);
|
|
856
|
+
await maybeOpenConsoleBrowser({
|
|
857
|
+
noOpen: options.noOpen === true,
|
|
858
|
+
serverUrl: server.url,
|
|
859
|
+
});
|
|
866
860
|
await new Promise((resolve) => {
|
|
867
861
|
const shutdown = () => resolve();
|
|
868
862
|
process.once("SIGINT", shutdown);
|
|
@@ -129,9 +129,56 @@ function boundedRedactedJson(value) {
|
|
|
129
129
|
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
|
+
/** Prefer readable Pi ToolResult text content; fall back to JSON shell. */
|
|
133
|
+
function projectToolResultPreview(value) {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
const preview = boundedRedactedText(value) ?? "";
|
|
136
|
+
return {
|
|
137
|
+
preview,
|
|
138
|
+
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
142
|
+
const record = value;
|
|
143
|
+
const content = record.content;
|
|
144
|
+
if (Array.isArray(content)) {
|
|
145
|
+
const texts = [];
|
|
146
|
+
for (const block of content) {
|
|
147
|
+
if (!block || typeof block !== "object")
|
|
148
|
+
continue;
|
|
149
|
+
const item = block;
|
|
150
|
+
if (typeof item.text === "string")
|
|
151
|
+
texts.push(item.text);
|
|
152
|
+
}
|
|
153
|
+
if (texts.length > 0) {
|
|
154
|
+
const joined = texts.join("\n");
|
|
155
|
+
const preview = boundedRedactedText(joined) ?? "";
|
|
156
|
+
return {
|
|
157
|
+
preview,
|
|
158
|
+
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (typeof record.text === "string") {
|
|
163
|
+
const preview = boundedRedactedText(record.text) ?? "";
|
|
164
|
+
return {
|
|
165
|
+
preview,
|
|
166
|
+
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (typeof record.output === "string") {
|
|
170
|
+
const preview = boundedRedactedText(record.output) ?? "";
|
|
171
|
+
return {
|
|
172
|
+
preview,
|
|
173
|
+
previewHash: createHash("sha256").update(preview).digest("hex"),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return boundedRedactedJson(value);
|
|
178
|
+
}
|
|
132
179
|
export function projectChatToolEventPayload(input) {
|
|
133
|
-
const { preview, previewHash } = boundedRedactedJson(input.value);
|
|
134
180
|
if (input.kind === "tool_call") {
|
|
181
|
+
const { preview, previewHash } = boundedRedactedJson(input.value);
|
|
135
182
|
return {
|
|
136
183
|
toolCallId: input.toolCallId,
|
|
137
184
|
toolName: input.toolName,
|
|
@@ -139,6 +186,7 @@ export function projectChatToolEventPayload(input) {
|
|
|
139
186
|
previewHash,
|
|
140
187
|
};
|
|
141
188
|
}
|
|
189
|
+
const { preview, previewHash } = projectToolResultPreview(input.value);
|
|
142
190
|
return {
|
|
143
191
|
toolCallId: input.toolCallId,
|
|
144
192
|
toolName: input.toolName,
|
|
@@ -1279,6 +1279,83 @@ export class ConsolePiRuntime {
|
|
|
1279
1279
|
* list, and the session ResourceLoader inventories. Undefined when the
|
|
1280
1280
|
* session is not active — callers fail closed instead of fabricating data.
|
|
1281
1281
|
*/
|
|
1282
|
+
/**
|
|
1283
|
+
* Browser-safe slash command projection for UI-11.
|
|
1284
|
+
* Sources: current Session extension commands, prompt templates, skills.
|
|
1285
|
+
* Never returns prompt/skill file bodies.
|
|
1286
|
+
*/
|
|
1287
|
+
async listSlashCommands(sessionId) {
|
|
1288
|
+
const scrub = (value) => {
|
|
1289
|
+
const raw = String(value ?? "");
|
|
1290
|
+
return raw
|
|
1291
|
+
.replace(/\/Users\/[^\s]+/gi, "~")
|
|
1292
|
+
.replace(/[A-Za-z]:\\Users\\[^\s]+/gi, "~")
|
|
1293
|
+
.slice(0, 240);
|
|
1294
|
+
};
|
|
1295
|
+
try {
|
|
1296
|
+
await this.ensureSessionReady(sessionId);
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
const session = this.sessions.get(sessionId);
|
|
1302
|
+
if (!session)
|
|
1303
|
+
return [];
|
|
1304
|
+
const out = [];
|
|
1305
|
+
const seen = new Set();
|
|
1306
|
+
const push = (command, label, description, source) => {
|
|
1307
|
+
const name = command.startsWith("/") ? command : `/${command}`;
|
|
1308
|
+
const key = name.toLowerCase();
|
|
1309
|
+
if (!key || key === "/" || seen.has(key))
|
|
1310
|
+
return;
|
|
1311
|
+
seen.add(key);
|
|
1312
|
+
out.push({
|
|
1313
|
+
command: name,
|
|
1314
|
+
label: (label || name).slice(0, 80),
|
|
1315
|
+
description: scrub(description),
|
|
1316
|
+
source,
|
|
1317
|
+
});
|
|
1318
|
+
};
|
|
1319
|
+
try {
|
|
1320
|
+
const cmds = session.extensionRunner?.getRegisteredCommands?.() ?? [];
|
|
1321
|
+
for (const cmd of cmds) {
|
|
1322
|
+
const name = cmd.invocationName || cmd.name;
|
|
1323
|
+
if (!name)
|
|
1324
|
+
continue;
|
|
1325
|
+
push(String(name), String(cmd.name || name), String(cmd.description || ""), "extension");
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
// non-blocking
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
for (const tpl of session.promptTemplates ?? []) {
|
|
1333
|
+
if (!tpl?.name)
|
|
1334
|
+
continue;
|
|
1335
|
+
push(String(tpl.name), String(tpl.name), String(tpl.description || ""), "prompt");
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
catch {
|
|
1339
|
+
// non-blocking
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
const services = this.serviceScopes.get(sessionId);
|
|
1343
|
+
const resourceLoader = services?.resourceLoader;
|
|
1344
|
+
const skills = resourceLoader?.getSkills?.()?.skills ?? [];
|
|
1345
|
+
for (const skill of skills) {
|
|
1346
|
+
if (!skill?.name)
|
|
1347
|
+
continue;
|
|
1348
|
+
const command = skill.name.startsWith("skill:")
|
|
1349
|
+
? `/${skill.name}`
|
|
1350
|
+
: `/skill:${skill.name}`;
|
|
1351
|
+
push(command, skill.name, String(skill.description || ""), "skill");
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
catch {
|
|
1355
|
+
// non-blocking
|
|
1356
|
+
}
|
|
1357
|
+
return out;
|
|
1358
|
+
}
|
|
1282
1359
|
async getRuntimeSnapshot(sessionId, options) {
|
|
1283
1360
|
try {
|
|
1284
1361
|
await this.ensureSessionReady(sessionId);
|
|
@@ -1340,7 +1417,7 @@ export class ConsolePiRuntime {
|
|
|
1340
1417
|
const skills = [];
|
|
1341
1418
|
const extensions = [];
|
|
1342
1419
|
const diagnostics = [];
|
|
1343
|
-
|
|
1420
|
+
const resolvedResources = [];
|
|
1344
1421
|
const resourceFailure = (subface, error) => {
|
|
1345
1422
|
diagnostics.push({
|
|
1346
1423
|
type: "error",
|
|
@@ -1643,6 +1643,25 @@ async function handleChatFiles(res, deps, sessionId, query) {
|
|
|
1643
1643
|
.map((path) => ({ path }));
|
|
1644
1644
|
sendJson(res, 200, { ok: true, files });
|
|
1645
1645
|
}
|
|
1646
|
+
export async function handleChatCommands(res, deps, sessionId) {
|
|
1647
|
+
try {
|
|
1648
|
+
const commands = typeof deps.runtime.listSlashCommands === "function"
|
|
1649
|
+
? await deps.runtime.listSlashCommands(sessionId)
|
|
1650
|
+
: [];
|
|
1651
|
+
res.statusCode = 200;
|
|
1652
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
1653
|
+
res.end(JSON.stringify({ ok: true, commands }));
|
|
1654
|
+
}
|
|
1655
|
+
catch (error) {
|
|
1656
|
+
res.statusCode = 200;
|
|
1657
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
1658
|
+
res.end(JSON.stringify({
|
|
1659
|
+
ok: true,
|
|
1660
|
+
commands: [],
|
|
1661
|
+
warning: error instanceof Error ? error.message : "commands unavailable",
|
|
1662
|
+
}));
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1646
1665
|
export async function handleRuntimeContext(res, deps, sessionId) {
|
|
1647
1666
|
const ensured = await ensureActiveSession(deps, sessionId);
|
|
1648
1667
|
if (!ensured.ok) {
|
|
@@ -2929,7 +2948,7 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
2929
2948
|
await handleSkillAutoInvocation(req, res, deps, decodeURIComponent(skillAutoInvocation[1]), decodeURIComponent(skillAutoInvocation[2]));
|
|
2930
2949
|
return true;
|
|
2931
2950
|
}
|
|
2932
|
-
const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|runtime-snapshot|models|model|reload)$/);
|
|
2951
|
+
const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|runtime-snapshot|models|model|reload|commands)$/);
|
|
2933
2952
|
if (m4Route) {
|
|
2934
2953
|
const id = decodeURIComponent(m4Route[1]);
|
|
2935
2954
|
const route = m4Route[2];
|
|
@@ -2961,6 +2980,10 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
2961
2980
|
await handleReloadSession(req, res, deps, id);
|
|
2962
2981
|
return true;
|
|
2963
2982
|
}
|
|
2983
|
+
if (route === "commands" && method === "GET") {
|
|
2984
|
+
await handleChatCommands(res, deps, id);
|
|
2985
|
+
return true;
|
|
2986
|
+
}
|
|
2964
2987
|
}
|
|
2965
2988
|
const sessionMainline = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/branch\/mainline$/);
|
|
2966
2989
|
if (method === "POST" && sessionMainline) {
|
|
@@ -1,16 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chat slash command catalog (UI-11).
|
|
3
|
+
*
|
|
4
|
+
* Local Operator + Pi Web commands are always available. Dynamic extension /
|
|
5
|
+
* prompt / skill commands are merged via mergeSlashCommands with conflict
|
|
6
|
+
* priority: operator/web > extension > skill > prompt.
|
|
7
|
+
*/
|
|
1
8
|
export const SHORTCUTS = [
|
|
2
|
-
{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
{
|
|
10
|
+
command: "/task",
|
|
11
|
+
label: "Task context",
|
|
12
|
+
description: "查看当前任务、Draft 与 assessment",
|
|
13
|
+
action: "focus-panel:task",
|
|
14
|
+
source: "operator",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
command: "/contract",
|
|
18
|
+
label: "Contract",
|
|
19
|
+
description: "查看 contract diff 与 apply gate",
|
|
20
|
+
action: "focus-panel:contract",
|
|
21
|
+
source: "operator",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: "/dag",
|
|
25
|
+
label: "DAG",
|
|
26
|
+
description: "查看 DAG spine、确认与运行状态",
|
|
27
|
+
action: "focus-panel:dag",
|
|
28
|
+
source: "operator",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
command: "/interview",
|
|
32
|
+
label: "Interview",
|
|
33
|
+
description: "打开 Requirement Interview",
|
|
34
|
+
action: "focus:interview",
|
|
35
|
+
source: "operator",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
command: "/reload",
|
|
39
|
+
label: "Reload session",
|
|
40
|
+
description: "重新加载当前 Pi 会话的设置、扩展、技能、提示词、主题与上下文文件(不产生消息)",
|
|
41
|
+
action: "reload",
|
|
42
|
+
source: "operator",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
command: "/help",
|
|
46
|
+
label: "Shortcuts",
|
|
47
|
+
description: "显示可用 shortcut",
|
|
48
|
+
action: "show-help",
|
|
49
|
+
source: "operator",
|
|
50
|
+
},
|
|
8
51
|
];
|
|
9
|
-
/**
|
|
52
|
+
/** Pi Web commands with real browser-equivalent behavior (UI-11). */
|
|
53
|
+
export const WEB_SLASH_COMMANDS = [
|
|
54
|
+
{
|
|
55
|
+
command: "/compact",
|
|
56
|
+
label: "压缩上下文",
|
|
57
|
+
description: "压缩当前会话上下文,释放 token",
|
|
58
|
+
action: "compact",
|
|
59
|
+
source: "web",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
command: "/name",
|
|
63
|
+
label: "重命名会话",
|
|
64
|
+
description: "设置当前会话标题",
|
|
65
|
+
action: "name",
|
|
66
|
+
source: "web",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
command: "/session",
|
|
70
|
+
label: "会话信息",
|
|
71
|
+
description: "显示当前会话元信息",
|
|
72
|
+
action: "session",
|
|
73
|
+
source: "web",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
command: "/copy",
|
|
77
|
+
label: "复制回复",
|
|
78
|
+
description: "复制最后一条助手消息原文",
|
|
79
|
+
action: "copy",
|
|
80
|
+
source: "web",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
const SOURCE_PRIORITY = {
|
|
84
|
+
operator: 0,
|
|
85
|
+
web: 1,
|
|
86
|
+
extension: 2,
|
|
87
|
+
skill: 3,
|
|
88
|
+
prompt: 4,
|
|
89
|
+
};
|
|
90
|
+
function normalizeCommandName(raw) {
|
|
91
|
+
const token = raw.trim().split(/\s+/, 1)[0] ?? "";
|
|
92
|
+
if (!token)
|
|
93
|
+
return "";
|
|
94
|
+
return token.startsWith("/") ? token.toLowerCase() : `/${token.toLowerCase()}`;
|
|
95
|
+
}
|
|
96
|
+
/** Pure navigation/display resolver for local Operator + Web commands. */
|
|
10
97
|
export function resolveShortcut(input) {
|
|
11
98
|
const token = input.trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
12
99
|
if (!token?.startsWith("/"))
|
|
13
100
|
return undefined;
|
|
14
|
-
const
|
|
15
|
-
|
|
101
|
+
const local = [...SHORTCUTS, ...WEB_SLASH_COMMANDS].find((shortcut) => shortcut.command === token);
|
|
102
|
+
if (local)
|
|
103
|
+
return { command: local.command, action: local.action };
|
|
104
|
+
return { command: token, action: "unknown" };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Merge Operator, Web, and dynamic Session commands with conflict dedupe.
|
|
108
|
+
* Priority: operator/web > extension > skill > prompt. /reload is single-card.
|
|
109
|
+
*/
|
|
110
|
+
export function mergeSlashCommands(dynamic = []) {
|
|
111
|
+
const byName = new Map();
|
|
112
|
+
const upsert = (item) => {
|
|
113
|
+
const key = normalizeCommandName(item.command);
|
|
114
|
+
if (!key)
|
|
115
|
+
return;
|
|
116
|
+
const existing = byName.get(key);
|
|
117
|
+
if (!existing) {
|
|
118
|
+
byName.set(key, { ...item, command: key });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (SOURCE_PRIORITY[item.source] < SOURCE_PRIORITY[existing.source]) {
|
|
122
|
+
byName.set(key, { ...item, command: key });
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
for (const item of SHORTCUTS) {
|
|
126
|
+
upsert({
|
|
127
|
+
command: item.command,
|
|
128
|
+
label: item.label,
|
|
129
|
+
description: item.description,
|
|
130
|
+
source: item.source,
|
|
131
|
+
action: item.action,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
for (const item of WEB_SLASH_COMMANDS) {
|
|
135
|
+
upsert({
|
|
136
|
+
command: item.command,
|
|
137
|
+
label: item.label,
|
|
138
|
+
description: item.description,
|
|
139
|
+
source: item.source,
|
|
140
|
+
action: item.action,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
for (const item of dynamic) {
|
|
144
|
+
const command = normalizeCommandName(item.command);
|
|
145
|
+
if (!command)
|
|
146
|
+
continue;
|
|
147
|
+
upsert({
|
|
148
|
+
command,
|
|
149
|
+
label: item.label || command,
|
|
150
|
+
description: item.description || "",
|
|
151
|
+
source: item.source,
|
|
152
|
+
action: "pi-prompt",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return [...byName.values()].sort((a, b) => {
|
|
156
|
+
const src = SOURCE_PRIORITY[a.source] - SOURCE_PRIORITY[b.source];
|
|
157
|
+
if (src !== 0)
|
|
158
|
+
return src;
|
|
159
|
+
return a.command.localeCompare(b.command);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Filter + rank slash candidates by name/description. */
|
|
163
|
+
export function filterSlashCommands(commands, rawQuery) {
|
|
164
|
+
const token = (rawQuery.trim().split(/\s+/, 1)[0] ?? "").toLowerCase();
|
|
165
|
+
// After a space (args), hide the palette.
|
|
166
|
+
if (/\s/.test(rawQuery.trimStart().slice(1)) || rawQuery.includes(" ")) {
|
|
167
|
+
// Only hide when user has typed past the command token.
|
|
168
|
+
const after = rawQuery.trimStart();
|
|
169
|
+
if (after.includes(" "))
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const q = token.startsWith("/") ? token : token ? `/${token}` : "/";
|
|
173
|
+
const needle = q === "/" ? "" : q.slice(1);
|
|
174
|
+
const scored = [];
|
|
175
|
+
for (const item of commands) {
|
|
176
|
+
const name = item.command.toLowerCase();
|
|
177
|
+
const bare = name.startsWith("/") ? name.slice(1) : name;
|
|
178
|
+
const desc = item.description.toLowerCase();
|
|
179
|
+
let score = 100;
|
|
180
|
+
if (!needle) {
|
|
181
|
+
score = 10 + SOURCE_PRIORITY[item.source];
|
|
182
|
+
}
|
|
183
|
+
else if (name === q || bare === needle) {
|
|
184
|
+
score = 0;
|
|
185
|
+
}
|
|
186
|
+
else if (name.startsWith(q) || bare.startsWith(needle)) {
|
|
187
|
+
score = 1;
|
|
188
|
+
}
|
|
189
|
+
else if (bare.includes(needle) || name.includes(needle)) {
|
|
190
|
+
score = 2;
|
|
191
|
+
}
|
|
192
|
+
else if (desc.includes(needle)) {
|
|
193
|
+
score = 3;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
scored.push({ ...item, score });
|
|
199
|
+
}
|
|
200
|
+
return scored.sort((a, b) => {
|
|
201
|
+
if (a.score !== b.score)
|
|
202
|
+
return a.score - b.score;
|
|
203
|
+
const src = SOURCE_PRIORITY[a.source] - SOURCE_PRIORITY[b.source];
|
|
204
|
+
if (src !== 0)
|
|
205
|
+
return src;
|
|
206
|
+
return a.command.localeCompare(b.command);
|
|
207
|
+
});
|
|
16
208
|
}
|
|
209
|
+
export const SLASH_SOURCE_LABEL = {
|
|
210
|
+
operator: "Operator",
|
|
211
|
+
web: "Pi Web",
|
|
212
|
+
extension: "Extensions",
|
|
213
|
+
prompt: "Prompts",
|
|
214
|
+
skill: "Skills",
|
|
215
|
+
};
|