@promptctl/cc-candybar 1.34.1 → 1.36.0

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.
@@ -38,7 +38,7 @@ import {
38
38
  PROTOCOL_VERSION,
39
39
  encodeFrame,
40
40
  makeFrameReader,
41
- sanitizeTermCols,
41
+ parseClientHints,
42
42
  } from "./protocol";
43
43
  import type { Request, Response } from "./protocol";
44
44
  import { GitDataProvider } from "./cache/git";
@@ -836,17 +836,21 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
836
836
  req.cwd,
837
837
  sessionConfigFile,
838
838
  );
839
- // [LAW:single-enforcer] Width capture lives at the wire boundary.
840
- // The client (Rust + TTY) is the only process that can see the real
841
- // terminal; the daemon is detached. We do NOT consult getTerminalWidth's
842
- // env/stderr fallbacks here — they would let the daemon's stale
843
- // launch-time COLUMNS env shape rendering for a different terminal,
844
- // which is exactly the wrong source.
839
+ // [LAW:parse-dont-validate] The ONE checkpoint for everything the client
840
+ // observed and the daemon cannot. Raw `req.*` hint fields are not read
841
+ // past this line; `hints` is the stamped type the render path consumes.
842
+ //
843
+ // [LAW:single-enforcer] Every hint is captured client-side because the
844
+ // daemon is detached and shared: its env answers for whichever shell
845
+ // spawned it. We do NOT consult getTerminalWidth's env/stderr fallbacks
846
+ // for width, and we do NOT consult SSH_* for remoteness — both would
847
+ // describe a different session than the one being rendered.
845
848
  // [LAW:one-source-of-truth] Both branches feed raw cols through
846
849
  // applyClaudeCodeReserve, so `width` always means "usable cells
847
850
  // post-reserve" with no semantic split between wire-supplied and
848
851
  // fallback values.
849
- const termCols = sanitizeTermCols(req.termCols);
852
+ const hints = parseClientHints(req);
853
+ const termCols = hints.termCols;
850
854
  const width = applyClaudeCodeReserve(termCols ?? DEFAULT_TERMINAL_WIDTH);
851
855
  const renderOpts: BuildLineOptions = { ...RENDER_OPTS_BASE, width };
852
856
  // [LAW:dataflow-not-control-flow] Two outcomes fall out of one rule:
@@ -926,6 +930,7 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
926
930
  req.cwd,
927
931
  entry.state.neededInputPaths,
928
932
  effective,
933
+ hints,
929
934
  );
930
935
  // [LAW:one-source-of-truth][LAW:dataflow-not-control-flow] basePalette
931
936
  // is derived from the same effective theme resolved above — so a theme
package/src/index.ts CHANGED
@@ -36,6 +36,33 @@ function detectTermCols(): number | undefined {
36
36
  return undefined;
37
37
  }
38
38
 
39
+ // The env vars an SSH login shell inherits from sshd. Any one of them present
40
+ // and non-empty means this session arrived over the network.
41
+ //
42
+ // [LAW:one-source-of-truth] This vocabulary is mirrored by the Rust client
43
+ // (rust-client/src/main.rs) and diffed by scripts/check-protocol.mjs, which
44
+ // anchors on the declaration below — keep it a named const holding string
45
+ // literals, or repoint the CHECKS row in the same commit. Both runtimes must
46
+ // agree on what "SSH" means or the fast path and the fallback path would
47
+ // disagree about the same session.
48
+ //
49
+ // All three are checked, not just SSH_CONNECTION: SSH_CLIENT is what older
50
+ // sshd builds (and the user's git-taculous zsh theme) key on, and SSH_TTY is
51
+ // the one that survives some `sudo` env_keep policies. Extra names can only
52
+ // widen recall of a fact that is otherwise reported as a plain `false`.
53
+ const SSH_ENV_VARS = ["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"] as const;
54
+
55
+ // [LAW:dataflow-not-control-flow] A fold over the vocabulary, not a chain of
56
+ // ifs — adding a name is a data edit.
57
+ //
58
+ // Unlike detectTermCols this is TOTAL: the client reads its own environment, so
59
+ // "no SSH var set" is the affirmative answer "local", never a failure to
60
+ // determine. It therefore always reports, and the daemon reads an ABSENT `ssh`
61
+ // hint as "this client is too old to answer" rather than as "local".
62
+ function detectSsh(): boolean {
63
+ return SSH_ENV_VARS.some((name) => (process.env[name] ?? "") !== "");
64
+ }
65
+
39
66
  function showHelpText(): void {
40
67
  console.log(HELP_TEXT);
41
68
  }
@@ -134,15 +161,18 @@ echo '{"session_id":"test-session","workspace":{"project_dir":"/path/to/project"
134
161
  // caches). On daemon miss we spawn detached and emit empty output; the
135
162
  // next status-line refresh hits the warm daemon and renders for real.
136
163
  //
137
- // [LAW:single-enforcer] Terminal width is captured here, in the user's
164
+ // [LAW:single-enforcer] Client hints are captured here, in the user's
138
165
  // shell environment, then trusted by the daemon. The daemon's own env
139
- // reflects whichever shell launched it minutes/hours ago, so it can't
140
- // measure the active terminal only the live client can.
166
+ // reflects whichever shell launched it minutes/hours ago, so it can
167
+ // measure neither the active terminal nor whether THIS session came in
168
+ // over SSH — only the live client can. One daemon serves a local session
169
+ // and an SSH session at the same time, so the answer genuinely differs per
170
+ // request.
141
171
  const outcome = await tryRenderViaDaemon(
142
172
  hookData,
143
173
  process.argv,
144
174
  process.cwd(),
145
- detectTermCols(),
175
+ { termCols: detectTermCols(), ssh: detectSsh() },
146
176
  );
147
177
  // [LAW:types-are-the-program] Three variants, one per outcome kind. The
148
178
  // "kick on every failure" pattern was the load-bearing half of the