telecodex 0.1.2 → 0.1.4
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/README.md +104 -139
- package/dist/bot/auth.js +9 -8
- package/dist/bot/commandSupport.js +27 -14
- package/dist/bot/createBot.js +43 -0
- package/dist/bot/handlers/messageHandlers.js +6 -5
- package/dist/bot/handlers/operationalHandlers.js +84 -72
- package/dist/bot/handlers/projectHandlers.js +90 -76
- package/dist/bot/handlers/sessionConfigHandlers.js +141 -111
- package/dist/bot/inputService.js +30 -14
- package/dist/codex/configOverrides.js +50 -0
- package/dist/codex/sessionCatalog.js +66 -12
- package/dist/runtime/bootstrap.js +60 -37
- package/dist/runtime/startTelecodex.js +28 -21
- package/dist/store/fileState.js +100 -8
- package/dist/store/projects.js +3 -0
- package/dist/store/sessions.js +3 -0
- package/dist/telegram/delivery.js +41 -5
- package/dist/telegram/formatted.js +183 -0
- package/dist/telegram/messageBuffer.js +10 -0
- package/dist/telegram/splitMessage.js +1 -1
- package/package.json +5 -3
|
@@ -2,34 +2,37 @@ import { presetFromProfile } from "../../config.js";
|
|
|
2
2
|
import { generateBindingCode } from "../../runtime/bindingCodes.js";
|
|
3
3
|
import { formatSessionRuntimeStatus } from "../../runtime/sessionRuntime.js";
|
|
4
4
|
import { refreshSessionIfActiveTurnIsStale } from "../inputService.js";
|
|
5
|
-
import { contextLogFields, formatHelpText, formatPrivateStatus, formatProjectStatus, formatReasoningEffort, getProjectForContext,
|
|
5
|
+
import { contextLogFields, formatHelpText, formatPrivateStatus, formatProjectStatus, formatReasoningEffort, getProjectForContext, hasTopicContext, isPrivateChat, parseSubcommand, requireScopedSession, } from "../commandSupport.js";
|
|
6
6
|
import { formatIsoTimestamp, sessionLogFields } from "../sessionFlow.js";
|
|
7
|
+
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
7
8
|
export function registerOperationalHandlers(deps) {
|
|
8
9
|
const { bot, config, store, projects, codex, logger } = deps;
|
|
9
10
|
bot.command(["start", "help"], async (ctx) => {
|
|
10
|
-
await ctx
|
|
11
|
+
await replyNotice(ctx, formatHelpText(ctx, projects));
|
|
11
12
|
});
|
|
12
13
|
bot.command("admin", async (ctx) => {
|
|
13
14
|
if (!isPrivateChat(ctx)) {
|
|
14
|
-
await ctx
|
|
15
|
+
await replyNotice(ctx, "Use /admin in the bot private chat.");
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
const authorizedUserId = store.getAuthorizedUserId();
|
|
18
19
|
if (authorizedUserId == null) {
|
|
19
|
-
await ctx
|
|
20
|
+
await replyNotice(ctx, "Admin binding is not completed yet.");
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
const { command } = parseSubcommand(ctx.match.trim());
|
|
23
24
|
const binding = store.getBindingCodeState();
|
|
24
25
|
if (!command || command === "status") {
|
|
25
|
-
await ctx
|
|
26
|
-
"Admin status",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
await replyDocument(ctx, {
|
|
27
|
+
title: "Admin status",
|
|
28
|
+
fields: [
|
|
29
|
+
textField("authorized telegram user id", authorizedUserId),
|
|
30
|
+
textField("pending handoff", binding?.mode === "rebind"
|
|
31
|
+
? `active until ${formatIsoTimestamp(binding.expiresAt)} (${binding.maxAttempts - binding.attempts} attempts remaining)`
|
|
32
|
+
: "none"),
|
|
33
|
+
],
|
|
34
|
+
footer: "Usage: /admin | /admin rebind | /admin cancel",
|
|
35
|
+
});
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
if (command === "rebind") {
|
|
@@ -38,124 +41,133 @@ export function registerOperationalHandlers(deps) {
|
|
|
38
41
|
mode: "rebind",
|
|
39
42
|
issuedByUserId: authorizedUserId,
|
|
40
43
|
});
|
|
41
|
-
await ctx
|
|
42
|
-
"Admin handoff code created.",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"Send this code from the target Telegram account in this bot's private chat to transfer control.",
|
|
49
|
-
|
|
44
|
+
await replyDocument(ctx, {
|
|
45
|
+
title: "Admin handoff code created.",
|
|
46
|
+
fields: [
|
|
47
|
+
textField("expires at", formatIsoTimestamp(next.expiresAt)),
|
|
48
|
+
textField("max failed attempts", next.maxAttempts),
|
|
49
|
+
codeField("code", next.code),
|
|
50
|
+
],
|
|
51
|
+
footer: "Send this code from the target Telegram account in this bot's private chat to transfer control.",
|
|
52
|
+
});
|
|
50
53
|
return;
|
|
51
54
|
}
|
|
52
55
|
if (command === "cancel") {
|
|
53
56
|
if (binding?.mode !== "rebind") {
|
|
54
|
-
await ctx
|
|
57
|
+
await replyNotice(ctx, "No pending admin handoff.");
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
60
|
store.clearBindingCode();
|
|
58
|
-
await ctx
|
|
61
|
+
await replyNotice(ctx, "Cancelled the pending admin handoff.");
|
|
59
62
|
return;
|
|
60
63
|
}
|
|
61
|
-
await ctx
|
|
64
|
+
await replyUsage(ctx, "/admin | /admin rebind | /admin cancel");
|
|
62
65
|
});
|
|
63
66
|
bot.command("status", async (ctx) => {
|
|
64
67
|
if (isPrivateChat(ctx)) {
|
|
65
|
-
await ctx
|
|
68
|
+
await replyNotice(ctx, formatPrivateStatus(store, projects));
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
71
|
const project = getProjectForContext(ctx, projects);
|
|
69
72
|
if (!project) {
|
|
70
|
-
await ctx
|
|
73
|
+
await replyNotice(ctx, "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.");
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
if (!hasTopicContext(ctx)) {
|
|
74
|
-
await ctx
|
|
77
|
+
await replyNotice(ctx, formatProjectStatus(project));
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
|
-
const session =
|
|
80
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
78
81
|
if (!session)
|
|
79
82
|
return;
|
|
80
83
|
const latestSession = await refreshSessionIfActiveTurnIsStale(session, store, codex, bot, logger);
|
|
81
84
|
const queueDepth = store.getQueuedInputCount(latestSession.sessionKey);
|
|
82
85
|
const queuedPreview = store.listQueuedInputs(latestSession.sessionKey, 3);
|
|
83
86
|
const activeRun = codex.getActiveRun(latestSession.sessionKey);
|
|
84
|
-
await ctx
|
|
85
|
-
"Status",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
87
|
+
await replyDocument(ctx, {
|
|
88
|
+
title: "Status",
|
|
89
|
+
fields: [
|
|
90
|
+
codeField("project", project.name),
|
|
91
|
+
codeField("root", project.cwd),
|
|
92
|
+
codeField("thread", latestSession.codexThreadId ?? "not created"),
|
|
93
|
+
textField("state", formatSessionRuntimeStatus(latestSession.runtimeStatus)),
|
|
94
|
+
textField("state detail", latestSession.runtimeStatusDetail ?? "none"),
|
|
95
|
+
textField("state updated", formatIsoTimestamp(latestSession.runtimeStatusUpdatedAt)),
|
|
96
|
+
codeField("active turn", latestSession.activeTurnId ?? "none"),
|
|
97
|
+
textField("active run", activeRun ? formatIsoTimestamp(activeRun.startedAt) : "none"),
|
|
98
|
+
codeField("active run thread", activeRun?.threadId ?? "none"),
|
|
99
|
+
textField("active run last event", activeRun?.lastEventType ?? "none"),
|
|
100
|
+
textField("active run last update", activeRun ? formatIsoTimestamp(activeRun.lastEventAt) : "none"),
|
|
101
|
+
textField("queue", queueDepth),
|
|
102
|
+
textField("queue next", formatQueuedPreview(queuedPreview)),
|
|
103
|
+
codeField("cwd", latestSession.cwd),
|
|
104
|
+
textField("preset", presetFromProfile(latestSession)),
|
|
105
|
+
textField("sandbox", latestSession.sandboxMode),
|
|
106
|
+
textField("approval", latestSession.approvalPolicy),
|
|
107
|
+
textField("network", latestSession.networkAccessEnabled ? "on" : "off"),
|
|
108
|
+
textField("web", latestSession.webSearchMode ?? "codex-default"),
|
|
109
|
+
textField("git check", latestSession.skipGitRepoCheck ? "skip" : "enforce"),
|
|
110
|
+
textField("add dirs", latestSession.additionalDirectories.length),
|
|
111
|
+
textField("schema", latestSession.outputSchema ? "set" : "none"),
|
|
112
|
+
textField("model", latestSession.model),
|
|
113
|
+
textField("effort", formatReasoningEffort(latestSession.reasoningEffort)),
|
|
114
|
+
],
|
|
115
|
+
});
|
|
111
116
|
});
|
|
112
117
|
bot.command("queue", async (ctx) => {
|
|
113
|
-
const session =
|
|
118
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
114
119
|
if (!session)
|
|
115
120
|
return;
|
|
116
121
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
117
122
|
if (!command) {
|
|
118
123
|
const queued = store.listQueuedInputs(session.sessionKey, 5);
|
|
119
124
|
const queueDepth = store.getQueuedInputCount(session.sessionKey);
|
|
120
|
-
await ctx
|
|
121
|
-
"Queue",
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
await replyDocument(ctx, {
|
|
126
|
+
title: "Queue",
|
|
127
|
+
fields: [
|
|
128
|
+
textField("state", formatSessionRuntimeStatus(session.runtimeStatus)),
|
|
129
|
+
codeField("active turn", session.activeTurnId ?? "none"),
|
|
130
|
+
textField("queue", queueDepth),
|
|
131
|
+
],
|
|
132
|
+
sections: [
|
|
133
|
+
{
|
|
134
|
+
title: "Items",
|
|
135
|
+
lines: queued.length > 0 ? [formatQueuedItems(queued)] : ["none"],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
footer: "Usage: /queue | /queue drop <id> | /queue clear",
|
|
139
|
+
});
|
|
128
140
|
return;
|
|
129
141
|
}
|
|
130
142
|
if (command === "clear") {
|
|
131
143
|
const removed = store.clearQueuedInputs(session.sessionKey);
|
|
132
|
-
await ctx
|
|
144
|
+
await replyNotice(ctx, `Cleared the queue and removed ${removed} pending message(s).`);
|
|
133
145
|
return;
|
|
134
146
|
}
|
|
135
147
|
if (command === "drop") {
|
|
136
148
|
const id = Number(args);
|
|
137
149
|
if (!Number.isInteger(id) || id <= 0) {
|
|
138
|
-
await ctx
|
|
150
|
+
await replyUsage(ctx, "/queue drop <id>");
|
|
139
151
|
return;
|
|
140
152
|
}
|
|
141
153
|
const removed = store.removeQueuedInputForSession(session.sessionKey, id);
|
|
142
|
-
await ctx
|
|
154
|
+
await replyNotice(ctx, removed ? `Removed queued item #${id}.` : `Queued item #${id} was not found.`);
|
|
143
155
|
return;
|
|
144
156
|
}
|
|
145
|
-
await ctx
|
|
157
|
+
await replyUsage(ctx, "/queue | /queue drop <id> | /queue clear");
|
|
146
158
|
});
|
|
147
159
|
bot.command("stop", async (ctx) => {
|
|
148
|
-
const session =
|
|
160
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
149
161
|
if (!session)
|
|
150
162
|
return;
|
|
151
163
|
const latest = store.get(session.sessionKey) ?? session;
|
|
152
164
|
if (!codex.isRunning(session.sessionKey)) {
|
|
153
|
-
await ctx
|
|
165
|
+
await replyNotice(ctx, "There is no active Codex SDK turn right now.");
|
|
154
166
|
return;
|
|
155
167
|
}
|
|
156
168
|
try {
|
|
157
169
|
codex.interrupt(session.sessionKey);
|
|
158
|
-
await ctx
|
|
170
|
+
await replyNotice(ctx, "Interrupt requested for the current run. Waiting for Codex SDK to stop.");
|
|
159
171
|
}
|
|
160
172
|
catch (error) {
|
|
161
173
|
logger?.warn("interrupt turn failed", {
|
|
@@ -163,7 +175,7 @@ export function registerOperationalHandlers(deps) {
|
|
|
163
175
|
...sessionLogFields(latest),
|
|
164
176
|
error,
|
|
165
177
|
});
|
|
166
|
-
await ctx
|
|
178
|
+
await replyError(ctx, `Interrupt failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
167
179
|
}
|
|
168
180
|
});
|
|
169
181
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { contextLogFields, ensureTopicSession, formatPrivateProjectList, formatProjectStatus, formatTopicName, getProjectForContext,
|
|
2
|
+
import { contextLogFields, ensureTopicSession, formatPrivateProjectList, formatProjectStatus, formatTopicName, getProjectForContext, hasTopicContext, isPrivateChat, isSupergroupChat, parseSubcommand, postTopicReadyMessage, requireScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
|
|
3
3
|
import { formatSessionRuntimeStatus } from "../../runtime/sessionRuntime.js";
|
|
4
|
+
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
4
5
|
const PROJECT_REQUIRED_MESSAGE = "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.";
|
|
5
6
|
export function registerProjectHandlers(deps) {
|
|
6
7
|
const { bot, config, store, projects, logger } = deps;
|
|
@@ -8,24 +9,24 @@ export function registerProjectHandlers(deps) {
|
|
|
8
9
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
9
10
|
if (isPrivateChat(ctx)) {
|
|
10
11
|
if (!command || command === "list" || command === "status") {
|
|
11
|
-
await ctx
|
|
12
|
+
await replyNotice(ctx, formatPrivateProjectList(projects));
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
|
-
await ctx
|
|
15
|
+
await replyNotice(ctx, "Use /project bind inside a supergroup with topics enabled. Private chat is only for admin entry points.");
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
if (!isSupergroupChat(ctx)) {
|
|
18
|
-
await ctx
|
|
19
|
+
await replyNotice(ctx, "Use telecodex inside a supergroup with forum topics enabled.");
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
if (!command || command === "status") {
|
|
22
23
|
const project = getProjectForContext(ctx, projects);
|
|
23
|
-
await ctx
|
|
24
|
+
await replyNotice(ctx, project ? formatProjectStatus(project) : PROJECT_REQUIRED_MESSAGE);
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
if (command === "bind") {
|
|
27
28
|
if (!args) {
|
|
28
|
-
await ctx
|
|
29
|
+
await replyUsage(ctx, "/project bind <absolute-path>");
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
try {
|
|
@@ -39,19 +40,21 @@ export function registerProjectHandlers(deps) {
|
|
|
39
40
|
project: project.name,
|
|
40
41
|
cwd: project.cwd,
|
|
41
42
|
});
|
|
42
|
-
const session =
|
|
43
|
+
const session = await requireScopedSession(ctx, store, projects, config, { requireTopic: false });
|
|
43
44
|
if (session && session.cwd !== project.cwd) {
|
|
44
45
|
store.setCwd(session.sessionKey, project.cwd);
|
|
45
46
|
}
|
|
46
|
-
await ctx
|
|
47
|
-
"Project binding updated.",
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
await replyDocument(ctx, {
|
|
48
|
+
title: "Project binding updated.",
|
|
49
|
+
fields: [
|
|
50
|
+
codeField("project", project.name),
|
|
51
|
+
codeField("root", project.cwd),
|
|
52
|
+
],
|
|
53
|
+
footer: "This supergroup now represents one project, and each topic maps to one Codex thread.",
|
|
54
|
+
});
|
|
52
55
|
}
|
|
53
56
|
catch (error) {
|
|
54
|
-
await ctx
|
|
57
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
55
58
|
}
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
@@ -60,36 +63,36 @@ export function registerProjectHandlers(deps) {
|
|
|
60
63
|
...contextLogFields(ctx),
|
|
61
64
|
});
|
|
62
65
|
projects.remove(String(ctx.chat.id));
|
|
63
|
-
await ctx
|
|
66
|
+
await replyNotice(ctx, "Removed the project binding for this supergroup.");
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
66
|
-
await ctx
|
|
69
|
+
await replyUsage(ctx, ["/project", "/project bind <absolute-path>", "/project unbind"]);
|
|
67
70
|
});
|
|
68
71
|
bot.command("thread", async (ctx) => {
|
|
69
72
|
if (isPrivateChat(ctx)) {
|
|
70
|
-
await ctx
|
|
73
|
+
await replyNotice(ctx, "The thread command is only available inside project supergroups.");
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
74
77
|
if (!command) {
|
|
75
78
|
if (hasTopicContext(ctx)) {
|
|
76
|
-
const session =
|
|
79
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
77
80
|
if (!session)
|
|
78
81
|
return;
|
|
79
|
-
await ctx
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"/thread new <topic-name>",
|
|
89
|
-
|
|
82
|
+
await replyDocument(ctx, {
|
|
83
|
+
title: "Current thread",
|
|
84
|
+
fields: [
|
|
85
|
+
codeField("thread", session.codexThreadId ?? "not created"),
|
|
86
|
+
textField("state", formatSessionRuntimeStatus(session.runtimeStatus)),
|
|
87
|
+
textField("state detail", session.runtimeStatusDetail ?? "none"),
|
|
88
|
+
textField("queue", store.getQueuedInputCount(session.sessionKey)),
|
|
89
|
+
codeField("cwd", session.cwd),
|
|
90
|
+
],
|
|
91
|
+
footer: ["Manage threads in this project:", "/thread list", "/thread resume <threadId>", "/thread new <topic-name>"],
|
|
92
|
+
});
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
92
|
-
await ctx
|
|
95
|
+
await replyUsage(ctx, ["/thread list", "/thread resume <threadId>", "/thread new <topic-name>"]);
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
98
|
if (command === "list") {
|
|
@@ -98,7 +101,7 @@ export function registerProjectHandlers(deps) {
|
|
|
98
101
|
}
|
|
99
102
|
if (command === "resume") {
|
|
100
103
|
if (!args) {
|
|
101
|
-
await ctx
|
|
104
|
+
await replyUsage(ctx, "/thread resume <threadId>");
|
|
102
105
|
return;
|
|
103
106
|
}
|
|
104
107
|
await resumeThreadIntoTopic(ctx, deps, args);
|
|
@@ -108,7 +111,7 @@ export function registerProjectHandlers(deps) {
|
|
|
108
111
|
await createFreshThreadTopic(ctx, deps, args);
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
|
-
await ctx
|
|
114
|
+
await replyUsage(ctx, ["/thread list", "/thread resume <threadId>", "/thread new <topic-name>"]);
|
|
112
115
|
});
|
|
113
116
|
bot.on(["message:forum_topic_created", "message:forum_topic_edited"], async (ctx) => {
|
|
114
117
|
const threadId = ctx.message.message_thread_id;
|
|
@@ -128,7 +131,7 @@ async function resumeThreadIntoTopic(ctx, deps, threadId) {
|
|
|
128
131
|
const { bot, config, store, projects, logger, threadCatalog } = deps;
|
|
129
132
|
const project = getProjectForContext(ctx, projects);
|
|
130
133
|
if (!project) {
|
|
131
|
-
await ctx
|
|
134
|
+
await replyNotice(ctx, PROJECT_REQUIRED_MESSAGE);
|
|
132
135
|
return;
|
|
133
136
|
}
|
|
134
137
|
const thread = await threadCatalog.findProjectThreadById({
|
|
@@ -136,12 +139,14 @@ async function resumeThreadIntoTopic(ctx, deps, threadId) {
|
|
|
136
139
|
threadId,
|
|
137
140
|
});
|
|
138
141
|
if (!thread) {
|
|
139
|
-
await ctx
|
|
140
|
-
"Could not find a saved Codex thread with that id under this project.",
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
await replyDocument(ctx, {
|
|
143
|
+
title: "Could not find a saved Codex thread with that id under this project.",
|
|
144
|
+
fields: [
|
|
145
|
+
codeField("project root", project.cwd),
|
|
146
|
+
codeField("thread", threadId),
|
|
147
|
+
],
|
|
148
|
+
footer: "Run /thread list to inspect the saved project threads first.",
|
|
149
|
+
});
|
|
145
150
|
return;
|
|
146
151
|
}
|
|
147
152
|
const topicName = formatTopicName(thread.preview, `Resumed ${thread.id.slice(0, 8)}`);
|
|
@@ -161,14 +166,16 @@ async function resumeThreadIntoTopic(ctx, deps, threadId) {
|
|
|
161
166
|
threadId: thread.id,
|
|
162
167
|
topicName: forumTopic.name,
|
|
163
168
|
});
|
|
164
|
-
await ctx
|
|
165
|
-
"Created a topic and bound it to the existing thread id.",
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
await replyDocument(ctx, {
|
|
170
|
+
title: "Created a topic and bound it to the existing thread id.",
|
|
171
|
+
fields: [
|
|
172
|
+
textField("topic", forumTopic.name),
|
|
173
|
+
textField("topic id", forumTopic.message_thread_id),
|
|
174
|
+
codeField("thread", thread.id),
|
|
175
|
+
codeField("cwd", thread.cwd),
|
|
176
|
+
],
|
|
177
|
+
footer: "Future messages in this topic will continue on that thread through the Codex SDK.",
|
|
178
|
+
});
|
|
172
179
|
await postTopicReadyMessage(bot, session, [
|
|
173
180
|
"This topic is now bound to an existing Codex thread id.",
|
|
174
181
|
`thread: ${thread.id}`,
|
|
@@ -179,7 +186,7 @@ async function createFreshThreadTopic(ctx, deps, requestedName) {
|
|
|
179
186
|
const { bot, config, store, projects, logger } = deps;
|
|
180
187
|
const project = getProjectForContext(ctx, projects);
|
|
181
188
|
if (!project) {
|
|
182
|
-
await ctx
|
|
189
|
+
await replyNotice(ctx, PROJECT_REQUIRED_MESSAGE);
|
|
183
190
|
return;
|
|
184
191
|
}
|
|
185
192
|
const topicName = formatTopicName(requestedName, "New Thread");
|
|
@@ -197,12 +204,14 @@ async function createFreshThreadTopic(ctx, deps, requestedName) {
|
|
|
197
204
|
sessionKey: session.sessionKey,
|
|
198
205
|
topicName: forumTopic.name,
|
|
199
206
|
});
|
|
200
|
-
await ctx
|
|
201
|
-
"Created a new topic.",
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
await replyDocument(ctx, {
|
|
208
|
+
title: "Created a new topic.",
|
|
209
|
+
fields: [
|
|
210
|
+
textField("topic", forumTopic.name),
|
|
211
|
+
textField("topic id", forumTopic.message_thread_id),
|
|
212
|
+
],
|
|
213
|
+
footer: "Your first normal message will start a new Codex SDK thread.",
|
|
214
|
+
});
|
|
206
215
|
await postTopicReadyMessage(bot, session, [
|
|
207
216
|
"New topic created.",
|
|
208
217
|
"Send a message to start a new Codex thread.",
|
|
@@ -214,7 +223,7 @@ async function listProjectThreads(ctx, deps) {
|
|
|
214
223
|
const { projects, store, threadCatalog } = deps;
|
|
215
224
|
const project = getProjectForContext(ctx, projects);
|
|
216
225
|
if (!project) {
|
|
217
|
-
await ctx
|
|
226
|
+
await replyNotice(ctx, PROJECT_REQUIRED_MESSAGE);
|
|
218
227
|
return;
|
|
219
228
|
}
|
|
220
229
|
const threads = await threadCatalog.listProjectThreads({
|
|
@@ -222,30 +231,35 @@ async function listProjectThreads(ctx, deps) {
|
|
|
222
231
|
limit: 8,
|
|
223
232
|
});
|
|
224
233
|
if (threads.length === 0) {
|
|
225
|
-
await ctx
|
|
226
|
-
"No saved Codex threads were found for this project yet.",
|
|
227
|
-
|
|
228
|
-
|
|
234
|
+
await replyDocument(ctx, {
|
|
235
|
+
title: "No saved Codex threads were found for this project yet.",
|
|
236
|
+
fields: [codeField("project root", project.cwd)],
|
|
237
|
+
});
|
|
229
238
|
return;
|
|
230
239
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
await replyDocument(ctx, {
|
|
241
|
+
title: "Saved Codex threads",
|
|
242
|
+
fields: [
|
|
243
|
+
codeField("project", project.name),
|
|
244
|
+
codeField("root", project.cwd),
|
|
245
|
+
],
|
|
246
|
+
sections: threads.map((thread, index) => {
|
|
234
247
|
const relativeCwd = path.relative(project.cwd, thread.cwd) || ".";
|
|
235
248
|
const bound = store.getByThreadId(thread.id);
|
|
236
|
-
return
|
|
237
|
-
`${index + 1}. ${thread.preview}`,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
return {
|
|
250
|
+
title: `${index + 1}. ${thread.preview}`,
|
|
251
|
+
fields: [
|
|
252
|
+
codeField("id", thread.id),
|
|
253
|
+
codeField("resume", `/thread resume ${thread.id}`),
|
|
254
|
+
codeField("cwd", relativeCwd),
|
|
255
|
+
textField("updated", thread.updatedAt),
|
|
256
|
+
textField("source", thread.source ?? "unknown"),
|
|
257
|
+
...(bound
|
|
258
|
+
? [textField("bound", bound.telegramTopicName ?? bound.messageThreadId ?? bound.sessionKey)]
|
|
259
|
+
: []),
|
|
260
|
+
],
|
|
261
|
+
};
|
|
246
262
|
}),
|
|
247
|
-
"",
|
|
248
|
-
|
|
249
|
-
];
|
|
250
|
-
await ctx.reply(lines.join("\n"));
|
|
263
|
+
footer: "Copy an id or resume command from the code-formatted fields above.",
|
|
264
|
+
});
|
|
251
265
|
}
|