Skip to content

Commit 120cbfe

Browse files
committed
feat: add per-chat verbose controls
1 parent 3677ee4 commit 120cbfe

5 files changed

Lines changed: 137 additions & 17 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ General:
133133
- `/auto <task>` - force a one-off `codex exec --full-auto`
134134
- `/plan <task>` - ask Codex for a plan only, without direct file modification intent
135135
- `/model [name|reset]` - show or set the model override for the current chat
136+
- `/verbose [on|off]` - show or toggle system notices for the current chat
136137
- `/skill list` - show skill switches for the current chat
137138
- `/skill status` - alias of `/skill list`
138139
- `/skill on <name>` - enable a skill for the current chat
@@ -175,6 +176,7 @@ Telegram adaptation notes:
175176
- `/sh` is implemented by the bot, never invokes a shell interpreter, and only accepts configured command prefixes
176177
- `/sh` is read-only by default; dangerous prefixes can be configured and require `--confirm` when writable mode is enabled
177178
- `/plan` translates to a planning-only prompt instead of passing a raw `/plan` slash command to Codex
179+
- `/verbose off` keeps Telegram output quiet by hiding fallback, startup, and session-exit notices for the current chat
178180

179181
## Streaming and Reasoning Visualization
180182

src/bot/handlers.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export function registerHandlers({
108108
"/auto <task> - 强制用 codex exec --full-auto 运行任务",
109109
"/plan <task> - 仅生成执行计划,不直接修改代码",
110110
"/model [name|reset] - 查看或设置当前 chat 的模型",
111+
"/verbose [on|off] - 查看或切换系统提示输出",
111112
"/skill list - 查看当前 chat 的 skill 开关",
112113
"/skill status - 同 /skill list",
113114
"/skill on <name> - 启用 skill",
@@ -140,6 +141,7 @@ export function registerHandlers({
140141
status.ptySupported === null ? "unknown" : status.ptySupported ? "yes" : "no (exec fallback)"
141142
}`,
142143
`preferred model: ${status.preferredModel || "inherit codex default"}`,
144+
`verbose: ${status.verboseOutput ? "on" : "off"}`,
143145
`command: ${status.command}`,
144146
`workspace root: ${status.workspaceRoot}`,
145147
`workdir: ${status.workdir}`,
@@ -506,6 +508,31 @@ export function registerHandlers({
506508
);
507509
});
508510

511+
bot.command("verbose", async (ctx) => {
512+
const value = extractCommandPayload(ctx.message.text, "verbose");
513+
if (!value) {
514+
await sendChunkedMarkdown(
515+
ctx,
516+
`当前系统提示输出: ${ptyManager.isVerbose(ctx.chat.id) ? "on" : "off"}`
517+
);
518+
return;
519+
}
520+
521+
if (/^(on|true|1)$/i.test(value)) {
522+
ptyManager.setVerbose(ctx.chat.id, true);
523+
await sendChunkedMarkdown(ctx, "系统提示输出已开启。");
524+
return;
525+
}
526+
527+
if (/^(off|false|0)$/i.test(value)) {
528+
ptyManager.setVerbose(ctx.chat.id, false);
529+
await sendChunkedMarkdown(ctx, "系统提示输出已关闭。");
530+
return;
531+
}
532+
533+
await sendChunkedMarkdown(ctx, "用法: /verbose [on|off]");
534+
});
535+
509536
bot.command("interrupt", async (ctx) => {
510537
const ok = ptyManager.interrupt(ctx.chat.id);
511538
await sendChunkedMarkdown(ctx, ok ? "已发送 Ctrl+C。" : "当前 chat 没有活动 PTY 会话。");

src/runner/ptyManager.js

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class PtyManager {
3636

3737
const state = {
3838
preferredModel: null,
39+
verboseOutput: false,
3940
currentWorkdir: this.config.runner.cwd,
4041
recentWorkdirs: [this.config.runner.cwd],
4142
ptySupported: null,
@@ -83,6 +84,18 @@ export class PtyManager {
8384
return args;
8485
}
8586

87+
isVerbose(chatId) {
88+
const state = this.ensureChatState(chatId);
89+
return Boolean(state.verboseOutput);
90+
}
91+
92+
setVerbose(chatId, enabled) {
93+
const state = this.ensureChatState(chatId);
94+
state.verboseOutput = Boolean(enabled);
95+
this.onChange?.(this.exportState());
96+
return state.verboseOutput;
97+
}
98+
8699
getWorkdir(chatId) {
87100
const state = this.ensureChatState(chatId);
88101
return state.currentWorkdir || this.config.runner.cwd;
@@ -305,12 +318,14 @@ export class PtyManager {
305318
this.onChange?.(this.exportState());
306319

307320
this.enqueueFlush(session.chatId);
308-
await this.bot.telegram
309-
.sendMessage(
310-
session.chatId,
311-
`Codex session exited (mode=${session.mode}, code=${exitCode}, signal=${signal}).`
312-
)
313-
.catch(() => {});
321+
if (this.isVerbose(session.chatId)) {
322+
await this.bot.telegram
323+
.sendMessage(
324+
session.chatId,
325+
`Codex session exited (mode=${session.mode}, code=${exitCode}, signal=${signal}).`
326+
)
327+
.catch(() => {});
328+
}
314329
session.throttledFlush?.cancel();
315330
this.sessions.delete(session.chatId);
316331
});
@@ -480,7 +495,7 @@ export class PtyManager {
480495
trackConversation: false
481496
});
482497

483-
if (options.notice) {
498+
if (options.notice && this.isVerbose(chatId)) {
484499
await this.bot.telegram.sendMessage(chatId, options.notice);
485500
}
486501

@@ -527,12 +542,14 @@ export class PtyManager {
527542
workdir: this.getWorkdir(chatId),
528543
resumeSessionId: projectState.lastSessionId || ""
529544
});
530-
await this.bot.telegram.sendMessage(
531-
chatId,
532-
projectState.lastSessionId
533-
? "PTY unavailable on this host. Restoring the current project's Codex conversation in `codex exec resume` mode."
534-
: "PTY unavailable on this host. Falling back to `codex exec` mode for this request."
535-
);
545+
if (this.isVerbose(chatId)) {
546+
await this.bot.telegram.sendMessage(
547+
chatId,
548+
projectState.lastSessionId
549+
? "PTY unavailable on this host. Restoring the current project's Codex conversation in `codex exec resume` mode."
550+
: "PTY unavailable on this host. Falling back to `codex exec` mode for this request."
551+
);
552+
}
536553
return {
537554
started: true,
538555
mode: "exec",
@@ -541,7 +558,7 @@ export class PtyManager {
541558
};
542559
}
543560

544-
if (!session.streamMessageIds.length) {
561+
if (!session.streamMessageIds.length && this.isVerbose(chatId)) {
545562
const sent = await this.bot.telegram.sendMessage(
546563
chatId,
547564
projectState.lastSessionId
@@ -647,6 +664,7 @@ export class PtyManager {
647664

648665
chats[chatId] = {
649666
preferredModel: state.preferredModel,
667+
verboseOutput: Boolean(state.verboseOutput),
650668
currentWorkdir: this.serializeWorkdir(state.currentWorkdir),
651669
recentWorkdirs: (state.recentWorkdirs || []).map((workdir) => this.serializeWorkdir(workdir)),
652670
projects
@@ -704,6 +722,7 @@ export class PtyManager {
704722

705723
this.chatState.set(String(chatId), {
706724
preferredModel: rawState?.preferredModel?.trim?.() || null,
725+
verboseOutput: Boolean(rawState?.verboseOutput),
707726
currentWorkdir,
708727
recentWorkdirs: [currentWorkdir, ...recentWorkdirs.filter((workdir) => workdir !== currentWorkdir)].slice(0, 6),
709728
ptySupported: null,
@@ -726,6 +745,7 @@ export class PtyManager {
726745
lastExitSignal: projectState.lastExitSignal,
727746
projectSessionId: projectState.lastSessionId || null,
728747
preferredModel: state.preferredModel,
748+
verboseOutput: Boolean(state.verboseOutput),
729749
ptySupported: state.ptySupported,
730750
workdir: this.getWorkdir(key),
731751
relativeWorkdir: this.getRelativeWorkdir(key),

tests/ptyManager.test.js

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { PtyManager } from "../src/runner/ptyManager.js";
88
function createManager(overrides = {}) {
99
const runnerCwd = overrides.runnerCwd || process.cwd();
1010
const workspaceRoot = overrides.workspaceRoot || runnerCwd;
11+
const telegram =
12+
overrides.telegram || {
13+
sendMessage: async () => ({})
14+
};
1115
return new PtyManager({
1216
bot: {
13-
telegram: {
14-
sendMessage: async () => ({})
15-
}
17+
telegram
1618
},
1719
config: {
1820
runner: {
@@ -48,6 +50,15 @@ test("pty manager stores model preference per chat", () => {
4850
assert.equal(manager.getStatus(123).preferredModel, null);
4951
});
5052

53+
test("pty manager stores verbose preference per chat", () => {
54+
const manager = createManager();
55+
56+
assert.equal(manager.isVerbose(123), false);
57+
manager.setVerbose(123, true);
58+
assert.equal(manager.isVerbose(123), true);
59+
assert.equal(manager.getStatus(123).verboseOutput, true);
60+
});
61+
5162
test("pty manager status exposes runner workdir and MCP server names", () => {
5263
const manager = createManager();
5364
const status = manager.getStatus(456);
@@ -185,3 +196,61 @@ test("pty manager exports and restores per-project conversation state", () => {
185196
restored.switchWorkdir(99, "project-a");
186197
assert.equal(restored.getStatus(99).projectSessionId, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
187198
});
199+
200+
test("pty manager exports and restores verbose preference", () => {
201+
const manager = createManager();
202+
manager.setVerbose(42, true);
203+
204+
const restored = createManager();
205+
restored.restoreState(manager.exportState());
206+
207+
assert.equal(restored.isVerbose(42), true);
208+
});
209+
210+
test("pty manager hides exec fallback notices when verbose output is off", async () => {
211+
const sentMessages = [];
212+
const manager = createManager({
213+
telegram: {
214+
sendMessage: async (chatId, text) => {
215+
sentMessages.push({ chatId, text });
216+
return { message_id: sentMessages.length };
217+
}
218+
}
219+
});
220+
221+
manager.ensureSession = () => null;
222+
manager.startExecSessionWithOptions = () => ({
223+
mode: "exec",
224+
streamMessageIds: [],
225+
chatId: "77"
226+
});
227+
228+
await manager.sendPrompt({ chat: { id: 77 } }, "who are u");
229+
230+
assert.equal(sentMessages.length, 0);
231+
});
232+
233+
test("pty manager shows exec fallback notices when verbose output is on", async () => {
234+
const sentMessages = [];
235+
const manager = createManager({
236+
telegram: {
237+
sendMessage: async (chatId, text) => {
238+
sentMessages.push({ chatId, text });
239+
return { message_id: sentMessages.length };
240+
}
241+
}
242+
});
243+
244+
manager.setVerbose(77, true);
245+
manager.ensureSession = () => null;
246+
manager.startExecSessionWithOptions = () => ({
247+
mode: "exec",
248+
streamMessageIds: [],
249+
chatId: "77"
250+
});
251+
252+
await manager.sendPrompt({ chat: { id: 77 } }, "who are u");
253+
254+
assert.equal(sentMessages.length, 1);
255+
assert.match(sentMessages[0].text, /PTY unavailable/);
256+
});

tests/runtimeStateStore.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ test("runtime state store saves and loads MCP and skill state", async () => {
2323
runner: {
2424
chats: {
2525
"42": {
26+
verboseOutput: true,
2627
currentWorkdir: "project-a",
2728
recentWorkdirs: ["project-a", "project-b"],
2829
projects: {
@@ -50,6 +51,7 @@ test("runtime state store saves and loads MCP and skill state", async () => {
5051
assert.deepEqual(state.runner, {
5152
chats: {
5253
"42": {
54+
verboseOutput: true,
5355
currentWorkdir: "project-a",
5456
recentWorkdirs: ["project-a", "project-b"],
5557
projects: {

0 commit comments

Comments
 (0)