@tea-agent/loop-agent 0.23.1 → 0.24.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/CHANGELOG.md +61 -1
- package/README.md +1 -1
- package/bin/agent-worker.js +0 -0
- package/dist/commands/init.js +2 -2
- package/dist/executors/shell-executor.js +20 -7
- package/dist/shared/operator/capabilities.js +475 -2
- package/dist/task/dag-source-paths.js +50 -0
- package/dist/task/frontend-project-capability.js +316 -0
- package/dist/task/task-demand-routing.js +432 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/artifact-card.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +495 -0
- package/dist/worker/console/chat/chat-ui-policy.js +25 -0
- package/dist/worker/console/chat/compaction-errors.js +64 -0
- package/dist/worker/console/chat/composer-draft-store.js +45 -0
- package/dist/worker/console/chat/context-panel.js +54 -0
- package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
- package/dist/worker/console/chat/explore-tools.js +299 -0
- package/dist/worker/console/chat/human-gate-card.js +37 -0
- package/dist/worker/console/chat/interview-adapter.js +149 -0
- package/dist/worker/console/chat/operation-card.js +23 -0
- package/dist/worker/console/chat/pi-console-config.js +158 -0
- package/dist/worker/console/chat/pi-runtime.js +581 -43
- package/dist/worker/console/chat/repo-browser.js +140 -0
- package/dist/worker/console/chat/repo-walk.js +116 -0
- package/dist/worker/console/chat/resource-loader.js +18 -17
- package/dist/worker/console/chat/routes.js +1359 -65
- package/dist/worker/console/chat/runtime-context.js +24 -0
- package/dist/worker/console/chat/runtime-selection.js +37 -0
- package/dist/worker/console/chat/session-store.js +210 -11
- package/dist/worker/console/chat/shortcuts.js +15 -0
- package/dist/worker/console/chat/tool-adapter.js +81 -194
- package/dist/worker/console/chat/tools.js +72 -48
- package/dist/worker/console/chat/usage.js +37 -0
- package/dist/worker/console/chat/workspace-landing.js +56 -0
- package/dist/worker/console/dag-confirmation.js +42 -8
- package/dist/worker/console/human-gate-token.js +130 -0
- package/dist/worker/console/interview/grill-me.js +28 -5
- package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
- package/dist/worker/console/operation-runner.js +6 -2
- package/dist/worker/console/operation-sse.js +26 -0
- package/dist/worker/console/operator-actions.js +420 -7
- package/dist/worker/console/server.js +14 -2
- package/dist/worker/console/static/assets/index-Cv68Ge5n.js +27 -0
- package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/routes.js +8 -4
- package/dist/workflows/dag/backend-test-intake-context.js +10 -12
- package/dist/workflows/dag/backend-test-markdown-workflow.js +232 -47
- package/dist/workflows/dag/backend-test-result-contract.js +233 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
- package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
- package/dist/workflows/dag/frontend-project-capability.js +1 -316
- package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
- package/dist/workflows/dag/init-hybrid.js +46 -43
- package/dist/workflows/dag/rerun-plan.js +6 -10
- package/dist/workflows/dag/task-demand-routing.js +1 -383
- package/docs/README.md +1 -1
- package/docs/architecture/README.md +5 -5
- package/docs/architecture/evolution.md +4 -4
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/frontend-implementation-contract.schema.json +1 -1
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-implementation/SKILL.md +7 -0
- package/skills/frontend-implementation/references/node-contracts.md +1 -1
- package/skills/frontend-verification/SKILL.md +4 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/index-DVl7Jxt5.js +0 -25
- package/dist/worker/console/static/assets/index-lVcIr9Ju.css +0 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { scrubSecrets } from "./explore-tools.js";
|
|
3
|
+
function summary(text, max = 320) {
|
|
4
|
+
if (!text)
|
|
5
|
+
return undefined;
|
|
6
|
+
return scrubSecrets(text).scrubbed
|
|
7
|
+
.replace(/\bghp_[A-Za-z0-9]{20,}\b/g, "[REDACTED]")
|
|
8
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi, "Bearer [REDACTED]")
|
|
9
|
+
.slice(0, max);
|
|
10
|
+
}
|
|
11
|
+
export function projectRuntimeContext(input) {
|
|
12
|
+
return {
|
|
13
|
+
readOnly: true,
|
|
14
|
+
systemPrompt: {
|
|
15
|
+
hash: createHash("sha256").update(input.systemPrompt).digest("hex"),
|
|
16
|
+
summary: summary(input.systemPrompt),
|
|
17
|
+
},
|
|
18
|
+
skills: input.skills.map((skill) => ({ ...skill, description: summary(skill.description, 200) ?? "" })),
|
|
19
|
+
resources: { mode: "closed", noContextFiles: true, noSkills: true, noExtensions: true, hasBash: false, activeToolCount: input.activeTools.length },
|
|
20
|
+
model: input.model,
|
|
21
|
+
thinkingLevel: input.thinkingLevel,
|
|
22
|
+
suffix: input.systemPromptSuffix ? { present: true, summary: summary(input.systemPromptSuffix) } : { present: false },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure runtime-selection helpers shared by the Operator Chat UI and tests.
|
|
3
|
+
*
|
|
4
|
+
* These functions encode the merge/match semantics between persisted session
|
|
5
|
+
* runtime state (model + thinking level) and the read-only runtime context
|
|
6
|
+
* snapshot. They are intentionally side-effect-free so they can be unit-tested
|
|
7
|
+
* in a plain Node environment (the React component owns the network race
|
|
8
|
+
* guards; these helpers only define what "matches" and "merged" mean).
|
|
9
|
+
*/
|
|
10
|
+
/** Format a model reference for `<option value>` keys and equality checks. */
|
|
11
|
+
export function formatModelRef(provider, modelId) {
|
|
12
|
+
return `${provider}/${modelId}`;
|
|
13
|
+
}
|
|
14
|
+
/** Normalize a server record into the optional model/thinking patch fields. */
|
|
15
|
+
export function runtimeSelectionFromRecord(record) {
|
|
16
|
+
return {
|
|
17
|
+
...(record.modelProvider && record.modelId
|
|
18
|
+
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
19
|
+
: {}),
|
|
20
|
+
...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Merge a patch onto a session, preferring patch values and keeping the id. */
|
|
24
|
+
export function mergeSessionRuntime(current, patch) {
|
|
25
|
+
return {
|
|
26
|
+
...current,
|
|
27
|
+
model: patch.model ?? current.model,
|
|
28
|
+
thinkingLevel: patch.thinkingLevel ?? current.thinkingLevel,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** True when `current` already carries the values in `patch`. */
|
|
32
|
+
export function sessionRuntimeMatches(current, patch) {
|
|
33
|
+
const next = mergeSessionRuntime(current, patch);
|
|
34
|
+
return (next.model?.provider === current.model?.provider &&
|
|
35
|
+
next.model?.modelId === current.model?.modelId &&
|
|
36
|
+
next.thinkingLevel === current.thinkingLevel);
|
|
37
|
+
}
|
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* operator action call. Every invocation is re-authorized here.
|
|
12
12
|
*/
|
|
13
13
|
import path from "node:path";
|
|
14
|
+
import { readdir, rm } from "node:fs/promises";
|
|
14
15
|
import { readJsonIfExists, writeSecureJson, } from "../app-data.js";
|
|
15
16
|
import { dispatchOperatorAction } from "../operator-actions.js";
|
|
16
17
|
import { authorizeOperatorChatTool } from "./tools.js";
|
|
18
|
+
import { projectOperationForChat } from "./chat-event-store.js";
|
|
19
|
+
import { scrubSecrets } from "./explore-tools.js";
|
|
17
20
|
function recordPath(appData, sessionId) {
|
|
18
21
|
return path.join(appData.chats, `${sessionId}.json`);
|
|
19
22
|
}
|
|
@@ -28,8 +31,60 @@ export class ChatSessionStore {
|
|
|
28
31
|
constructor(appData) {
|
|
29
32
|
this.appData = appData;
|
|
30
33
|
}
|
|
34
|
+
/** Read + migrate legacy V1 records into the V2 in-memory shape. */
|
|
31
35
|
async get(sessionId) {
|
|
32
|
-
|
|
36
|
+
const raw = await readJsonIfExists(recordPath(this.appData, sessionId));
|
|
37
|
+
if (!raw || raw.sessionId !== sessionId)
|
|
38
|
+
return undefined;
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: 2,
|
|
41
|
+
sessionId,
|
|
42
|
+
repoFingerprint: raw.repoFingerprint ?? this.appData.fingerprint,
|
|
43
|
+
repoRootDisplay: raw.repoRootDisplay ?? this.appData.repoRoot,
|
|
44
|
+
state: raw.state === "archived" ? "archived" : "active",
|
|
45
|
+
...(typeof raw.title === "string" ? { title: raw.title } : {}),
|
|
46
|
+
...(typeof raw.sessionFile === "string" ? { sessionFile: raw.sessionFile } : {}),
|
|
47
|
+
createdAt: raw.createdAt ?? new Date(0).toISOString(),
|
|
48
|
+
updatedAt: raw.updatedAt ?? raw.createdAt ?? new Date(0).toISOString(),
|
|
49
|
+
...(typeof raw.modelProvider === "string" ? { modelProvider: raw.modelProvider } : {}),
|
|
50
|
+
...(typeof raw.modelId === "string" ? { modelId: raw.modelId } : {}),
|
|
51
|
+
...(typeof raw.thinkingLevel === "string" ? { thinkingLevel: raw.thinkingLevel } : {}),
|
|
52
|
+
messages: Array.isArray(raw.messages) ? raw.messages : [],
|
|
53
|
+
toolInvocations: Array.isArray(raw.toolInvocations) ? raw.toolInvocations : [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async listMessages(sessionId, options = {}) {
|
|
57
|
+
const record = await this.get(sessionId);
|
|
58
|
+
if (!record)
|
|
59
|
+
throw new Error(`chat session not found: ${sessionId}`);
|
|
60
|
+
const limit = Math.max(1, Math.min(50, Math.floor(options.limit ?? 30)));
|
|
61
|
+
const beforeIndex = options.beforeId
|
|
62
|
+
? record.messages.findIndex((message) => message.id === options.beforeId)
|
|
63
|
+
: record.messages.length;
|
|
64
|
+
const end = beforeIndex < 0 ? record.messages.length : beforeIndex;
|
|
65
|
+
const start = Math.max(0, end - limit);
|
|
66
|
+
const messages = record.messages.slice(start, end);
|
|
67
|
+
return {
|
|
68
|
+
messages,
|
|
69
|
+
hasMore: start > 0,
|
|
70
|
+
...(messages[0] ? { nextCursor: messages[0].id } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** List repo-bound sessions, newest first. Deleted records are absent. */
|
|
74
|
+
async list(options) {
|
|
75
|
+
let names;
|
|
76
|
+
try {
|
|
77
|
+
names = await readdir(this.appData.chats);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const records = await Promise.all(names
|
|
83
|
+
.filter((name) => name.endsWith(".json"))
|
|
84
|
+
.map((name) => this.get(name.slice(0, -".json".length))));
|
|
85
|
+
return records
|
|
86
|
+
.filter((record) => Boolean(record) && (options?.includeArchived || record.state === "active"))
|
|
87
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
33
88
|
}
|
|
34
89
|
/** Serialize a mutating op per session. */
|
|
35
90
|
serialize(sessionId, op) {
|
|
@@ -44,8 +99,12 @@ export class ChatSessionStore {
|
|
|
44
99
|
return this.serialize(input.sessionId, async () => {
|
|
45
100
|
const now = new Date().toISOString();
|
|
46
101
|
const record = {
|
|
47
|
-
schemaVersion:
|
|
102
|
+
schemaVersion: 2,
|
|
48
103
|
sessionId: input.sessionId,
|
|
104
|
+
repoFingerprint: this.appData.fingerprint,
|
|
105
|
+
repoRootDisplay: this.appData.repoRoot,
|
|
106
|
+
state: "active",
|
|
107
|
+
...(input.sessionFile ? { sessionFile: input.sessionFile } : {}),
|
|
49
108
|
createdAt: now,
|
|
50
109
|
updatedAt: now,
|
|
51
110
|
modelProvider: input.modelProvider,
|
|
@@ -57,6 +116,73 @@ export class ChatSessionStore {
|
|
|
57
116
|
return record;
|
|
58
117
|
});
|
|
59
118
|
}
|
|
119
|
+
async forkFrom(input) {
|
|
120
|
+
const source = await this.get(input.sourceSessionId);
|
|
121
|
+
if (!source)
|
|
122
|
+
throw new Error(`chat session not found: ${input.sourceSessionId}`);
|
|
123
|
+
return this.serialize(input.targetSessionId, async () => {
|
|
124
|
+
const now = new Date().toISOString();
|
|
125
|
+
const record = {
|
|
126
|
+
...source,
|
|
127
|
+
sessionId: input.targetSessionId,
|
|
128
|
+
state: "active",
|
|
129
|
+
...(input.targetSessionFile ? { sessionFile: input.targetSessionFile } : { sessionFile: undefined }),
|
|
130
|
+
createdAt: now,
|
|
131
|
+
updatedAt: now,
|
|
132
|
+
messages: structuredClone(source.messages),
|
|
133
|
+
toolInvocations: structuredClone(source.toolInvocations),
|
|
134
|
+
};
|
|
135
|
+
await writeSecureJson(recordPath(this.appData, input.targetSessionId), record);
|
|
136
|
+
return record;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async rename(sessionId, title) {
|
|
140
|
+
return this.serialize(sessionId, async () => {
|
|
141
|
+
const record = await this.get(sessionId);
|
|
142
|
+
if (!record)
|
|
143
|
+
throw new Error(`chat session not found: ${sessionId}`);
|
|
144
|
+
record.title = title.trim().slice(0, 160) || undefined;
|
|
145
|
+
record.updatedAt = new Date().toISOString();
|
|
146
|
+
await writeSecureJson(recordPath(this.appData, sessionId), record);
|
|
147
|
+
return record;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async setRuntimeSelection(sessionId, selection) {
|
|
151
|
+
return this.serialize(sessionId, async () => {
|
|
152
|
+
const record = await this.get(sessionId);
|
|
153
|
+
if (!record)
|
|
154
|
+
throw new Error(`chat session not found: ${sessionId}`);
|
|
155
|
+
if (selection.modelProvider !== undefined)
|
|
156
|
+
record.modelProvider = selection.modelProvider;
|
|
157
|
+
if (selection.modelId !== undefined)
|
|
158
|
+
record.modelId = selection.modelId;
|
|
159
|
+
if (selection.thinkingLevel !== undefined)
|
|
160
|
+
record.thinkingLevel = selection.thinkingLevel;
|
|
161
|
+
record.updatedAt = new Date().toISOString();
|
|
162
|
+
await writeSecureJson(recordPath(this.appData, sessionId), record);
|
|
163
|
+
return record;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async setState(sessionId, state) {
|
|
167
|
+
return this.serialize(sessionId, async () => {
|
|
168
|
+
const record = await this.get(sessionId);
|
|
169
|
+
if (!record)
|
|
170
|
+
throw new Error(`chat session not found: ${sessionId}`);
|
|
171
|
+
record.state = state;
|
|
172
|
+
record.updatedAt = new Date().toISOString();
|
|
173
|
+
await writeSecureJson(recordPath(this.appData, sessionId), record);
|
|
174
|
+
return record;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async remove(sessionId) {
|
|
178
|
+
return this.serialize(sessionId, async () => {
|
|
179
|
+
const record = await this.get(sessionId);
|
|
180
|
+
if (!record)
|
|
181
|
+
return false;
|
|
182
|
+
await rm(recordPath(this.appData, sessionId), { force: true });
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
60
186
|
async appendMessage(sessionId, message) {
|
|
61
187
|
return this.serialize(sessionId, async () => {
|
|
62
188
|
const record = await this.get(sessionId);
|
|
@@ -94,6 +220,16 @@ export class ChatSessionStore {
|
|
|
94
220
|
* High-risk actions (contractApply / runDag / etc.) are NOT registered as
|
|
95
221
|
* tools, so they never reach here; but we still deny defensively.
|
|
96
222
|
*/
|
|
223
|
+
function redactHumanGateToken(value) {
|
|
224
|
+
const clone = structuredClone(value);
|
|
225
|
+
const confirmation = clone.confirmation;
|
|
226
|
+
if (confirmation && typeof confirmation === "object") {
|
|
227
|
+
delete confirmation.humanGateToken;
|
|
228
|
+
delete confirmation.dagBytesPath;
|
|
229
|
+
}
|
|
230
|
+
delete clone.humanGateToken;
|
|
231
|
+
return clone;
|
|
232
|
+
}
|
|
97
233
|
export async function dispatchChatToolCall(ctx, toolName, args, clientRequestId) {
|
|
98
234
|
const decision = authorizeOperatorChatTool(toolName);
|
|
99
235
|
if (!decision.ok) {
|
|
@@ -110,25 +246,31 @@ export async function dispatchChatToolCall(ctx, toolName, args, clientRequestId)
|
|
|
110
246
|
clientRequestId,
|
|
111
247
|
});
|
|
112
248
|
if (result.kind === "accepted") {
|
|
113
|
-
|
|
114
|
-
// surface a pointer rather than inline data.
|
|
249
|
+
const canonical = await ctx.operations.get(result.body.operationId);
|
|
115
250
|
return {
|
|
116
251
|
ok: true,
|
|
117
252
|
toolName: decision.toolId,
|
|
118
253
|
result: {
|
|
254
|
+
schemaVersion: 1,
|
|
255
|
+
accepted: true,
|
|
119
256
|
operationId: result.body.operationId,
|
|
120
257
|
state: result.body.state,
|
|
121
|
-
|
|
258
|
+
action: result.body.action,
|
|
259
|
+
...(canonical ? { operation: projectOperationForChat(canonical) } : {}),
|
|
260
|
+
note: "operation accepted; a durable Chat operation reference will follow canonical status",
|
|
122
261
|
},
|
|
123
262
|
};
|
|
124
263
|
}
|
|
125
264
|
const body = result.body;
|
|
126
265
|
const ok = body.ok === true;
|
|
127
266
|
if (ok) {
|
|
267
|
+
const raw = body.result ?? body;
|
|
128
268
|
return {
|
|
129
269
|
ok: true,
|
|
130
270
|
toolName: decision.toolId,
|
|
131
|
-
result:
|
|
271
|
+
result: decision.toolId === "prepareDagConfirmation" || decision.toolId === "prepareMutationGate"
|
|
272
|
+
? redactHumanGateToken(raw)
|
|
273
|
+
: raw,
|
|
132
274
|
};
|
|
133
275
|
}
|
|
134
276
|
const error = body.error ?? {};
|
|
@@ -168,9 +310,18 @@ export async function runChatTurn(options) {
|
|
|
168
310
|
// chat history / audit trail is durably written (also makes the outcome
|
|
169
311
|
// observable to tests and to a reconnecting client reading the record).
|
|
170
312
|
const pending = [];
|
|
313
|
+
let settledEvent;
|
|
171
314
|
try {
|
|
172
315
|
await runtime.prompt(sessionId, text, (event) => {
|
|
173
|
-
|
|
316
|
+
if (event.type === "agent_settled") {
|
|
317
|
+
// The durable operation-ref linker may perform an async canonical
|
|
318
|
+
// operation read. Hold settled until those writes finish so replay
|
|
319
|
+
// never observes agent_settled before its accepted operation ref.
|
|
320
|
+
settledEvent = event;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
options.onEvent?.(event);
|
|
324
|
+
}
|
|
174
325
|
switch (event.type) {
|
|
175
326
|
case "message_update":
|
|
176
327
|
case "message_end":
|
|
@@ -188,6 +339,15 @@ export async function runChatTurn(options) {
|
|
|
188
339
|
// that would double-execute every tool call (P0.2).
|
|
189
340
|
break;
|
|
190
341
|
case "tool_result": {
|
|
342
|
+
const acceptedOperationId = acceptedOperationIdFromResult(event.toolName, event.result);
|
|
343
|
+
if (acceptedOperationId && options.onOperationAccepted) {
|
|
344
|
+
pending.push(Promise.resolve(options.onOperationAccepted({
|
|
345
|
+
sessionId,
|
|
346
|
+
toolCallId: event.toolCallId,
|
|
347
|
+
toolName: event.toolName,
|
|
348
|
+
operationId: acceptedOperationId,
|
|
349
|
+
})));
|
|
350
|
+
}
|
|
191
351
|
// tool_execution_end carries the real result of the tool call.
|
|
192
352
|
// Persist exactly once (the tool message + audit trail). This is
|
|
193
353
|
// the single point of record for tool outcomes (P0.2).
|
|
@@ -212,7 +372,7 @@ export async function runChatTurn(options) {
|
|
|
212
372
|
turnError = { code: "CHAT_TURN_ERROR", message: event.message };
|
|
213
373
|
break;
|
|
214
374
|
}
|
|
215
|
-
}, { signal: options.signal });
|
|
375
|
+
}, { signal: options.signal, images: options.images });
|
|
216
376
|
}
|
|
217
377
|
catch (error) {
|
|
218
378
|
await Promise.allSettled(pending);
|
|
@@ -220,6 +380,8 @@ export async function runChatTurn(options) {
|
|
|
220
380
|
return { ok: false, error: { code: "CHAT_TURN_FAILED", message } };
|
|
221
381
|
}
|
|
222
382
|
await Promise.allSettled(pending);
|
|
383
|
+
if (settledEvent)
|
|
384
|
+
options.onEvent?.(settledEvent);
|
|
223
385
|
return { ok: !turnError, error: turnError };
|
|
224
386
|
}
|
|
225
387
|
/**
|
|
@@ -227,12 +389,49 @@ export async function runChatTurn(options) {
|
|
|
227
389
|
* huge operator-action payload (e.g. a full dagReport) cannot bloat the chat
|
|
228
390
|
* history record.
|
|
229
391
|
*/
|
|
392
|
+
function acceptedOperationIdFromResult(toolName, result) {
|
|
393
|
+
const candidates = [result];
|
|
394
|
+
if (result && typeof result === "object") {
|
|
395
|
+
const record = result;
|
|
396
|
+
candidates.push(record.details);
|
|
397
|
+
if (Array.isArray(record.content)) {
|
|
398
|
+
for (const item of record.content) {
|
|
399
|
+
if (!item || typeof item !== "object")
|
|
400
|
+
continue;
|
|
401
|
+
const text = item.text;
|
|
402
|
+
if (typeof text !== "string" || text.length > 16_000)
|
|
403
|
+
continue;
|
|
404
|
+
try {
|
|
405
|
+
candidates.push(JSON.parse(text));
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// Only the controlled JSON tool-result envelope is accepted.
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const candidate of candidates) {
|
|
414
|
+
if (!candidate || typeof candidate !== "object")
|
|
415
|
+
continue;
|
|
416
|
+
const accepted = candidate;
|
|
417
|
+
if (accepted.schemaVersion === 1 &&
|
|
418
|
+
accepted.accepted === true &&
|
|
419
|
+
accepted.action === toolName &&
|
|
420
|
+
typeof accepted.operationId === "string" &&
|
|
421
|
+
accepted.operationId.trim()) {
|
|
422
|
+
return accepted.operationId.trim();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
230
427
|
function safeStringifyResult(result) {
|
|
231
428
|
try {
|
|
232
|
-
const
|
|
233
|
-
|
|
429
|
+
const raw = JSON.stringify(result ?? {});
|
|
430
|
+
const { scrubbed } = scrubSecrets(raw);
|
|
431
|
+
return scrubbed.length > 4000 ? `${scrubbed.slice(0, 4000)}…[truncated]` : scrubbed;
|
|
234
432
|
}
|
|
235
433
|
catch {
|
|
236
|
-
|
|
434
|
+
const { scrubbed } = scrubSecrets(String(result));
|
|
435
|
+
return scrubbed.length > 4000 ? `${scrubbed.slice(0, 4000)}…[truncated]` : scrubbed;
|
|
237
436
|
}
|
|
238
437
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const SHORTCUTS = [
|
|
2
|
+
{ command: "/task", label: "Task context", description: "查看当前任务、Draft 与 assessment", action: "focus-panel:task" },
|
|
3
|
+
{ command: "/contract", label: "Contract", description: "查看 contract diff 与 apply gate", action: "focus-panel:contract" },
|
|
4
|
+
{ command: "/dag", label: "DAG", description: "查看 DAG spine、确认与运行状态", action: "focus-panel:dag" },
|
|
5
|
+
{ command: "/interview", label: "Interview", description: "打开 Requirement Interview", action: "focus:interview" },
|
|
6
|
+
{ command: "/help", label: "Shortcuts", description: "显示可用 shortcut", action: "show-help" },
|
|
7
|
+
];
|
|
8
|
+
/** Pure navigation/display resolver. It deliberately has no mutation decisions. */
|
|
9
|
+
export function resolveShortcut(input) {
|
|
10
|
+
const token = input.trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
11
|
+
if (!token?.startsWith("/"))
|
|
12
|
+
return undefined;
|
|
13
|
+
const found = SHORTCUTS.find((shortcut) => shortcut.command === token);
|
|
14
|
+
return found ? { command: found.command, action: found.action } : { command: token, action: "unknown" };
|
|
15
|
+
}
|