bonecode 1.2.2 → 1.3.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.
Files changed (70) hide show
  1. package/README.md +20 -0
  2. package/dist/src/engine/agent/prompt/compaction.txt +9 -0
  3. package/dist/src/engine/agent/prompt/explore.txt +18 -0
  4. package/dist/src/engine/agent/prompt/scout.txt +36 -0
  5. package/dist/src/engine/agent/prompt/summary.txt +11 -0
  6. package/dist/src/engine/agent/prompt/title.txt +44 -0
  7. package/dist/src/engine/session/prompt/anthropic.txt +105 -0
  8. package/dist/src/engine/session/prompt/beast.txt +147 -0
  9. package/dist/src/engine/session/prompt/bonescript.txt +391 -0
  10. package/dist/src/engine/session/prompt/build-switch.txt +5 -0
  11. package/dist/src/engine/session/prompt/codex.txt +79 -0
  12. package/dist/src/engine/session/prompt/copilot-gpt-5.txt +143 -0
  13. package/dist/src/engine/session/prompt/default.txt +105 -0
  14. package/dist/src/engine/session/prompt/gemini.txt +155 -0
  15. package/dist/src/engine/session/prompt/gpt.txt +107 -0
  16. package/dist/src/engine/session/prompt/kimi.txt +95 -0
  17. package/dist/src/engine/session/prompt/max-steps.txt +16 -0
  18. package/dist/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -0
  19. package/dist/src/engine/session/prompt/plan.txt +26 -0
  20. package/dist/src/engine/session/prompt/trinity.txt +97 -0
  21. package/dist/src/engine/session/prompt.js +35 -2
  22. package/dist/src/engine/session/prompt.js.map +1 -1
  23. package/dist/src/engine/skill/prompt/customize-opencode.md +377 -0
  24. package/dist/src/engine/tool/apply_patch.txt +33 -0
  25. package/dist/src/engine/tool/edit.txt +10 -0
  26. package/dist/src/engine/tool/glob.txt +6 -0
  27. package/dist/src/engine/tool/grep.txt +8 -0
  28. package/dist/src/engine/tool/lsp.txt +24 -0
  29. package/dist/src/engine/tool/plan-enter.txt +14 -0
  30. package/dist/src/engine/tool/plan-exit.txt +13 -0
  31. package/dist/src/engine/tool/question.txt +10 -0
  32. package/dist/src/engine/tool/read.txt +14 -0
  33. package/dist/src/engine/tool/repo_clone.txt +5 -0
  34. package/dist/src/engine/tool/repo_overview.txt +4 -0
  35. package/dist/src/engine/tool/shell/shell.txt +77 -0
  36. package/dist/src/engine/tool/skill.txt +5 -0
  37. package/dist/src/engine/tool/task.txt +58 -0
  38. package/dist/src/engine/tool/task_status.txt +13 -0
  39. package/dist/src/engine/tool/todowrite.txt +167 -0
  40. package/dist/src/engine/tool/tool/apply_patch.txt +33 -0
  41. package/dist/src/engine/tool/tool/edit.txt +10 -0
  42. package/dist/src/engine/tool/tool/glob.txt +6 -0
  43. package/dist/src/engine/tool/tool/grep.txt +8 -0
  44. package/dist/src/engine/tool/tool/lsp.txt +24 -0
  45. package/dist/src/engine/tool/tool/plan-enter.txt +14 -0
  46. package/dist/src/engine/tool/tool/plan-exit.txt +13 -0
  47. package/dist/src/engine/tool/tool/question.txt +10 -0
  48. package/dist/src/engine/tool/tool/read.txt +14 -0
  49. package/dist/src/engine/tool/tool/repo_clone.txt +5 -0
  50. package/dist/src/engine/tool/tool/repo_overview.txt +4 -0
  51. package/dist/src/engine/tool/tool/shell/shell.txt +77 -0
  52. package/dist/src/engine/tool/tool/skill.txt +5 -0
  53. package/dist/src/engine/tool/tool/task.txt +58 -0
  54. package/dist/src/engine/tool/tool/task_status.txt +13 -0
  55. package/dist/src/engine/tool/tool/todowrite.txt +167 -0
  56. package/dist/src/engine/tool/tool/webfetch.txt +13 -0
  57. package/dist/src/engine/tool/tool/websearch.txt +14 -0
  58. package/dist/src/engine/tool/tool/write.txt +8 -0
  59. package/dist/src/engine/tool/webfetch.txt +13 -0
  60. package/dist/src/engine/tool/websearch.txt +14 -0
  61. package/dist/src/engine/tool/write.txt +8 -0
  62. package/dist/src/tui.js +118 -29
  63. package/dist/src/tui.js.map +1 -1
  64. package/package.json +2 -2
  65. package/scripts/copy_prompts.js +58 -0
  66. package/scripts/test_bonescript_primer.js +111 -0
  67. package/scripts/test_tui_render.js +278 -0
  68. package/src/engine/session/prompt/bonescript.txt +391 -0
  69. package/src/engine/session/prompt.ts +36 -2
  70. package/src/tui.ts +115 -30
@@ -0,0 +1,391 @@
1
+ # BoneScript — the canonical way to build backends in BoneCode
2
+
3
+ You are working inside a BoneCode project. **BoneScript is a real, deterministic declarative language** that compiles `.bone` system descriptions into complete, runnable Node.js + PostgreSQL backends. It is published on npm as `bonescript-compiler`. The repository is https://github.com/Doorman11991/BoneScript.
4
+
5
+ BoneScript is NOT pseudocode. NOT optional. NOT a generic scripting language. When the user asks you to build a backend feature, your default approach MUST be:
6
+
7
+ 1. **Define** — write or update the relevant `.bone` file using the `edit` or `write` tool
8
+ 2. **Compile** — run `bonec compile <file>` (or `npm run compile`) via the shell tool
9
+ 3. **Extend** — add custom logic only in `extensions/` (these files survive recompilation)
10
+ 4. **Check** — run `bonec check <file>` to validate the spec
11
+
12
+ You must NEVER hand-write models, routes, controllers, services, schemas, or migrations. The compiler generates all of that from `.bone` files. You must NEVER run `prisma migrate`, `drizzle-kit push`, `typeorm migration`, `nest g`, `rails generate`, etc.
13
+
14
+ ## When BoneScript applies
15
+
16
+ Use BoneScript whenever the user asks for any of these:
17
+ - Domain entities (User, Order, Product, Shop, Session, etc.)
18
+ - Persistence (tables, indexes, migrations)
19
+ - API endpoints (REST or GraphQL)
20
+ - State machines (`pending → paid → shipped → delivered`)
21
+ - Events with delivery guarantees (at_least_once, exactly_once)
22
+ - Saga / multi-step transactional flows with compensation
23
+ - WebSocket channels for realtime
24
+ - Permissions, rate limits, auth (JWT, OAuth2, API key)
25
+ - Background workers, scheduled jobs
26
+
27
+ Use plain Python/TypeScript/etc. only for:
28
+ - **Pure simulations** (game loops, math, render) with no persistence layer
29
+ - **Frontend** components
30
+ - **Custom logic** inside `extensions/` that BoneScript cannot generate (LLM calls, embeddings, file system, git, third-party APIs)
31
+
32
+ If the request is ambiguous (e.g. "a 2D market simulation"), ask the user: "Is this a self-contained simulation script (plain code) or a backend service with persistence (BoneScript)?"
33
+
34
+ ## BoneScript syntax — authoritative reference
35
+
36
+ ### `system` block
37
+
38
+ Every `.bone` file declares one `system`:
39
+
40
+ ```bone
41
+ system Marketplace {
42
+ domain: marketplace
43
+
44
+ // entities, stores, events, capabilities, flows, channels, policies
45
+ }
46
+ ```
47
+
48
+ The `domain:` key picks a starter template (`marketplace`, `saas_platform`, `multiplayer_game`, `iot_system`, `social_network`, `realtime_collaboration`, `ecommerce`, `event_driven`, `api_gateway`, or `blank`).
49
+
50
+ ### `entity` — stateful object with constraints, states, relations
51
+
52
+ ```bone
53
+ entity Order {
54
+ owns: [
55
+ buyer_id: uuid,
56
+ listing_id: uuid,
57
+ seller_id: uuid,
58
+ quantity: uint,
59
+ total: uint,
60
+ status: string
61
+ ]
62
+ constraints: [
63
+ quantity >= 1,
64
+ total > 0,
65
+ status in ["pending", "paid", "shipped", "delivered", "cancelled"]
66
+ ]
67
+ states: pending -> paid -> shipped -> delivered | cancelled
68
+ auth: jwt
69
+ index: [buyer_id, seller_id, status]
70
+ relation listing: belongs_to Listing
71
+ relation buyer: belongs_to Buyer
72
+ }
73
+ ```
74
+
75
+ Field types: `string`, `uint`, `int`, `float`, `bool`, `uuid`, `timestamp`, `json`, `optional<T>`.
76
+ Constraints: `>=`, `<=`, `==`, `in [...]`, `field.length in N..M`, `field.unique`.
77
+ States are unidirectional unless explicitly branched with `|` (terminal states).
78
+
79
+ ### `store` — generated database table
80
+
81
+ ```bone
82
+ store OrderStore {
83
+ engine: postgresql
84
+ schema: {
85
+ id: uuid,
86
+ buyer_id: uuid,
87
+ listing_id: uuid,
88
+ quantity: uint,
89
+ total: uint,
90
+ status: string,
91
+ state: string,
92
+ created_at: timestamp,
93
+ updated_at: timestamp
94
+ }
95
+ partition: buyer_id // optional — for sharding
96
+ replicas: 1
97
+ }
98
+ ```
99
+
100
+ The compiler emits SQL migrations with proper indexes, FK constraints, and triggers. Never write migration SQL by hand.
101
+
102
+ ### `event` — durable, typed message with delivery semantics
103
+
104
+ ```bone
105
+ event OrderPlaced {
106
+ payload: {
107
+ order_id: uuid,
108
+ buyer_id: uuid,
109
+ total: uint,
110
+ placed_at: timestamp
111
+ }
112
+ delivery: at_least_once // or exactly_once
113
+ ttl: 30d // 1h, 7d, 90d, etc.
114
+ }
115
+ ```
116
+
117
+ `at_least_once` retries with exponential backoff; `exactly_once` deduplicates via the `event_processed` table. Switch modes globally with `EVENT_MODE=durable|in_process` env var.
118
+
119
+ ### `capability` — generated endpoint with state-machine enforcement
120
+
121
+ ```bone
122
+ capability place_order(buyer: Buyer, listing: Listing, quantity: uint) {
123
+ requires: [
124
+ buyer.state == "active",
125
+ listing.state == "active",
126
+ listing.stock >= quantity,
127
+ buyer.balance >= listing.price * quantity
128
+ ]
129
+ effects: [
130
+ listing.stock -= quantity,
131
+ buyer.balance -= listing.price * quantity
132
+ ]
133
+ emits: OrderPlaced
134
+ sync: transactional // or eventual / realtime
135
+ timeout: 30s
136
+ idempotent: false
137
+ }
138
+ ```
139
+
140
+ The compiler generates an Express route, validates the `requires` predicates, applies the `effects` atomically in a SQL transaction, publishes the event via the outbox, and enforces `timeout`. Never touch the generated route file.
141
+
142
+ ### `pipeline:` capability — multi-step with auto-rollback
143
+
144
+ ```bone
145
+ capability checkout(buyer: Buyer, cart: Cart) {
146
+ pipeline: {
147
+ validate_inventory(cart)
148
+ charge_payment(buyer, cart.total) as payment
149
+ create_order(buyer, cart, payment)
150
+ on_error: rollback
151
+ }
152
+ sync: transactional
153
+ }
154
+ ```
155
+
156
+ ### `algorithm:` capability — built-in algorithm catalog
157
+
158
+ ```bone
159
+ capability find_route(start: string, end: string) {
160
+ algorithm: shortest_path using { graph: road_network, source: start, target: end }
161
+ returns: json
162
+ }
163
+ ```
164
+
165
+ Available: `shortest_path`, `topological_sort`, `binary_search`, `bipartite_matching`, `round_robin`, `weighted_average`, `percentile`, `rank_by`, `consistent_hash`.
166
+
167
+ ### `flow` — saga with backward compensation
168
+
169
+ ```bone
170
+ flow checkout {
171
+ step validate: place_order(buyer, listing, quantity)
172
+ compensate: cancel_order(order)
173
+
174
+ step pay: process_payment(order, buyer)
175
+ compensate: cancel_order(order)
176
+
177
+ step confirm: ship_order(seller, order)
178
+ compensate: cancel_order(order)
179
+ }
180
+ ```
181
+
182
+ If any step fails, the compiler runs all preceding `compensate` actions in reverse order.
183
+
184
+ ### `channel` — WebSocket pub/sub
185
+
186
+ ```bone
187
+ channel game_lobby {
188
+ transport: websocket
189
+ ordering: causal // or fifo / unordered
190
+ participants: set<Player>
191
+ persistence: last_100 // last_N messages retained
192
+ filter: participant.id == event.player_id
193
+ }
194
+ ```
195
+
196
+ ### `policy` — rate limit + access control + audit
197
+
198
+ ```bone
199
+ policy api_limits {
200
+ rate_limit: 200 per 1m // per 1s, 1m, 1h, 1d
201
+ access: [buyer, seller, admin]
202
+ audit: true
203
+ encryption: in_transit // or at_rest, both, none
204
+ }
205
+ ```
206
+
207
+ ### `extension_point` — escape hatch for custom logic
208
+
209
+ ```bone
210
+ extension_point calculate_shipping_cost(order: Order) {
211
+ returns: uint
212
+ stable: true // compilation fails if not implemented
213
+ }
214
+ ```
215
+
216
+ Implement in `extensions/`:
217
+
218
+ ```ts
219
+ // extensions/shipping.ts
220
+ export async function calculate_shipping_cost(order: { id: string; total: number; ... }): Promise<number> {
221
+ // custom logic here — preserved across recompilation
222
+ return Math.ceil(order.total * 0.05)
223
+ }
224
+ ```
225
+
226
+ ### Cross-entity constraints
227
+
228
+ ```bone
229
+ constraint listing_price_limit: Listing.price <= 1000000
230
+ constraint order_quantity_limit: Order.quantity <= 100
231
+ ```
232
+
233
+ ## What gets generated from a `.bone` file
234
+
235
+ Running `bonec compile shop.bone` produces:
236
+
237
+ ```
238
+ output/
239
+ ├── src/
240
+ │ ├── index.ts Express server with all routes wired
241
+ │ ├── db.ts Postgres connection pool
242
+ │ ├── events.ts Durable event bus (transactional outbox)
243
+ │ ├── auth.ts JWT / OAuth2 / API key middleware
244
+ │ ├── publishers.ts Typed event publisher functions
245
+ │ ├── health.ts /health/live, /health/ready, /health/metrics
246
+ │ ├── flows.ts Saga runtime with backward compensation
247
+ │ ├── websocket.ts WebSocket server (if channels declared)
248
+ │ ├── routes/ One file per entity — CRUD + capabilities
249
+ │ ├── state_machines/ One file per entity with states
250
+ │ └── models/ TypeScript interfaces + Zod validators
251
+ ├── migrations/ SQL schemas with indexes, triggers, FKs
252
+ ├── openapi.json OpenAPI 3.0 schema
253
+ ├── Dockerfile
254
+ ├── docker-compose.yaml Postgres + Redis for local dev
255
+ ├── k8s/deployment.yaml
256
+ └── .github/workflows/ CI/CD pipeline
257
+ ```
258
+
259
+ **Never edit anything in `output/` (or `generated/`). It's overwritten on every compile.** All your custom code goes in `extensions/`.
260
+
261
+ ## CLI commands
262
+
263
+ | Command | Purpose |
264
+ |---------|---------|
265
+ | `bonec init <name> --domain <template>` | Scaffold a new project |
266
+ | `bonec compile <file>` | Full 7-stage compile → runnable backend |
267
+ | `bonec check <file>` | Validate without generating |
268
+ | `bonec watch <file>` | Recompile on save |
269
+ | `bonec diff <old> <new>` | Show schema migration diff |
270
+ | `bonec fmt <file>` | Format in place |
271
+ | `bonec test [output-dir]` | Run generated regression tests |
272
+ | `bonec verify-determinism <file>` | Confirm two compiles produce identical output |
273
+
274
+ The compiler is on npm: `npm install -g bonescript-compiler`. Inside a BoneCode project, `npm run compile` typically wraps `bonec compile`.
275
+
276
+ ## Worked example — 2D market simulation done right
277
+
278
+ User: "build me a 2D market simulation with 2000 shops over 100 simulated years"
279
+
280
+ The first question to ask: **is it a simulation script or a backend?**
281
+ - If it's just a runnable visualization with no need for persistent state, REST APIs, or multiplayer — write plain Python/TS.
282
+ - If shops have state, transactions are queryable, multiple users can poke at the world, OR you want to run the simulation as a service — use BoneScript.
283
+
284
+ For the backend version:
285
+
286
+ 1. Create `bone/market.bone`:
287
+
288
+ ```bone
289
+ system Market {
290
+ domain: marketplace
291
+
292
+ entity Shop {
293
+ owns: [
294
+ name: string,
295
+ x_pos: float,
296
+ y_pos: float,
297
+ specialty: string,
298
+ gold: uint,
299
+ reputation: float
300
+ ]
301
+ constraints: [
302
+ specialty in ["food", "tools", "weapons", "luxury", "general"],
303
+ gold >= 0,
304
+ reputation >= 0,
305
+ reputation <= 1
306
+ ]
307
+ states: founded -> active -> struggling -> bankrupt | thriving
308
+ index: [specialty]
309
+ }
310
+
311
+ entity Transaction {
312
+ owns: [
313
+ shop_id: uuid,
314
+ year: uint,
315
+ amount: uint,
316
+ kind: string
317
+ ]
318
+ constraints: [
319
+ amount > 0,
320
+ year >= 0,
321
+ kind in ["sale", "purchase", "tax"]
322
+ ]
323
+ index: [shop_id, year]
324
+ relation shop: belongs_to Shop
325
+ }
326
+
327
+ event TransactionRecorded {
328
+ payload: {
329
+ transaction_id: uuid,
330
+ shop_id: uuid,
331
+ year: uint,
332
+ amount: uint
333
+ }
334
+ delivery: at_least_once
335
+ ttl: 90d
336
+ }
337
+
338
+ capability record_transaction(shop: Shop, year: uint, amount: uint, kind: string) {
339
+ requires: [
340
+ shop.state in ["active", "thriving", "struggling"],
341
+ amount > 0
342
+ ]
343
+ effects: [
344
+ shop.gold = shop.gold + amount
345
+ ]
346
+ emits: TransactionRecorded
347
+ sync: transactional
348
+ timeout: 5s
349
+ idempotent: true
350
+ }
351
+
352
+ extension_point simulate_year(year: uint) {
353
+ returns: json
354
+ stable: true
355
+ }
356
+
357
+ flow advance_year {
358
+ step demand: simulate_year(year)
359
+ compensate: noop()
360
+ }
361
+
362
+ policy api_limits {
363
+ rate_limit: 1000 per 1m
364
+ access: [user, admin]
365
+ audit: true
366
+ }
367
+ }
368
+ ```
369
+
370
+ 2. `npm run compile` (or `bonec compile bone/market.bone`)
371
+
372
+ 3. Implement `simulate_year` in `extensions/simulation.ts`:
373
+
374
+ ```ts
375
+ export async function simulate_year(year: number) {
376
+ // Read all shops, calculate demand, call record_transaction for each
377
+ // This is the only place where you write custom logic.
378
+ }
379
+ ```
380
+
381
+ 4. The generated backend gives you `POST /shops`, `GET /shops/:id`, `POST /shops/:id/record_transaction`, `GET /transactions?shop_id=...`, the state machine, durable events, OpenAPI spec, and a TypeScript SDK — all from the `.bone` file.
382
+
383
+ 5. The 2000-shop × 100-year loop lives in a runner script that calls the generated capabilities (or in `simulate_year` itself).
384
+
385
+ This is how you build real backends in BoneCode. Don't fall back to writing raw Python or hand-rolled Express routes when the user asks for a backend feature. If the user actually wants a script, ask first.
386
+
387
+ ## Reference links (for the user, not for you to fetch)
388
+
389
+ - BoneScript: https://github.com/Doorman11991/BoneScript
390
+ - Compiler: https://www.npmjs.com/package/bonescript-compiler
391
+ - OpenCode plugin: https://github.com/Doorman11991/opencode-bonescript-backend
@@ -598,6 +598,11 @@ async function buildSystemPromptWithRAG(
598
598
  // Base system prompt (provider-specific, from OpenCode)
599
599
  const base = getSystemPrompt(model_id, provider_id, agent_name);
600
600
 
601
+ // BoneScript primer — loaded for every session so the model knows about
602
+ // BoneScript before any tool call. Without this, models default to
603
+ // generic Python/TS and never use the .bone workflow.
604
+ const bonescriptPrimer = loadBonescriptPrimer();
605
+
601
606
  // Environment context
602
607
  const envContext = [
603
608
  `Working directory: ${worktree}`,
@@ -621,7 +626,7 @@ async function buildSystemPromptWithRAG(
621
626
  const project_id = sessionRow.rows[0]?.project_id || "";
622
627
  if (!project_id) {
623
628
  // No project linked yet — skip RAG context
624
- return [base, envContext, instructions].filter(Boolean).join("\n\n");
629
+ return [base, bonescriptPrimer, envContext, instructions].filter(Boolean).join("\n\n");
625
630
  }
626
631
 
627
632
  const ctxResult = await buildContext({
@@ -641,7 +646,36 @@ async function buildSystemPromptWithRAG(
641
646
  }
642
647
  }
643
648
 
644
- return [base, envContext, instructions, codebaseContext].filter(Boolean).join("\n\n");
649
+ return [base, bonescriptPrimer, envContext, instructions, codebaseContext].filter(Boolean).join("\n\n");
650
+ }
651
+
652
+ // ─── BoneScript primer loader ─────────────────────────────────────────────────
653
+
654
+ let _bonescriptPrimer: string | null = null;
655
+ function loadBonescriptPrimer(): string {
656
+ if (_bonescriptPrimer !== null) return _bonescriptPrimer;
657
+ try {
658
+ const fs = require("fs");
659
+ const path = require("path");
660
+ // Look for the primer in the prompt directory next to this compiled module.
661
+ // After compilation, this lives at dist/src/engine/session/prompt.js, so the
662
+ // .txt file is at dist/src/engine/session/prompt/bonescript.txt.
663
+ const candidates = [
664
+ path.join(__dirname, "prompt", "bonescript.txt"),
665
+ path.join(__dirname, "..", "..", "..", "src", "engine", "session", "prompt", "bonescript.txt"),
666
+ ];
667
+ for (const candidate of candidates) {
668
+ if (fs.existsSync(candidate)) {
669
+ _bonescriptPrimer = fs.readFileSync(candidate, "utf-8");
670
+ return _bonescriptPrimer || "";
671
+ }
672
+ }
673
+ _bonescriptPrimer = "";
674
+ return "";
675
+ } catch {
676
+ _bonescriptPrimer = "";
677
+ return "";
678
+ }
645
679
  }
646
680
 
647
681
  // ─── Language Model Factory ───────────────────────────────────────────────────
package/src/tui.ts CHANGED
@@ -393,19 +393,78 @@ interface ToolTrack {
393
393
  * "text" — pass deltas through verbatim (with partial-fence detection)
394
394
  * "fence" — buffer everything; emit marker on close fence
395
395
  */
396
+ /**
397
+ * Apply lightweight inline markdown styling to a streamed chunk.
398
+ *
399
+ * - `# `, `## `, `### ` at line-start become bold colored headers
400
+ * - `**text**` becomes bold
401
+ * - `` `code` `` becomes inverted gray
402
+ * - `- ` and `* ` at line-start become a bullet
403
+ *
404
+ * Operates per-line on the chunk. Cross-chunk markdown (a `**` opening in one
405
+ * delta and closing in another) is not handled — we just leave those raw.
406
+ * That's acceptable because it's rare and the model's output is still readable.
407
+ */
408
+ function applyInlineMarkdown(text: string): string {
409
+ if (!text) return text;
410
+ // Split on newlines so we can match line-anchored patterns (headers, bullets)
411
+ const lines = text.split("\n");
412
+ const styled = lines.map((line) => {
413
+ // Headers
414
+ let m = line.match(/^(#{1,6})\s+(.*)$/);
415
+ if (m) {
416
+ const level = m[1].length;
417
+ const content = m[2];
418
+ if (level === 1) return `${CYAN}${BOLD}${content}${R}`;
419
+ if (level === 2) return `${CYAN}${BOLD}${content}${R}`;
420
+ if (level === 3) return `${WHITE}${BOLD}${content}${R}`;
421
+ return `${WHITE}${content}${R}`;
422
+ }
423
+ // Bullets at line start
424
+ m = line.match(/^(\s*)[-*]\s+(.*)$/);
425
+ if (m) {
426
+ line = `${m[1]}${GRAY}•${R} ${m[2]}`;
427
+ }
428
+ // Numbered lists
429
+ m = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
430
+ if (m) {
431
+ line = `${m[1]}${GRAY}${m[2]}${R} ${m[3]}`;
432
+ }
433
+ // Bold (**text**)
434
+ line = line.replace(/\*\*([^*]+)\*\*/g, `${BOLD}$1${R}`);
435
+ // Inline code (`code`)
436
+ line = line.replace(/`([^`]+)`/g, `${GRAY}$1${R}`);
437
+ return line;
438
+ });
439
+ return styled.join("\n");
440
+ }
441
+
442
+ /**
443
+ * Stateful code-fence collapser.
444
+ *
445
+ * Replaces ```lang ... ``` blocks with a one-line marker
446
+ * " ┃ code: lang, N lines" so streaming code blocks don't flood the TUI.
447
+ *
448
+ * State:
449
+ * "text" — pass through verbatim (with partial-fence buffering)
450
+ * "fence" — silently buffer body; emit final marker on close
451
+ *
452
+ * Newline handling:
453
+ * We strip up to one trailing newline before the opening fence and one
454
+ * leading newline after the closing fence so the marker sits on its own
455
+ * line without doubling blank lines around it.
456
+ */
396
457
  function makeCodeFenceCollapser() {
397
458
  let mode: "text" | "fence" = "text";
398
459
  let lang = "";
399
- let buffered = ""; // bytes accumulated inside a fence
400
- let pending = ""; // partial-fence buffer (when we see backticks but don't know yet)
401
-
402
- const lines = (s: string) => s.split("\n").length;
460
+ let buffered = "";
461
+ let pending = "";
403
462
 
404
- function flushPending(): string {
405
- const out = pending;
406
- pending = "";
407
- return out;
408
- }
463
+ const countLines = (s: string) => {
464
+ if (!s) return 0;
465
+ const trimmed = s.replace(/^\n+/, "").replace(/\n+$/, "");
466
+ return trimmed ? trimmed.split("\n").length : 0;
467
+ };
409
468
 
410
469
  return {
411
470
  feed(chunk: string): string {
@@ -416,23 +475,26 @@ function makeCodeFenceCollapser() {
416
475
 
417
476
  while (i < buf.length) {
418
477
  if (mode === "text") {
419
- // Look for ``` to enter fence mode
420
478
  const fenceIdx = buf.indexOf("```", i);
421
479
  if (fenceIdx === -1) {
422
- // No fence in remaining text emit it all, but hold the last 2 chars
423
- // in case they're part of an upcoming fence.
480
+ // No fence in remaining text. Hold last 2 chars in case they're
481
+ // the start of an upcoming fence.
424
482
  const safeEnd = Math.max(i, buf.length - 2);
425
483
  result += buf.slice(i, safeEnd);
426
484
  pending = buf.slice(safeEnd);
427
485
  i = buf.length;
428
486
  break;
429
487
  }
430
- // Emit text up to the fence
431
- result += buf.slice(i, fenceIdx);
432
- // Read the language (everything until newline)
488
+ // Emit text up to the fence, but trim a single trailing newline so
489
+ // the marker sits on its own line without extra blank space.
490
+ let preFence = buf.slice(i, fenceIdx);
491
+ if (preFence.endsWith("\n")) preFence = preFence.slice(0, -1);
492
+ result += preFence;
493
+
494
+ // Read the language declaration (everything up to the next newline).
433
495
  const nlIdx = buf.indexOf("\n", fenceIdx + 3);
434
496
  if (nlIdx === -1) {
435
- // Don't have the full opening line yet — buffer
497
+ // Opening line not complete yet — buffer for next feed.
436
498
  pending = buf.slice(fenceIdx);
437
499
  i = buf.length;
438
500
  break;
@@ -441,31 +503,36 @@ function makeCodeFenceCollapser() {
441
503
  i = nlIdx + 1;
442
504
  mode = "fence";
443
505
  buffered = "";
444
- // Print a leading marker (will be amended when we close)
445
- result += `${GRAY}┃ code: ${lang}…${R}`;
506
+ // Don't emit anything yet — the final marker is written when we
507
+ // see the closing fence so we have the line count.
446
508
  continue;
447
509
  }
448
510
 
449
511
  // mode === "fence" — look for closing ```
450
512
  const closeIdx = buf.indexOf("```", i);
451
513
  if (closeIdx === -1) {
514
+ // No close fence yet — buffer the body.
452
515
  buffered += buf.slice(i);
453
- // Hold last 2 chars in case they're part of an upcoming close fence
516
+ // Hold last 2 chars in case they're part of an upcoming close fence.
454
517
  const safeEnd = Math.max(i, buf.length - 2);
455
518
  buffered = buffered.slice(0, buffered.length - (buf.length - safeEnd));
456
519
  pending = buf.slice(safeEnd);
457
520
  i = buf.length;
458
521
  break;
459
522
  }
460
- // Closing fence found
523
+
524
+ // Closing fence found.
461
525
  buffered += buf.slice(i, closeIdx);
462
- const lineCount = lines(buffered.replace(/\n+$/, ""));
463
- // Replace the placeholder we already wrote with the final marker.
464
- // Carriage return + clear line + reprint marker.
465
- result += `\r${ESC}[2K ${GRAY}┃ code: ${lang}, ${lineCount} line${lineCount === 1 ? "" : "s"}${R}\n`;
526
+ const lineCount = countLines(buffered);
527
+ // Emit the marker on its own line with leading and trailing newlines.
528
+ // The outer indenter will prepend " " to each line break.
529
+ result += `\n${GRAY}┃ code: ${lang}, ${lineCount} line${lineCount === 1 ? "" : "s"}${R}`;
466
530
  i = closeIdx + 3;
467
- // Skip optional newline after closing fence
531
+ // Skip a single newline immediately after the close fence to avoid
532
+ // doubling blank lines.
468
533
  if (buf[i] === "\n") i++;
534
+ // Add a trailing newline so the next text starts on a fresh line.
535
+ result += "\n";
469
536
  mode = "text";
470
537
  lang = "";
471
538
  buffered = "";
@@ -474,12 +541,12 @@ function makeCodeFenceCollapser() {
474
541
  return result;
475
542
  },
476
543
  flush(): string {
477
- const tail = flushPending();
544
+ const tail = pending;
545
+ pending = "";
478
546
  if (mode === "fence") {
479
- // Stream ended mid-fence — close it with an approximate count
480
- const lineCount = lines(buffered.replace(/\n+$/, ""));
547
+ const lineCount = countLines(buffered);
481
548
  mode = "text";
482
- return tail + `\r${ESC}[2K ${GRAY}┃ code: ${lang}, ${lineCount}+ lines${R}\n`;
549
+ return tail + `\n${GRAY}┃ code: ${lang}, ${lineCount}+ lines${R}\n`;
483
550
  }
484
551
  return tail;
485
552
  },
@@ -563,8 +630,10 @@ async function streamPrompt(opts: {
563
630
  // code blocks appear as "[code: lang]" placeholders instead of
564
631
  // dumping raw source.
565
632
  const piece = collapseCodeFences(text);
633
+ // Apply lightweight markdown styling (headers, bold, inline code)
634
+ const styled = applyInlineMarkdown(piece);
566
635
  // Print with leading-newline indenting (so each new line gets the 3-space prefix)
567
- const indented = piece.replace(/\n/g, `\n `);
636
+ const indented = styled.replace(/\n/g, `\n `);
568
637
  out(indented);
569
638
  fullText += text;
570
639
  continue;
@@ -727,6 +796,22 @@ export async function runTUI(opts: {
727
796
  port: number; token: string; model: string;
728
797
  provider: string; worktree: string; sessionId?: string;
729
798
  }): Promise<void> {
799
+ // Force UTF-8 output on Windows so emoji and box-drawing chars render
800
+ // correctly. Without this, console code page 437/850 corrupts multi-byte
801
+ // sequences into "??" or mojibake.
802
+ if (process.platform === "win32") {
803
+ try {
804
+ // Set output encoding for stdout/stderr
805
+ (process.stdout as any).setEncoding?.("utf-8");
806
+ (process.stderr as any).setEncoding?.("utf-8");
807
+ // Switch the console to UTF-8 (code page 65001)
808
+ const { execSync } = require("child_process");
809
+ execSync("chcp 65001", { stdio: "ignore" });
810
+ } catch {
811
+ // chcp not available — that's fine, we tried
812
+ }
813
+ }
814
+
730
815
  let { model, provider } = opts;
731
816
  const { port, token, worktree } = opts;
732
817
  let sessionId = opts.sessionId || null;