@bubblebrain-ai/bubble 0.0.22 → 0.0.24

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
@@ -1,38 +1,37 @@
1
1
  # Bubble
2
2
 
3
- Bubble is a terminal coding agent for working inside local project folders. It can read and edit files, run commands with approval controls, use project skills, connect MCP tools, and keep persistent memory across sessions.
3
+ Bubble is a terminal coding agent. It works inside a local project folder: it reads and edits files, runs commands behind configurable approval controls, searches and navigates code with language-server intelligence, browses the web, loads reusable skills, connects MCP tools, fans work out to subagents, and keeps persistent memory across sessions.
4
+
5
+ It is provider-agnostic. Bring an API key for OpenAI, Anthropic, Google, DeepSeek, Moonshot/Kimi, Zhipu, Z.AI, MiniMax, Groq, Together, Fireworks, a local OpenAI-compatible endpoint, and more — or sign in to ChatGPT with OAuth and drive the Codex models directly.
6
+
7
+ ---
4
8
 
5
9
  ## Requirements
6
10
 
7
- - Node.js 20+ and npm for installation
8
- - Bun for running Bubble
11
+ - Node.js 20 or newer (used to install the launcher)
12
+ - [Bun](https://bun.sh) (used to run the agent)
9
13
 
10
- Install Bun if it is not already available:
14
+ Install Bun if you do not already have it:
11
15
 
12
16
  ```bash
13
17
  curl -fsSL https://bun.sh/install | bash
14
18
  ```
15
19
 
16
- ## Install
20
+ The `npm install` step puts a small Node.js launcher named `bubble` on your PATH. When you run `bubble`, the launcher locates Bun and starts the real runtime under it. If Bun is missing, it prints the install command above instead of failing with a low-level error.
17
21
 
18
- From npm:
22
+ ## Install
19
23
 
20
24
  ```bash
21
25
  npm install -g @bubblebrain-ai/bubble
22
26
  ```
23
27
 
24
- From a local package tarball:
28
+ To install from a local tarball:
25
29
 
26
30
  ```bash
27
- npm install -g ./bubblebrain-ai-bubble-0.0.3.tgz
31
+ npm install -g ./bubblebrain-ai-bubble-<version>.tgz
28
32
  ```
29
33
 
30
- The npm command installs a small Node.js launcher named `bubble`. When you run
31
- `bubble`, the launcher checks for Bun and starts the real Bubble runtime with
32
- `bun`. If Bun is missing, it prints the install command above instead of failing
33
- with a low-level runtime error.
34
-
35
- ## Usage
34
+ ## Quick start
36
35
 
37
36
  Start Bubble in the current directory:
38
37
 
@@ -40,31 +39,192 @@ Start Bubble in the current directory:
40
39
  bubble
41
40
  ```
42
41
 
43
- Start Bubble for a specific project:
42
+ On first launch, connect a model:
43
+
44
+ - Run `/login` to sign in to ChatGPT (OAuth) and use the Codex models, or
45
+ - Run `/provider` to add any other provider with an API key.
46
+
47
+ Then just type what you want done. Bubble plans, edits files, and runs commands, asking for approval where the current permission mode requires it.
48
+
49
+ Point Bubble at a different project:
44
50
 
45
51
  ```bash
46
52
  bubble --cwd /path/to/project
47
53
  ```
48
54
 
49
- Show CLI options:
55
+ Resume your last conversation:
50
56
 
51
57
  ```bash
52
- bubble --help
58
+ bubble --resume
59
+ ```
60
+
61
+ ## Model providers
62
+
63
+ Bubble ships with a catalog of built-in providers. Configure them inside the app — no environment variables required.
64
+
65
+ | How | What it does |
66
+ | --- | --- |
67
+ | `/login` | OAuth sign-in for ChatGPT; unlocks the OpenAI Codex models without an API key. |
68
+ | `/provider` | Open a picker to connect, switch, add, or remove a provider. |
69
+ | `/key <provider> <key>` | Set the API key for a provider. |
70
+ | `/model` | Pick the active model and reasoning effort. |
71
+
72
+ Built-in providers include OpenAI, Anthropic, Google, DeepSeek, Moonshot (CN and international), Kimi for Coding, Zhipu AI, Z.AI, Alibaba DashScope, MiniMax, StepFun, Groq, Together AI, Fireworks, and a `local` profile for any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, etc.).
73
+
74
+ ### Custom providers and models
75
+
76
+ For full control — custom base URLs, self-hosted gateways, extra models, or pinning a protocol — define providers in `~/.bubble/models.json`:
77
+
78
+ ```json
79
+ {
80
+ "providers": {
81
+ "my-gateway": {
82
+ "baseURL": "https://gateway.internal/v1",
83
+ "apiKey": "sk-...",
84
+ "protocol": "openai-chat",
85
+ "models": [
86
+ { "id": "my-model", "name": "My Model" }
87
+ ]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ `protocol` accepts `openai-chat` (default) or `anthropic-messages`. Entries in `models.json` take precedence over the built-in catalog.
94
+
95
+ ### Reasoning effort
96
+
97
+ Models that support it expose a reasoning-effort control: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Set it with `/model <id> --reasoning-effort <level>` in the app, or at launch with `--reasoning` (medium) or `--reasoning-effort <level>`.
98
+
99
+ ## Permission modes
100
+
101
+ Bubble gates risky actions behind a permission mode. Press `Tab` to cycle modes during a session, or set a default with `--plan` / `--dangerously-skip-permissions` / project settings.
102
+
103
+ | Mode | Behavior |
104
+ | --- | --- |
105
+ | Default (Build) | File edits and writes auto-approve; bash and other tools prompt unless covered by an allow rule. |
106
+ | Plan | Read-only investigation. The agent proposes a plan and waits for your approval before making changes. |
107
+ | Bypass | Auto-approves every tool and disables all safety prompts. Enable deliberately with `--dangerously-skip-permissions`. |
108
+
109
+ Allow/deny rules are configured per scope and persisted across sessions. Manage them in-app with `/permissions`, or edit the settings files directly:
110
+
111
+ - `~/.bubble/settings.json` — user scope (applies everywhere)
112
+ - `<project>/.bubble/settings.json` — project scope (commit to share with your team)
113
+ - `<project>/.bubble/settings.local.json` — local overrides (gitignore)
114
+
115
+ Rules use a simple pattern syntax, for example:
116
+
117
+ ```json
118
+ {
119
+ "permissions": {
120
+ "defaultMode": "default",
121
+ "allow": [
122
+ "Bash(git status)",
123
+ "Bash(npm run:*)",
124
+ "Read(./src/**)",
125
+ "WebFetch(domain:github.com)"
126
+ ],
127
+ "deny": ["Read(~/.ssh/**)"]
128
+ }
129
+ }
53
130
  ```
54
131
 
55
- ## Configuration
132
+ ## What Bubble can do
56
133
 
57
- Bubble stores user configuration, sessions, permissions, skills, and memory under:
134
+ **Files and code.** Read, write, and make targeted edits; find files by glob; search contents with ripgrep; and navigate with language-server operations (go-to-definition, find references, hover, document/workspace symbols, call hierarchy).
135
+
136
+ **Shell and dev servers.** Run bounded bash commands with streaming output, and start/stop/inspect long-running dev servers (`npm run dev`, Vite, Next, etc.) with readiness checks and captured logs.
137
+
138
+ **Web.** Search the web and fetch/extract page contents on demand.
139
+
140
+ **Skills.** Drop reusable instructions and assets into a `SKILL.md`-based directory and invoke them with `/<skill-name>`. Bubble discovers skills from `~/.bubble/skills`, `~/.agents/skills`, `~/.claude/skills`, and the project's `.bubble/skills`. Browse them with `/skills`.
141
+
142
+ **MCP tools.** Connect Model Context Protocol servers (stdio, HTTP, or SSE) under the `mcpServers` key of any settings file. Their tools and prompts become available to the agent; manage connections with `/mcp`.
143
+
144
+ **Subagents.** Bubble can spawn background subagents with independent context, send them follow-ups, wait on their results, and fan a task out across a team — with concurrency limits and token budgets. Define custom agent profiles in `~/.bubble/agents` or a project's `.bubble/agents`.
145
+
146
+ **Persistent memory.** A background pipeline distills durable facts, preferences, and decisions from past sessions and recalls them later. Inspect and maintain it with `/memory status`, `/memory search <query>`, and `/memory refresh`.
147
+
148
+ **Sessions.** Every conversation is saved. Resume the latest with `bubble --resume`, or browse and switch sessions in-app with `/session`. Use `/rewind` to roll the conversation — and optionally your file edits — back to an earlier point.
149
+
150
+ ## Useful slash commands
151
+
152
+ | Command | Description |
153
+ | --- | --- |
154
+ | `/help` | List available commands. |
155
+ | `/model` | Switch model and reasoning effort. |
156
+ | `/provider`, `/login`, `/logout`, `/key` | Connect and manage providers. |
157
+ | `/session`, `/rewind`, `/clear` | Manage conversation history. |
158
+ | `/skills` | Open the searchable skills picker. |
159
+ | `/mcp` | List or reconnect MCP servers. |
160
+ | `/memory` | Inspect and maintain persistent memory. |
161
+ | `/permissions` | View or edit allow/deny rules. |
162
+ | `/context`, `/stats`, `/compact` | Inspect context usage, model stats, and compact the session. |
163
+ | `/lsp`, `/hooks` | Manage language servers and lifecycle hooks. |
164
+ | `/theme`, `/sidebar` | Adjust the interface. |
165
+ | `/feedback` | Send feedback or report a bug. |
166
+
167
+ ## Non-interactive mode
168
+
169
+ Run a single prompt and print the result — useful for scripts and pipelines:
170
+
171
+ ```bash
172
+ bubble --print "summarize what this repo does"
173
+ echo "fix the failing test" | bubble --print
174
+ ```
175
+
176
+ Combine with `--model`, `--cwd`, and `--plan` as needed.
177
+
178
+ ## CLI reference
58
179
 
59
180
  ```text
60
- ~/.bubble
181
+ Usage:
182
+ bubble [options] [prompt] Start interactive TUI
183
+ bubble update [--check] Update to the latest version (alias: upgrade)
184
+ bubble serve --feishu [options] Run as a Feishu bot host
185
+
186
+ Options (default):
187
+ -m, --model <model> Model to use
188
+ --cwd <dir> Working directory (default: current)
189
+ -k, --api-key <key> API key for the active provider
190
+ -r, --resume Resume a previous session (latest by default)
191
+ --session <name> Session name to create or resume
192
+ --reasoning Enable reasoning mode at medium effort
193
+ --reasoning-effort <l> Set reasoning effort: off|minimal|low|medium|high|xhigh|max
194
+ --plan Start in plan mode (read-only investigation; propose before executing)
195
+ --dangerously-skip-permissions
196
+ Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
197
+ -p, --print Non-interactive mode (single prompt)
198
+ -v, --version Print the installed version and exit
199
+ -h, --help Show this help
61
200
  ```
62
201
 
63
- In the app, use `/login` or provider commands to configure model access.
202
+ Run `bubble update` at any time to upgrade to the latest published version.
203
+
204
+ ## Configuration and storage
64
205
 
65
- ### ChatGPT Network Configuration
206
+ Bubble keeps everything under `~/.bubble`:
207
+
208
+ ```text
209
+ ~/.bubble/
210
+ config.json User preferences (theme, default model, recent models)
211
+ models.json Custom providers and models
212
+ auth.json OAuth credentials (file mode 0600)
213
+ settings.json User-scope permissions, MCP servers, hooks
214
+ sessions/ Saved conversations, grouped by project directory
215
+ memories/ Persistent memory store
216
+ skills/ User skills
217
+ agents/ Custom subagent profiles
218
+ ```
66
219
 
67
- ChatGPT OAuth and GPT/Codex requests respect standard proxy variables:
220
+ Environment variables:
221
+
222
+ - `BUBBLE_HOME` — override the data directory (defaults to `~/.bubble`).
223
+ - `BUBBLE_DEV=1` — use `~/.bubble-dev` instead, for development.
224
+
225
+ ### Network configuration
226
+
227
+ ChatGPT OAuth and GPT/Codex requests respect the standard proxy variables:
68
228
 
69
229
  ```bash
70
230
  export HTTPS_PROXY=http://proxy.example.com:8080
@@ -72,28 +232,31 @@ export HTTP_PROXY=http://proxy.example.com:8080
72
232
  export NO_PROXY=localhost,127.0.0.1
73
233
  ```
74
234
 
75
- If your network uses a corporate or custom HTTPS CA, start Bubble with:
235
+ If your network uses a corporate or custom HTTPS CA:
76
236
 
77
237
  ```bash
78
238
  NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
79
239
  ```
80
240
 
81
- You can also use `BUBBLE_EXTRA_CA_CERTS` for Bubble's ChatGPT requests:
241
+ `BUBBLE_EXTRA_CA_CERTS` applies the same trust to Bubble's ChatGPT requests specifically. Do not disable TLS verification with `NODE_TLS_REJECT_UNAUTHORIZED=0`.
242
+
243
+ ## Development
82
244
 
83
245
  ```bash
84
- BUBBLE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
246
+ bun install # install dependencies
247
+ npm run build # compile TypeScript to dist/
248
+ npm test # run the test suite (vitest)
249
+ npm start # run the built agent
85
250
  ```
86
251
 
87
- Do not disable TLS verification with `NODE_TLS_REJECT_UNAUTHORIZED=0`.
252
+ `npm run dev` compiles and launches in one step. The TUI is built on [OpenTUI](https://github.com/anomalyco/opentui) and Solid.
88
253
 
89
- ## Memory
254
+ ## Feishu host (optional)
90
255
 
91
- Bubble maintains persistent memory automatically from prior sessions. Useful commands:
256
+ Bubble can also run as a Feishu (Lark) bot host:
92
257
 
93
- ```text
94
- /memory status
95
- /memory search <query>
96
- /memory refresh
258
+ ```bash
259
+ bubble serve --feishu --setup
97
260
  ```
98
261
 
99
- Memory is maintained by a background pipeline and can be refreshed manually when you want new session information to be indexed immediately.
262
+ `--setup` runs the binding wizard, `--kill-old` replaces a conflicting instance for the same App ID, and `--dry-run` connects once and exits as a smoke test.
@@ -54,17 +54,37 @@ export function sanitizeAssistantProviderMetadata(metadata) {
54
54
  if (!metadata || !anthropic || !blocks?.length)
55
55
  return metadata;
56
56
  let changed = false;
57
- const sanitizedBlocks = blocks.map((block) => {
58
- if (block.type !== "text" || typeof block.text !== "string") {
59
- return block;
57
+ const sanitizedBlocks = [];
58
+ for (const block of blocks) {
59
+ // Plaintext text blocks are unsigned, so rewriting them in place is safe.
60
+ if (block.type === "text" && typeof block.text === "string") {
61
+ const sanitizedText = sanitizeInternalReminderBlocks(block.text);
62
+ if (sanitizedText !== block.text) {
63
+ changed = true;
64
+ sanitizedBlocks.push({ ...block, text: sanitizedText });
65
+ }
66
+ else {
67
+ sanitizedBlocks.push(block);
68
+ }
69
+ continue;
60
70
  }
61
- const sanitizedText = sanitizeInternalReminderBlocks(block.text);
62
- if (sanitizedText === block.text) {
63
- return block;
71
+ // Extended-thinking blocks carry an Anthropic signature over their exact
72
+ // text; rewriting the text would invalidate the signature and the API
73
+ // would reject the replayed block. So when a thinking block's text carries
74
+ // internal markup (e.g. an echoed system reminder), DROP the whole block
75
+ // rather than mutate it. Thinking text is never user-visible — the display
76
+ // path renders message.reasoning, not contentBlocks — so dropping loses
77
+ // nothing on screen; it only keeps the verbatim reminder out of the
78
+ // persisted metadata and the Anthropic replay payload. redacted_thinking
79
+ // holds encrypted `data` (no plaintext field) and cannot carry a reminder.
80
+ if (block.type === "thinking" && typeof block.thinking === "string") {
81
+ if (sanitizeInternalReminderBlocks(block.thinking) !== block.thinking) {
82
+ changed = true;
83
+ continue;
84
+ }
64
85
  }
65
- changed = true;
66
- return { ...block, text: sanitizedText };
67
- });
86
+ sanitizedBlocks.push(block);
87
+ }
68
88
  if (!changed)
69
89
  return metadata;
70
90
  return {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pure parser for the `/goal` slash command.
3
+ *
4
+ * Forms:
5
+ * /goal -> show summary
6
+ * /goal <objective> [--budget N] -> set a new goal
7
+ * /goal clear | pause | resume
8
+ * /goal edit <new objective>
9
+ *
10
+ * --budget accepts plain integers and k/m suffixes: 200000, 200k, 1.5m.
11
+ */
12
+ export type GoalCommandKind = "show" | "set" | "clear" | "pause" | "resume" | "edit";
13
+ export interface GoalCommand {
14
+ kind: GoalCommandKind;
15
+ objective?: string;
16
+ tokenBudget?: number;
17
+ error?: string;
18
+ }
19
+ export declare function parseGoalCommand(input: string): GoalCommand;
20
+ export declare function parseBudgetValue(raw: string): number | undefined;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Pure parser for the `/goal` slash command.
3
+ *
4
+ * Forms:
5
+ * /goal -> show summary
6
+ * /goal <objective> [--budget N] -> set a new goal
7
+ * /goal clear | pause | resume
8
+ * /goal edit <new objective>
9
+ *
10
+ * --budget accepts plain integers and k/m suffixes: 200000, 200k, 1.5m.
11
+ */
12
+ const SUBCOMMANDS = new Set(["clear", "pause", "resume", "edit"]);
13
+ export function parseGoalCommand(input) {
14
+ const body = input.trim().replace(/^\/goal\b/, "").trim();
15
+ if (!body)
16
+ return { kind: "show" };
17
+ const firstToken = body.split(/\s+/, 1)[0].toLowerCase();
18
+ const rest = body.slice(firstToken.length).trim();
19
+ if (SUBCOMMANDS.has(firstToken)) {
20
+ if (firstToken === "edit") {
21
+ if (!rest)
22
+ return { kind: "edit", error: "Usage: /goal edit <new objective>" };
23
+ const { text, tokenBudget, error } = extractBudget(rest);
24
+ if (error)
25
+ return { kind: "edit", error };
26
+ const objective = text.trim();
27
+ if (!objective)
28
+ return { kind: "edit", error: "Usage: /goal edit <new objective>" };
29
+ return { kind: "edit", objective, tokenBudget };
30
+ }
31
+ // clear / pause / resume take no arguments.
32
+ if (rest)
33
+ return { kind: firstToken, error: `/goal ${firstToken} takes no arguments` };
34
+ return { kind: firstToken };
35
+ }
36
+ // Anything else is a new objective.
37
+ const { text, tokenBudget, error } = extractBudget(body);
38
+ if (error)
39
+ return { kind: "set", error };
40
+ const objective = text.trim();
41
+ if (!objective)
42
+ return { kind: "set", error: "Usage: /goal <objective> [--budget N]" };
43
+ return { kind: "set", objective, tokenBudget };
44
+ }
45
+ function extractBudget(s) {
46
+ const match = s.match(/--budget(?:=|\s+)(\S+)/);
47
+ if (!match || match.index === undefined)
48
+ return { text: s };
49
+ const value = parseBudgetValue(match[1]);
50
+ if (value === undefined || value <= 0) {
51
+ return { text: s, error: `Invalid --budget value: "${match[1]}" (use e.g. 200000, 200k, 1.5m)` };
52
+ }
53
+ const text = (s.slice(0, match.index) + s.slice(match.index + match[0].length))
54
+ .replace(/\s+/g, " ")
55
+ .trim();
56
+ return { text, tokenBudget: value };
57
+ }
58
+ export function parseBudgetValue(raw) {
59
+ const match = raw.trim().match(/^(\d+(?:\.\d+)?)([kmKM]?)$/);
60
+ if (!match)
61
+ return undefined;
62
+ let value = parseFloat(match[1]);
63
+ if (!Number.isFinite(value))
64
+ return undefined;
65
+ const suffix = match[2].toLowerCase();
66
+ if (suffix === "k")
67
+ value *= 1_000;
68
+ else if (suffix === "m")
69
+ value *= 1_000_000;
70
+ return Math.round(value);
71
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure decision logic for the goal auto-continuation loop.
3
+ *
4
+ * Kept out of the TUI so the stop conditions can be unit-tested directly. The
5
+ * TUI calls shouldContinueGoal() after each goal turn finishes and either fires
6
+ * another turn or stops with the returned reason.
7
+ *
8
+ * The agent decides when the work is done — there is intentionally NO turn-count
9
+ * cap (unlike a fixed iteration limit). The loop only stops when:
10
+ * - the model marks the goal complete/blocked (via update_goal),
11
+ * - the user pauses/clears it,
12
+ * - the run is interrupted or the provider errors (out of quota, network, …),
13
+ * - or a user-set token budget is exhausted.
14
+ * Otherwise it keeps going.
15
+ */
16
+ import type { GoalState } from "./store.js";
17
+ export type GoalStopReason = "complete" | "blocked" | "paused" | "budget" | "error" | "cancelled" | "user_input" | "no_goal";
18
+ export interface ContinueDecisionInput {
19
+ goal: GoalState | null;
20
+ /** The last run was interrupted/cancelled by the user. */
21
+ cancelled?: boolean;
22
+ /** The last run failed with a provider/run error (quota, network, API). */
23
+ errored?: boolean;
24
+ /** Number of user inputs queued to run next (a real message preempts the goal). */
25
+ queuedInputs?: number;
26
+ }
27
+ export interface ContinueDecision {
28
+ continue: boolean;
29
+ reason?: GoalStopReason;
30
+ }
31
+ export declare function shouldContinueGoal(input: ContinueDecisionInput): ContinueDecision;
32
+ /** Human-readable one-liner explaining why auto-continuation stopped. */
33
+ export declare function stopReasonNotice(reason: GoalStopReason | undefined): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pure decision logic for the goal auto-continuation loop.
3
+ *
4
+ * Kept out of the TUI so the stop conditions can be unit-tested directly. The
5
+ * TUI calls shouldContinueGoal() after each goal turn finishes and either fires
6
+ * another turn or stops with the returned reason.
7
+ *
8
+ * The agent decides when the work is done — there is intentionally NO turn-count
9
+ * cap (unlike a fixed iteration limit). The loop only stops when:
10
+ * - the model marks the goal complete/blocked (via update_goal),
11
+ * - the user pauses/clears it,
12
+ * - the run is interrupted or the provider errors (out of quota, network, …),
13
+ * - or a user-set token budget is exhausted.
14
+ * Otherwise it keeps going.
15
+ */
16
+ export function shouldContinueGoal(input) {
17
+ const { goal } = input;
18
+ if (!goal)
19
+ return { continue: false, reason: "no_goal" };
20
+ if (input.errored)
21
+ return { continue: false, reason: "error" };
22
+ if (input.cancelled)
23
+ return { continue: false, reason: "cancelled" };
24
+ if ((input.queuedInputs ?? 0) > 0)
25
+ return { continue: false, reason: "user_input" };
26
+ switch (goal.status) {
27
+ case "complete":
28
+ return { continue: false, reason: "complete" };
29
+ case "blocked":
30
+ return { continue: false, reason: "blocked" };
31
+ case "paused":
32
+ return { continue: false, reason: "paused" };
33
+ case "budget_limited":
34
+ return { continue: false, reason: "budget" };
35
+ case "active":
36
+ break;
37
+ }
38
+ // Only an explicit, user-set token budget bounds the loop; with no budget it
39
+ // runs until the model finishes, the user stops it, or the provider errors.
40
+ if (goal.tokenBudget !== undefined && goal.tokensUsed >= goal.tokenBudget) {
41
+ return { continue: false, reason: "budget" };
42
+ }
43
+ return { continue: true };
44
+ }
45
+ /** Human-readable one-liner explaining why auto-continuation stopped. */
46
+ export function stopReasonNotice(reason) {
47
+ switch (reason) {
48
+ case "complete":
49
+ return "Goal complete.";
50
+ case "blocked":
51
+ return "Goal marked blocked — /goal resume to retry.";
52
+ case "paused":
53
+ return "Goal paused — /goal resume to continue.";
54
+ case "budget":
55
+ return "Goal hit its token budget — /goal resume to continue.";
56
+ case "error":
57
+ return "Goal paused — the provider errored. Fix it, then /goal resume.";
58
+ case "cancelled":
59
+ return "Goal paused (interrupted) — /goal resume to continue.";
60
+ case "user_input":
61
+ return "Goal paused for your input — it resumes after this turn.";
62
+ default:
63
+ return "";
64
+ }
65
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Display helpers for the goal feature — shared by the goal tools (model-facing
3
+ * summary), the `/goal` summary command, and the TUI status-line indicator.
4
+ */
5
+ import type { GoalState, GoalStatus } from "./store.js";
6
+ export declare function goalStatusLabel(status: GoalStatus): string;
7
+ /** Compact token count: 950, 1.2K, 63.9K, 1.5M. */
8
+ export declare function formatTokensCompact(tokens: number): string;
9
+ /** Full multi-detail summary, e.g. for the model's get_goal result. */
10
+ export declare function goalSummaryText(goal: GoalState): string;
11
+ /**
12
+ * Terminal notice shown when a goal finishes, with the accurate final token
13
+ * spend. Call only after the finishing run's tokens have been accounted (the
14
+ * update_goal tool can't report this — see goal/tools.ts).
15
+ */
16
+ export declare function goalCompleteNotice(goal: GoalState): string;
17
+ /** Compact single-line indicator for the status line / sidebar. */
18
+ export declare function goalIndicatorLine(goal: GoalState, maxObjective?: number): string;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Display helpers for the goal feature — shared by the goal tools (model-facing
3
+ * summary), the `/goal` summary command, and the TUI status-line indicator.
4
+ */
5
+ export function goalStatusLabel(status) {
6
+ switch (status) {
7
+ case "active":
8
+ return "active";
9
+ case "paused":
10
+ return "paused";
11
+ case "blocked":
12
+ return "blocked";
13
+ case "budget_limited":
14
+ return "budget limited";
15
+ case "complete":
16
+ return "complete";
17
+ }
18
+ }
19
+ /** Compact token count: 950, 1.2K, 63.9K, 1.5M. */
20
+ export function formatTokensCompact(tokens) {
21
+ const n = Math.max(0, Math.round(tokens));
22
+ if (n < 1_000)
23
+ return String(n);
24
+ if (n < 1_000_000)
25
+ return `${trimZero(n / 1_000)}K`;
26
+ return `${trimZero(n / 1_000_000)}M`;
27
+ }
28
+ function trimZero(value) {
29
+ const rounded = Math.round(value * 10) / 10;
30
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
31
+ }
32
+ function tokensPart(goal) {
33
+ if (goal.tokenBudget !== undefined) {
34
+ return `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok`;
35
+ }
36
+ if (goal.tokensUsed > 0)
37
+ return `${formatTokensCompact(goal.tokensUsed)} tok`;
38
+ return undefined;
39
+ }
40
+ /** Full multi-detail summary, e.g. for the model's get_goal result. */
41
+ export function goalSummaryText(goal) {
42
+ const parts = [
43
+ `Objective: ${goal.objective}`,
44
+ `Status: ${goalStatusLabel(goal.status)}.`,
45
+ `Turns: ${goal.turnsSpent}.`,
46
+ ];
47
+ const tokens = tokensPart(goal);
48
+ if (tokens)
49
+ parts.push(`Tokens: ${tokens}.`);
50
+ if (goal.tokenBudget !== undefined) {
51
+ const remaining = Math.max(0, goal.tokenBudget - goal.tokensUsed);
52
+ parts.push(`Remaining budget: ${formatTokensCompact(remaining)} tok.`);
53
+ }
54
+ return parts.join(" ");
55
+ }
56
+ /**
57
+ * Terminal notice shown when a goal finishes, with the accurate final token
58
+ * spend. Call only after the finishing run's tokens have been accounted (the
59
+ * update_goal tool can't report this — see goal/tools.ts).
60
+ */
61
+ export function goalCompleteNotice(goal) {
62
+ const tokens = goal.tokenBudget !== undefined
63
+ ? `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok`
64
+ : `${formatTokensCompact(goal.tokensUsed)} tok`;
65
+ const turns = `${goal.turnsSpent} ${goal.turnsSpent === 1 ? "turn" : "turns"}`;
66
+ return `Goal complete — ${tokens} used over ${turns}.`;
67
+ }
68
+ /** Compact single-line indicator for the status line / sidebar. */
69
+ export function goalIndicatorLine(goal, maxObjective = 48) {
70
+ const segments = [`goal: ${goalStatusLabel(goal.status)}`, `${goal.turnsSpent} turns`];
71
+ const tokens = tokensPart(goal);
72
+ if (tokens)
73
+ segments.push(tokens);
74
+ const objective = truncateObjective(goal.objective, maxObjective);
75
+ return `${segments.join(" · ")} — ${objective}`;
76
+ }
77
+ function truncateObjective(objective, max) {
78
+ const single = objective.replace(/\s+/g, " ").trim();
79
+ if (single.length <= max)
80
+ return single;
81
+ return `${single.slice(0, Math.max(0, max - 1))}…`;
82
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Model-facing prompts for the autonomous `/goal` feature.
3
+ *
4
+ * Ported and trimmed from Codex's `ext/goal/templates/goals/*.md`. These are
5
+ * injected into the model context (wrapped as an internal context block, so
6
+ * they never render as a user bubble) at the start of each goal turn. The
7
+ * objective is treated as untrusted data: XML-escaped and fenced in
8
+ * <objective> so it cannot be read as higher-priority instructions.
9
+ */
10
+ import type { GoalState } from "./store.js";
11
+ export declare function continuationPrompt(goal: GoalState): string;
12
+ export declare function initialPrompt(goal: GoalState): string;
13
+ export declare function budgetLimitPrompt(goal: GoalState): string;