mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(statusline): print nothing instead of the bare word codeman
Ported from #416 (discussion #405): a statusline reading just `codeman` is what a hand-run claude in a managed repo showed, and it reads as a broken config rather than a footer. Three paths produced it and all three now yield an empty footer: the exporter's `|| echo codeman` fallback (now `curl -sfk ... || true`, with -f keeping an HTTP error body off stdout), the unknown-session answer of POST /api/status-telemetry, and formatSessionStatusText() with nothing to show. The exporter script marker moves to V4 so live installs pick the new content up on the next spawn. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+14
-10
@@ -856,17 +856,19 @@ const STATUSLINE_MARKER = '/api/status-telemetry';
|
|||||||
* (present in every managed session via tmux setenv), so the config is static.
|
* (present in every managed session via tmux setenv), so the config is static.
|
||||||
*/
|
*/
|
||||||
export function generateStatusLineCommand(): string {
|
export function generateStatusLineCommand(): string {
|
||||||
// `curl -sk`: CODEMAN_API_URL is loopback HTTPS with a self-signed cert in the
|
// `curl -sfk`: CODEMAN_API_URL is loopback HTTPS with a self-signed cert in the
|
||||||
// production setup; without -k curl returns 000 and the statusline shows
|
// production setup; without -k curl returns 000 and the statusline shows
|
||||||
// nothing. -k is safe here (loopback only). Falls back to a brand string so the
|
// nothing. -k is safe here (loopback only); -f keeps an HTTP error body off
|
||||||
// footer is never blank if Codeman is unreachable.
|
// the statusline. On any failure it prints NOTHING: the old `|| echo codeman`
|
||||||
|
// is the bare word that a hand-run `claude` in a managed repo rendered, and
|
||||||
|
// that reads as a broken config (discussion #405).
|
||||||
return (
|
return (
|
||||||
`INPUT=$(cat 2>/dev/null || echo '{}'); ` +
|
`INPUT=$(cat 2>/dev/null || echo '{}'); ` +
|
||||||
`printf '{"sessionId":"%s","data":%s}' "$CODEMAN_SESSION_ID" "$INPUT" | ` +
|
`printf '{"sessionId":"%s","data":%s}' "$CODEMAN_SESSION_ID" "$INPUT" | ` +
|
||||||
`curl -sk -X POST "$CODEMAN_API_URL${STATUSLINE_MARKER}" ` +
|
`curl -sfk -X POST "$CODEMAN_API_URL${STATUSLINE_MARKER}" ` +
|
||||||
`-H 'Content-Type: application/json' ` +
|
`-H 'Content-Type: application/json' ` +
|
||||||
`-H "X-Codeman-Hook-Secret: $(cat "$CODEMAN_HOOK_SECRET_FILE" 2>/dev/null)" ` +
|
`-H "X-Codeman-Hook-Secret: $(cat "$CODEMAN_HOOK_SECRET_FILE" 2>/dev/null)" ` +
|
||||||
`--data @- 2>/dev/null || echo codeman`
|
`--data @- 2>/dev/null || true`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,7 +915,7 @@ export async function applyStatusLineConfig(casePath: string, enabled: boolean):
|
|||||||
* whenever the script content changes so `ensureStatusLineExporterScript`'s
|
* whenever the script content changes so `ensureStatusLineExporterScript`'s
|
||||||
* content comparison rewrites stale copies on next use.
|
* content comparison rewrites stale copies on next use.
|
||||||
*/
|
*/
|
||||||
const STATUSLINE_EXPORTER_SCRIPT_MARKER = 'CODEMAN_STATUSLINE_EXPORTER_V3';
|
const STATUSLINE_EXPORTER_SCRIPT_MARKER = 'CODEMAN_STATUSLINE_EXPORTER_V4';
|
||||||
|
|
||||||
function statusLineExporterScriptContent(): string {
|
function statusLineExporterScriptContent(): string {
|
||||||
// Where the telemetry POST runs depends on who owns the footer. When the pane's
|
// Where the telemetry POST runs depends on who owns the footer. When the pane's
|
||||||
@@ -930,12 +932,14 @@ function statusLineExporterScriptContent(): string {
|
|||||||
// Absent a user statusline, NOTHING else will print the footer, so the POST runs
|
// Absent a user statusline, NOTHING else will print the footer, so the POST runs
|
||||||
// in the FOREGROUND and ITS OWN stdout becomes the footer — `/api/status-telemetry`
|
// in the FOREGROUND and ITS OWN stdout becomes the footer — `/api/status-telemetry`
|
||||||
// returns formatSessionStatusText(...) (model/tokens/context %) precisely so this
|
// returns formatSessionStatusText(...) (model/tokens/context %) precisely so this
|
||||||
// can happen — falling back to the plain "codeman" marker only if curl itself
|
// can happen. If curl itself fails (refused/unreachable Codeman, or an HTTP
|
||||||
// fails (`|| echo codeman`, refused/unreachable Codeman). `--max-time` bounds a
|
// error, which `-f` keeps off stdout) the footer is simply EMPTY (`|| true`):
|
||||||
|
// the old `|| echo codeman` rendered a bare brand word that reads as a broken
|
||||||
|
// config, the symptom discussion #405 opened with. `--max-time` bounds a
|
||||||
// HUNG (not just refused) Codeman so it cannot wedge the render indefinitely.
|
// HUNG (not just refused) Codeman so it cannot wedge the render indefinitely.
|
||||||
const post =
|
const post =
|
||||||
`printf '{"sessionId":"%s","data":%s}' "$CODEMAN_SESSION_ID" "$INPUT" | ` +
|
`printf '{"sessionId":"%s","data":%s}' "$CODEMAN_SESSION_ID" "$INPUT" | ` +
|
||||||
`curl -sk --max-time 5 -X POST "$CODEMAN_API_URL${STATUSLINE_MARKER}" ` +
|
`curl -sfk --max-time 5 -X POST "$CODEMAN_API_URL${STATUSLINE_MARKER}" ` +
|
||||||
`-H 'Content-Type: application/json' ` +
|
`-H 'Content-Type: application/json' ` +
|
||||||
`-H "X-Codeman-Hook-Secret: $(cat "$CODEMAN_HOOK_SECRET_FILE" 2>/dev/null)" ` +
|
`-H "X-Codeman-Hook-Secret: $(cat "$CODEMAN_HOOK_SECRET_FILE" 2>/dev/null)" ` +
|
||||||
`--data @-`;
|
`--data @-`;
|
||||||
@@ -947,7 +951,7 @@ function statusLineExporterScriptContent(): string {
|
|||||||
` ( ${post} ) >/dev/null 2>&1 </dev/null &\n` +
|
` ( ${post} ) >/dev/null 2>&1 </dev/null &\n` +
|
||||||
` printf '%s' "$INPUT" | sh -c "$CODEMAN_USER_STATUSLINE_CMD"\n` +
|
` printf '%s' "$INPUT" | sh -c "$CODEMAN_USER_STATUSLINE_CMD"\n` +
|
||||||
`else\n` +
|
`else\n` +
|
||||||
` ${post} 2>/dev/null || echo codeman\n` +
|
` ${post} 2>/dev/null || true\n` +
|
||||||
`fi\n`
|
`fi\n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,10 +173,13 @@ export function parseSessionStatus(data: RawStatuslinePayload | undefined): Sess
|
|||||||
* Format the in-terminal statusline footer: the CURRENT SESSION's status —
|
* Format the in-terminal statusline footer: the CURRENT SESSION's status —
|
||||||
* `Opus 4.8 (1M context) in:562,411 out:1,188 ctx:56%` — NOT the plan limits,
|
* `Opus 4.8 (1M context) in:562,411 out:1,188 ctx:56%` — NOT the plan limits,
|
||||||
* which live in the Codeman header chip. Claude requires a statusLine command to
|
* which live in the Codeman header chip. Claude requires a statusLine command to
|
||||||
* emit the rate_limits JSON at all, so this is what that command prints back.
|
* emit the rate_limits JSON at all, so this is what that command prints back
|
||||||
|
* when it has no statusline of the user's own to wrap. With nothing to show it
|
||||||
|
* returns '' rather than a brand word: a bare `codeman` on the statusline is
|
||||||
|
* the symptom discussion #405 opened with.
|
||||||
*/
|
*/
|
||||||
export function formatSessionStatusText(s: SessionStatus | null): string {
|
export function formatSessionStatusText(s: SessionStatus | null): string {
|
||||||
if (!s) return 'codeman';
|
if (!s) return '';
|
||||||
const groups: string[] = [];
|
const groups: string[] = [];
|
||||||
if (s.modelDisplayName) groups.push(s.modelDisplayName);
|
if (s.modelDisplayName) groups.push(s.modelDisplayName);
|
||||||
const tok: string[] = [];
|
const tok: string[] = [];
|
||||||
@@ -184,7 +187,7 @@ export function formatSessionStatusText(s: SessionStatus | null): string {
|
|||||||
if (s.outputTokens != null) tok.push(`out:${withCommas(s.outputTokens)}`);
|
if (s.outputTokens != null) tok.push(`out:${withCommas(s.outputTokens)}`);
|
||||||
if (tok.length) groups.push(tok.join(' '));
|
if (tok.length) groups.push(tok.join(' '));
|
||||||
if (s.contextUsedPercentage != null) groups.push(`ctx:${Math.round(clampPct(s.contextUsedPercentage))}%`);
|
if (s.contextUsedPercentage != null) groups.push(`ctx:${Math.round(clampPct(s.contextUsedPercentage))}%`);
|
||||||
return groups.length ? groups.join(' ') : 'codeman';
|
return groups.length ? groups.join(' ') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
* (localhost-only; hook-secret-gated while a tunnel runs — see middleware/auth).
|
* (localhost-only; hook-secret-gated while a tunnel runs — see middleware/auth).
|
||||||
*
|
*
|
||||||
* Returns a compact plain-text status string for the exporter to print as the
|
* Returns a compact plain-text status string for the exporter to print as the
|
||||||
* in-terminal footer (print-through), so injecting our statusLine doesn't leave
|
* in-terminal footer (print-through) when it has no statusline of the user's
|
||||||
* the terminal footer blank.
|
* own to wrap. An unknown session gets an EMPTY body: the old brand-word
|
||||||
|
* answer rendered as the statusline itself (discussion #405).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FastifyInstance } from 'fastify';
|
import { FastifyInstance } from 'fastify';
|
||||||
@@ -36,10 +37,11 @@ export function registerStatusTelemetryRoutes(app: FastifyInstance, ctx: Session
|
|||||||
|
|
||||||
reply.type('text/plain; charset=utf-8');
|
reply.type('text/plain; charset=utf-8');
|
||||||
|
|
||||||
// Unknown session — minimal footer, no broadcast.
|
// Unknown session: nothing to broadcast and nothing to print. Never a brand
|
||||||
|
// word here, it would render as the statusline.
|
||||||
if (!ctx.sessions.has(sessionId)) {
|
if (!ctx.sessions.has(sessionId)) {
|
||||||
lastSig.delete(sessionId);
|
lastSig.delete(sessionId);
|
||||||
return 'codeman';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = data as RawStatuslinePayload | undefined;
|
const payload = data as RawStatuslinePayload | undefined;
|
||||||
|
|||||||
@@ -1516,10 +1516,17 @@ describe('statusline exporter script (real shell execution)', () => {
|
|||||||
expect(result.stdout.trim()).toBe('model: opus | 42% used');
|
expect(result.stdout.trim()).toBe('model: opus | 42% used');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('no-user-statusline branch: falls back to the plain "codeman" marker when curl fails', async () => {
|
it('no-user-statusline branch: prints NOTHING when curl fails (never a bare brand word)', async () => {
|
||||||
writeFakeCurl(`exit 1`);
|
writeFakeCurl(`exit 1`);
|
||||||
const result = await runExporter(baseEnv);
|
const result = await runExporter(baseEnv);
|
||||||
expect(result.stdout.trim()).toBe('codeman');
|
expect(result.stdout).toBe('');
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('asks curl to fail on HTTP errors (-f) so an error body never becomes the footer', async () => {
|
||||||
|
const scriptPath = await resolveStatusLineCliCommand(testDir, true);
|
||||||
|
expect(readFileSync(scriptPath!, 'utf-8')).toContain('curl -sfk');
|
||||||
|
expect(readFileSync(scriptPath!, 'utf-8')).not.toContain('echo codeman');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('wrap branch: never blocks a reader-to-EOF on a slow/hung curl (background subshell closes stdin too)', async () => {
|
it('wrap branch: never blocks a reader-to-EOF on a slow/hung curl (background subshell closes stdin too)', async () => {
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ describe('POST /api/status-telemetry', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not broadcast for an unknown session; returns the brand footer', async () => {
|
it('does not broadcast for an unknown session; returns an EMPTY footer, never a brand word', async () => {
|
||||||
const res = await post({ sessionId: 'does-not-exist', data: REAL });
|
const res = await post({ sessionId: 'does-not-exist', data: REAL });
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
expect(res.body).toBe('codeman');
|
expect(res.body).toBe('');
|
||||||
expect(h.ctx.broadcast).not.toHaveBeenCalled();
|
expect(h.ctx.broadcast).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { buildSpawnCommand } from '../src/tmux-manager.js';
|
import { buildSpawnCommand } from '../src/tmux-manager.js';
|
||||||
|
|
||||||
const EXPORTER_CMD = 'curl -sk -X POST "$CODEMAN_API_URL/api/status-telemetry" --data @- 2>/dev/null || echo codeman';
|
const EXPORTER_CMD = 'curl -sfk -X POST "$CODEMAN_API_URL/api/status-telemetry" --data @- 2>/dev/null || true';
|
||||||
|
|
||||||
/** Extract the `--settings <arg>` fragment from a built command and have a
|
/** Extract the `--settings <arg>` fragment from a built command and have a
|
||||||
* real shell resolve its quoting, printing the arg back out verbatim. */
|
* real shell resolve its quoting, printing the arg back out verbatim. */
|
||||||
|
|||||||
@@ -119,8 +119,9 @@ describe('formatSessionStatusText', () => {
|
|||||||
expect(formatSessionStatusText({ modelDisplayName: 'Opus 4.8 (1M context)' })).toBe('Opus 4.8 (1M context)');
|
expect(formatSessionStatusText({ modelDisplayName: 'Opus 4.8 (1M context)' })).toBe('Opus 4.8 (1M context)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to a brand string when there is no data', () => {
|
it('prints nothing when there is no data (a bare brand word reads as a broken statusline)', () => {
|
||||||
expect(formatSessionStatusText(null)).toBe('codeman');
|
expect(formatSessionStatusText(null)).toBe('');
|
||||||
|
expect(formatSessionStatusText({} as never)).toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user