feat: drop the typed first-message hint from the Discord bridge (#741)

Owner decision. Typing the note into a brand-new non-Claude tab spent a model
turn per tab and, for a spawned worker or a scheduled run, landed with the first
real prompt. Claude keeps its hint through the system prompt, and every other CLI
is told nothing. The setting goes with the path it gated: sessionIdle, the fresh
set, isHintableSession and the session:idle wiring. sendInput loses `fromUser`,
which was only ever false for the hint, and the deps test pins that a reply is a
person's own input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-04 17:03:14 +01:00
1 parent 7c8d440928
commit 9d04a4fb02
14 files changed
+51 -189

No files matched your search

-7
View File
@@ -3319,13 +3319,6 @@
</div>
<label class="switch switch-sm"><input type="checkbox" id="discordStartupHint"><span class="slider"></span></label>
</div>
<div class="set-row">
<div class="set-row-text">
<span class="set-row-label">Also tell other CLIs, as a first message</span>
<span class="set-row-desc">Sends the same note once as a message to a brand-new Codex, OpenCode or similar tab when it first becomes idle. Costs one model turn per tab, and for a spawned worker or a scheduled run it arrives with the first real prompt. Never sent to shell, restored, remote or Docker tabs. Off by default.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="discordTypedHint"><span class="slider"></span></label>
</div>
<div class="set-row has-field">
<div class="set-row-text">
<span class="set-row-label">Which events</span>
-2
View File
@@ -1771,7 +1771,6 @@ Object.assign(CodemanApp.prototype, {
document.getElementById('discordPingMode').value = d.pingMode;
document.getElementById('discordScope').value = d.scope;
document.getElementById('discordStartupHint').checked = d.startupHint !== false;
document.getElementById('discordTypedHint').checked = d.typedHint === true;
const clearBtn = document.getElementById('discordClearBtn');
if (clearBtn) clearBtn.style.display = d.hasToken ? '' : 'none';
const gw = { off: 'Not connected (disabled).', connecting: 'Connecting to Discord…', connected: 'Connected: replies are being read.', error: `Not connected: ${d.gatewayError || 'error'}` };
@@ -1797,7 +1796,6 @@ Object.assign(CodemanApp.prototype, {
pingMode: document.getElementById('discordPingMode').value,
scope: document.getElementById('discordScope').value,
startupHint: document.getElementById('discordStartupHint').checked,
typedHint: document.getElementById('discordTypedHint').checked,
};
const token = document.getElementById('discordToken').value.trim();
if (token) payload.token = token; // blank = keep the saved one (Remove token is the way to clear it)
-1
View File
@@ -2268,7 +2268,6 @@ export const DiscordUpdateSchema = z
pingMode: z.enum(DISCORD_PING_MODES).optional(),
scope: z.enum(WEBHOOK_SCOPES).optional(),
startupHint: z.boolean().optional(),
typedHint: z.boolean().optional(),
})
.strict();
+9 -32
View File
@@ -520,13 +520,12 @@ export class WebServer extends EventEmitter {
private discordBridge = new DiscordBridge({
configDir: getDataDir(),
fetchImpl: webviewFetch,
sendInput: async (sessionId, text, fromUser) => {
sendInput: async (sessionId, text) => {
const session = this.sessions.get(sessionId);
if (!session) return false;
// fromUser is true only for a Discord reply (a person typed it); the startup hint is Codeman's own
// text and must not auto-name the tab or count as the person's input.
if (await session.writeViaMux(text, { fromUser }).catch(() => false)) return true;
return session.write(text, { fromUser });
// A Discord reply is a person's own input, so it names the tab like any other prompt.
if (await session.writeViaMux(text, { fromUser: true }).catch(() => false)) return true;
return session.write(text, { fromUser: true });
},
agentExited: (id) => !!this.sessions.get(id)?.paneExit,
awaitingChoice: (id) => approvalInbox.listPending().some((item) => item.sessionId === id && item.kind !== 'idle'),
@@ -2786,41 +2785,19 @@ export class WebServer extends EventEmitter {
this.bridgeSessionEvent(event, data);
}
/**
* Whether the startup hint may be typed into this tab: an external CLI (never Claude, which has it as a
* launch flag, and never a shell), running locally. A remote (ssh) or Docker tab has no `codeman` on its
* PATH, so the hint would tell the agent to run a command it does not have.
*/
private isHintableSession(sessionId: string): boolean {
const session = this.sessions.get(sessionId);
return !!session && isExternalCliMode(session.mode) && !session.remote && !session.docker;
}
/** Keep the Discord thread in step with the tab: created, renamed, deleted. Never throws. */
private bridgeSessionEvent(event: string, data: unknown): void {
// Deletion is NOT handled here: `session:deleted` also fires for a detach (killMux=false), where the
// session keeps running and can be re-attached. The real delete calls the bridge from the cleanup path.
if (event !== SseEvent.SessionCreated && event !== SseEvent.SessionUpdated && event !== SseEvent.SessionIdle) {
return;
}
const d = (data ?? {}) as {
id?: string;
name?: string;
createdAt?: number;
session?: { id?: string; name?: string; createdAt?: number };
};
if (event !== SseEvent.SessionCreated && event !== SseEvent.SessionUpdated) return;
const d = (data ?? {}) as { id?: string; name?: string; session?: { id?: string; name?: string } };
const s = d.session ?? d;
if (!s.id) return;
const id = s.id;
const run =
event === SseEvent.SessionIdle
? // Claude is told at launch; a shell, a remote or a Docker tab must never be typed at.
this.discordBridge.sessionIdle(id, this.isHintableSession(id))
: event === SseEvent.SessionCreated
? this.discordBridge
.sessionCreated(id, s.name, s.createdAt)
.then(() => this.discordBridge.sessionRenamed(id, s.name))
: this.discordBridge.sessionRenamed(id, s.name);
event === SseEvent.SessionCreated
? this.discordBridge.sessionCreated(id, s.name).then(() => this.discordBridge.sessionRenamed(id, s.name))
: this.discordBridge.sessionRenamed(id, s.name);
void run.catch(() => undefined);
}