mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 05:59:43 +02:00
fix(statusline): sticky telemetry collection, footer print-through, EOF fix
Responds to Ark0N's review round on the ephemeral-CLI-flag statusline injection rework: - Rebase-detail fixes: registry-gated telemetry eligibility via getCli(mode)?.capabilities.statusLineTelemetry instead of a hardcoded mode === 'claude' check, using the capability flag master's CLI-registry refactor already declares for exactly this purpose. - Design question settled: sticky (a). Rather than persisting the toggle as a new field and threading it through every session-creation path (cron, Ralph Loop API, quick-start), eliminated the per-session field entirely. readPlanUsageTelemetryEnabled() (hooks-config.ts) reads the existing showPlanUsageLimits setting fresh from settings.json at every claude create/respawn (TmuxManager.createSession/respawnPane) - no per-session state to survive a restart, and it applies uniformly to every creation path for free, since they all flow through the same TmuxManager methods. This required fixing a real bug found along the way: showPlanUsageLimits was not actually round-tripping through settings.json on save - settings-ui.js explicitly excluded it from the PUT body as a pure per-device display key. It now flows through normally (both true and false); the load-side per-device merge behavior is unchanged. Removed entirely as a result: the statusLineTelemetry field from CreateSessionSchema/SettingsUpdateSchema, CreateSessionOptions/ RespawnPaneOptions, Session._statusLineTelemetry (this is what makes the restart-persistence bug moot rather than patched), and the frontend send sites. - Footer print-through restored: the no-user-statusline branch of the exporter script now runs the telemetry POST in the foreground so its own stdout becomes the in-terminal footer, falling back to a plain "codeman" marker only on curl failure. - Background-subshell EOF fix: the wrap-a-real-statusline branch closes stdin too, not just stdout/stderr (`>/dev/null 2>&1 </dev/null &`) - the un-redirected subshell process itself, not curl, was what held a reader-to-EOF's pipe open for however long curl took to finish. Added curl --max-time 5 so a hung (not just refused) Codeman cannot wedge the render. Tests: real-shell-execution tests for the footer/EOF fixes (fake curl stand-in on PATH, real sh subprocess spawns, real elapsed-time measurements - verified non-vacuous against a hand-reconstructed old-style script), unit tests for readPlanUsageTelemetryEnabled. Adapted two existing tests whose payloads referenced the removed field. Fixed during independent code review: a stray indentation break and a test exercising the wrong (legacy) exporter code path. Docs synced: CLAUDE.md, docs/usage-limits-display-plan.md (old disk-based section marked superseded, kept for history), docs/architecture-invariants.md. Full suite green: 352 files, 6780 passed, 12 skipped, 0 failed. tsc/lint/format:check/frontend-syntax all clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e15e8e43e8
commit
d5b75af628
+61
-27
@@ -39,6 +39,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import type { HookEventType } from './types.js';
|
||||
import { HOOK_TIMEOUT_SECONDS } from './config/auth-config.js';
|
||||
import { dataPath } from './config/instance.js';
|
||||
import { readJsonConfig, SETTINGS_PATH } from './web/route-helpers.js';
|
||||
|
||||
/**
|
||||
* Serializes read-modify-write access to a `settings.local.json` path. Every
|
||||
@@ -912,32 +913,41 @@ export async function applyStatusLineConfig(casePath: string, enabled: boolean):
|
||||
* whenever the script content changes so `ensureStatusLineExporterScript`'s
|
||||
* content comparison rewrites stale copies on next use.
|
||||
*/
|
||||
const STATUSLINE_EXPORTER_SCRIPT_MARKER = 'CODEMAN_STATUSLINE_EXPORTER_V2';
|
||||
const STATUSLINE_EXPORTER_SCRIPT_MARKER = 'CODEMAN_STATUSLINE_EXPORTER_V3';
|
||||
|
||||
function statusLineExporterScriptContent(): string {
|
||||
// The telemetry POST runs in a BACKGROUND subshell so it can never add
|
||||
// latency to a render, and its own stdout/stderr are discarded so they
|
||||
// never leak into the visible statusline. If the pane's env carries
|
||||
// CODEMAN_USER_STATUSLINE_CMD (set via tmux setenv by TmuxManager when
|
||||
// findEffectiveUserStatusLineCommand found the user's own REAL statusLine —
|
||||
// see that function's doc comment), the SAME stdin blob is fed to it and
|
||||
// ITS stdout becomes ours, so the user keeps seeing their own statusline
|
||||
// untouched. Absent that, fall back to the plain "codeman" marker.
|
||||
// Where the telemetry POST runs depends on who owns the footer. When the pane's
|
||||
// env carries CODEMAN_USER_STATUSLINE_CMD (set via tmux setenv by TmuxManager
|
||||
// when findEffectiveUserStatusLineCommand found the user's own REAL statusLine —
|
||||
// see that function's doc comment), the user's command owns the footer, so the
|
||||
// POST runs in a BACKGROUND subshell with stdin/stdout/stderr all closed
|
||||
// (`>/dev/null 2>&1 </dev/null &`) — closing stdout/stderr keeps it from adding
|
||||
// latency or leaking into the visible statusline, and closing stdin too is what
|
||||
// lets a host reading this script's own stdout to EOF (`sh script | cat`) see
|
||||
// that EOF promptly: without it the backgrounded curl keeps the pipe's write end
|
||||
// open until IT exits, so the reader blocks for however long curl takes (measured
|
||||
// ~5s with a stand-in) instead of the ~9ms it takes once stdin is closed too.
|
||||
// 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`
|
||||
// returns formatSessionStatusText(...) (model/tokens/context %) precisely so this
|
||||
// can happen — falling back to the plain "codeman" marker only if curl itself
|
||||
// fails (`|| echo codeman`, refused/unreachable Codeman). `--max-time` bounds a
|
||||
// HUNG (not just refused) Codeman so it cannot wedge the render indefinitely.
|
||||
const post =
|
||||
`printf '{"sessionId":"%s","data":%s}' "$CODEMAN_SESSION_ID" "$INPUT" | ` +
|
||||
`curl -sk --max-time 5 -X POST "$CODEMAN_API_URL${STATUSLINE_MARKER}" ` +
|
||||
`-H 'Content-Type: application/json' ` +
|
||||
`-H "X-Codeman-Hook-Secret: $(cat "$CODEMAN_HOOK_SECRET_FILE" 2>/dev/null)" ` +
|
||||
`--data @-`;
|
||||
return (
|
||||
`#!/bin/sh\n` +
|
||||
`# ${STATUSLINE_EXPORTER_SCRIPT_MARKER} — auto-generated by Codeman; safe to delete, regenerated on demand.\n` +
|
||||
`INPUT=$(cat 2>/dev/null || echo '{}')\n` +
|
||||
`(\n` +
|
||||
` printf '{"sessionId":"%s","data":%s}' "$CODEMAN_SESSION_ID" "$INPUT" | ` +
|
||||
`curl -sk -X POST "$CODEMAN_API_URL${STATUSLINE_MARKER}" ` +
|
||||
`-H 'Content-Type: application/json' ` +
|
||||
`-H "X-Codeman-Hook-Secret: $(cat "$CODEMAN_HOOK_SECRET_FILE" 2>/dev/null)" ` +
|
||||
`--data @- >/dev/null 2>&1\n` +
|
||||
`) &\n` +
|
||||
`if [ -n "$CODEMAN_USER_STATUSLINE_CMD" ]; then\n` +
|
||||
` ( ${post} ) >/dev/null 2>&1 </dev/null &\n` +
|
||||
` printf '%s' "$INPUT" | sh -c "$CODEMAN_USER_STATUSLINE_CMD"\n` +
|
||||
`else\n` +
|
||||
` echo codeman\n` +
|
||||
` ${post} 2>/dev/null || echo codeman\n` +
|
||||
`fi\n`
|
||||
);
|
||||
}
|
||||
@@ -1019,13 +1029,34 @@ export async function ensureStatusLineExporterScript(): Promise<string> {
|
||||
return scriptPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether plan-usage telemetry collection is CURRENTLY wanted — read FRESH
|
||||
* from the persisted `showPlanUsageLimits` setting on every call, never
|
||||
* cached and never per-session. Reusing that setting rather than inventing a
|
||||
* second persisted flag: it's the SAME boolean the App Settings chip checkbox
|
||||
* already writes (see `planUsageChipEnabled()` in settings-ui.js).
|
||||
*
|
||||
* This is what lets the on/off decision survive a Codeman restart (there is
|
||||
* no per-session state to lose — see the now-removed `Session._statusLineTelemetry`,
|
||||
* which WAS such a per-session field and went stale on every restart) and
|
||||
* apply uniformly across every claude session-creation path — interactive
|
||||
* create, cron, the Ralph Loop API, quick-start — with none of them needing
|
||||
* to thread a request-time flag through: they all already construct a
|
||||
* session via TmuxManager.createSession/respawnPane, which reads this at
|
||||
* spawn time.
|
||||
*/
|
||||
export async function readPlanUsageTelemetryEnabled(): Promise<boolean> {
|
||||
const settings = await readJsonConfig<Record<string, unknown>>(SETTINGS_PATH, 'settings.json', {});
|
||||
return settings.showPlanUsageLimits === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the statusLine command to pass as an EPHEMERAL `claude --settings`
|
||||
* CLI flag for this one process (see buildClaudeSettingsFlag in
|
||||
* tmux-manager.ts) — never written to disk. This supersedes the old
|
||||
* applyStatusLineConfig(path, true) disk-write: a file-based statusLine
|
||||
* leaked into any plain `claude` run in that directory outside Codeman
|
||||
* entirely (it took precedence over the user's own global/project
|
||||
* CLI flag for this one process (see buildSpawnCommandFromRegistry in
|
||||
* session-cli-registry-bridge.ts) — never written to disk. This supersedes
|
||||
* the old applyStatusLineConfig(path, true) disk-write: a file-based
|
||||
* statusLine leaked into any plain `claude` run in that directory outside
|
||||
* Codeman entirely (it took precedence over the user's own global/project
|
||||
* statusline with no disclosure and no way to remove it — found live
|
||||
* 2026-08-31).
|
||||
*
|
||||
@@ -1033,14 +1064,17 @@ export async function ensureStatusLineExporterScript(): Promise<string> {
|
||||
* exporter into this workspace's settings.local.json, it is stripped here
|
||||
* (isOurs-guarded, same as applyStatusLineConfig's removal branch) so every
|
||||
* workspace migrates off the disk-based mechanism the first time a session
|
||||
* starts there again — no manual cleanup required.
|
||||
* starts there again — no manual cleanup required. This self-heal runs
|
||||
* regardless of `telemetryEnabled`, so a legacy leftover is cleaned up even
|
||||
* while the setting is currently off.
|
||||
*
|
||||
* Returns undefined when telemetry wasn't requested, or when the workspace
|
||||
* already has its OWN hand-configured statusLine (never override a real one).
|
||||
* Returns undefined when telemetry isn't currently enabled (see
|
||||
* readPlanUsageTelemetryEnabled), or when the workspace already has its OWN
|
||||
* hand-configured statusLine (never override a real one).
|
||||
*/
|
||||
export async function resolveStatusLineCliCommand(
|
||||
casePath: string,
|
||||
telemetryRequested: boolean
|
||||
telemetryEnabled: boolean
|
||||
): Promise<string | undefined> {
|
||||
const settingsPath = join(casePath, '.claude', 'settings.local.json');
|
||||
let userHasOwnStatusLine = false;
|
||||
@@ -1059,7 +1093,7 @@ export async function resolveStatusLineCliCommand(
|
||||
// Malformed — leave it alone, same guard applyStatusLineConfig itself uses.
|
||||
}
|
||||
}
|
||||
if (!telemetryRequested || userHasOwnStatusLine) return undefined;
|
||||
if (!telemetryEnabled || userHasOwnStatusLine) return undefined;
|
||||
return ensureStatusLineExporterScript();
|
||||
}
|
||||
|
||||
|
||||
@@ -90,13 +90,6 @@ export interface CreateSessionOptions {
|
||||
envOverrides?: Record<string, string>;
|
||||
/** Claude CLI effort level, injected as a `--settings` soft default (overridable via /effort in-session) */
|
||||
effort?: EffortLevel;
|
||||
/**
|
||||
* Claude-only: request the plan-usage statusLine exporter for this session,
|
||||
* injected as an EPHEMERAL `--settings` CLI flag (never written to disk — see
|
||||
* generateStatusLineCommand/resolveStatusLineCliCommand). Skipped when the
|
||||
* workspace already has its own hand-configured statusLine.
|
||||
*/
|
||||
statusLineTelemetry?: boolean;
|
||||
/** tmux history-limit (scrollback lines) allocated when this session is created. */
|
||||
historyLimit?: number;
|
||||
/** Remote execution metadata for local tmux sessions wrapping SSH */
|
||||
@@ -132,8 +125,6 @@ export interface RespawnPaneOptions {
|
||||
envOverrides?: Record<string, string>;
|
||||
/** Claude CLI effort level (preserved across respawns, injected via `--settings`) */
|
||||
effort?: EffortLevel;
|
||||
/** Preserved across respawns — see CreateSessionOptions.statusLineTelemetry. */
|
||||
statusLineTelemetry?: boolean;
|
||||
/** Original tmux history-limit retained for config parity; respawn cannot resize the existing pane. */
|
||||
historyLimit?: number;
|
||||
/** Remote execution metadata for local tmux sessions wrapping SSH */
|
||||
|
||||
@@ -577,11 +577,6 @@ export class Session extends EventEmitter {
|
||||
// the CLAUDE_CODE_EFFORT_LEVEL env var, which would hard-lock the session.
|
||||
private _effort: EffortLevel | undefined;
|
||||
|
||||
// Claude-only: request the plan-usage statusLine exporter for this session,
|
||||
// injected as an ephemeral `--settings` CLI flag at spawn (never written to
|
||||
// disk). Preserved across respawns like _effort above.
|
||||
private _statusLineTelemetry: boolean | undefined;
|
||||
|
||||
// tmux history-limit (scrollback lines) allocated when this session's pane is created.
|
||||
private readonly _tmuxHistoryLimit: number;
|
||||
|
||||
@@ -678,8 +673,6 @@ export class Session extends EventEmitter {
|
||||
envOverrides?: Record<string, string>;
|
||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||
effort?: EffortLevel;
|
||||
/** Claude-only: request the plan-usage statusLine exporter (ephemeral --settings flag, never disk-written) */
|
||||
statusLineTelemetry?: boolean;
|
||||
/** tmux history-limit (scrollback lines) allocated when this session's pane is created. */
|
||||
tmuxHistoryLimit?: number;
|
||||
/** Restored per-session attachment history. May include server-private external paths. */
|
||||
@@ -833,7 +826,6 @@ export class Session extends EventEmitter {
|
||||
if (config.effort && isEffortLevel(config.effort)) {
|
||||
this._effort = config.effort;
|
||||
}
|
||||
this._statusLineTelemetry = config.statusLineTelemetry;
|
||||
this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT;
|
||||
this._remote = config.remote;
|
||||
this._docker = config.docker;
|
||||
@@ -1754,7 +1746,6 @@ export class Session extends EventEmitter {
|
||||
resumeSessionId: this._resumeSessionId,
|
||||
envOverrides: this._envOverrides,
|
||||
effort: this._effort,
|
||||
statusLineTelemetry: this._statusLineTelemetry,
|
||||
historyLimit: this._tmuxHistoryLimit,
|
||||
remote: this._remote,
|
||||
docker: this._docker,
|
||||
@@ -2070,7 +2061,6 @@ export class Session extends EventEmitter {
|
||||
resumeSessionId: this._resumeSessionId,
|
||||
envOverrides: this._envOverrides,
|
||||
effort: this._effort,
|
||||
statusLineTelemetry: this._statusLineTelemetry,
|
||||
historyLimit: this._tmuxHistoryLimit,
|
||||
remote: this._remote,
|
||||
docker: this._docker,
|
||||
|
||||
+13
-11
@@ -67,7 +67,11 @@ import {
|
||||
legacyConfigForMode,
|
||||
} from './session-cli-registry-bridge.js';
|
||||
import type { CliEntry } from './config/cli-registry/types.js';
|
||||
import { resolveStatusLineCliCommand, findEffectiveUserStatusLineCommand } from './hooks-config.js';
|
||||
import {
|
||||
resolveStatusLineCliCommand,
|
||||
readPlanUsageTelemetryEnabled,
|
||||
findEffectiveUserStatusLineCommand,
|
||||
} from './hooks-config.js';
|
||||
import {
|
||||
buildSshConnectionArgs,
|
||||
defaultRemoteCommandForMode,
|
||||
@@ -1756,7 +1760,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
resumeSessionId,
|
||||
envOverrides,
|
||||
effort,
|
||||
statusLineTelemetry,
|
||||
historyLimit = DEFAULT_TMUX_HISTORY_LIMIT,
|
||||
remote,
|
||||
docker,
|
||||
@@ -1815,13 +1818,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
|
||||
|
||||
// Claude-only, local spawns only (remote/docker have their own separate
|
||||
// command builders — out of scope here). Also self-heals: strips any
|
||||
// legacy disk-written exporter from an older Codeman build the first
|
||||
// time a session starts in that workspace again.
|
||||
// Registry-gated (capabilities.statusLineTelemetry — claude only today), local
|
||||
// spawns only (remote/docker have their own separate command builders — out of
|
||||
// scope here). Also self-heals: strips any legacy disk-written exporter from an
|
||||
// older Codeman build the first time a session starts in that workspace again.
|
||||
const statusLineCommand =
|
||||
mode === 'claude' && !remote && !docker
|
||||
? await resolveStatusLineCliCommand(workingDir, statusLineTelemetry === true)
|
||||
getCli(mode)?.capabilities.statusLineTelemetry && !remote && !docker
|
||||
? await resolveStatusLineCliCommand(workingDir, await readPlanUsageTelemetryEnabled())
|
||||
: undefined;
|
||||
// The user's own REAL statusLine, if any (walked via Claude Code's own
|
||||
// settings precedence) — exported below so the shared exporter script
|
||||
@@ -2070,7 +2073,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
resumeSessionId,
|
||||
envOverrides,
|
||||
effort,
|
||||
statusLineTelemetry,
|
||||
remote,
|
||||
docker,
|
||||
name,
|
||||
@@ -2088,8 +2090,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
// See createSession()'s identical resolution for rationale.
|
||||
const statusLineCommand =
|
||||
mode === 'claude' && !remote && !docker
|
||||
? await resolveStatusLineCliCommand(workingDir, statusLineTelemetry === true)
|
||||
getCli(mode)?.capabilities.statusLineTelemetry && !remote && !docker
|
||||
? await resolveStatusLineCliCommand(workingDir, await readPlanUsageTelemetryEnabled())
|
||||
: undefined;
|
||||
const userStatusLineCommand = statusLineCommand ? await findEffectiveUserStatusLineCommand(workingDir) : undefined;
|
||||
|
||||
|
||||
@@ -1062,13 +1062,6 @@ Object.assign(CodemanApp.prototype, {
|
||||
...(hasEnvOverrides ? { envOverrides } : {}),
|
||||
...(effort ? { effort } : {}),
|
||||
...(modelOverride !== undefined ? { modelOverride } : {}),
|
||||
// Plan-usage statusLine exporter (App Settings → Display). The server
|
||||
// ADDS our exporter on create when true; when false it intentionally
|
||||
// leaves any existing exporter in place (a per-repo settings.local.json
|
||||
// is shared by sibling sessions, so create-with-false must not yank it
|
||||
// — see the comment in session-routes create). Disabling the setting
|
||||
// removes it via the App Settings toggle path (system-routes), not here.
|
||||
statusLineTelemetry: this.planUsageChipEnabled(globalSettings),
|
||||
})
|
||||
}).then(r => r.json())
|
||||
);
|
||||
|
||||
@@ -2271,22 +2271,26 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
// Save to server (includes notification prefs for cross-browser persistence).
|
||||
// Strip device-specific DISPLAY keys so they never sync across devices —
|
||||
// localEcho/cjk/extendedKeyboard/skin are per-platform, and showPlanUsageLimits
|
||||
// is per-device too (desktop can show the usage chip while mobile stays hidden).
|
||||
// localEcho/cjk/extendedKeyboard/skin are per-platform.
|
||||
// webglRendererEnabled is per-device as well (renderer choice is GPU-specific,
|
||||
// and syncing would leak mobile's hidden-checkbox false onto desktop); it's
|
||||
// also absent from SettingsUpdateSchema, which is .strict() — sending it
|
||||
// would 400 the whole settings PUT.
|
||||
// Telemetry COLLECTION is requested out-of-band via statusLineTelemetry (sent on
|
||||
// ENABLE only, so a device with the chip OFF never strips the exporter that
|
||||
// another device's chip depends on — see system-routes settings handler).
|
||||
// showPlanUsageLimits is the ONE exception to "per-device keys never sync":
|
||||
// its DISPLAY stays per-device (loadAppSettingsFromServer only seeds it into
|
||||
// localStorage when a device has no value yet — same as every other display
|
||||
// key), but it ALSO doubles as the server-side plan-usage telemetry
|
||||
// COLLECTION switch (readPlanUsageTelemetryEnabled in hooks-config.ts, read
|
||||
// fresh at every claude session create/respawn), so unlike the others it
|
||||
// MUST flow through in `serverSettings` below on every save — including
|
||||
// OFF, which used to be un-sendable under the old one-way "ENABLE only"
|
||||
// action field this replaces.
|
||||
const {
|
||||
localEchoEnabled: _leo,
|
||||
cjkInputEnabled: _cjk,
|
||||
extendedKeyboardBar: _ekb,
|
||||
skin: _skin,
|
||||
language: _language,
|
||||
showPlanUsageLimits: _pul,
|
||||
showAttachmentsButton: _ahb,
|
||||
showFileViewerButton: _fvb,
|
||||
webglRendererEnabled: _wgl,
|
||||
@@ -2316,7 +2320,6 @@ Object.assign(CodemanApp.prototype, {
|
||||
try {
|
||||
const res = await this._apiPut('/api/settings', {
|
||||
...serverSettings,
|
||||
...(settings.showPlanUsageLimits ? { statusLineTelemetry: true } : {}),
|
||||
notificationPreferences: notifPrefsToSave,
|
||||
voiceSettings,
|
||||
});
|
||||
@@ -2579,10 +2582,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Resolved per-device state of the plan-usage chip. Desktop defaults ON,
|
||||
// handhelds default OFF (the mobile block in getDefaultSettings() sets false,
|
||||
// and the mobile-header-buttons-policy guard depends on that staying false).
|
||||
// Single source of truth for THREE call sites that must never disagree: the
|
||||
// App Settings checkbox, the chip's visibility, and the statusLineTelemetry
|
||||
// flag sent on session create. A chip shown without telemetry renders "—"
|
||||
// forever, which is exactly the drift this helper prevents.
|
||||
// Single source of truth for the two call sites that must never disagree:
|
||||
// the App Settings checkbox and the chip's visibility. Telemetry COLLECTION
|
||||
// no longer has a THIRD client-side call site here at all — the server reads
|
||||
// this same persisted setting directly (readPlanUsageTelemetryEnabled in
|
||||
// hooks-config.ts), fresh, at every claude session create/respawn.
|
||||
planUsageChipEnabled(settings = null) {
|
||||
const s = settings ?? this.loadAppSettingsFromStorage();
|
||||
return s.showPlanUsageLimits ?? this.getDefaultSettings().showPlanUsageLimits ?? true;
|
||||
@@ -3074,11 +3078,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
'sessionLineageLines',
|
||||
]);
|
||||
// The plan-usage chip is a PER-DEVICE display setting (desktop default ON,
|
||||
// handheld default OFF): desktop can show it while mobile stays hidden. It
|
||||
// used to sync, so an older server.json may still carry a value — drop it
|
||||
// so the server value is NEVER
|
||||
// seeded into a device that didn't explicitly enable it (collection is handled
|
||||
// separately via the statusLineTelemetry action, not this display flag).
|
||||
// handheld default OFF): desktop can show it while mobile stays hidden. Drop
|
||||
// the server's stored value here so it is NEVER seeded into a device that
|
||||
// didn't explicitly enable it — even though this SAME setting also drives
|
||||
// server-side telemetry collection now (readPlanUsageTelemetryEnabled in
|
||||
// hooks-config.ts), that's a read the server does directly from settings.json
|
||||
// at spawn time; it has nothing to do with what gets merged into THIS
|
||||
// device's local display preference.
|
||||
delete appSettings.showPlanUsageLimits;
|
||||
// Merge settings: non-display keys always sync from server,
|
||||
// display keys only seed from server when localStorage has no value
|
||||
|
||||
@@ -947,18 +947,19 @@ export function registerSessionRoutes(
|
||||
await updateCaseModel(workingDir, body.modelOverride || null);
|
||||
}
|
||||
|
||||
// Plan-usage telemetry request (App Settings → header chip). NO LONGER a
|
||||
// disk write here — a settings.local.json statusLine took precedence over
|
||||
// the user's own global/project statusLine for ANY `claude` run in that
|
||||
// directory, including entirely outside Codeman, with no disclosure and no
|
||||
// way to undo it (real bug, found 2026-08-31). The request now flows
|
||||
// through as an ordinary session field (statusLineTelemetryRequested below)
|
||||
// and Session/TmuxManager resolve it into an EPHEMERAL `claude --settings`
|
||||
// CLI flag at actual spawn time (resolveStatusLineCliCommand in
|
||||
// hooks-config.ts) — never written to disk, so a plain `claude` run outside
|
||||
// Codeman is untouched. That resolution also self-heals: it strips any
|
||||
// legacy disk-written exporter an older Codeman build left behind.
|
||||
const statusLineTelemetryRequested = body.statusLineTelemetry === true;
|
||||
// Plan-usage telemetry (App Settings → header chip): no request-time field
|
||||
// here anymore, and NO disk write — a settings.local.json statusLine used
|
||||
// to take precedence over the user's own global/project statusLine for ANY
|
||||
// `claude` run in that directory, including entirely outside Codeman, with
|
||||
// no disclosure and no way to undo it (real bug, found 2026-08-31).
|
||||
// TmuxManager.createSession reads the persisted `showPlanUsageLimits`
|
||||
// setting FRESH at spawn (readPlanUsageTelemetryEnabled in hooks-config.ts)
|
||||
// and resolves it into an EPHEMERAL `claude --settings` CLI flag — never
|
||||
// written to disk, so a plain `claude` run outside Codeman is untouched —
|
||||
// and applies uniformly to every claude creation path (this route, cron,
|
||||
// the Ralph Loop API, quick-start), not just this one. That resolution
|
||||
// also self-heals: it strips any legacy disk-written exporter an older
|
||||
// Codeman build left behind.
|
||||
|
||||
// Hooks for the workspace this session runs in (install vs refresh-only is the
|
||||
// `workspaceHooksEnabled` setting; see applyWorkspaceHooks). Never for a remote
|
||||
@@ -1092,7 +1093,6 @@ export function registerSessionRoutes(
|
||||
resumeSessionId: validatedResumeId,
|
||||
envOverrides: await clampEnvOverridesForOwner(owner, body.envOverrides),
|
||||
effort: body.effort,
|
||||
statusLineTelemetry: statusLineTelemetryRequested,
|
||||
tmuxHistoryLimit: terminalHistoryConfig.tmuxHistoryLimit,
|
||||
remote,
|
||||
owner,
|
||||
|
||||
@@ -990,11 +990,9 @@ export function registerSystemRoutes(
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
// statusLineTelemetry and acknowledgeUnauthTunnel are ACTION fields (not stored
|
||||
// settings) — strip them before persisting so settings.json stays clean.
|
||||
// statusLineTelemetry is an action field only (see below); it must never
|
||||
// land in settingsToStore.
|
||||
const { statusLineTelemetry: _statusLineTelemetry, acknowledgeUnauthTunnel, ...settingsToStore } = settings;
|
||||
// acknowledgeUnauthTunnel is an ACTION field (not a stored setting) — strip
|
||||
// it before persisting so settings.json stays clean.
|
||||
const { acknowledgeUnauthTunnel, ...settingsToStore } = settings;
|
||||
const merged = { ...existing, ...settingsToStore };
|
||||
await fs.writeFile(SETTINGS_PATH, JSON.stringify(merged, null, 2));
|
||||
|
||||
@@ -1007,7 +1005,7 @@ export function registerSystemRoutes(
|
||||
// Service toggles resolve from `merged` (existing + incoming), NEVER from the
|
||||
// raw request body. A PARTIAL PUT omits keys it does not intend to change, and
|
||||
// reading the body directly turned every omission into "apply the default":
|
||||
// a body of just `{statusLineTelemetry:true}` would START the subagent watcher
|
||||
// a body of just `{showPlanUsageLimits:true}` would START the subagent watcher
|
||||
// (`?? true`) and STOP the workflow + image watchers (`?? false`), silently
|
||||
// undoing the user's persisted config. Reading `merged` makes any PUT reconcile
|
||||
// services to the effective stored settings instead, which also self-heals
|
||||
@@ -1033,19 +1031,23 @@ export function registerSystemRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
// Plan-usage chip: its DISPLAY is per-device (client-side, see settings-ui.js).
|
||||
// Telemetry COLLECTION was previously server-side and enable-sticky here —
|
||||
// toggling the chip ON re-injected a statusLine.command into every ACTIVE
|
||||
// Claude session's settings.local.json so live % started flowing without a
|
||||
// new session. That disk write is exactly the bug fixed 2026-08-31 (it took
|
||||
// precedence over the user's own statusline for ANY `claude` run in that
|
||||
// directory, including outside Codeman, with no way to undo it). Telemetry
|
||||
// is now requested per-session at CREATE/RESPAWN time only (statusLineTelemetry
|
||||
// threaded through cron/ralph-loop/quick-start/interactive-create, see those
|
||||
// route handlers), resolved into an ephemeral `--settings` CLI flag — fixed at
|
||||
// spawn, so there is nothing to (re)inject into an ALREADY-RUNNING session
|
||||
// here, unlike the old disk mechanism. The action field above is received and
|
||||
// discarded; flipping the chip ON only affects sessions created from now on.
|
||||
// Plan-usage chip: its DISPLAY is per-device (client-side, see settings-ui.js),
|
||||
// but `showPlanUsageLimits` ALSO doubles as the telemetry COLLECTION switch,
|
||||
// persisted here in settingsToStore like any other setting (no special-casing
|
||||
// needed — see readPlanUsageTelemetryEnabled's doc comment in hooks-config.ts).
|
||||
// Telemetry COLLECTION used to be a SEPARATE, action-only, sticky mechanism
|
||||
// here: toggling the chip ON re-injected a statusLine.command into every
|
||||
// ACTIVE Claude session's settings.local.json so live % started flowing
|
||||
// without a new session. That disk write was the bug fixed 2026-08-31 (it
|
||||
// took precedence over the user's own statusline for ANY `claude` run in
|
||||
// that directory, including outside Codeman, with no way to undo it).
|
||||
// Collection is now decided by TmuxManager.createSession/respawnPane reading
|
||||
// `showPlanUsageLimits` FRESH from settings.json at spawn time — no
|
||||
// per-session field, no per-request threading through cron/Ralph-loop/
|
||||
// quick-start/interactive-create (they all reach the same read), and no
|
||||
// (re)injection into an already-running session needed here: the NEXT
|
||||
// respawn (a Ralph cycle, `/clear`, a PTY-exit restart) already picks up
|
||||
// whatever this PUT just persisted.
|
||||
|
||||
// Handle tunnel toggle dynamically
|
||||
if ('tunnelEnabled' in settings) {
|
||||
|
||||
+4
-8
@@ -524,8 +524,6 @@ export const CreateSessionSchema = z.object({
|
||||
effort: effortLevelSchema,
|
||||
/** Model override to write to .claude/settings.local.json (e.g., "opus[1m]"). Empty string clears. */
|
||||
modelOverride: z.string().max(50).optional(),
|
||||
/** Inject the Claude statusLine source for the shared plan-usage chip. Claude sessions only; Codex is host-polled. */
|
||||
statusLineTelemetry: z.boolean().optional(),
|
||||
openCodeConfig: OpenCodeConfigSchema,
|
||||
codexConfig: CodexConfigSchema,
|
||||
geminiConfig: GeminiConfigSchema,
|
||||
@@ -1289,13 +1287,11 @@ export const SettingsUpdateSchema = z
|
||||
showFileBrowser: z.boolean().optional(),
|
||||
showSubagents: z.boolean().optional(),
|
||||
showMultiMonitorButton: z.boolean().optional(),
|
||||
// Doubles as the plan-usage telemetry COLLECTION switch, read fresh from
|
||||
// disk by readPlanUsageTelemetryEnabled() (hooks-config.ts) at every claude
|
||||
// session create/respawn — not just the chip's DISPLAY preference. See that
|
||||
// function's doc comment for why one persisted field serves both.
|
||||
showPlanUsageLimits: z.boolean().optional(),
|
||||
// Action field (NOT persisted as a setting): when true, (re)injects the
|
||||
// plan-usage statusLine exporter into active Claude sessions so live usage %
|
||||
// starts flowing. Sent on ENABLE only — the chip's DISPLAY is per-device
|
||||
// (client-side), but telemetry COLLECTION is server-side, so the per-device
|
||||
// toggle signals it out-of-band here rather than via showPlanUsageLimits.
|
||||
statusLineTelemetry: z.boolean().optional(),
|
||||
showRedrawButton: z.boolean().optional(),
|
||||
// Input
|
||||
gestureControlEnabled: z.boolean().optional(),
|
||||
|
||||
+4
-1
@@ -1450,7 +1450,10 @@ export class WebServer extends EventEmitter {
|
||||
// PER-DEVICE by the client (settings-ui.js applyHeaderVisibilitySettings). It
|
||||
// used to be server-revealed from a synced setting, but that leaked the desktop
|
||||
// choice onto mobile — display is now per-device only (like the response viewer).
|
||||
// Telemetry collection stays server-side via the statusLineTelemetry action.
|
||||
// Telemetry collection stays server-side, reading `showPlanUsageLimits` fresh
|
||||
// from settings.json at every claude session create/respawn (see
|
||||
// readPlanUsageTelemetryEnabled in hooks-config.ts) — the same setting this
|
||||
// display-visibility check reads, doing double duty.
|
||||
// Detached single-session ("solo") window: inject the target session id so
|
||||
// the client can enter solo mode even if a (network-first) service worker
|
||||
// later serves a cached shell. The client primarily detects solo mode from
|
||||
|
||||
Reference in New Issue
Block a user