@grinev/opencode-telegram-bot 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -0
- package/README.md +26 -15
- package/dist/bot/commands/definitions.js +7 -4
- package/dist/bot/commands/help.js +7 -1
- package/dist/bot/commands/new.js +4 -0
- package/dist/bot/commands/projects.js +18 -9
- package/dist/bot/commands/rename.js +49 -1
- package/dist/bot/commands/sessions.js +15 -2
- package/dist/bot/commands/stop.js +3 -5
- package/dist/bot/handlers/agent.js +12 -1
- package/dist/bot/handlers/context.js +15 -25
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +15 -2
- package/dist/bot/handlers/permission.js +81 -12
- package/dist/bot/handlers/question.js +97 -9
- package/dist/bot/handlers/variant.js +12 -1
- package/dist/bot/index.js +92 -16
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/config.js +31 -0
- package/dist/i18n/en.js +23 -4
- package/dist/i18n/ru.js +23 -4
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/guard.js +87 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/permission/manager.js +60 -38
- package/dist/pinned/manager.js +7 -5
- package/dist/question/manager.js +33 -0
- package/dist/rename/manager.js +3 -0
- package/dist/settings/manager.js +6 -1
- package/dist/summary/aggregator.js +87 -6
- package/dist/summary/formatter.js +91 -15
- package/dist/summary/tool-message-batcher.js +182 -0
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { interactionManager } from "./manager.js";
|
|
2
|
+
function normalizeIncomingCommand(text) {
|
|
3
|
+
const trimmed = text.trim();
|
|
4
|
+
if (!trimmed.startsWith("/")) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const token = trimmed.split(/\s+/)[0];
|
|
8
|
+
const withoutMention = token.split("@")[0].toLowerCase();
|
|
9
|
+
if (withoutMention.length <= 1) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return withoutMention;
|
|
13
|
+
}
|
|
14
|
+
function classifyIncomingInput(ctx) {
|
|
15
|
+
if (ctx.callbackQuery?.data) {
|
|
16
|
+
return { inputType: "callback" };
|
|
17
|
+
}
|
|
18
|
+
const text = ctx.message?.text;
|
|
19
|
+
if (typeof text === "string") {
|
|
20
|
+
const command = normalizeIncomingCommand(text);
|
|
21
|
+
if (command) {
|
|
22
|
+
return { inputType: "command", command };
|
|
23
|
+
}
|
|
24
|
+
return { inputType: "text" };
|
|
25
|
+
}
|
|
26
|
+
return { inputType: "other" };
|
|
27
|
+
}
|
|
28
|
+
function getExpectedInputBlockReason(expectedInput) {
|
|
29
|
+
switch (expectedInput) {
|
|
30
|
+
case "callback":
|
|
31
|
+
return "expected_callback";
|
|
32
|
+
case "command":
|
|
33
|
+
return "expected_command";
|
|
34
|
+
case "text":
|
|
35
|
+
case "mixed":
|
|
36
|
+
return "expected_text";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function createAllowDecision(inputType, state, command) {
|
|
40
|
+
return {
|
|
41
|
+
allow: true,
|
|
42
|
+
inputType,
|
|
43
|
+
state,
|
|
44
|
+
command,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function createBlockDecision(inputType, state, reason, command) {
|
|
48
|
+
return {
|
|
49
|
+
allow: false,
|
|
50
|
+
inputType,
|
|
51
|
+
state,
|
|
52
|
+
reason,
|
|
53
|
+
command,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isAllowedRenameCancelCallback(ctx, state) {
|
|
57
|
+
return (state.kind === "rename" &&
|
|
58
|
+
state.expectedInput === "text" &&
|
|
59
|
+
ctx.callbackQuery?.data === "rename:cancel");
|
|
60
|
+
}
|
|
61
|
+
export function resolveInteractionGuardDecision(ctx) {
|
|
62
|
+
const state = interactionManager.getSnapshot();
|
|
63
|
+
const { inputType, command } = classifyIncomingInput(ctx);
|
|
64
|
+
if (!state) {
|
|
65
|
+
return createAllowDecision(inputType, null, command);
|
|
66
|
+
}
|
|
67
|
+
if (interactionManager.isExpired()) {
|
|
68
|
+
interactionManager.clear("expired");
|
|
69
|
+
return createBlockDecision(inputType, state, "expired", command);
|
|
70
|
+
}
|
|
71
|
+
if (inputType === "command") {
|
|
72
|
+
if (command && state.allowedCommands.includes(command)) {
|
|
73
|
+
return createAllowDecision(inputType, state, command);
|
|
74
|
+
}
|
|
75
|
+
return createBlockDecision(inputType, state, "command_not_allowed", command);
|
|
76
|
+
}
|
|
77
|
+
if (state.expectedInput === "mixed") {
|
|
78
|
+
return createAllowDecision(inputType, state, command);
|
|
79
|
+
}
|
|
80
|
+
if (inputType === "callback" && isAllowedRenameCancelCallback(ctx, state)) {
|
|
81
|
+
return createAllowDecision(inputType, state, command);
|
|
82
|
+
}
|
|
83
|
+
if (state.expectedInput === inputType) {
|
|
84
|
+
return createAllowDecision(inputType, state, command);
|
|
85
|
+
}
|
|
86
|
+
return createBlockDecision(inputType, state, getExpectedInputBlockReason(state.expectedInput), command);
|
|
87
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/stop"];
|
|
3
|
+
function normalizeCommand(command) {
|
|
4
|
+
const trimmed = command.trim().toLowerCase();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
9
|
+
const withoutMention = withSlash.split("@")[0];
|
|
10
|
+
if (withoutMention.length <= 1) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return withoutMention;
|
|
14
|
+
}
|
|
15
|
+
function normalizeAllowedCommands(commands) {
|
|
16
|
+
if (commands === undefined) {
|
|
17
|
+
return [...DEFAULT_ALLOWED_INTERACTION_COMMANDS];
|
|
18
|
+
}
|
|
19
|
+
const normalized = new Set();
|
|
20
|
+
for (const command of commands) {
|
|
21
|
+
const value = normalizeCommand(command);
|
|
22
|
+
if (value) {
|
|
23
|
+
normalized.add(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return Array.from(normalized);
|
|
27
|
+
}
|
|
28
|
+
function cloneState(state) {
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
allowedCommands: [...state.allowedCommands],
|
|
32
|
+
metadata: { ...state.metadata },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
class InteractionManager {
|
|
36
|
+
state = null;
|
|
37
|
+
start(options) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
let expiresAt = null;
|
|
40
|
+
if (this.state) {
|
|
41
|
+
this.clear("state_replaced");
|
|
42
|
+
}
|
|
43
|
+
if (typeof options.expiresInMs === "number") {
|
|
44
|
+
expiresAt = now + options.expiresInMs;
|
|
45
|
+
}
|
|
46
|
+
const nextState = {
|
|
47
|
+
kind: options.kind,
|
|
48
|
+
expectedInput: options.expectedInput,
|
|
49
|
+
allowedCommands: normalizeAllowedCommands(options.allowedCommands),
|
|
50
|
+
metadata: options.metadata ? { ...options.metadata } : {},
|
|
51
|
+
createdAt: now,
|
|
52
|
+
expiresAt,
|
|
53
|
+
};
|
|
54
|
+
this.state = nextState;
|
|
55
|
+
logger.info(`[InteractionManager] Started interaction: kind=${nextState.kind}, expectedInput=${nextState.expectedInput}, allowedCommands=${nextState.allowedCommands.join(",") || "none"}`);
|
|
56
|
+
return cloneState(nextState);
|
|
57
|
+
}
|
|
58
|
+
get() {
|
|
59
|
+
if (!this.state) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return cloneState(this.state);
|
|
63
|
+
}
|
|
64
|
+
getSnapshot() {
|
|
65
|
+
return this.get();
|
|
66
|
+
}
|
|
67
|
+
isActive() {
|
|
68
|
+
return this.state !== null;
|
|
69
|
+
}
|
|
70
|
+
isExpired(referenceTimeMs = Date.now()) {
|
|
71
|
+
if (!this.state || this.state.expiresAt === null) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return referenceTimeMs >= this.state.expiresAt;
|
|
75
|
+
}
|
|
76
|
+
transition(options) {
|
|
77
|
+
if (!this.state) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
this.state = {
|
|
82
|
+
...this.state,
|
|
83
|
+
kind: options.kind ?? this.state.kind,
|
|
84
|
+
expectedInput: options.expectedInput ?? this.state.expectedInput,
|
|
85
|
+
allowedCommands: options.allowedCommands !== undefined
|
|
86
|
+
? normalizeAllowedCommands(options.allowedCommands)
|
|
87
|
+
: [...this.state.allowedCommands],
|
|
88
|
+
metadata: options.metadata ? { ...options.metadata } : { ...this.state.metadata },
|
|
89
|
+
expiresAt: options.expiresInMs === undefined
|
|
90
|
+
? this.state.expiresAt
|
|
91
|
+
: options.expiresInMs === null
|
|
92
|
+
? null
|
|
93
|
+
: now + options.expiresInMs,
|
|
94
|
+
};
|
|
95
|
+
logger.debug(`[InteractionManager] Transitioned interaction: kind=${this.state.kind}, expectedInput=${this.state.expectedInput}, allowedCommands=${this.state.allowedCommands.join(",") || "none"}`);
|
|
96
|
+
return cloneState(this.state);
|
|
97
|
+
}
|
|
98
|
+
clear(reason = "manual") {
|
|
99
|
+
if (!this.state) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
logger.info(`[InteractionManager] Cleared interaction: reason=${reason}, kind=${this.state.kind}, expectedInput=${this.state.expectedInput}`);
|
|
103
|
+
this.state = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export const interactionManager = new InteractionManager();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,77 +1,99 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.js";
|
|
2
2
|
class PermissionManager {
|
|
3
3
|
state = {
|
|
4
|
-
|
|
5
|
-
messageId: null,
|
|
6
|
-
isActive: false,
|
|
4
|
+
requestsByMessageId: new Map(),
|
|
7
5
|
};
|
|
8
6
|
/**
|
|
9
|
-
*
|
|
7
|
+
* Register a new permission request message
|
|
10
8
|
*/
|
|
11
|
-
startPermission(request) {
|
|
12
|
-
logger.debug(`[PermissionManager] startPermission: id=${request.id}, permission=${request.permission}`);
|
|
13
|
-
if (this.state.
|
|
14
|
-
logger.warn(
|
|
15
|
-
this.clear();
|
|
9
|
+
startPermission(request, messageId) {
|
|
10
|
+
logger.debug(`[PermissionManager] startPermission: id=${request.id}, permission=${request.permission}, messageId=${messageId}`);
|
|
11
|
+
if (this.state.requestsByMessageId.has(messageId)) {
|
|
12
|
+
logger.warn(`[PermissionManager] Message ID already tracked, replacing: ${messageId}`);
|
|
16
13
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
request,
|
|
20
|
-
messageId: null,
|
|
21
|
-
isActive: true,
|
|
22
|
-
};
|
|
14
|
+
this.state.requestsByMessageId.set(messageId, request);
|
|
15
|
+
logger.info(`[PermissionManager] New permission request: type=${request.permission}, patterns=${request.patterns.join(", ")}, pending=${this.state.requestsByMessageId.size}`);
|
|
23
16
|
}
|
|
24
17
|
/**
|
|
25
|
-
* Get
|
|
18
|
+
* Get permission request by Telegram message ID
|
|
26
19
|
*/
|
|
27
|
-
getRequest() {
|
|
28
|
-
|
|
20
|
+
getRequest(messageId) {
|
|
21
|
+
if (messageId === null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return this.state.requestsByMessageId.get(messageId) ?? null;
|
|
29
25
|
}
|
|
30
26
|
/**
|
|
31
|
-
* Get request ID for API reply
|
|
27
|
+
* Get request ID for API reply by Telegram message ID
|
|
32
28
|
*/
|
|
33
|
-
getRequestID() {
|
|
34
|
-
return this.
|
|
29
|
+
getRequestID(messageId) {
|
|
30
|
+
return this.getRequest(messageId)?.id ?? null;
|
|
35
31
|
}
|
|
36
32
|
/**
|
|
37
|
-
* Get permission type (bash, edit, etc.)
|
|
33
|
+
* Get permission type (bash, edit, etc.) by message ID
|
|
38
34
|
*/
|
|
39
|
-
getPermissionType() {
|
|
40
|
-
return this.
|
|
35
|
+
getPermissionType(messageId) {
|
|
36
|
+
return this.getRequest(messageId)?.permission ?? null;
|
|
41
37
|
}
|
|
42
38
|
/**
|
|
43
|
-
* Get patterns (commands/files)
|
|
39
|
+
* Get patterns (commands/files) by message ID
|
|
44
40
|
*/
|
|
45
|
-
getPatterns() {
|
|
46
|
-
return this.
|
|
41
|
+
getPatterns(messageId) {
|
|
42
|
+
return this.getRequest(messageId)?.patterns ?? [];
|
|
47
43
|
}
|
|
48
44
|
/**
|
|
49
|
-
*
|
|
45
|
+
* Check if callback message ID belongs to active permission request
|
|
50
46
|
*/
|
|
51
|
-
|
|
52
|
-
this.state.messageId
|
|
47
|
+
isActiveMessage(messageId) {
|
|
48
|
+
return messageId !== null && this.state.requestsByMessageId.has(messageId);
|
|
53
49
|
}
|
|
54
50
|
/**
|
|
55
|
-
* Get Telegram message ID
|
|
51
|
+
* Get latest Telegram message ID
|
|
56
52
|
*/
|
|
57
53
|
getMessageId() {
|
|
58
|
-
|
|
54
|
+
const messageIds = this.getMessageIds();
|
|
55
|
+
if (messageIds.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return messageIds[messageIds.length - 1];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get Telegram message IDs for all active requests
|
|
62
|
+
*/
|
|
63
|
+
getMessageIds() {
|
|
64
|
+
return Array.from(this.state.requestsByMessageId.keys());
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Remove permission request by Telegram message ID
|
|
68
|
+
*/
|
|
69
|
+
removeByMessageId(messageId) {
|
|
70
|
+
const request = this.getRequest(messageId);
|
|
71
|
+
if (!request || messageId === null) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
this.state.requestsByMessageId.delete(messageId);
|
|
75
|
+
logger.debug(`[PermissionManager] Removed permission request: id=${request.id}, messageId=${messageId}, pending=${this.state.requestsByMessageId.size}`);
|
|
76
|
+
return request;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get number of active permission requests
|
|
80
|
+
*/
|
|
81
|
+
getPendingCount() {
|
|
82
|
+
return this.state.requestsByMessageId.size;
|
|
59
83
|
}
|
|
60
84
|
/**
|
|
61
|
-
* Check if
|
|
85
|
+
* Check if there are active permission requests
|
|
62
86
|
*/
|
|
63
87
|
isActive() {
|
|
64
|
-
return this.state.
|
|
88
|
+
return this.state.requestsByMessageId.size > 0;
|
|
65
89
|
}
|
|
66
90
|
/**
|
|
67
91
|
* Clear state after reply
|
|
68
92
|
*/
|
|
69
93
|
clear() {
|
|
70
|
-
logger.debug(
|
|
94
|
+
logger.debug(`[PermissionManager] Clearing permission state: pending=${this.state.requestsByMessageId.size}`);
|
|
71
95
|
this.state = {
|
|
72
|
-
|
|
73
|
-
messageId: null,
|
|
74
|
-
isActive: false,
|
|
96
|
+
requestsByMessageId: new Map(),
|
|
75
97
|
};
|
|
76
98
|
}
|
|
77
99
|
}
|
package/dist/pinned/manager.js
CHANGED
|
@@ -266,7 +266,7 @@ class PinnedMessageManager {
|
|
|
266
266
|
logger.debug(`[PinnedManager] loadDiffsFromMessages: ${messagesData.length} messages`);
|
|
267
267
|
const filesMap = new Map();
|
|
268
268
|
let toolCount = 0;
|
|
269
|
-
let
|
|
269
|
+
let fileToolCount = 0;
|
|
270
270
|
for (const { parts } of messagesData) {
|
|
271
271
|
for (const part of parts) {
|
|
272
272
|
if (part.type !== "tool")
|
|
@@ -275,10 +275,12 @@ class PinnedMessageManager {
|
|
|
275
275
|
const toolPart = part;
|
|
276
276
|
if (toolPart.state.status !== "completed")
|
|
277
277
|
continue;
|
|
278
|
-
if (toolPart.tool === "edit" ||
|
|
279
|
-
|
|
278
|
+
if (toolPart.tool === "edit" ||
|
|
279
|
+
toolPart.tool === "write" ||
|
|
280
|
+
toolPart.tool === "apply_patch") {
|
|
281
|
+
fileToolCount++;
|
|
280
282
|
}
|
|
281
|
-
if (toolPart.tool === "edit" &&
|
|
283
|
+
if ((toolPart.tool === "edit" || toolPart.tool === "apply_patch") &&
|
|
282
284
|
toolPart.state.metadata &&
|
|
283
285
|
"filediff" in toolPart.state.metadata) {
|
|
284
286
|
const filediff = toolPart.state.metadata.filediff;
|
|
@@ -318,7 +320,7 @@ class PinnedMessageManager {
|
|
|
318
320
|
}
|
|
319
321
|
}
|
|
320
322
|
}
|
|
321
|
-
logger.debug(`[PinnedManager] loadDiffsFromMessages: found ${toolCount} tool parts, ${
|
|
323
|
+
logger.debug(`[PinnedManager] loadDiffsFromMessages: found ${toolCount} tool parts, ${fileToolCount} file tools`);
|
|
322
324
|
if (filesMap.size > 0) {
|
|
323
325
|
this.state.changedFiles = Array.from(filesMap.values());
|
|
324
326
|
logger.info(`[PinnedManager] Loaded ${this.state.changedFiles.length} file diffs from messages`);
|
package/dist/question/manager.js
CHANGED
|
@@ -5,6 +5,8 @@ class QuestionManager {
|
|
|
5
5
|
currentIndex: 0,
|
|
6
6
|
selectedOptions: new Map(),
|
|
7
7
|
customAnswers: new Map(),
|
|
8
|
+
customInputQuestionIndex: null,
|
|
9
|
+
activeMessageId: null,
|
|
8
10
|
messageIds: [],
|
|
9
11
|
isActive: false,
|
|
10
12
|
requestID: null,
|
|
@@ -22,6 +24,8 @@ class QuestionManager {
|
|
|
22
24
|
currentIndex: 0,
|
|
23
25
|
selectedOptions: new Map(),
|
|
24
26
|
customAnswers: new Map(),
|
|
27
|
+
customInputQuestionIndex: null,
|
|
28
|
+
activeMessageId: null,
|
|
25
29
|
messageIds: [],
|
|
26
30
|
isActive: true,
|
|
27
31
|
requestID,
|
|
@@ -87,6 +91,8 @@ class QuestionManager {
|
|
|
87
91
|
}
|
|
88
92
|
nextQuestion() {
|
|
89
93
|
this.state.currentIndex++;
|
|
94
|
+
this.state.customInputQuestionIndex = null;
|
|
95
|
+
this.state.activeMessageId = null;
|
|
90
96
|
logger.debug(`[QuestionManager] Moving to next question: ${this.state.currentIndex}/${this.state.questions.length}`);
|
|
91
97
|
}
|
|
92
98
|
hasNextQuestion() {
|
|
@@ -101,6 +107,29 @@ class QuestionManager {
|
|
|
101
107
|
addMessageId(messageId) {
|
|
102
108
|
this.state.messageIds.push(messageId);
|
|
103
109
|
}
|
|
110
|
+
setActiveMessageId(messageId) {
|
|
111
|
+
this.state.activeMessageId = messageId;
|
|
112
|
+
}
|
|
113
|
+
getActiveMessageId() {
|
|
114
|
+
return this.state.activeMessageId;
|
|
115
|
+
}
|
|
116
|
+
isActiveMessage(messageId) {
|
|
117
|
+
return (this.state.isActive &&
|
|
118
|
+
this.state.activeMessageId !== null &&
|
|
119
|
+
messageId === this.state.activeMessageId);
|
|
120
|
+
}
|
|
121
|
+
startCustomInput(questionIndex) {
|
|
122
|
+
if (!this.state.isActive || !this.state.questions[questionIndex]) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.state.customInputQuestionIndex = questionIndex;
|
|
126
|
+
}
|
|
127
|
+
clearCustomInput() {
|
|
128
|
+
this.state.customInputQuestionIndex = null;
|
|
129
|
+
}
|
|
130
|
+
isWaitingForCustomInput(questionIndex) {
|
|
131
|
+
return this.state.customInputQuestionIndex === questionIndex;
|
|
132
|
+
}
|
|
104
133
|
getMessageIds() {
|
|
105
134
|
return [...this.state.messageIds];
|
|
106
135
|
}
|
|
@@ -111,6 +140,8 @@ class QuestionManager {
|
|
|
111
140
|
cancel() {
|
|
112
141
|
logger.info("[QuestionManager] Poll cancelled");
|
|
113
142
|
this.state.isActive = false;
|
|
143
|
+
this.state.customInputQuestionIndex = null;
|
|
144
|
+
this.state.activeMessageId = null;
|
|
114
145
|
}
|
|
115
146
|
clear() {
|
|
116
147
|
this.state = {
|
|
@@ -118,6 +149,8 @@ class QuestionManager {
|
|
|
118
149
|
currentIndex: 0,
|
|
119
150
|
selectedOptions: new Map(),
|
|
120
151
|
customAnswers: new Map(),
|
|
152
|
+
customInputQuestionIndex: null,
|
|
153
|
+
activeMessageId: null,
|
|
121
154
|
messageIds: [],
|
|
122
155
|
isActive: false,
|
|
123
156
|
requestID: null,
|
package/dist/rename/manager.js
CHANGED
|
@@ -23,6 +23,9 @@ class RenameManager {
|
|
|
23
23
|
getMessageId() {
|
|
24
24
|
return this.state.messageId;
|
|
25
25
|
}
|
|
26
|
+
isActiveMessage(messageId) {
|
|
27
|
+
return (this.state.isWaiting && this.state.messageId !== null && this.state.messageId === messageId);
|
|
28
|
+
}
|
|
26
29
|
isWaitingForName() {
|
|
27
30
|
return this.state.isWaiting;
|
|
28
31
|
}
|
package/dist/settings/manager.js
CHANGED
|
@@ -119,5 +119,10 @@ export function __resetSettingsForTests() {
|
|
|
119
119
|
settingsWriteQueue = Promise.resolve();
|
|
120
120
|
}
|
|
121
121
|
export async function loadSettings() {
|
|
122
|
-
|
|
122
|
+
const loadedSettings = (await readSettingsFile());
|
|
123
|
+
if ("toolMessagesIntervalSec" in loadedSettings) {
|
|
124
|
+
delete loadedSettings.toolMessagesIntervalSec;
|
|
125
|
+
void writeSettingsFile(loadedSettings);
|
|
126
|
+
}
|
|
127
|
+
currentSettings = loadedSettings;
|
|
123
128
|
}
|
|
@@ -1,6 +1,29 @@
|
|
|
1
|
-
import { prepareCodeFile } from "./formatter.js";
|
|
1
|
+
import { normalizePathForDisplay, prepareCodeFile } from "./formatter.js";
|
|
2
2
|
import { logger } from "../utils/logger.js";
|
|
3
3
|
import { getCurrentProject } from "../settings/manager.js";
|
|
4
|
+
function extractFirstUpdatedFileFromTitle(title) {
|
|
5
|
+
for (const rawLine of title.split("\n")) {
|
|
6
|
+
const line = rawLine.trim();
|
|
7
|
+
if (line.length >= 3 && line[1] === " " && /[AMDURC]/.test(line[0])) {
|
|
8
|
+
return line.slice(2).trim();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
function countDiffChangesFromText(text) {
|
|
14
|
+
let additions = 0;
|
|
15
|
+
let deletions = 0;
|
|
16
|
+
for (const line of text.split("\n")) {
|
|
17
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
18
|
+
additions++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
22
|
+
deletions++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { additions, deletions };
|
|
26
|
+
}
|
|
4
27
|
class SummaryAggregator {
|
|
5
28
|
currentSessionId = null;
|
|
6
29
|
currentMessageParts = new Map();
|
|
@@ -19,6 +42,7 @@ class SummaryAggregator {
|
|
|
19
42
|
onPermissionCallback = null;
|
|
20
43
|
onSessionDiffCallback = null;
|
|
21
44
|
onFileChangeCallback = null;
|
|
45
|
+
onClearedCallback = null;
|
|
22
46
|
processedToolStates = new Set();
|
|
23
47
|
bot = null;
|
|
24
48
|
chatId = null;
|
|
@@ -61,6 +85,9 @@ class SummaryAggregator {
|
|
|
61
85
|
setOnFileChange(callback) {
|
|
62
86
|
this.onFileChangeCallback = callback;
|
|
63
87
|
}
|
|
88
|
+
setOnCleared(callback) {
|
|
89
|
+
this.onClearedCallback = callback;
|
|
90
|
+
}
|
|
64
91
|
startTypingIndicator() {
|
|
65
92
|
if (this.typingTimer) {
|
|
66
93
|
return;
|
|
@@ -145,6 +172,14 @@ class SummaryAggregator {
|
|
|
145
172
|
this.processedToolStates.clear();
|
|
146
173
|
this.messageCount = 0;
|
|
147
174
|
this.lastUpdated = 0;
|
|
175
|
+
if (this.onClearedCallback) {
|
|
176
|
+
try {
|
|
177
|
+
this.onClearedCallback();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logger.error("[Aggregator] Error in clear callback:", err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
148
183
|
}
|
|
149
184
|
handleMessageUpdated(event) {
|
|
150
185
|
const { info } = event.properties;
|
|
@@ -160,8 +195,11 @@ class SummaryAggregator {
|
|
|
160
195
|
this.startTypingIndicator();
|
|
161
196
|
// Notify that agent started thinking
|
|
162
197
|
if (this.onThinkingCallback) {
|
|
198
|
+
const callback = this.onThinkingCallback;
|
|
163
199
|
setImmediate(() => {
|
|
164
|
-
|
|
200
|
+
if (typeof callback === "function") {
|
|
201
|
+
callback(info.sessionID);
|
|
202
|
+
}
|
|
165
203
|
});
|
|
166
204
|
}
|
|
167
205
|
}
|
|
@@ -264,6 +302,7 @@ class SummaryAggregator {
|
|
|
264
302
|
if (!this.processedToolStates.has(notifiedKey)) {
|
|
265
303
|
this.processedToolStates.add(notifiedKey);
|
|
266
304
|
const toolData = {
|
|
305
|
+
sessionId: part.sessionID,
|
|
267
306
|
messageId: messageID,
|
|
268
307
|
callId: part.callID,
|
|
269
308
|
tool: part.tool,
|
|
@@ -283,7 +322,8 @@ class SummaryAggregator {
|
|
|
283
322
|
if (!this.processedToolStates.has(fileKey)) {
|
|
284
323
|
this.processedToolStates.add(fileKey);
|
|
285
324
|
if (part.tool === "write" && input && "content" in input && "filePath" in input) {
|
|
286
|
-
const
|
|
325
|
+
const filePath = normalizePathForDisplay(input.filePath);
|
|
326
|
+
const fileData = prepareCodeFile(input.content, filePath, "write");
|
|
287
327
|
if (fileData && this.onToolFileCallback) {
|
|
288
328
|
logger.debug(`[Aggregator] Sending write file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
|
|
289
329
|
this.onToolFileCallback(fileData);
|
|
@@ -292,7 +332,7 @@ class SummaryAggregator {
|
|
|
292
332
|
if (this.onFileChangeCallback) {
|
|
293
333
|
const lines = input.content.split("\n").length;
|
|
294
334
|
this.onFileChangeCallback({
|
|
295
|
-
file:
|
|
335
|
+
file: filePath,
|
|
296
336
|
additions: lines,
|
|
297
337
|
deletions: 0,
|
|
298
338
|
});
|
|
@@ -303,12 +343,12 @@ class SummaryAggregator {
|
|
|
303
343
|
"diff" in state.metadata &&
|
|
304
344
|
"filediff" in state.metadata) {
|
|
305
345
|
const filediff = state.metadata.filediff;
|
|
306
|
-
const filePath = filediff.file;
|
|
346
|
+
const filePath = filediff.file ? normalizePathForDisplay(filediff.file) : undefined;
|
|
307
347
|
const diff = state.metadata.diff;
|
|
308
348
|
if (filePath && diff) {
|
|
309
349
|
const fileData = prepareCodeFile(diff, filePath, "edit");
|
|
310
350
|
if (fileData && this.onToolFileCallback) {
|
|
311
|
-
logger.debug(`[Aggregator] Sending
|
|
351
|
+
logger.debug(`[Aggregator] Sending ${part.tool} file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
|
|
312
352
|
this.onToolFileCallback(fileData);
|
|
313
353
|
}
|
|
314
354
|
// Notify about file change for pinned message
|
|
@@ -321,6 +361,47 @@ class SummaryAggregator {
|
|
|
321
361
|
}
|
|
322
362
|
}
|
|
323
363
|
}
|
|
364
|
+
else if (part.tool === "apply_patch") {
|
|
365
|
+
const metadata = state.metadata;
|
|
366
|
+
const filePathFromInput = input && typeof input.filePath === "string"
|
|
367
|
+
? normalizePathForDisplay(input.filePath)
|
|
368
|
+
: input && typeof input.path === "string"
|
|
369
|
+
? normalizePathForDisplay(input.path)
|
|
370
|
+
: "";
|
|
371
|
+
const filePathFromTitle = title ? extractFirstUpdatedFileFromTitle(title) : "";
|
|
372
|
+
const filePath = (metadata?.filediff?.file && normalizePathForDisplay(metadata.filediff.file)) ||
|
|
373
|
+
filePathFromInput ||
|
|
374
|
+
normalizePathForDisplay(filePathFromTitle);
|
|
375
|
+
const diffText = typeof metadata?.diff === "string"
|
|
376
|
+
? metadata.diff
|
|
377
|
+
: input && typeof input.patchText === "string"
|
|
378
|
+
? input.patchText
|
|
379
|
+
: "";
|
|
380
|
+
if (filePath && diffText) {
|
|
381
|
+
const fileData = prepareCodeFile(diffText, filePath, "edit");
|
|
382
|
+
if (fileData && this.onToolFileCallback) {
|
|
383
|
+
logger.debug(`[Aggregator] Sending apply_patch file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
|
|
384
|
+
this.onToolFileCallback(fileData);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (filePath && this.onFileChangeCallback) {
|
|
388
|
+
if (metadata?.filediff) {
|
|
389
|
+
this.onFileChangeCallback({
|
|
390
|
+
file: filePath,
|
|
391
|
+
additions: metadata.filediff.additions || 0,
|
|
392
|
+
deletions: metadata.filediff.deletions || 0,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
else if (diffText) {
|
|
396
|
+
const changes = countDiffChangesFromText(diffText);
|
|
397
|
+
this.onFileChangeCallback({
|
|
398
|
+
file: filePath,
|
|
399
|
+
additions: changes.additions,
|
|
400
|
+
deletions: changes.deletions,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
324
405
|
}
|
|
325
406
|
}
|
|
326
407
|
}
|