opencode-telegram-group-topics-bot 0.11.2
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 +74 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/agent/manager.js +60 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +47 -0
- package/dist/bot/commands/abort.js +116 -0
- package/dist/bot/commands/commands.js +389 -0
- package/dist/bot/commands/constants.js +20 -0
- package/dist/bot/commands/definitions.js +25 -0
- package/dist/bot/commands/help.js +27 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +247 -0
- package/dist/bot/commands/opencode-start.js +85 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +304 -0
- package/dist/bot/commands/rename.js +173 -0
- package/dist/bot/commands/sessions.js +491 -0
- package/dist/bot/commands/start.js +67 -0
- package/dist/bot/commands/status.js +138 -0
- package/dist/bot/constants.js +49 -0
- package/dist/bot/handlers/agent.js +127 -0
- package/dist/bot/handlers/context.js +125 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +124 -0
- package/dist/bot/handlers/model.js +152 -0
- package/dist/bot/handlers/permission.js +281 -0
- package/dist/bot/handlers/prompt.js +263 -0
- package/dist/bot/handlers/question.js +285 -0
- package/dist/bot/handlers/variant.js +147 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +945 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/scope.js +222 -0
- package/dist/bot/telegram-constants.js +3 -0
- package/dist/bot/telegram-rate-limiter.js +263 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/keyboard.js +85 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
- package/dist/bot/utils/session-error-filter.js +34 -0
- package/dist/bot/utils/topic-link.js +29 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +103 -0
- package/dist/i18n/de.js +330 -0
- package/dist/i18n/en.js +330 -0
- package/dist/i18n/es.js +330 -0
- package/dist/i18n/index.js +102 -0
- package/dist/i18n/ru.js +330 -0
- package/dist/i18n/zh.js +330 -0
- package/dist/index.js +28 -0
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/constants.js +25 -0
- package/dist/interaction/guard.js +100 -0
- package/dist/interaction/manager.js +113 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +115 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/manager.js +257 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +159 -0
- package/dist/opencode/prompt-submit-error.js +101 -0
- package/dist/permission/manager.js +92 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/manager.js +405 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +186 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +64 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/runtime/process-error-handlers.js +24 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +87 -0
- package/dist/settings/manager.js +283 -0
- package/dist/stt/client.js +64 -0
- package/dist/summary/aggregator.js +625 -0
- package/dist/summary/formatter.js +417 -0
- package/dist/summary/tool-message-batcher.js +277 -0
- package/dist/topic/colors.js +8 -0
- package/dist/topic/constants.js +10 -0
- package/dist/topic/manager.js +161 -0
- package/dist/topic/title-constants.js +2 -0
- package/dist/topic/title-format.js +10 -0
- package/dist/topic/title-sync.js +17 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +175 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
function asObject(value) {
|
|
2
|
+
if (!value || typeof value !== "object") {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function readString(value) {
|
|
8
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
9
|
+
}
|
|
10
|
+
function readStatusCode(error) {
|
|
11
|
+
const root = asObject(error);
|
|
12
|
+
if (!root) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const direct = root.statusCode ?? root.status;
|
|
16
|
+
if (typeof direct === "number") {
|
|
17
|
+
return direct;
|
|
18
|
+
}
|
|
19
|
+
const data = asObject(root.data);
|
|
20
|
+
if (data) {
|
|
21
|
+
const fromData = data.statusCode ?? data.status;
|
|
22
|
+
if (typeof fromData === "number") {
|
|
23
|
+
return fromData;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const response = asObject(root.response);
|
|
27
|
+
if (response && typeof response.status === "number") {
|
|
28
|
+
return response.status;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function collectErrorTexts(error) {
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
return error.message;
|
|
35
|
+
}
|
|
36
|
+
if (typeof error === "string") {
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
39
|
+
const root = asObject(error);
|
|
40
|
+
if (!root) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
const parts = [];
|
|
44
|
+
const directMessage = readString(root.message);
|
|
45
|
+
const directError = readString(root.error);
|
|
46
|
+
if (directMessage) {
|
|
47
|
+
parts.push(directMessage);
|
|
48
|
+
}
|
|
49
|
+
if (directError) {
|
|
50
|
+
parts.push(directError);
|
|
51
|
+
}
|
|
52
|
+
const data = asObject(root.data);
|
|
53
|
+
if (data) {
|
|
54
|
+
const dataMessage = readString(data.message);
|
|
55
|
+
const responseBody = readString(data.responseBody);
|
|
56
|
+
if (dataMessage) {
|
|
57
|
+
parts.push(dataMessage);
|
|
58
|
+
}
|
|
59
|
+
if (responseBody) {
|
|
60
|
+
parts.push(responseBody);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(root.errors)) {
|
|
64
|
+
for (const item of root.errors) {
|
|
65
|
+
const itemObject = asObject(item);
|
|
66
|
+
if (!itemObject) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const itemMessage = readString(itemObject.message) ?? readString(itemObject.error);
|
|
70
|
+
if (itemMessage) {
|
|
71
|
+
parts.push(itemMessage);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (parts.length > 0) {
|
|
76
|
+
return parts.join(" ");
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return JSON.stringify(error);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return String(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function looksBusyText(text) {
|
|
86
|
+
return /(\bbusy\b|already running|currently running|still running|in progress|another run|another task|please wait)/i.test(text);
|
|
87
|
+
}
|
|
88
|
+
function looksMissingSessionText(text) {
|
|
89
|
+
return /(session.*not found|not found.*session|unknown session)/i.test(text);
|
|
90
|
+
}
|
|
91
|
+
export function classifyPromptSubmitError(error) {
|
|
92
|
+
const statusCode = readStatusCode(error);
|
|
93
|
+
const text = collectErrorTexts(error);
|
|
94
|
+
if (statusCode === 409 || looksBusyText(text)) {
|
|
95
|
+
return "busy";
|
|
96
|
+
}
|
|
97
|
+
if (statusCode === 404 || looksMissingSessionText(text)) {
|
|
98
|
+
return "session_not_found";
|
|
99
|
+
}
|
|
100
|
+
return "other";
|
|
101
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
class PermissionManager {
|
|
3
|
+
stateByScope = new Map();
|
|
4
|
+
getState(scopeKey) {
|
|
5
|
+
const state = this.stateByScope.get(scopeKey);
|
|
6
|
+
if (state) {
|
|
7
|
+
return state;
|
|
8
|
+
}
|
|
9
|
+
const next = {
|
|
10
|
+
requestsByMessageId: new Map(),
|
|
11
|
+
};
|
|
12
|
+
this.stateByScope.set(scopeKey, next);
|
|
13
|
+
return next;
|
|
14
|
+
}
|
|
15
|
+
startPermission(request, messageId, scopeKey = "global") {
|
|
16
|
+
const state = this.getState(scopeKey);
|
|
17
|
+
logger.debug(`[PermissionManager] startPermission: id=${request.id}, permission=${request.permission}, messageId=${messageId}`);
|
|
18
|
+
if (state.requestsByMessageId.has(messageId)) {
|
|
19
|
+
logger.warn(`[PermissionManager] Message ID already tracked, replacing: ${messageId}`);
|
|
20
|
+
}
|
|
21
|
+
state.requestsByMessageId.set(messageId, request);
|
|
22
|
+
logger.info(`[PermissionManager] New permission request: type=${request.permission}, patterns=${request.patterns.join(", ")}, pending=${state.requestsByMessageId.size}`);
|
|
23
|
+
}
|
|
24
|
+
getRequest(messageId, scopeKey = "global") {
|
|
25
|
+
if (messageId === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return this.getState(scopeKey).requestsByMessageId.get(messageId) ?? null;
|
|
29
|
+
}
|
|
30
|
+
getRequestByID(requestID, scopeKey = "global") {
|
|
31
|
+
const entries = Array.from(this.getState(scopeKey).requestsByMessageId.entries());
|
|
32
|
+
for (const [messageId, request] of entries) {
|
|
33
|
+
if (request.id === requestID) {
|
|
34
|
+
return { messageId, request };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
getRequestID(messageId, scopeKey = "global") {
|
|
40
|
+
return this.getRequest(messageId, scopeKey)?.id ?? null;
|
|
41
|
+
}
|
|
42
|
+
getPermissionType(messageId, scopeKey = "global") {
|
|
43
|
+
return this.getRequest(messageId, scopeKey)?.permission ?? null;
|
|
44
|
+
}
|
|
45
|
+
getPatterns(messageId, scopeKey = "global") {
|
|
46
|
+
return this.getRequest(messageId, scopeKey)?.patterns ?? [];
|
|
47
|
+
}
|
|
48
|
+
isActiveMessage(messageId, scopeKey = "global") {
|
|
49
|
+
return messageId !== null && this.getState(scopeKey).requestsByMessageId.has(messageId);
|
|
50
|
+
}
|
|
51
|
+
getMessageId(scopeKey = "global") {
|
|
52
|
+
const messageIds = this.getMessageIds(scopeKey);
|
|
53
|
+
if (messageIds.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return messageIds[messageIds.length - 1];
|
|
57
|
+
}
|
|
58
|
+
getMessageIds(scopeKey = "global") {
|
|
59
|
+
return Array.from(this.getState(scopeKey).requestsByMessageId.keys());
|
|
60
|
+
}
|
|
61
|
+
removeByMessageId(messageId, scopeKey = "global") {
|
|
62
|
+
const state = this.getState(scopeKey);
|
|
63
|
+
const request = this.getRequest(messageId, scopeKey);
|
|
64
|
+
if (!request || messageId === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
state.requestsByMessageId.delete(messageId);
|
|
68
|
+
logger.debug(`[PermissionManager] Removed permission request: id=${request.id}, messageId=${messageId}, pending=${state.requestsByMessageId.size}`);
|
|
69
|
+
return request;
|
|
70
|
+
}
|
|
71
|
+
removeByRequestID(requestID, scopeKey = "global") {
|
|
72
|
+
const match = this.getRequestByID(requestID, scopeKey);
|
|
73
|
+
if (!match) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return this.removeByMessageId(match.messageId, scopeKey);
|
|
77
|
+
}
|
|
78
|
+
getPendingCount(scopeKey = "global") {
|
|
79
|
+
return this.getState(scopeKey).requestsByMessageId.size;
|
|
80
|
+
}
|
|
81
|
+
isActive(scopeKey = "global") {
|
|
82
|
+
return this.getState(scopeKey).requestsByMessageId.size > 0;
|
|
83
|
+
}
|
|
84
|
+
clear(scopeKey = "global") {
|
|
85
|
+
const state = this.getState(scopeKey);
|
|
86
|
+
logger.debug(`[PermissionManager] Clearing permission state: pending=${state.requestsByMessageId.size}`);
|
|
87
|
+
this.stateByScope.set(scopeKey, {
|
|
88
|
+
requestsByMessageId: new Map(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export const permissionManager = new PermissionManager();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
import { opencodeClient } from "../opencode/client.js";
|
|
3
|
+
import { getScopedPinnedMessageId, setScopedPinnedMessageId, clearScopedPinnedMessageId, getCurrentProject, } from "../settings/manager.js";
|
|
4
|
+
import { getStoredModel } from "../model/manager.js";
|
|
5
|
+
import { t } from "../i18n/index.js";
|
|
6
|
+
import { getThreadIdFromScopeKey, getThreadSendOptions } from "../bot/scope.js";
|
|
7
|
+
class PinnedMessageManager {
|
|
8
|
+
contexts = new Map();
|
|
9
|
+
onKeyboardUpdateCallback;
|
|
10
|
+
createDefaultState(scopeKey) {
|
|
11
|
+
return {
|
|
12
|
+
scopeKey,
|
|
13
|
+
messageId: null,
|
|
14
|
+
chatId: null,
|
|
15
|
+
threadId: null,
|
|
16
|
+
sessionId: null,
|
|
17
|
+
sessionTitle: t("pinned.default_session_title"),
|
|
18
|
+
projectName: "",
|
|
19
|
+
tokensUsed: 0,
|
|
20
|
+
tokensLimit: 0,
|
|
21
|
+
lastUpdated: 0,
|
|
22
|
+
changedFiles: [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
getContext(scopeKey) {
|
|
26
|
+
const existing = this.contexts.get(scopeKey);
|
|
27
|
+
if (existing) {
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
const savedId = getScopedPinnedMessageId(scopeKey);
|
|
31
|
+
const context = {
|
|
32
|
+
api: null,
|
|
33
|
+
chatId: null,
|
|
34
|
+
state: {
|
|
35
|
+
...this.createDefaultState(scopeKey),
|
|
36
|
+
messageId: savedId ?? null,
|
|
37
|
+
},
|
|
38
|
+
contextLimit: null,
|
|
39
|
+
debounceTimer: null,
|
|
40
|
+
};
|
|
41
|
+
this.contexts.set(scopeKey, context);
|
|
42
|
+
return context;
|
|
43
|
+
}
|
|
44
|
+
initialize(api, chatId, scopeKey = "global", threadId = null) {
|
|
45
|
+
const context = this.getContext(scopeKey);
|
|
46
|
+
const scopeThreadId = getThreadIdFromScopeKey(scopeKey);
|
|
47
|
+
const resolvedThreadId = threadId ?? scopeThreadId ?? context.state.threadId;
|
|
48
|
+
context.api = api;
|
|
49
|
+
context.chatId = chatId;
|
|
50
|
+
context.state.chatId = chatId;
|
|
51
|
+
context.state.threadId = resolvedThreadId;
|
|
52
|
+
}
|
|
53
|
+
persistPinnedId(scopeKey, messageId) {
|
|
54
|
+
setScopedPinnedMessageId(scopeKey, messageId);
|
|
55
|
+
}
|
|
56
|
+
clearPersistedPinnedId(scopeKey) {
|
|
57
|
+
clearScopedPinnedMessageId(scopeKey);
|
|
58
|
+
}
|
|
59
|
+
async onSessionChange(sessionId, sessionTitle, scopeKey = "global") {
|
|
60
|
+
const context = this.getContext(scopeKey);
|
|
61
|
+
const state = context.state;
|
|
62
|
+
state.tokensUsed = 0;
|
|
63
|
+
state.sessionId = sessionId;
|
|
64
|
+
state.sessionTitle = sessionTitle || t("pinned.default_session_title");
|
|
65
|
+
const project = getCurrentProject(scopeKey);
|
|
66
|
+
state.projectName =
|
|
67
|
+
project?.name || this.extractProjectName(project?.worktree) || t("pinned.unknown");
|
|
68
|
+
await this.fetchContextLimit(scopeKey);
|
|
69
|
+
if (this.onKeyboardUpdateCallback && state.tokensLimit > 0) {
|
|
70
|
+
this.onKeyboardUpdateCallback(state.tokensUsed, state.tokensLimit, scopeKey);
|
|
71
|
+
}
|
|
72
|
+
state.changedFiles = [];
|
|
73
|
+
await this.unpinOldMessage(scopeKey);
|
|
74
|
+
await this.createPinnedMessage(scopeKey);
|
|
75
|
+
await this.loadDiffsFromApi(sessionId, scopeKey);
|
|
76
|
+
}
|
|
77
|
+
async onSessionTitleUpdate(newTitle, scopeKey = "global") {
|
|
78
|
+
const state = this.getContext(scopeKey).state;
|
|
79
|
+
if (state.sessionTitle !== newTitle && newTitle) {
|
|
80
|
+
state.sessionTitle = newTitle;
|
|
81
|
+
await this.updatePinnedMessage(scopeKey);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async loadContextFromHistory(sessionId, directory, scopeKey = "global") {
|
|
85
|
+
const context = this.getContext(scopeKey);
|
|
86
|
+
try {
|
|
87
|
+
const { data: messagesData, error } = await opencodeClient.session.messages({
|
|
88
|
+
sessionID: sessionId,
|
|
89
|
+
directory,
|
|
90
|
+
});
|
|
91
|
+
if (error || !messagesData) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let maxContextSize = 0;
|
|
95
|
+
messagesData.forEach(({ info }) => {
|
|
96
|
+
if (info.role !== "assistant") {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const assistantInfo = info;
|
|
100
|
+
if (assistantInfo.summary) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const contextSize = (assistantInfo.tokens?.input || 0) + (assistantInfo.tokens?.cache?.read || 0);
|
|
104
|
+
if (contextSize > maxContextSize) {
|
|
105
|
+
maxContextSize = contextSize;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
context.state.tokensUsed = maxContextSize;
|
|
109
|
+
context.state.sessionId = sessionId;
|
|
110
|
+
await this.updatePinnedMessage(scopeKey);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
logger.error("[PinnedManager] Error loading context from history:", err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async onSessionCompacted(sessionId, directory, scopeKey = "global") {
|
|
117
|
+
await this.loadContextFromHistory(sessionId, directory, scopeKey);
|
|
118
|
+
}
|
|
119
|
+
async onMessageComplete(tokens, scopeKey = "global") {
|
|
120
|
+
if (this.getContextLimit(scopeKey) === 0) {
|
|
121
|
+
await this.fetchContextLimit(scopeKey);
|
|
122
|
+
}
|
|
123
|
+
const context = this.getContext(scopeKey);
|
|
124
|
+
context.state.tokensUsed = tokens.input + tokens.cacheRead;
|
|
125
|
+
await this.refreshSessionTitle(scopeKey);
|
|
126
|
+
this.scheduleDebouncedUpdate(scopeKey, 1200);
|
|
127
|
+
}
|
|
128
|
+
setOnKeyboardUpdate(callback) {
|
|
129
|
+
this.onKeyboardUpdateCallback = callback;
|
|
130
|
+
}
|
|
131
|
+
getContextInfo(scopeKey = "global") {
|
|
132
|
+
const context = this.getContext(scopeKey);
|
|
133
|
+
const limit = context.state.tokensLimit > 0 ? context.state.tokensLimit : context.contextLimit || 0;
|
|
134
|
+
if (limit === 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return { tokensUsed: context.state.tokensUsed, tokensLimit: limit };
|
|
138
|
+
}
|
|
139
|
+
getContextLimit(scopeKey = "global") {
|
|
140
|
+
const context = this.getContext(scopeKey);
|
|
141
|
+
return context.contextLimit || context.state.tokensLimit || 0;
|
|
142
|
+
}
|
|
143
|
+
async refreshContextLimit(scopeKey = "global") {
|
|
144
|
+
await this.fetchContextLimit(scopeKey);
|
|
145
|
+
}
|
|
146
|
+
async onSessionDiff(diffs, scopeKey = "global") {
|
|
147
|
+
const context = this.getContext(scopeKey);
|
|
148
|
+
if (diffs.length === 0 && context.state.changedFiles.length > 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
context.state.changedFiles = diffs;
|
|
152
|
+
this.scheduleDebouncedUpdate(scopeKey, 1200);
|
|
153
|
+
}
|
|
154
|
+
addFileChange(change, scopeKey = "global") {
|
|
155
|
+
const context = this.getContext(scopeKey);
|
|
156
|
+
const existing = context.state.changedFiles.find((f) => f.file === change.file);
|
|
157
|
+
if (existing) {
|
|
158
|
+
existing.additions += change.additions;
|
|
159
|
+
existing.deletions += change.deletions;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
context.state.changedFiles.push(change);
|
|
163
|
+
}
|
|
164
|
+
this.scheduleDebouncedUpdate(scopeKey);
|
|
165
|
+
}
|
|
166
|
+
scheduleDebouncedUpdate(scopeKey, delayMs = 1500) {
|
|
167
|
+
const context = this.getContext(scopeKey);
|
|
168
|
+
if (context.debounceTimer) {
|
|
169
|
+
clearTimeout(context.debounceTimer);
|
|
170
|
+
}
|
|
171
|
+
context.debounceTimer = setTimeout(() => {
|
|
172
|
+
context.debounceTimer = null;
|
|
173
|
+
void this.updatePinnedMessage(scopeKey);
|
|
174
|
+
}, delayMs);
|
|
175
|
+
}
|
|
176
|
+
async loadDiffsFromApi(sessionId, scopeKey) {
|
|
177
|
+
try {
|
|
178
|
+
const project = getCurrentProject(scopeKey);
|
|
179
|
+
if (!project) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const { data, error } = await opencodeClient.session.diff({
|
|
183
|
+
sessionID: sessionId,
|
|
184
|
+
directory: project.worktree,
|
|
185
|
+
});
|
|
186
|
+
const context = this.getContext(scopeKey);
|
|
187
|
+
if (!error && data && data.length > 0) {
|
|
188
|
+
context.state.changedFiles = data.map((d) => ({
|
|
189
|
+
file: d.file,
|
|
190
|
+
additions: d.additions,
|
|
191
|
+
deletions: d.deletions,
|
|
192
|
+
}));
|
|
193
|
+
await this.updatePinnedMessage(scopeKey);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
logger.debug("[PinnedManager] Could not load diffs from API:", err);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async refreshSessionTitle(scopeKey) {
|
|
201
|
+
const context = this.getContext(scopeKey);
|
|
202
|
+
const sessionId = context.state.sessionId;
|
|
203
|
+
const project = getCurrentProject(scopeKey);
|
|
204
|
+
if (!sessionId || !project) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const { data: sessionData } = await opencodeClient.session.get({
|
|
209
|
+
sessionID: sessionId,
|
|
210
|
+
directory: project.worktree,
|
|
211
|
+
});
|
|
212
|
+
if (sessionData && sessionData.title !== context.state.sessionTitle) {
|
|
213
|
+
context.state.sessionTitle = sessionData.title;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Ignore refresh failures.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
extractProjectName(worktree) {
|
|
221
|
+
if (!worktree) {
|
|
222
|
+
return "";
|
|
223
|
+
}
|
|
224
|
+
const parts = worktree.replace(/\\/g, "/").split("/");
|
|
225
|
+
return parts[parts.length - 1] || "";
|
|
226
|
+
}
|
|
227
|
+
makeRelativePath(filePath, scopeKey) {
|
|
228
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
229
|
+
const project = getCurrentProject(scopeKey);
|
|
230
|
+
if (!project?.worktree) {
|
|
231
|
+
return normalized;
|
|
232
|
+
}
|
|
233
|
+
const worktree = project.worktree.replace(/\\/g, "/");
|
|
234
|
+
if (normalized.startsWith(worktree)) {
|
|
235
|
+
let relative = normalized.slice(worktree.length);
|
|
236
|
+
if (relative.startsWith("/")) {
|
|
237
|
+
relative = relative.slice(1);
|
|
238
|
+
}
|
|
239
|
+
return relative || normalized;
|
|
240
|
+
}
|
|
241
|
+
const segments = normalized.split("/");
|
|
242
|
+
if (segments.length <= 3) {
|
|
243
|
+
return normalized;
|
|
244
|
+
}
|
|
245
|
+
return ".../" + segments.slice(-3).join("/");
|
|
246
|
+
}
|
|
247
|
+
async fetchContextLimit(scopeKey) {
|
|
248
|
+
const context = this.getContext(scopeKey);
|
|
249
|
+
try {
|
|
250
|
+
const model = getStoredModel(scopeKey);
|
|
251
|
+
if (!model.providerID || !model.modelID) {
|
|
252
|
+
context.contextLimit = 200000;
|
|
253
|
+
context.state.tokensLimit = context.contextLimit;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const { data: providersData, error } = await opencodeClient.config.providers();
|
|
257
|
+
if (error || !providersData) {
|
|
258
|
+
context.contextLimit = 200000;
|
|
259
|
+
context.state.tokensLimit = context.contextLimit;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
for (const provider of providersData.providers) {
|
|
263
|
+
if (provider.id !== model.providerID) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const modelInfo = provider.models[model.modelID];
|
|
267
|
+
if (modelInfo?.limit?.context) {
|
|
268
|
+
context.contextLimit = modelInfo.limit.context;
|
|
269
|
+
context.state.tokensLimit = context.contextLimit;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
context.contextLimit = 200000;
|
|
274
|
+
context.state.tokensLimit = context.contextLimit;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
context.contextLimit = 200000;
|
|
278
|
+
context.state.tokensLimit = context.contextLimit;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
formatTokenCount(count) {
|
|
282
|
+
if (count >= 1000000) {
|
|
283
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
284
|
+
}
|
|
285
|
+
if (count >= 1000) {
|
|
286
|
+
return `${Math.round(count / 1000)}K`;
|
|
287
|
+
}
|
|
288
|
+
return count.toString();
|
|
289
|
+
}
|
|
290
|
+
formatMessage(scopeKey) {
|
|
291
|
+
const context = this.getContext(scopeKey);
|
|
292
|
+
const state = context.state;
|
|
293
|
+
const percentage = state.tokensLimit > 0 ? Math.round((state.tokensUsed / state.tokensLimit) * 100) : 0;
|
|
294
|
+
const currentModel = getStoredModel(scopeKey);
|
|
295
|
+
const modelName = currentModel.providerID && currentModel.modelID
|
|
296
|
+
? `${currentModel.providerID}/${currentModel.modelID}`
|
|
297
|
+
: t("pinned.unknown");
|
|
298
|
+
const lines = [
|
|
299
|
+
`${state.sessionTitle}`,
|
|
300
|
+
t("pinned.line.project", { project: state.projectName }),
|
|
301
|
+
t("pinned.line.model", { model: modelName }),
|
|
302
|
+
t("pinned.line.context", {
|
|
303
|
+
used: this.formatTokenCount(state.tokensUsed),
|
|
304
|
+
limit: this.formatTokenCount(state.tokensLimit),
|
|
305
|
+
percent: percentage,
|
|
306
|
+
}),
|
|
307
|
+
];
|
|
308
|
+
if (state.changedFiles.length > 0) {
|
|
309
|
+
const maxFiles = 10;
|
|
310
|
+
const filesToShow = state.changedFiles.slice(0, maxFiles);
|
|
311
|
+
lines.push("");
|
|
312
|
+
lines.push(t("pinned.files.title", { count: state.changedFiles.length }));
|
|
313
|
+
for (const fileChange of filesToShow) {
|
|
314
|
+
const parts = [];
|
|
315
|
+
if (fileChange.additions > 0)
|
|
316
|
+
parts.push(`+${fileChange.additions}`);
|
|
317
|
+
if (fileChange.deletions > 0)
|
|
318
|
+
parts.push(`-${fileChange.deletions}`);
|
|
319
|
+
const diff = parts.length > 0 ? ` (${parts.join(" ")})` : "";
|
|
320
|
+
lines.push(t("pinned.files.item", { path: this.makeRelativePath(fileChange.file, scopeKey), diff }));
|
|
321
|
+
}
|
|
322
|
+
if (state.changedFiles.length > maxFiles) {
|
|
323
|
+
lines.push(t("pinned.files.more", { count: state.changedFiles.length - maxFiles }));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
async createPinnedMessage(scopeKey) {
|
|
329
|
+
const context = this.getContext(scopeKey);
|
|
330
|
+
if (!context.api || !context.chatId) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const threadId = context.state.threadId ?? getThreadIdFromScopeKey(scopeKey);
|
|
334
|
+
const sent = await context.api.sendMessage(context.chatId, this.formatMessage(scopeKey), {
|
|
335
|
+
...getThreadSendOptions(threadId),
|
|
336
|
+
});
|
|
337
|
+
context.state.messageId = sent.message_id;
|
|
338
|
+
context.state.lastUpdated = Date.now();
|
|
339
|
+
this.persistPinnedId(scopeKey, sent.message_id);
|
|
340
|
+
await context.api.pinChatMessage(context.chatId, sent.message_id, {
|
|
341
|
+
disable_notification: true,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async updatePinnedMessage(scopeKey) {
|
|
345
|
+
const context = this.getContext(scopeKey);
|
|
346
|
+
if (!context.api || !context.chatId || !context.state.messageId) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
await context.api.editMessageText(context.chatId, context.state.messageId, this.formatMessage(scopeKey));
|
|
351
|
+
context.state.lastUpdated = Date.now();
|
|
352
|
+
if (this.onKeyboardUpdateCallback && context.state.tokensLimit > 0) {
|
|
353
|
+
this.onKeyboardUpdateCallback(context.state.tokensUsed, context.state.tokensLimit, scopeKey);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
if (err instanceof Error && err.message.includes("message is not modified")) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (err instanceof Error && err.message.includes("message to edit not found")) {
|
|
361
|
+
context.state.messageId = null;
|
|
362
|
+
this.clearPersistedPinnedId(scopeKey);
|
|
363
|
+
await this.createPinnedMessage(scopeKey);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
logger.error("[PinnedManager] Error updating pinned message:", err);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async unpinOldMessage(scopeKey) {
|
|
370
|
+
const context = this.getContext(scopeKey);
|
|
371
|
+
if (!context.api || !context.chatId) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
if (context.state.messageId) {
|
|
376
|
+
await context.api.unpinChatMessage(context.chatId, context.state.messageId).catch(() => { });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
context.state.messageId = null;
|
|
381
|
+
this.clearPersistedPinnedId(scopeKey);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
getState(scopeKey = "global") {
|
|
385
|
+
return { ...this.getContext(scopeKey).state };
|
|
386
|
+
}
|
|
387
|
+
isInitialized(scopeKey = "global") {
|
|
388
|
+
const context = this.contexts.get(scopeKey);
|
|
389
|
+
return Boolean(context?.api && context?.chatId);
|
|
390
|
+
}
|
|
391
|
+
async clear(scopeKey = "global") {
|
|
392
|
+
const context = this.getContext(scopeKey);
|
|
393
|
+
if (context.api && context.chatId && context.state.messageId) {
|
|
394
|
+
await context.api.unpinChatMessage(context.chatId, context.state.messageId).catch(() => { });
|
|
395
|
+
await context.api.deleteMessage(context.chatId, context.state.messageId).catch(() => { });
|
|
396
|
+
}
|
|
397
|
+
if (context.debounceTimer) {
|
|
398
|
+
clearTimeout(context.debounceTimer);
|
|
399
|
+
context.debounceTimer = null;
|
|
400
|
+
}
|
|
401
|
+
context.state = this.createDefaultState(scopeKey);
|
|
402
|
+
this.clearPersistedPinnedId(scopeKey);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
export const pinnedMessageManager = new PinnedMessageManager();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|