grok-telegram-bot 2.3.0 → 2.4.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/.env.example +26 -0
- package/CHANGELOG.md +55 -0
- package/package.json +1 -1
- package/scripts/analyze-jsonl.ts +33 -0
- package/scripts/delayed-restart.ps1 +29 -0
- package/scripts/probe-exit-response-shape.py +77 -0
- package/scripts/probe-plan-exit.py +60 -0
- package/scripts/probe-plan-exit2.py +48 -0
- package/scripts/probe-plan-fields.py +41 -0
- package/scripts/probe-plan-fields2.py +58 -0
- package/scripts/probe-plan-response-path.py +48 -0
- package/scripts/sample-claude-tooluse.ts +21 -0
- package/scripts/sample-kiro-events.ts +31 -0
- package/scripts/smoke-exit-plan.ts +274 -0
- package/scripts/smoke-exit-shapes.ts +252 -0
- package/scripts/smoke-import.mjs +82 -0
- package/scripts/smoke-import.ts +73 -0
- package/src/app/accounts.ts +84 -0
- package/src/app/instance-lock.ts +6 -0
- package/src/app/types.ts +19 -2
- package/src/app/updater.ts +17 -6
- package/src/app/usage.ts +204 -7
- package/src/bot/account-rotator.ts +71 -2
- package/src/bot/bot.ts +36 -0
- package/src/bot/chat-controller.ts +35 -0
- package/src/bot/commands.ts +2 -0
- package/src/bot/complexity-gate.ts +69 -0
- package/src/bot/deps.ts +19 -0
- package/src/bot/handlers/accounts.ts +55 -5
- package/src/bot/handlers/import-session.ts +290 -0
- package/src/bot/handlers/menu.ts +17 -38
- package/src/bot/handlers/message.ts +1 -0
- package/src/bot/handlers/running.ts +35 -5
- package/src/bot/handlers/session-card.ts +12 -0
- package/src/bot/handlers/sessions.ts +14 -3
- package/src/bot/handlers/usage.ts +118 -16
- package/src/bot/menu/keyboard.ts +5 -4
- package/src/bot/menu/status-panel.ts +19 -6
- package/src/bot/prompt-content.ts +4 -0
- package/src/bot/reauth-controller.ts +2 -2
- package/src/bot/session-fork.ts +11 -0
- package/src/bot/session-runtime.ts +831 -64
- package/src/bot/suggestions.ts +429 -0
- package/src/config.ts +41 -0
- package/src/grok/client.ts +106 -20
- package/src/grok/plan-approval.ts +72 -0
- package/src/grok/session-log.ts +16 -0
- package/src/grok/types.ts +21 -2
- package/src/import/build-import.ts +132 -0
- package/src/import/history-readers.ts +681 -0
- package/src/import/list-running.ts +100 -0
- package/src/import/sources.ts +78 -0
- package/src/index.ts +179 -24
- package/src/render/diff.ts +11 -2
- package/src/render/file-summary.ts +31 -1
- package/src/render/markdown.ts +293 -35
- package/src/render/plan.ts +127 -0
- package/src/render/session-comment.ts +261 -0
- package/src/render/tool-call-detail.ts +400 -19
- package/src/render/tool-call-merge.ts +115 -0
- package/src/render/tool-call.ts +405 -142
- package/src/render/truncate.ts +85 -0
- package/src/service/windows.ts +14 -2
- package/src/sessions/history.ts +57 -0
- package/src/sessions/store.ts +3 -0
- package/src/sessions/types.ts +5 -0
- package/src/stream/streamer.ts +73 -9
- package/src/tasks/runner.ts +4 -3
package/src/render/tool-call.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* Format ACP tool-call updates into clear, RAW markdown blocks so they read
|
|
3
3
|
* distinctly from the agent's prose and thinking. Each tool kind gets its own
|
|
4
4
|
* rich detail: commands in bash blocks, diffs in diff blocks, search queries,
|
|
5
|
-
* file paths, URLs, content previews,
|
|
5
|
+
* file paths (with offset/limit), URLs, content previews, MCP args.
|
|
6
|
+
*
|
|
7
|
+
* Grok often reports kind "other" with title/name `read_file` / `grep` / etc.
|
|
8
|
+
* {@link resolveToolIdentity} maps those to real display kinds so users never
|
|
9
|
+
* see a bare "Other" or a false "Call MCP: read_file".
|
|
6
10
|
*/
|
|
7
11
|
import type { SessionUpdate } from "../grok/types.js";
|
|
8
12
|
import { renderUnifiedDiff } from "./diff.js";
|
|
9
13
|
import {
|
|
10
|
-
|
|
14
|
+
resolveToolIdentity,
|
|
11
15
|
extractPath,
|
|
12
16
|
extractDestPath,
|
|
13
17
|
extractSearchQuery,
|
|
@@ -16,28 +20,40 @@ import {
|
|
|
16
20
|
extractCommand,
|
|
17
21
|
extractContent,
|
|
18
22
|
extractFilters,
|
|
23
|
+
extractReadRange,
|
|
24
|
+
extractToolOutput,
|
|
25
|
+
extractToolName,
|
|
26
|
+
formatArgLines,
|
|
19
27
|
truncate,
|
|
20
28
|
collectContent,
|
|
21
29
|
gatherPaths,
|
|
22
30
|
PREVIEW_MAX,
|
|
23
31
|
CONTENT_PREVIEW_MAX,
|
|
32
|
+
OUTPUT_PREVIEW_MAX,
|
|
33
|
+
OUTPUT_PREVIEW_LINES,
|
|
34
|
+
type ToolDisplayKind,
|
|
24
35
|
} from "./tool-call-detail.js";
|
|
36
|
+
import { formatLiveTerminalOutput, truncateMiddle, truncateMiddleLines } from "./truncate.js";
|
|
25
37
|
|
|
26
38
|
const KIND_ICON: Record<string, string> = {
|
|
27
|
-
read: "\u{1F4D6}",
|
|
28
|
-
edit: "\u270F\uFE0F",
|
|
29
|
-
write: "\u{1F4DD}",
|
|
39
|
+
read: "\u{1F4D6}", // 📖
|
|
40
|
+
edit: "\u270F\uFE0F", // ✏️
|
|
41
|
+
write: "\u{1F4DD}", // 📝
|
|
30
42
|
create: "\u{1F4DD}",
|
|
31
|
-
execute: "\u{1F4BB}",
|
|
32
|
-
search: "\u{1F50E}",
|
|
33
|
-
delete: "\u{1F5D1}\uFE0F",
|
|
34
|
-
move: "\u{1F4E6}",
|
|
43
|
+
execute: "\u{1F4BB}", // 💻
|
|
44
|
+
search: "\u{1F50E}", // 🔎
|
|
45
|
+
delete: "\u{1F5D1}\uFE0F", // 🗑️
|
|
46
|
+
move: "\u{1F4E6}", // 📦
|
|
35
47
|
rename: "\u{1F4E6}",
|
|
36
|
-
fetch: "\u{1F310}",
|
|
48
|
+
fetch: "\u{1F310}", // 🌐
|
|
37
49
|
web_search: "\u{1F310}",
|
|
38
50
|
web_fetch: "\u{1F310}",
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
list: "\u{1F4C1}", // 📁
|
|
52
|
+
todo: "\u2705", // ✅
|
|
53
|
+
think: "\u{1F4AD}", // 💭
|
|
54
|
+
image: "\u{1F5BC}\uFE0F", // 🖼️
|
|
55
|
+
mcp: "\u{1F9E9}", // 🧩
|
|
56
|
+
other: "\u{1F527}", // 🔧
|
|
41
57
|
};
|
|
42
58
|
|
|
43
59
|
const STATUS_ICON: Record<string, string> = {
|
|
@@ -54,39 +70,74 @@ export interface ToolFormatOptions {
|
|
|
54
70
|
|
|
55
71
|
/** Returns a RAW markdown block describing the tool call, or "" to skip. */
|
|
56
72
|
export function formatToolCall(u: SessionUpdate, opts: ToolFormatOptions): string {
|
|
57
|
-
const
|
|
58
|
-
|
|
73
|
+
const raw = flattenRawInput(u);
|
|
74
|
+
// Inject path from ACP locations when rawInput is thin.
|
|
75
|
+
if (u.locations?.[0]?.path && !raw.path && !raw.target_file && !raw.file_path) {
|
|
76
|
+
raw.path = u.locations[0].path;
|
|
77
|
+
if (u.locations[0].line != null && raw.start_line == null) raw.start_line = u.locations[0].line;
|
|
78
|
+
}
|
|
79
|
+
const id = resolveToolIdentity(u, raw);
|
|
59
80
|
const status = u.status ? (STATUS_ICON[u.status] ?? "") : "";
|
|
60
81
|
const tail = status ? " " + status : "";
|
|
61
82
|
|
|
62
|
-
//
|
|
63
|
-
|
|
83
|
+
// Plan mode enter/exit — surface clearly (exit failures used to look opaque).
|
|
84
|
+
const planCard = formatPlanModeTool(u, raw, id.toolName, tail);
|
|
85
|
+
if (planCard) return planCard;
|
|
86
|
+
|
|
87
|
+
// Skill load (SKILL.md path) — not a normal edit.
|
|
88
|
+
if (id.kind !== "edit" && id.kind !== "delete" && id.kind !== "move" && id.kind !== "write" && id.kind !== "create") {
|
|
64
89
|
const skill = detectSkill(u, raw);
|
|
65
90
|
if (skill) return "\u{1F4DA} **Loaded skill: " + skill + "**" + tail;
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
// MCP
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
93
|
+
// Real MCP (namespaced) — badge + rich method-specific detail when possible.
|
|
94
|
+
if (id.isMcp) {
|
|
95
|
+
const label = id.mcpServer
|
|
96
|
+
? `\u{1F9E9} **MCP ${id.mcpServer} \u00B7 ${id.mcpMethod || id.toolName}**${tail}`
|
|
97
|
+
: `\u{1F9E9} **MCP: ${id.mcpMethod || id.toolName}**${tail}`;
|
|
98
|
+
// Prefer rich formatter for known methods (read/search/…); keep MCP header.
|
|
99
|
+
if (id.kind !== "mcp" && id.kind !== "other") {
|
|
100
|
+
const body = formatByKind(id.kind, u, raw, "", opts, id.mcpMethod || id.toolName);
|
|
101
|
+
return label + "\n" + body;
|
|
102
|
+
}
|
|
103
|
+
let out = label;
|
|
104
|
+
const args = formatArgLines(raw);
|
|
105
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
106
|
+
const result = extractToolOutput(u);
|
|
107
|
+
if (result) {
|
|
108
|
+
out +=
|
|
109
|
+
"\n**Output:**\n" +
|
|
110
|
+
fence(truncateMiddleLines(result, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) +
|
|
111
|
+
"\n";
|
|
112
|
+
}
|
|
75
113
|
return out;
|
|
76
114
|
}
|
|
77
115
|
|
|
116
|
+
return formatByKind(id.kind, u, raw, tail, opts, id.toolName);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatByKind(
|
|
120
|
+
kind: ToolDisplayKind | string,
|
|
121
|
+
u: SessionUpdate,
|
|
122
|
+
raw: Record<string, unknown>,
|
|
123
|
+
tail: string,
|
|
124
|
+
opts: ToolFormatOptions,
|
|
125
|
+
toolName: string,
|
|
126
|
+
): string {
|
|
78
127
|
switch (kind) {
|
|
79
128
|
case "execute":
|
|
80
|
-
return formatExecute(raw, tail);
|
|
129
|
+
return formatExecute(u, raw, tail, toolName);
|
|
81
130
|
case "edit":
|
|
82
|
-
return formatEdit(u, raw, tail, opts);
|
|
131
|
+
return formatEdit(u, raw, tail, opts, toolName);
|
|
83
132
|
case "write":
|
|
84
133
|
case "create":
|
|
85
|
-
return formatWrite(kind, raw, tail);
|
|
134
|
+
return formatWrite(u, kind, raw, tail);
|
|
86
135
|
case "read":
|
|
87
|
-
return formatRead(raw, tail);
|
|
136
|
+
return formatRead(u, raw, tail, toolName);
|
|
137
|
+
case "list":
|
|
138
|
+
return formatList(u, raw, tail, toolName);
|
|
88
139
|
case "search":
|
|
89
|
-
return formatSearch(raw, tail);
|
|
140
|
+
return formatSearch(u, raw, tail, toolName);
|
|
90
141
|
case "delete":
|
|
91
142
|
return formatDelete(raw, tail);
|
|
92
143
|
case "move":
|
|
@@ -94,37 +145,90 @@ export function formatToolCall(u: SessionUpdate, opts: ToolFormatOptions): strin
|
|
|
94
145
|
return formatMove(kind, raw, tail);
|
|
95
146
|
case "fetch":
|
|
96
147
|
case "web_fetch":
|
|
97
|
-
return formatFetch(raw, tail);
|
|
148
|
+
return formatFetch(u, raw, tail);
|
|
98
149
|
case "web_search":
|
|
99
|
-
return formatWebSearch(raw, tail);
|
|
150
|
+
return formatWebSearch(u, raw, tail);
|
|
151
|
+
case "todo":
|
|
152
|
+
return formatTodo(raw, tail, toolName);
|
|
153
|
+
case "think":
|
|
154
|
+
return formatThink(raw, tail, toolName, u);
|
|
155
|
+
case "image":
|
|
156
|
+
return formatImage(raw, tail, toolName);
|
|
100
157
|
default:
|
|
101
|
-
return formatGeneric(u, raw, tail, kind);
|
|
158
|
+
return formatGeneric(u, raw, tail, kind, toolName);
|
|
102
159
|
}
|
|
103
160
|
}
|
|
104
161
|
|
|
105
|
-
// ---- helpers for code fences
|
|
162
|
+
// ---- helpers for code fences ----
|
|
106
163
|
|
|
107
|
-
/**
|
|
164
|
+
/** Fence that lengthens itself when the body contains backticks (avoids MD break). */
|
|
108
165
|
function fence(text: string, lang?: string): string {
|
|
109
|
-
|
|
166
|
+
let tickLen = 3;
|
|
167
|
+
const runs = text.match(/`+/g);
|
|
168
|
+
if (runs) {
|
|
169
|
+
const max = Math.max(...runs.map((r) => r.length));
|
|
170
|
+
if (max >= tickLen) tickLen = max + 1;
|
|
171
|
+
}
|
|
172
|
+
const marker = "`".repeat(tickLen);
|
|
110
173
|
return marker + (lang || "") + "\n" + text + "\n" + marker;
|
|
111
174
|
}
|
|
112
175
|
|
|
176
|
+
/** Merge rawInput with nested arguments/input objects Grok sometimes uses. */
|
|
177
|
+
function flattenRawInput(u: SessionUpdate): Record<string, unknown> {
|
|
178
|
+
const raw = { ...((u.rawInput || {}) as Record<string, unknown>) };
|
|
179
|
+
for (const key of ["arguments", "input", "args", "parameters"]) {
|
|
180
|
+
const nested = raw[key];
|
|
181
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
182
|
+
for (const [k, v] of Object.entries(nested as Record<string, unknown>)) {
|
|
183
|
+
if (raw[k] === undefined) raw[k] = v;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return raw;
|
|
188
|
+
}
|
|
189
|
+
|
|
113
190
|
// ---- per-kind formatters ----
|
|
114
191
|
|
|
115
|
-
function formatExecute(
|
|
192
|
+
function formatExecute(
|
|
193
|
+
u: SessionUpdate,
|
|
194
|
+
raw: Record<string, unknown>,
|
|
195
|
+
tail: string,
|
|
196
|
+
toolName: string,
|
|
197
|
+
): string {
|
|
116
198
|
const cmd = extractCommand(raw);
|
|
117
|
-
const cwd = strOf(raw.cwd);
|
|
199
|
+
const cwd = strOf(raw.cwd) || strOf(raw.working_directory) || strOf(raw.workingDirectory);
|
|
118
200
|
const title = "Run command" + (cwd ? " in " + truncate(cwd, 80) : "");
|
|
119
201
|
let out = "\u{1F4BB} **" + title + "**" + tail;
|
|
120
|
-
if (
|
|
202
|
+
if (toolName && toolName !== "execute" && toolName !== "shell" && toolName !== "bash") {
|
|
203
|
+
out += `\n tool: \`${toolName}\``;
|
|
204
|
+
}
|
|
205
|
+
if (cmd) out += "\n" + fence(truncateMiddle(cmd, PREVIEW_MAX), "bash") + "\n";
|
|
206
|
+
else {
|
|
207
|
+
// No command field — dump args so the user still sees what ran.
|
|
208
|
+
const args = formatArgLines(raw);
|
|
209
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
210
|
+
}
|
|
211
|
+
// Display: first output line + live tail (one block, updated in place via upsert).
|
|
212
|
+
// Full stdout remains in the agent session / merged tool snapshot.
|
|
213
|
+
const result = extractToolOutput(u);
|
|
214
|
+
if (result) {
|
|
215
|
+
const live = formatLiveTerminalOutput(result, 12, OUTPUT_PREVIEW_MAX);
|
|
216
|
+
out += "\n**Output:**\n" + fence(live) + "\n";
|
|
217
|
+
}
|
|
121
218
|
return out;
|
|
122
219
|
}
|
|
123
220
|
|
|
124
|
-
function formatEdit(
|
|
221
|
+
function formatEdit(
|
|
222
|
+
u: SessionUpdate,
|
|
223
|
+
raw: Record<string, unknown>,
|
|
224
|
+
tail: string,
|
|
225
|
+
opts: ToolFormatOptions,
|
|
226
|
+
toolName: string,
|
|
227
|
+
): string {
|
|
125
228
|
const path = extractPath(raw);
|
|
126
229
|
const title = "Edit " + (path || "file");
|
|
127
230
|
let out = "\u270F\uFE0F **" + title + "**" + tail;
|
|
231
|
+
if (toolName && !/edit|replace/i.test(toolName)) out += `\n tool: \`${toolName}\``;
|
|
128
232
|
if (opts.showDiffs) {
|
|
129
233
|
const diff = buildEditDiff(u, raw, opts.diffMaxLines);
|
|
130
234
|
if (diff && diff.block) {
|
|
@@ -135,44 +239,100 @@ function formatEdit(u: SessionUpdate, raw: Record<string, unknown>, tail: string
|
|
|
135
239
|
return out;
|
|
136
240
|
}
|
|
137
241
|
|
|
138
|
-
function formatWrite(kind: string, raw: Record<string, unknown>, tail: string): string {
|
|
242
|
+
function formatWrite(u: SessionUpdate, kind: string, raw: Record<string, unknown>, tail: string): string {
|
|
139
243
|
const path = extractPath(raw);
|
|
140
244
|
const verb = kind === "create" ? "Create" : "Write";
|
|
141
245
|
let out = "\u{1F4DD} **" + verb + " " + (path || "file") + "**" + tail;
|
|
142
|
-
const content = extractContent(raw);
|
|
246
|
+
const content = extractContent(raw) || extractToolOutput(u);
|
|
143
247
|
if (content) {
|
|
144
|
-
out += "\n" + fence(
|
|
248
|
+
out += "\n" + fence(truncateMiddleLines(content, OUTPUT_PREVIEW_LINES, CONTENT_PREVIEW_MAX), detectLang(path)) + "\n";
|
|
145
249
|
}
|
|
146
250
|
return out;
|
|
147
251
|
}
|
|
148
252
|
|
|
149
|
-
function formatRead(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
253
|
+
function formatRead(
|
|
254
|
+
u: SessionUpdate,
|
|
255
|
+
raw: Record<string, unknown>,
|
|
256
|
+
tail: string,
|
|
257
|
+
toolName: string,
|
|
258
|
+
): string {
|
|
259
|
+
const path = extractPath(raw, u);
|
|
260
|
+
const range = extractReadRange(raw);
|
|
154
261
|
let title = "Read " + (path || "file");
|
|
155
262
|
const parts: string[] = [];
|
|
156
|
-
if (
|
|
157
|
-
if (offset) parts.push("offset " + offset);
|
|
158
|
-
if (limit) parts.push("limit " + limit);
|
|
263
|
+
if (range.startLine) parts.push("line " + range.startLine);
|
|
264
|
+
if (range.offset) parts.push("offset " + range.offset);
|
|
265
|
+
if (range.limit) parts.push("limit " + range.limit);
|
|
266
|
+
if (range.pages) parts.push("pages " + range.pages);
|
|
159
267
|
if (parts.length) title += " (" + parts.join(", ") + ")";
|
|
160
|
-
|
|
268
|
+
let out = "\u{1F4D6} **" + title + "**" + tail;
|
|
269
|
+
if (toolName && toolName !== "read" && toolName !== "read_file") {
|
|
270
|
+
out += `\n tool: \`${toolName}\``;
|
|
271
|
+
}
|
|
272
|
+
// Extra range detail lines for clarity when many fields present.
|
|
273
|
+
if (parts.length >= 2) {
|
|
274
|
+
out += "\n " + parts.join(" \u00B7 ");
|
|
275
|
+
}
|
|
276
|
+
const body = extractToolOutput(u);
|
|
277
|
+
if (body) {
|
|
278
|
+
out += "\n" + fence(truncateMiddleLines(body, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX), detectLang(path)) + "\n";
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatList(
|
|
284
|
+
u: SessionUpdate,
|
|
285
|
+
raw: Record<string, unknown>,
|
|
286
|
+
tail: string,
|
|
287
|
+
toolName: string,
|
|
288
|
+
): string {
|
|
289
|
+
const path =
|
|
290
|
+
extractPath(raw) ||
|
|
291
|
+
extractSearchPath(raw) ||
|
|
292
|
+
strOf(raw.target_directory) ||
|
|
293
|
+
strOf(raw.targetDirectory) ||
|
|
294
|
+
".";
|
|
295
|
+
let out = "\u{1F4C1} **List " + truncate(path, 120) + "**" + tail;
|
|
296
|
+
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
297
|
+
const filters = extractFilters(raw);
|
|
298
|
+
if (filters.include) out += "\n include: " + filters.include;
|
|
299
|
+
if (filters.exclude) out += "\n exclude: " + filters.exclude;
|
|
300
|
+
const body = extractToolOutput(u);
|
|
301
|
+
if (body) {
|
|
302
|
+
out += "\n" + fence(truncateMiddleLines(body, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
161
305
|
}
|
|
162
306
|
|
|
163
|
-
function formatSearch(
|
|
307
|
+
function formatSearch(
|
|
308
|
+
u: SessionUpdate,
|
|
309
|
+
raw: Record<string, unknown>,
|
|
310
|
+
tail: string,
|
|
311
|
+
toolName: string,
|
|
312
|
+
): string {
|
|
164
313
|
const query = extractSearchQuery(raw);
|
|
165
314
|
const path = extractSearchPath(raw);
|
|
166
315
|
const filters = extractFilters(raw);
|
|
167
|
-
|
|
316
|
+
const isGlob = /glob/i.test(toolName) || (!!filters.include && !query);
|
|
317
|
+
let title = isGlob ? "Glob" : "Search";
|
|
168
318
|
if (query) title += ": " + truncate(query, 120);
|
|
169
319
|
else if (path) title += " " + path;
|
|
170
320
|
let out = "\u{1F50E} **" + title + "**" + tail;
|
|
171
|
-
if (
|
|
321
|
+
if (toolName && toolName !== "search" && toolName !== "grep") {
|
|
322
|
+
out += `\n tool: \`${toolName}\``;
|
|
323
|
+
}
|
|
324
|
+
if (path && !(query && query.includes(path))) out += "\n \u{1F4C2} in: " + truncate(path, 100);
|
|
172
325
|
if (filters.include) out += "\n \u{1F4C1} include: " + filters.include;
|
|
173
326
|
if (filters.exclude) out += "\n \u{1F6AB} exclude: " + filters.exclude;
|
|
174
|
-
if (raw.case_sensitive !== undefined)
|
|
327
|
+
if (raw.case_sensitive !== undefined) {
|
|
175
328
|
out += "\n case-sensitive: " + (raw.case_sensitive ? "yes" : "no");
|
|
329
|
+
}
|
|
330
|
+
if (raw.multiline !== undefined) out += "\n multiline: " + (raw.multiline ? "yes" : "no");
|
|
331
|
+
if (raw.type) out += "\n type: " + String(raw.type);
|
|
332
|
+
const hits = extractToolOutput(u);
|
|
333
|
+
if (hits) {
|
|
334
|
+
out += "\n" + fence(truncateMiddleLines(hits, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
335
|
+
}
|
|
176
336
|
return out;
|
|
177
337
|
}
|
|
178
338
|
|
|
@@ -186,12 +346,21 @@ function formatMove(kind: string, raw: Record<string, unknown>, tail: string): s
|
|
|
186
346
|
const dst = extractDestPath(raw);
|
|
187
347
|
const verb = kind === "rename" ? "Rename" : "Move";
|
|
188
348
|
if (src && dst) {
|
|
189
|
-
return
|
|
349
|
+
return (
|
|
350
|
+
"\u{1F4E6} **" +
|
|
351
|
+
verb +
|
|
352
|
+
"**" +
|
|
353
|
+
tail +
|
|
354
|
+
"\n \u{1F4C4} " +
|
|
355
|
+
truncate(src, 100) +
|
|
356
|
+
"\n \u27A1\uFE0F " +
|
|
357
|
+
truncate(dst, 100)
|
|
358
|
+
);
|
|
190
359
|
}
|
|
191
360
|
return "\u{1F4E6} **" + verb + " " + (src || dst || "file") + "**" + tail;
|
|
192
361
|
}
|
|
193
362
|
|
|
194
|
-
function formatFetch(raw: Record<string, unknown>, tail: string): string {
|
|
363
|
+
function formatFetch(u: SessionUpdate, raw: Record<string, unknown>, tail: string): string {
|
|
195
364
|
const url = extractUrl(raw);
|
|
196
365
|
const method = strOf(raw.method) || strOf(raw.verb) || "GET";
|
|
197
366
|
let title = "Fetch URL";
|
|
@@ -205,26 +374,142 @@ function formatFetch(raw: Record<string, unknown>, tail: string): string {
|
|
|
205
374
|
}
|
|
206
375
|
const body = strOf(raw.body) || strOf(raw.data);
|
|
207
376
|
if (body) out += "\n body: " + truncate(body, 200);
|
|
377
|
+
const result = extractToolOutput(u);
|
|
378
|
+
if (result) {
|
|
379
|
+
out += "\n" + fence(truncateMiddleLines(result, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
380
|
+
}
|
|
208
381
|
return out;
|
|
209
382
|
}
|
|
210
383
|
|
|
211
|
-
function formatWebSearch(raw: Record<string, unknown>, tail: string): string {
|
|
384
|
+
function formatWebSearch(u: SessionUpdate, raw: Record<string, unknown>, tail: string): string {
|
|
212
385
|
const query = extractSearchQuery(raw) || extractUrl(raw);
|
|
213
|
-
const count = strOf(raw.count) || strOf(raw.num) || strOf(raw.num_results);
|
|
386
|
+
const count = strOf(raw.count) || strOf(raw.num) || strOf(raw.num_results) || numStr(raw.num_results);
|
|
214
387
|
let title = "Web search";
|
|
215
388
|
if (query) title += ": " + truncate(query, 150);
|
|
216
389
|
let out = "\u{1F310} **" + title + "**" + tail;
|
|
217
390
|
if (count) out += "\n results: " + count;
|
|
391
|
+
const result = extractToolOutput(u);
|
|
392
|
+
if (result) {
|
|
393
|
+
out += "\n" + fence(truncateMiddleLines(result, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function formatTodo(raw: Record<string, unknown>, tail: string, toolName: string): string {
|
|
399
|
+
let out = "\u2705 **Todos**" + tail;
|
|
400
|
+
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
401
|
+
const args = formatArgLines(raw);
|
|
402
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
218
403
|
return out;
|
|
219
404
|
}
|
|
220
405
|
|
|
221
|
-
function
|
|
406
|
+
function formatThink(
|
|
407
|
+
raw: Record<string, unknown>,
|
|
408
|
+
tail: string,
|
|
409
|
+
toolName: string,
|
|
410
|
+
u: SessionUpdate,
|
|
411
|
+
): string {
|
|
412
|
+
const desc =
|
|
413
|
+
strOf(raw.description) ||
|
|
414
|
+
strOf(raw.prompt) ||
|
|
415
|
+
strOf(raw.message) ||
|
|
416
|
+
strOf(raw.task) ||
|
|
417
|
+
u.title ||
|
|
418
|
+
toolName ||
|
|
419
|
+
"subagent";
|
|
420
|
+
let out = "\u{1F4AD} **Delegate / think**" + tail + "\n " + truncate(desc, 300);
|
|
421
|
+
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
422
|
+
const args = formatArgLines(raw);
|
|
423
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function formatImage(raw: Record<string, unknown>, tail: string, toolName: string): string {
|
|
428
|
+
const path = extractPath(raw) || strOf(raw.image) || strOf(raw.output);
|
|
429
|
+
let out = "\u{1F5BC}\uFE0F **Image**" + tail;
|
|
430
|
+
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
431
|
+
if (path) out += "\n " + truncate(path, 200);
|
|
432
|
+
const prompt = strOf(raw.prompt);
|
|
433
|
+
if (prompt) out += "\n prompt: " + truncate(prompt, 200);
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Enter/exit plan mode tools — show status + failure reason (e.g. client
|
|
439
|
+
* disconnected when the bridge did not answer the plan-approval reverse-request).
|
|
440
|
+
*/
|
|
441
|
+
function formatPlanModeTool(
|
|
442
|
+
u: SessionUpdate,
|
|
443
|
+
raw: Record<string, unknown>,
|
|
444
|
+
toolName: string,
|
|
445
|
+
tail: string,
|
|
446
|
+
): string | undefined {
|
|
447
|
+
const name = (toolName || u.title || "").toLowerCase();
|
|
448
|
+
const variant = String(raw.variant ?? raw.action ?? "").toLowerCase();
|
|
449
|
+
const isEnter =
|
|
450
|
+
name.includes("enter_plan") ||
|
|
451
|
+
name === "plan: enter" ||
|
|
452
|
+
variant.includes("enterplan");
|
|
453
|
+
const isExit =
|
|
454
|
+
name.includes("exit_plan") ||
|
|
455
|
+
name === "plan: exit" ||
|
|
456
|
+
variant.includes("exitplan");
|
|
457
|
+
if (!isEnter && !isExit) return undefined;
|
|
458
|
+
|
|
459
|
+
const icon = isExit ? "\u{1F4CB}" : "\u{1F4D1}";
|
|
460
|
+
const label = isExit ? "Plan: Exit" : "Plan: Enter";
|
|
461
|
+
let out = `${icon} **${label}**${tail}`;
|
|
462
|
+
if (variant) out += `\n variant: \`${String(raw.variant ?? raw.action)}\``;
|
|
463
|
+
const result = extractToolOutput(u);
|
|
464
|
+
if (result) {
|
|
465
|
+
// Prefer a short, visible failure/success reason over a huge dump.
|
|
466
|
+
const short = truncateMiddle(result.trim(), 500);
|
|
467
|
+
out += "\n" + fence(short) + "\n";
|
|
468
|
+
} else if ((u.status || "").toLowerCase() === "failed") {
|
|
469
|
+
out += "\n \u274C Plan approval failed (bridge should auto-approve exit).";
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function formatGeneric(
|
|
475
|
+
u: SessionUpdate,
|
|
476
|
+
raw: Record<string, unknown>,
|
|
477
|
+
tail: string,
|
|
478
|
+
kind: string,
|
|
479
|
+
toolName: string,
|
|
480
|
+
): string {
|
|
222
481
|
const icon = KIND_ICON[kind] ?? KIND_ICON.other;
|
|
223
482
|
const path = extractPath(raw);
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
483
|
+
const cmd = extractCommand(raw);
|
|
484
|
+
const query = extractSearchQuery(raw);
|
|
485
|
+
// Never show a bare "Other" / "Tool call" — prefer real tool name / path / cmd.
|
|
486
|
+
let label =
|
|
487
|
+
(toolName && !/^tool[_ ]?call$/i.test(toolName) ? toolName : "") ||
|
|
488
|
+
(u.title && !/^other$/i.test(u.title.trim()) && !/^tool[_ ]?call$/i.test(u.title.trim())
|
|
489
|
+
? u.title.trim()
|
|
490
|
+
: "") ||
|
|
491
|
+
(path ? capitalize(kind !== "other" ? kind : "use") + " " + path : "") ||
|
|
492
|
+
(cmd ? "Run command" : "") ||
|
|
493
|
+
(query ? "Search: " + truncate(query, 80) : "") ||
|
|
494
|
+
(kind && kind !== "other" ? capitalize(kind) : "") ||
|
|
495
|
+
(toolName || "Tool");
|
|
496
|
+
if (/^other$/i.test(label) || /^tool[_ ]?call$/i.test(label)) {
|
|
497
|
+
label = toolName && !/^tool[_ ]?call$/i.test(toolName) ? toolName : path || cmd || query || "Tool";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
let out = icon + " **" + label + "**" + tail;
|
|
501
|
+
if (toolName && toolName !== label) out += `\n tool: \`${toolName}\``;
|
|
502
|
+
if (path && !label.includes(path)) out += "\n \u{1F4C4} " + truncate(path, 120);
|
|
503
|
+
if (cmd) out += "\n" + fence(truncateMiddle(cmd, PREVIEW_MAX), "bash") + "\n";
|
|
504
|
+
else if (query && !label.includes(query)) out += "\n query: " + truncate(query, 150);
|
|
505
|
+
else {
|
|
506
|
+
const args = formatArgLines(raw);
|
|
507
|
+
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
508
|
+
}
|
|
509
|
+
const result = extractToolOutput(u);
|
|
510
|
+
if (result) {
|
|
511
|
+
out += "\n" + fence(truncateMiddleLines(result, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
512
|
+
}
|
|
228
513
|
return out;
|
|
229
514
|
}
|
|
230
515
|
|
|
@@ -235,7 +520,7 @@ function buildEditDiff(u: SessionUpdate, raw: Record<string, unknown>, maxLines:
|
|
|
235
520
|
const diffBlock = blocks.find((b) => b.type === "diff");
|
|
236
521
|
if (diffBlock) {
|
|
237
522
|
return renderUnifiedDiff({
|
|
238
|
-
path: strOf(diffBlock.path) ||
|
|
523
|
+
path: strOf(diffBlock.path) || extractPath(raw) || "file",
|
|
239
524
|
oldText: typeof diffBlock.oldText === "string" ? diffBlock.oldText : "",
|
|
240
525
|
newText: typeof diffBlock.newText === "string" ? diffBlock.newText : "",
|
|
241
526
|
maxLines,
|
|
@@ -244,11 +529,16 @@ function buildEditDiff(u: SessionUpdate, raw: Record<string, unknown>, maxLines:
|
|
|
244
529
|
const oldStr = strOf(raw.old_str) || strOf(raw.oldStr) || strOf(raw.old_string) || strOf(raw.find);
|
|
245
530
|
const newStr = strOf(raw.new_str) || strOf(raw.newStr) || strOf(raw.new_string) || strOf(raw.replace);
|
|
246
531
|
if (oldStr || newStr) {
|
|
247
|
-
return renderUnifiedDiff({
|
|
532
|
+
return renderUnifiedDiff({
|
|
533
|
+
path: extractPath(raw) || "file",
|
|
534
|
+
oldText: oldStr,
|
|
535
|
+
newText: newStr,
|
|
536
|
+
maxLines,
|
|
537
|
+
});
|
|
248
538
|
}
|
|
249
539
|
const content = strOf(raw.file_text) || strOf(raw.content) || strOf(raw.text);
|
|
250
540
|
if (content) {
|
|
251
|
-
return renderUnifiedDiff({ path:
|
|
541
|
+
return renderUnifiedDiff({ path: extractPath(raw) || "file", oldText: "", newText: content, maxLines });
|
|
252
542
|
}
|
|
253
543
|
return undefined;
|
|
254
544
|
}
|
|
@@ -258,62 +548,52 @@ function buildEditDiff(u: SessionUpdate, raw: Record<string, unknown>, maxLines:
|
|
|
258
548
|
function detectLang(path: string): string {
|
|
259
549
|
const ext = (path.split(".").pop() || "").toLowerCase();
|
|
260
550
|
const MAP: Record<string, string> = {
|
|
261
|
-
ts: "typescript",
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
551
|
+
ts: "typescript",
|
|
552
|
+
tsx: "tsx",
|
|
553
|
+
js: "javascript",
|
|
554
|
+
jsx: "jsx",
|
|
555
|
+
py: "python",
|
|
556
|
+
go: "go",
|
|
557
|
+
rs: "rust",
|
|
558
|
+
java: "java",
|
|
559
|
+
c: "c",
|
|
560
|
+
cpp: "cpp",
|
|
561
|
+
h: "c",
|
|
562
|
+
hpp: "cpp",
|
|
563
|
+
cs: "csharp",
|
|
564
|
+
rb: "ruby",
|
|
565
|
+
php: "php",
|
|
566
|
+
swift: "swift",
|
|
567
|
+
kt: "kotlin",
|
|
568
|
+
scala: "scala",
|
|
569
|
+
sh: "bash",
|
|
570
|
+
bash: "bash",
|
|
571
|
+
sql: "sql",
|
|
572
|
+
html: "html",
|
|
573
|
+
css: "css",
|
|
574
|
+
scss: "scss",
|
|
575
|
+
json: "json",
|
|
576
|
+
yaml: "yaml",
|
|
577
|
+
yml: "yaml",
|
|
578
|
+
xml: "xml",
|
|
579
|
+
md: "markdown",
|
|
580
|
+
toml: "toml",
|
|
581
|
+
ini: "ini",
|
|
582
|
+
cfg: "ini",
|
|
583
|
+
vue: "vue",
|
|
584
|
+
svelte: "svelte",
|
|
585
|
+
dart: "dart",
|
|
586
|
+
lua: "lua",
|
|
587
|
+
r: "r",
|
|
588
|
+
pl: "perl",
|
|
589
|
+
ps1: "powershell",
|
|
271
590
|
};
|
|
272
591
|
return MAP[ext] || "";
|
|
273
592
|
}
|
|
274
593
|
|
|
275
|
-
// ----
|
|
276
|
-
|
|
277
|
-
/** Compact one-line-per-key preview of an MCP call's arguments. */
|
|
278
|
-
function mcpArgPreview(raw: Record<string, unknown>): string {
|
|
279
|
-
const SKIP = new Set(["tool_name", "toolName", "name", "tool", "type", "_meta"]);
|
|
280
|
-
const lines: string[] = [];
|
|
281
|
-
for (const [key, val] of Object.entries(raw)) {
|
|
282
|
-
if (SKIP.has(key)) continue;
|
|
283
|
-
let s: string;
|
|
284
|
-
if (typeof val === "string") s = val;
|
|
285
|
-
else if (typeof val === "number" || typeof val === "boolean") s = String(val);
|
|
286
|
-
else {
|
|
287
|
-
try { s = JSON.stringify(val); } catch { s = String(val); }
|
|
288
|
-
}
|
|
289
|
-
if (s.length > 200) s = s.slice(0, 199) + "\u2026";
|
|
290
|
-
lines.push(key + ": " + s);
|
|
291
|
-
}
|
|
292
|
-
return truncate(lines.join("\n"), PREVIEW_MAX);
|
|
293
|
-
}
|
|
594
|
+
// ---- skill ----
|
|
294
595
|
|
|
295
|
-
/** Built-in Grok tools that must never be labelled as MCP calls. */
|
|
296
|
-
const BUILTIN_TOOLS = new Set([
|
|
297
|
-
"read", "write", "shell", "grep", "glob", "web_fetch", "web_search", "fs_read",
|
|
298
|
-
"fs_write", "fs_replace", "fs_search", "execute_bash", "report_issue", "use_aws",
|
|
299
|
-
"todo_list", "introspect", "knowledge", "thinking", "summary", "subagent",
|
|
300
|
-
"edit", "create", "delete", "move", "rename", "execute", "search", "fetch",
|
|
301
|
-
]);
|
|
302
|
-
/** Tool kinds that are first-class file/shell operations (never MCP). */
|
|
303
|
-
const FILE_KINDS = new Set([
|
|
304
|
-
"read", "edit", "execute", "search", "delete", "move", "write", "create",
|
|
305
|
-
"rename", "fetch", "web_fetch", "web_search",
|
|
306
|
-
]);
|
|
307
|
-
/** `.../skills/<name>/SKILL.md` - the signature of loading a skill. */
|
|
308
596
|
const SKILL_RE = /[\\/]skills[\\/]([^\\/]+)[\\/]SKILL\.md$/i;
|
|
309
|
-
/** Namespaced MCP tool-name shapes - [, server, method]. */
|
|
310
|
-
const MCP_NS = [
|
|
311
|
-
/^@([a-z0-9._-]+)[/_]{1,3}(.+)$/i,
|
|
312
|
-
/^([a-z0-9.-]+)___(.+)$/i,
|
|
313
|
-
/^([a-z0-9.-]+)__(.+)$/i,
|
|
314
|
-
/^([a-z0-9.-]+)\/(.+)$/i,
|
|
315
|
-
/^([a-z0-9-]+)\.(.+)$/i,
|
|
316
|
-
];
|
|
317
597
|
|
|
318
598
|
function detectSkill(u: SessionUpdate, raw: Record<string, unknown>): string | undefined {
|
|
319
599
|
for (const p of gatherPaths(u, raw)) {
|
|
@@ -323,34 +603,17 @@ function detectSkill(u: SessionUpdate, raw: Record<string, unknown>): string | u
|
|
|
323
603
|
return undefined;
|
|
324
604
|
}
|
|
325
605
|
|
|
326
|
-
function detectMcp(
|
|
327
|
-
u: SessionUpdate,
|
|
328
|
-
raw: Record<string, unknown>,
|
|
329
|
-
kind: string,
|
|
330
|
-
): { server?: string; method: string } | undefined {
|
|
331
|
-
const name = mcpToolName(u, raw);
|
|
332
|
-
if (!name) return undefined;
|
|
333
|
-
for (const re of MCP_NS) {
|
|
334
|
-
const m = re.exec(name);
|
|
335
|
-
if (m) return { server: m[1]!, method: m[2]! };
|
|
336
|
-
}
|
|
337
|
-
if (!BUILTIN_TOOLS.has(name.toLowerCase()) && !FILE_KINDS.has(kind)) {
|
|
338
|
-
return { method: name };
|
|
339
|
-
}
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function mcpToolName(u: SessionUpdate, raw: Record<string, unknown>): string {
|
|
344
|
-
const explicit = strOf(raw.tool_name) || strOf(raw.toolName) || strOf(raw.name) || strOf(raw.tool);
|
|
345
|
-
if (explicit) return explicit;
|
|
346
|
-
const t = (u.title || "").trim();
|
|
347
|
-
return /^[@a-z0-9._/-]+$/i.test(t) && !t.includes(":") ? t : "";
|
|
348
|
-
}
|
|
349
|
-
|
|
350
606
|
function capitalize(s: string): string {
|
|
351
607
|
return s.length ? s[0]!.toUpperCase() + s.slice(1) : s;
|
|
352
608
|
}
|
|
353
609
|
|
|
354
610
|
function strOf(v: unknown): string {
|
|
355
611
|
return typeof v === "string" ? v : "";
|
|
356
|
-
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function numStr(v: unknown): string {
|
|
615
|
+
return typeof v === "number" && Number.isFinite(v) ? String(v) : "";
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Re-export for tests / step-from-tool helpers.
|
|
619
|
+
export { extractToolName, resolveToolIdentity };
|