pi-daybreak-param 0.1.1 → 0.1.3

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.
package/README.md CHANGED
@@ -10,16 +10,16 @@ Persistent `/daybreak` configuration and explicit `access_programs.cyber` reques
10
10
  pi -e ./src/index.ts --daybreak-cyber daybreak_blue
11
11
  ```
12
12
 
13
- Install from GitHub:
13
+ Install from npm:
14
14
 
15
15
  ```sh
16
- pi install git:github.com/MercuriusDream/pi-daybreak-param
16
+ pi install npm:pi-daybreak-param
17
17
  ```
18
18
 
19
- Once published to npm:
19
+ Or install from GitHub (choose one source, not both):
20
20
 
21
21
  ```sh
22
- pi install npm:pi-daybreak-param
22
+ pi install git:github.com/MercuriusDream/pi-daybreak-param
23
23
  ```
24
24
 
25
25
  For local development: `pi install /absolute/path/to/pi-daybreak-param`. Reload an active Pi session with `/reload` after installation.
@@ -43,6 +43,12 @@ Menu and command selections are saved atomically to `~/.pi/agent/daybreak.json`
43
43
 
44
44
  An absent config intentionally means inherit/default. A malformed or unreadable config is an error that blocks OpenAI requests—even with a valid CLI flag. Correct it explicitly using `/daybreak <selection>` or repair the file. Other providers are outside the wrapper's scope.
45
45
 
46
+ ## Footer display
47
+
48
+ When Pi runs in its interactive TUI, the extension uses Pi's supported `setFooter()` API to render **model • requested program • reasoning effort** on one line (for example `gpt-6-sol • daybreak blue • max`). No separate Daybreak extension-status line is added. `standard` appears when explicitly selected; `default` adds no label. Configuration errors appear inline as `daybreak config error`, with their full reason available from `/daybreak status` and the startup notification. The label is the **requested** selection, not proof the server granted Daybreak.
49
+
50
+ Pi does not expose an API to insert one segment into its built-in footer, so the extension replaces that footer in TUI mode and reproduces its basic path, usage, context, model, and other extensions' status lines. Other extensions replacing the footer can override this display (or vice versa). Non-interactive modes do not install a custom footer. `/reload` may be needed in an already-running session after updating the package.
51
+
46
52
  ## Why `/daybreak`, not `/settings`?
47
53
 
48
54
  Pi 0.99.1's built-in `/settings` has hardcoded rows and callbacks, with no supported extension setting registration API. `/daybreak` therefore provides its own supported selection dialog and persistent configuration, without monkey-patching Pi or replacing the built-in settings screen.
package/docs/research.md CHANGED
@@ -36,7 +36,7 @@ Uses `pi.on`, `registerFlag`, `registerCommand`, `ctx.ui.setStatus`, and a thin
36
36
  - Explicit `default` is no override; `standard` is explicit opt-out. Explicit selection preserves other top-level request fields and other access-program keys.
37
37
  - Invalid CLI configuration is retained as an error state that blocks OpenAI calls; it is not converted into default behavior. Invalid commands throw. Invalid payloads/endpoint/model identity throw when applying an explicit selection.
38
38
  - Selection changes wait for idle. Menu/command selections persist atomically in the agent directory's `daybreak.json`. Each `session_start` validates that file, then selects the CLI override or saved value. Invalid reads/writes block OpenAI requests; only a genuinely absent config has the intentional initial inherit state.
39
- - UI status is guarded by `ctx.hasUI`. CLI flag works in print/JSON/RPC modes; no custom terminal UI is needed.
39
+ - Pi's `setStatus` renders on a separate footer line and cannot place text between the model and thinking effort. In TUI mode the extension now uses the documented `ctx.ui.setFooter()` API to reproduce key footer content and render the requested cyber program inline between model and effort. It clears its old `daybreak-param` status, preserves other extension statuses, and shows config errors inline. It does not install a footer in print/JSON/RPC modes. Other custom-footer extensions may conflict; Pi has no supported compositional inline segment hook.
40
40
  - Scope: requests through the `openai` provider and `openai-responses` API. An explicit program requires the canonical `https://api.openai.com/v1` endpoint and matching payload model ID. Both auth methods are preserved. Other providers, including legacy Codex, are outside this registration.
41
41
  - Existing payload handlers run before this final transformation. `default` does not remove values configured in `samplingParams` or added elsewhere. Another provider replacement can conflict with this registration.
42
42
  - No automatic retries with another program/model, permission-error masking, approval claims, or removal of safeguards.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-daybreak-param",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Persistent /daybreak access-program selection for Pi's OpenAI and Sign in with ChatGPT requests",
6
6
  "license": "MIT",
@@ -12,6 +12,6 @@
12
12
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
13
13
  "keywords": ["pi-package"],
14
14
  "pi": { "extensions": ["./src/index.ts"] },
15
- "peerDependencies": { "@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-ai": "*" },
15
+ "peerDependencies": { "@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-ai": "*", "@earendil-works/pi-tui": "*" },
16
16
  "scripts": { "test": "bun test" }
17
17
  }
package/src/controller.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { Api, SimpleStreamOptions, StreamFunction } from "@earendil-works/pi-ai";
3
- import { isDirectOpenAI, MODES, parseMode, selectCyberProgram, type CyberMode } from "./payload.ts";
3
+ import { MODES, parseMode, selectCyberProgram, type CyberMode } from "./payload.ts";
4
+ import { createFooter } from "./footer.ts";
4
5
  import type { ModeStore } from "./config.ts";
5
6
 
6
7
  const LABELS: Record<CyberMode, string> = {
@@ -18,11 +19,12 @@ export function registerDaybreak(pi: ExtensionAPI, streamOpenAI: StreamFunction<
18
19
 
19
20
  let mode: CyberMode = "default";
20
21
  let configError: Error | undefined = new Error("Daybreak configuration has not been initialized. OpenAI request blocked.");
21
- const refreshStatus = (ctx: ExtensionContext) => {
22
- if (ctx.hasUI) {
23
- ctx.ui.setStatus("daybreak-param", configError ? "Cyber config: ERROR" :
24
- mode !== "default" && isDirectOpenAI(ctx.model) ? `Cyber request: ${mode} (not verified)` : undefined);
25
- }
22
+ const installFooter = (ctx: ExtensionContext) => {
23
+ if (ctx.mode !== "tui") return;
24
+ // Pi offers no public hook into one segment of its built-in footer. A
25
+ // custom footer is necessary for an inline model • program • effort label.
26
+ ctx.ui.setStatus("daybreak-param", undefined); // clear stale status from older versions
27
+ ctx.ui.setFooter((_tui, theme, data) => createFooter(ctx, theme, data, () => mode, () => configError));
26
28
  };
27
29
 
28
30
  pi.on("session_start", (_event, ctx) => {
@@ -42,22 +44,22 @@ export function registerDaybreak(pi: ExtensionAPI, streamOpenAI: StreamFunction<
42
44
  configError = new Error(`Daybreak configuration error: ${error instanceof Error ? error.message : String(error)} OpenAI requests are blocked until corrected.`);
43
45
  if (ctx.hasUI) ctx.ui.notify(configError.message, "error");
44
46
  }
45
- refreshStatus(ctx);
47
+ installFooter(ctx);
46
48
  });
47
- pi.on("model_select", (_event, ctx) => refreshStatus(ctx));
49
+ pi.on("model_select", (_event, ctx) => installFooter(ctx));
48
50
 
49
51
  pi.registerCommand("daybreak", {
50
52
  description: `Configure persistent cyber selection, or /daybreak status: ${MODES.join(" | ")}`,
51
53
  handler: async (args, ctx) => {
52
54
  let value = args.trim();
53
55
  if (value === "status") {
54
- ctx.ui.notify(configError?.message ?? `Cyber request selection: ${mode}. This is not confirmation of Daybreak access.`, configError ? "error" : "info");
56
+ ctx.ui.notify(configError?.message ?? `Cyber request selection: ${mode}.`, configError ? "error" : "info");
55
57
  return;
56
58
  }
57
59
  await ctx.waitForIdle();
58
60
  if (!value) {
59
61
  if (!ctx.hasUI) throw new Error("Use /daybreak <selection> or --daybreak-cyber in non-UI mode.");
60
- const selected = await ctx.ui.select(`Cyber request selection: ${configError ? "CONFIG ERROR" : mode} (access not verified)`, Object.values(LABELS));
62
+ const selected = await ctx.ui.select(`Cyber request selection: ${configError ? "CONFIG ERROR" : mode}`, Object.values(LABELS));
61
63
  if (selected === undefined) return; // Explicit user cancellation changes nothing.
62
64
  const entry = Object.entries(LABELS).find(([, label]) => label === selected);
63
65
  if (!entry) throw new Error("Invalid Daybreak menu selection.");
@@ -69,12 +71,10 @@ export function registerDaybreak(pi: ExtensionAPI, streamOpenAI: StreamFunction<
69
71
  store.write(next);
70
72
  } catch (error) {
71
73
  configError = new Error(`Failed to save Daybreak configuration: ${error instanceof Error ? error.message : String(error)} OpenAI requests are blocked until corrected.`);
72
- refreshStatus(ctx);
73
74
  throw configError;
74
75
  }
75
76
  mode = next;
76
77
  configError = undefined;
77
- refreshStatus(ctx);
78
78
  ctx.ui.notify(next === "default"
79
79
  ? "Saved: inherit. Extension override disabled; existing payload and server defaults are unchanged."
80
80
  : `Saved: request ${next} on OpenAI Responses calls. OpenAI still checks model compatibility and your approved access.`, "info");
package/src/footer.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import type { ExtensionContext, ReadonlyFooterDataProvider } from "@earendil-works/pi-coding-agent";
4
+ import type { Theme } from "@earendil-works/pi-coding-agent";
5
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
6
+ import type { CyberMode } from "./payload.ts";
7
+
8
+ const DISPLAY: Record<Exclude<CyberMode, "default">, string> = {
9
+ standard: "standard",
10
+ daybreak_blue: "daybreak blue",
11
+ daybreak_red: "daybreak red",
12
+ };
13
+
14
+ export function modelLabel(model: ExtensionContext["model"], thinking: string | undefined, mode: CyberMode, error?: Error): string {
15
+ const slug = model?.id ?? "no-model";
16
+ const program = model?.provider === "openai" && error ? " • daybreak config error" :
17
+ model?.provider === "openai" && mode !== "default" ? ` • ${DISPLAY[mode]}` : "";
18
+ const effort = model?.reasoning ? ` • ${thinking && thinking !== "off" ? thinking : "thinking off"}` : "";
19
+ return `${slug}${program}${effort}`;
20
+ }
21
+
22
+ function tokens(value: number): string {
23
+ if (value < 1000) return `${value}`;
24
+ if (value < 10000) return `${(value / 1000).toFixed(1)}k`;
25
+ if (value < 1000000) return `${Math.round(value / 1000)}k`;
26
+ if (value < 10000000) return `${(value / 1000000).toFixed(1)}M`;
27
+ return `${Math.round(value / 1000000)}M`;
28
+ }
29
+
30
+ function cwdLabel(cwd: string): string {
31
+ const home = homedir();
32
+ const relativePath = relative(resolve(home), resolve(cwd));
33
+ return relativePath === "" ? "~" :
34
+ relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath)
35
+ ? `~${sep}${relativePath}` : cwd;
36
+ }
37
+
38
+ function cleanStatus(text: string): string {
39
+ return text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
40
+ }
41
+
42
+ export function createFooter(ctx: ExtensionContext, theme: Theme, data: ReadonlyFooterDataProvider, getMode: () => CyberMode, getError: () => Error | undefined) {
43
+ return {
44
+ invalidate() {},
45
+ render(width: number): string[] {
46
+ if (width <= 0) return [];
47
+ let path = cwdLabel(ctx.sessionManager.getCwd());
48
+ const branch = data.getGitBranch();
49
+ if (branch) path += ` (${branch})`;
50
+ const name = ctx.sessionManager.getSessionName();
51
+ if (name) path += ` • ${name}`;
52
+ const entries = ctx.sessionManager.getEntries();
53
+ let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
54
+ for (const entry of entries) {
55
+ if (entry.type !== "message") continue;
56
+ const message = entry.message;
57
+ if (message.role !== "assistant" && message.role !== "toolResult") continue;
58
+ const usage = message.usage;
59
+ if (!usage) continue;
60
+ input += usage.input;
61
+ output += usage.output;
62
+ cacheRead += usage.cacheRead;
63
+ cacheWrite += usage.cacheWrite;
64
+ cost += usage.cost.total;
65
+ }
66
+ const stats = [
67
+ input ? `↑${tokens(input)}` : "",
68
+ output ? `↓${tokens(output)}` : "",
69
+ cacheRead ? `R${tokens(cacheRead)}` : "",
70
+ cacheWrite ? `W${tokens(cacheWrite)}` : "",
71
+ cost ? `$${cost.toFixed(3)}` : "",
72
+ ].filter(Boolean);
73
+ const context = ctx.getContextUsage();
74
+ stats.push(`${context?.percent == null ? "?" : `${context.percent.toFixed(1)}%`}/${tokens(context?.contextWindow ?? ctx.model?.contextWindow ?? 0)}`);
75
+ const left = stats.join(" ");
76
+ let right = modelLabel(ctx.model, ctx.thinkingLevel, getMode(), getError());
77
+ if (data.getAvailableProviderCount() > 1 && ctx.model) right = `(${ctx.model.provider}) ${right}`;
78
+ const available = Math.max(0, width - visibleWidth(left) - 2);
79
+ if (visibleWidth(right) > available) right = truncateToWidth(right, available, "");
80
+ const padding = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
81
+ const lines = [
82
+ truncateToWidth(theme.fg("dim", path), width, ""),
83
+ truncateToWidth(theme.fg("dim", left + padding + right), width, ""),
84
+ ];
85
+ const statuses = [...data.getExtensionStatuses().entries()]
86
+ .filter(([key]) => key !== "daybreak-param")
87
+ .sort(([a], [b]) => a.localeCompare(b))
88
+ .map(([, text]) => cleanStatus(text));
89
+ if (statuses.length) lines.push(truncateToWidth(statuses.join(" "), width, ""));
90
+ return lines;
91
+ },
92
+ };
93
+ }