bonecode 1.1.0 → 1.2.1
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 +145 -9
- package/bin/bonecode +47 -42
- package/compat/opencode_adapter.ts +188 -17
- package/dist/bone/output/agent/src/algorithms.d.ts +1 -0
- package/dist/bone/output/agent/src/algorithms.js +3 -0
- package/dist/bone/output/agent/src/algorithms.js.map +1 -0
- package/dist/bone/output/agent/src/audit.d.ts +3 -0
- package/dist/bone/output/agent/src/audit.js +40 -0
- package/dist/bone/output/agent/src/audit.js.map +1 -0
- package/dist/bone/output/agent/src/auth.d.ts +8 -0
- package/dist/bone/output/agent/src/auth.js +56 -0
- package/dist/bone/output/agent/src/auth.js.map +1 -0
- package/dist/bone/output/agent/src/db.d.ts +6 -0
- package/dist/bone/output/agent/src/db.js +63 -0
- package/dist/bone/output/agent/src/db.js.map +1 -0
- package/dist/bone/output/agent/src/events.d.ts +25 -0
- package/dist/bone/output/agent/src/events.js +184 -0
- package/dist/bone/output/agent/src/events.js.map +1 -0
- package/dist/bone/output/agent/src/logger.d.ts +28 -0
- package/dist/bone/output/agent/src/logger.js +45 -0
- package/dist/bone/output/agent/src/logger.js.map +1 -0
- package/dist/bone/output/agent/src/metrics.d.ts +5 -0
- package/dist/bone/output/agent/src/metrics.js +60 -0
- package/dist/bone/output/agent/src/metrics.js.map +1 -0
- package/dist/bone/output/agent/src/routes/agent_instance.d.ts +1 -0
- package/dist/bone/output/agent/src/routes/agent_instance.js +253 -0
- package/dist/bone/output/agent/src/routes/agent_instance.js.map +1 -0
- package/dist/bone/output/agent/src/routes/build_step.d.ts +1 -0
- package/dist/bone/output/agent/src/routes/build_step.js +133 -0
- package/dist/bone/output/agent/src/routes/build_step.js.map +1 -0
- package/dist/bone/output/agent/src/routes/plan.d.ts +1 -0
- package/dist/bone/output/agent/src/routes/plan.js +119 -0
- package/dist/bone/output/agent/src/routes/plan.js.map +1 -0
- package/dist/bone/output/agent/src/routes/task.d.ts +1 -0
- package/dist/bone/output/agent/src/routes/task.js +133 -0
- package/dist/bone/output/agent/src/routes/task.js.map +1 -0
- package/dist/bone/output/agent/src/routes/tool_call.d.ts +1 -0
- package/dist/bone/output/agent/src/routes/tool_call.js +190 -0
- package/dist/bone/output/agent/src/routes/tool_call.js.map +1 -0
- package/dist/bone/output/agent/src/state_machines/agent_instance.d.ts +9 -0
- package/dist/bone/output/agent/src/state_machines/agent_instance.js +22 -0
- package/dist/bone/output/agent/src/state_machines/agent_instance.js.map +1 -0
- package/dist/bone/output/agent/src/state_machines/build_step.d.ts +9 -0
- package/dist/bone/output/agent/src/state_machines/build_step.js +20 -0
- package/dist/bone/output/agent/src/state_machines/build_step.js.map +1 -0
- package/dist/bone/output/agent/src/state_machines/plan.d.ts +9 -0
- package/dist/bone/output/agent/src/state_machines/plan.js +20 -0
- package/dist/bone/output/agent/src/state_machines/plan.js.map +1 -0
- package/dist/bone/output/agent/src/state_machines/task.d.ts +9 -0
- package/dist/bone/output/agent/src/state_machines/task.js +20 -0
- package/dist/bone/output/agent/src/state_machines/task.js.map +1 -0
- package/dist/bone/output/agent/src/state_machines/tool_call.d.ts +9 -0
- package/dist/bone/output/agent/src/state_machines/tool_call.js +20 -0
- package/dist/bone/output/agent/src/state_machines/tool_call.js.map +1 -0
- package/dist/bone/output/rag/src/algorithms.d.ts +1 -0
- package/dist/bone/output/rag/src/algorithms.js +3 -0
- package/dist/bone/output/rag/src/algorithms.js.map +1 -0
- package/dist/bone/output/rag/src/auth.d.ts +8 -0
- package/dist/bone/output/rag/src/auth.js +56 -0
- package/dist/bone/output/rag/src/auth.js.map +1 -0
- package/dist/bone/output/rag/src/db.d.ts +6 -0
- package/dist/bone/output/rag/src/db.js +63 -0
- package/dist/bone/output/rag/src/db.js.map +1 -0
- package/dist/bone/output/rag/src/events.d.ts +25 -0
- package/dist/bone/output/rag/src/events.js +184 -0
- package/dist/bone/output/rag/src/events.js.map +1 -0
- package/dist/bone/output/rag/src/extensions.d.ts +83 -0
- package/dist/bone/output/rag/src/extensions.js +329 -0
- package/dist/bone/output/rag/src/extensions.js.map +1 -0
- package/dist/bone/output/rag/src/flows.d.ts +24 -0
- package/dist/bone/output/rag/src/flows.js +236 -0
- package/dist/bone/output/rag/src/flows.js.map +1 -0
- package/dist/bone/output/rag/src/logger.d.ts +28 -0
- package/dist/bone/output/rag/src/logger.js +45 -0
- package/dist/bone/output/rag/src/logger.js.map +1 -0
- package/dist/bone/output/rag/src/metrics.d.ts +5 -0
- package/dist/bone/output/rag/src/metrics.js +60 -0
- package/dist/bone/output/rag/src/metrics.js.map +1 -0
- package/dist/bone/output/rag/src/routes/code_chunk.d.ts +1 -0
- package/dist/bone/output/rag/src/routes/code_chunk.js +100 -0
- package/dist/bone/output/rag/src/routes/code_chunk.js.map +1 -0
- package/dist/bone/output/rag/src/routes/code_file.d.ts +1 -0
- package/dist/bone/output/rag/src/routes/code_file.js +127 -0
- package/dist/bone/output/rag/src/routes/code_file.js.map +1 -0
- package/dist/bone/output/rag/src/routes/indexing_job.d.ts +1 -0
- package/dist/bone/output/rag/src/routes/indexing_job.js +113 -0
- package/dist/bone/output/rag/src/routes/indexing_job.js.map +1 -0
- package/dist/bone/output/rag/src/routes/knowledge_base.d.ts +1 -0
- package/dist/bone/output/rag/src/routes/knowledge_base.js +242 -0
- package/dist/bone/output/rag/src/routes/knowledge_base.js.map +1 -0
- package/dist/bone/output/rag/src/routes/memory_entry.d.ts +1 -0
- package/dist/bone/output/rag/src/routes/memory_entry.js +113 -0
- package/dist/bone/output/rag/src/routes/memory_entry.js.map +1 -0
- package/dist/bone/output/rag/src/state_machines/code_file.d.ts +9 -0
- package/dist/bone/output/rag/src/state_machines/code_file.js +21 -0
- package/dist/bone/output/rag/src/state_machines/code_file.js.map +1 -0
- package/dist/bone/output/rag/src/state_machines/indexing_job.d.ts +9 -0
- package/dist/bone/output/rag/src/state_machines/indexing_job.js +20 -0
- package/dist/bone/output/rag/src/state_machines/indexing_job.js.map +1 -0
- package/dist/bone/output/rag/src/state_machines/knowledge_base.d.ts +9 -0
- package/dist/bone/output/rag/src/state_machines/knowledge_base.js +21 -0
- package/dist/bone/output/rag/src/state_machines/knowledge_base.js.map +1 -0
- package/dist/bone/output/rag/src/state_machines/memory_entry.d.ts +9 -0
- package/dist/bone/output/rag/src/state_machines/memory_entry.js +18 -0
- package/dist/bone/output/rag/src/state_machines/memory_entry.js.map +1 -0
- package/dist/bone/output/session/src/algorithms.d.ts +1 -0
- package/dist/bone/output/session/src/algorithms.js +3 -0
- package/dist/bone/output/session/src/algorithms.js.map +1 -0
- package/dist/bone/output/session/src/audit.d.ts +3 -0
- package/dist/bone/output/session/src/audit.js +40 -0
- package/dist/bone/output/session/src/audit.js.map +1 -0
- package/dist/bone/output/session/src/auth.d.ts +8 -0
- package/dist/bone/output/session/src/auth.js +56 -0
- package/dist/bone/output/session/src/auth.js.map +1 -0
- package/dist/bone/output/session/src/db.d.ts +6 -0
- package/dist/bone/output/session/src/db.js +63 -0
- package/dist/bone/output/session/src/db.js.map +1 -0
- package/dist/bone/output/session/src/events.d.ts +26 -0
- package/dist/bone/output/session/src/events.js +212 -0
- package/dist/bone/output/session/src/events.js.map +1 -0
- package/dist/bone/output/session/src/extensions.d.ts +41 -0
- package/dist/bone/output/session/src/extensions.js +217 -0
- package/dist/bone/output/session/src/extensions.js.map +1 -0
- package/dist/bone/output/session/src/logger.d.ts +28 -0
- package/dist/bone/output/session/src/logger.js +44 -0
- package/dist/bone/output/session/src/logger.js.map +1 -0
- package/dist/bone/output/session/src/metrics.d.ts +5 -0
- package/dist/bone/output/session/src/metrics.js +60 -0
- package/dist/bone/output/session/src/metrics.js.map +1 -0
- package/dist/bone/output/session/src/routes/message.d.ts +1 -0
- package/dist/bone/output/session/src/routes/message.js +120 -0
- package/dist/bone/output/session/src/routes/message.js.map +1 -0
- package/dist/bone/output/session/src/routes/part.d.ts +1 -0
- package/dist/bone/output/session/src/routes/part.js +106 -0
- package/dist/bone/output/session/src/routes/part.js.map +1 -0
- package/dist/bone/output/session/src/routes/permission.d.ts +1 -0
- package/dist/bone/output/session/src/routes/permission.js +106 -0
- package/dist/bone/output/session/src/routes/permission.js.map +1 -0
- package/dist/bone/output/session/src/routes/project.d.ts +1 -0
- package/dist/bone/output/session/src/routes/project.js +106 -0
- package/dist/bone/output/session/src/routes/project.js.map +1 -0
- package/dist/bone/output/session/src/routes/session.d.ts +1 -0
- package/dist/bone/output/session/src/routes/session.js +308 -0
- package/dist/bone/output/session/src/routes/session.js.map +1 -0
- package/dist/bone/output/session/src/state_machines/session.d.ts +9 -0
- package/dist/bone/output/session/src/state_machines/session.js +21 -0
- package/dist/bone/output/session/src/state_machines/session.js.map +1 -0
- package/dist/bone/output/session/src/websocket.d.ts +15 -0
- package/dist/bone/output/session/src/websocket.js +215 -0
- package/dist/bone/output/session/src/websocket.js.map +1 -0
- package/dist/bone/output/workspace/src/algorithms.d.ts +1 -0
- package/dist/bone/output/workspace/src/algorithms.js +3 -0
- package/dist/bone/output/workspace/src/algorithms.js.map +1 -0
- package/dist/bone/output/workspace/src/auth.d.ts +8 -0
- package/dist/bone/output/workspace/src/auth.js +56 -0
- package/dist/bone/output/workspace/src/auth.js.map +1 -0
- package/dist/bone/output/workspace/src/db.d.ts +6 -0
- package/dist/bone/output/workspace/src/db.js +63 -0
- package/dist/bone/output/workspace/src/db.js.map +1 -0
- package/dist/bone/output/workspace/src/events.d.ts +25 -0
- package/dist/bone/output/workspace/src/events.js +184 -0
- package/dist/bone/output/workspace/src/events.js.map +1 -0
- package/dist/bone/output/workspace/src/logger.d.ts +28 -0
- package/dist/bone/output/workspace/src/logger.js +45 -0
- package/dist/bone/output/workspace/src/logger.js.map +1 -0
- package/dist/bone/output/workspace/src/metrics.d.ts +5 -0
- package/dist/bone/output/workspace/src/metrics.js +60 -0
- package/dist/bone/output/workspace/src/metrics.js.map +1 -0
- package/dist/bone/output/workspace/src/routes/codebase.d.ts +1 -0
- package/dist/bone/output/workspace/src/routes/codebase.js +113 -0
- package/dist/bone/output/workspace/src/routes/codebase.js.map +1 -0
- package/dist/bone/output/workspace/src/routes/snapshot.d.ts +1 -0
- package/dist/bone/output/workspace/src/routes/snapshot.js +151 -0
- package/dist/bone/output/workspace/src/routes/snapshot.js.map +1 -0
- package/dist/bone/output/workspace/src/routes/workspace.d.ts +1 -0
- package/dist/bone/output/workspace/src/routes/workspace.js +209 -0
- package/dist/bone/output/workspace/src/routes/workspace.js.map +1 -0
- package/dist/bone/output/workspace/src/state_machines/codebase.d.ts +9 -0
- package/dist/bone/output/workspace/src/state_machines/codebase.js +19 -0
- package/dist/bone/output/workspace/src/state_machines/codebase.js.map +1 -0
- package/dist/bone/output/workspace/src/state_machines/snapshot.d.ts +9 -0
- package/dist/bone/output/workspace/src/state_machines/snapshot.js +18 -0
- package/dist/bone/output/workspace/src/state_machines/snapshot.js.map +1 -0
- package/dist/bone/output/workspace/src/state_machines/workspace.d.ts +9 -0
- package/dist/bone/output/workspace/src/state_machines/workspace.js +19 -0
- package/dist/bone/output/workspace/src/state_machines/workspace.js.map +1 -0
- package/dist/compat/opencode_adapter.d.ts +25 -0
- package/dist/compat/opencode_adapter.js +599 -0
- package/dist/compat/opencode_adapter.js.map +1 -0
- package/dist/extensions/chunker.d.ts +24 -0
- package/dist/extensions/chunker.js +360 -0
- package/dist/extensions/chunker.js.map +1 -0
- package/dist/extensions/embedding_provider.d.ts +18 -0
- package/dist/extensions/embedding_provider.js +150 -0
- package/dist/extensions/embedding_provider.js.map +1 -0
- package/dist/extensions/llm_provider.d.ts +33 -0
- package/dist/extensions/llm_provider.js +338 -0
- package/dist/extensions/llm_provider.js.map +1 -0
- package/dist/extensions/mcp_bridge.d.ts +44 -0
- package/dist/extensions/mcp_bridge.js +151 -0
- package/dist/extensions/mcp_bridge.js.map +1 -0
- package/dist/extensions/rag_search.d.ts +38 -0
- package/dist/extensions/rag_search.js +242 -0
- package/dist/extensions/rag_search.js.map +1 -0
- package/dist/extensions/snapshot.d.ts +14 -0
- package/dist/extensions/snapshot.js +158 -0
- package/dist/extensions/snapshot.js.map +1 -0
- package/dist/extensions/tool_executor.d.ts +28 -0
- package/dist/extensions/tool_executor.js +268 -0
- package/dist/extensions/tool_executor.js.map +1 -0
- package/dist/src/cli.d.ts +15 -0
- package/dist/src/cli.js +687 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +44 -0
- package/dist/src/config.js +165 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context_builder.d.ts +51 -0
- package/dist/src/context_builder.js +558 -0
- package/dist/src/context_builder.js.map +1 -0
- package/dist/src/db_adapter.d.ts +24 -0
- package/dist/src/db_adapter.js +341 -0
- package/dist/src/db_adapter.js.map +1 -0
- package/dist/src/engine/session/compaction_logic.d.ts +11 -0
- package/dist/src/engine/session/compaction_logic.js +113 -0
- package/dist/src/engine/session/compaction_logic.js.map +1 -0
- package/dist/src/engine/session/instruction_loader.d.ts +5 -0
- package/dist/src/engine/session/instruction_loader.js +78 -0
- package/dist/src/engine/session/instruction_loader.js.map +1 -0
- package/dist/src/engine/session/overflow_check.d.ts +14 -0
- package/dist/src/engine/session/overflow_check.js +45 -0
- package/dist/src/engine/session/overflow_check.js.map +1 -0
- package/dist/src/engine/session/prompt.d.ts +45 -0
- package/dist/src/engine/session/prompt.js +584 -0
- package/dist/src/engine/session/prompt.js.map +1 -0
- package/dist/src/engine/session/provider_transform.d.ts +59 -0
- package/dist/src/engine/session/provider_transform.js +193 -0
- package/dist/src/engine/session/provider_transform.js.map +1 -0
- package/dist/src/engine/session/retry_logic.d.ts +12 -0
- package/dist/src/engine/session/retry_logic.js +72 -0
- package/dist/src/engine/session/retry_logic.js.map +1 -0
- package/dist/src/engine/session/system_prompt.d.ts +9 -0
- package/dist/src/engine/session/system_prompt.js +96 -0
- package/dist/src/engine/session/system_prompt.js.map +1 -0
- package/dist/src/engine/session/tool_registry.d.ts +5 -0
- package/dist/src/engine/session/tool_registry.js +117 -0
- package/dist/src/engine/session/tool_registry.js.map +1 -0
- package/dist/src/export.d.ts +13 -0
- package/dist/src/export.js +103 -0
- package/dist/src/export.js.map +1 -0
- package/dist/src/mdns.d.ts +7 -0
- package/dist/src/mdns.js +60 -0
- package/dist/src/mdns.js.map +1 -0
- package/dist/src/rag_worker.d.ts +38 -0
- package/dist/src/rag_worker.js +435 -0
- package/dist/src/rag_worker.js.map +1 -0
- package/dist/src/server.d.ts +11 -0
- package/dist/src/server.js +214 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/stats.d.ts +45 -0
- package/dist/src/stats.js +233 -0
- package/dist/src/stats.js.map +1 -0
- package/dist/src/tui.d.ts +29 -0
- package/dist/src/tui.js +1053 -0
- package/dist/src/tui.js.map +1 -0
- package/package.json +7 -4
- package/src/cli.ts +247 -5
- package/src/export.ts +122 -0
- package/src/mdns.ts +53 -0
- package/src/server.ts +32 -0
- package/src/stats.ts +290 -0
- package/src/tui.ts +749 -248
package/dist/src/tui.js
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BoneCode TUI — terminal interface modeled after OpenCode
|
|
4
|
+
*
|
|
5
|
+
* Three core behaviours:
|
|
6
|
+
* 1. Tool activity is displayed as concise status lines
|
|
7
|
+
* (← Edit src/foo.ts, → Read package.json, $ npm test) — never raw code dumps.
|
|
8
|
+
* Assistant text is shown inline; tool calls and tool outputs are summarized.
|
|
9
|
+
*
|
|
10
|
+
* 2. Typing "/" shows an inline command menu above the prompt with
|
|
11
|
+
* arrow-key selection and tab/enter to insert.
|
|
12
|
+
*
|
|
13
|
+
* 3. Ctrl+C aborts the in-flight LLM stream by aborting the fetch
|
|
14
|
+
* AND notifying the server to cancel the agent loop.
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.startInteractiveTUI = exports.runTUI = void 0;
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const readline = __importStar(require("readline"));
|
|
44
|
+
const http = __importStar(require("http"));
|
|
45
|
+
// ─── ANSI ─────────────────────────────────────────────────────────────────────
|
|
46
|
+
const ESC = "\x1b";
|
|
47
|
+
const R = `${ESC}[0m`;
|
|
48
|
+
const BOLD = `${ESC}[1m`;
|
|
49
|
+
const DIM = `${ESC}[2m`;
|
|
50
|
+
const CYAN = `${ESC}[96m`;
|
|
51
|
+
const GREEN = `${ESC}[92m`;
|
|
52
|
+
const YELLOW = `${ESC}[93m`;
|
|
53
|
+
const RED = `${ESC}[91m`;
|
|
54
|
+
const GRAY = `${ESC}[90m`;
|
|
55
|
+
const WHITE = `${ESC}[97m`;
|
|
56
|
+
const BLUE = `${ESC}[94m`;
|
|
57
|
+
// Box drawing
|
|
58
|
+
const VERT = "┃";
|
|
59
|
+
const CORN = "╹";
|
|
60
|
+
const SHADE = "▀";
|
|
61
|
+
const BLOCK = "▣";
|
|
62
|
+
const ARROW_R = "→";
|
|
63
|
+
const ARROW_L = "←";
|
|
64
|
+
const DOLLAR = "$";
|
|
65
|
+
const STAR = "✱";
|
|
66
|
+
const PCT = "%";
|
|
67
|
+
const GEAR = "⚙";
|
|
68
|
+
const HASH = "#";
|
|
69
|
+
function cols() { return process.stdout.columns || 80; }
|
|
70
|
+
function out(s) { process.stdout.write(s); }
|
|
71
|
+
function nl(s = "") { process.stdout.write(s + "\n"); }
|
|
72
|
+
function clearLine() { out(`\r${ESC}[2K`); }
|
|
73
|
+
// ─── Logo ─────────────────────────────────────────────────────────────────────
|
|
74
|
+
function printLogo() {
|
|
75
|
+
nl();
|
|
76
|
+
nl(`${GRAY}${BOLD}█▀▀▄ █▀▀█ █▀▀█ █▀▀▀ █▀▀▀ █▀▀█ █▀▀▄ █▀▀▀${R}`);
|
|
77
|
+
nl(`${GRAY}${BOLD}█▀▀▄ █ █ █ █ █▀▀ █ █ █ █ █ █▀▀ ${R}`);
|
|
78
|
+
nl(`${GRAY}${BOLD}▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀▀${R}`);
|
|
79
|
+
nl();
|
|
80
|
+
}
|
|
81
|
+
// ─── Package version ──────────────────────────────────────────────────────────
|
|
82
|
+
const PKG_ROOT = (() => {
|
|
83
|
+
const fromSrc = path.resolve(__dirname, "..");
|
|
84
|
+
const fromDist = path.resolve(__dirname, "..", "..");
|
|
85
|
+
if (fs.existsSync(path.join(fromDist, "package.json")))
|
|
86
|
+
return fromDist;
|
|
87
|
+
return fromSrc;
|
|
88
|
+
})();
|
|
89
|
+
function getVersion() {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8")).version;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return "0.0.0";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const COMMANDS = [
|
|
98
|
+
{ name: "/new", description: "Start a new session" },
|
|
99
|
+
{ name: "/session", description: "Show current session ID" },
|
|
100
|
+
{ name: "/sessions", description: "List recent sessions" },
|
|
101
|
+
{ name: "/model", description: "Switch model", args: "<provider/model>" },
|
|
102
|
+
{ name: "/provider", description: "Switch provider", args: "<id>" },
|
|
103
|
+
{ name: "/providers", description: "List all providers" },
|
|
104
|
+
{ name: "/clear", description: "Clear screen" },
|
|
105
|
+
{ name: "/history", description: "Show last 10 prompts" },
|
|
106
|
+
{ name: "/help", description: "Show this help" },
|
|
107
|
+
{ name: "/exit", description: "Exit BoneCode" },
|
|
108
|
+
];
|
|
109
|
+
// ─── @file autocomplete helpers ───────────────────────────────────────────────
|
|
110
|
+
const IGNORED_DIRS = new Set([
|
|
111
|
+
"node_modules", ".git", "dist", "build", ".next", "__pycache__",
|
|
112
|
+
".venv", "venv", "target", "vendor", ".cache", "coverage",
|
|
113
|
+
]);
|
|
114
|
+
const CODE_EXTS = new Set([
|
|
115
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs",
|
|
116
|
+
".java", ".kt", ".cs", ".cpp", ".c", ".h", ".rb", ".php", ".swift",
|
|
117
|
+
".md", ".mdx", ".json", ".yaml", ".yml", ".toml", ".env", ".sql", ".sh", ".graphql",
|
|
118
|
+
]);
|
|
119
|
+
function listFiles(worktree, prefix) {
|
|
120
|
+
const results = [];
|
|
121
|
+
function walk(dir, depth) {
|
|
122
|
+
if (results.length >= 50 || depth > 4)
|
|
123
|
+
return;
|
|
124
|
+
let entries;
|
|
125
|
+
try {
|
|
126
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const e of entries) {
|
|
132
|
+
if (results.length >= 50)
|
|
133
|
+
break;
|
|
134
|
+
if (IGNORED_DIRS.has(e.name) || e.name.startsWith("."))
|
|
135
|
+
continue;
|
|
136
|
+
const rel = path.relative(worktree, path.join(dir, e.name));
|
|
137
|
+
if (e.isDirectory()) {
|
|
138
|
+
if (!prefix || rel.startsWith(prefix) || prefix.startsWith(rel)) {
|
|
139
|
+
results.push(rel + "/");
|
|
140
|
+
walk(path.join(dir, e.name), depth + 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (CODE_EXTS.has(path.extname(e.name).toLowerCase())) {
|
|
144
|
+
if (!prefix || rel.startsWith(prefix))
|
|
145
|
+
results.push(rel);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
walk(worktree, 0);
|
|
150
|
+
return results.sort();
|
|
151
|
+
}
|
|
152
|
+
function buildCompleter(worktree) {
|
|
153
|
+
return (line) => {
|
|
154
|
+
if (line.startsWith("/")) {
|
|
155
|
+
// Tab-complete commands too (in addition to the inline menu)
|
|
156
|
+
const matches = COMMANDS.map(c => c.name).filter(c => c.startsWith(line));
|
|
157
|
+
return [matches, line];
|
|
158
|
+
}
|
|
159
|
+
const atIdx = line.lastIndexOf("@");
|
|
160
|
+
if (atIdx !== -1) {
|
|
161
|
+
const prefix = line.slice(atIdx + 1);
|
|
162
|
+
const completions = listFiles(worktree, prefix).map(f => line.slice(0, atIdx + 1) + f);
|
|
163
|
+
return [completions, line];
|
|
164
|
+
}
|
|
165
|
+
return [[], line];
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// ─── Server health ────────────────────────────────────────────────────────────
|
|
169
|
+
async function waitForServer(port, maxMs = 30000) {
|
|
170
|
+
const start = Date.now();
|
|
171
|
+
while (Date.now() - start < maxMs) {
|
|
172
|
+
try {
|
|
173
|
+
const ok = await new Promise((resolve) => {
|
|
174
|
+
const req = http.get(`http://localhost:${port}/health`, (res) => resolve(res.statusCode === 200));
|
|
175
|
+
req.on("error", () => resolve(false));
|
|
176
|
+
req.setTimeout(1000, () => { req.destroy(); resolve(false); });
|
|
177
|
+
});
|
|
178
|
+
if (ok)
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
await new Promise(r => setTimeout(r, 500));
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// ─── API ──────────────────────────────────────────────────────────────────────
|
|
187
|
+
async function apiGet(url, token) {
|
|
188
|
+
const r = await fetch(url, { headers: { "Authorization": `Bearer ${token}` } });
|
|
189
|
+
if (!r.ok)
|
|
190
|
+
throw new Error(`API ${r.status}`);
|
|
191
|
+
return r.json();
|
|
192
|
+
}
|
|
193
|
+
async function apiPost(url, body, token) {
|
|
194
|
+
return fetch(url, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async function apiDelete(url, token) {
|
|
201
|
+
await fetch(url, { method: "DELETE", headers: { "Authorization": `Bearer ${token}` } });
|
|
202
|
+
}
|
|
203
|
+
async function createSession(port, token, worktree, title) {
|
|
204
|
+
const r = await apiPost(`http://localhost:${port}/v2/session`, { title, directory: worktree }, token);
|
|
205
|
+
if (!r.ok)
|
|
206
|
+
throw new Error(`Failed to create session: ${await r.text()}`);
|
|
207
|
+
const sess = await r.json();
|
|
208
|
+
if (!sess.id)
|
|
209
|
+
throw new Error("No session ID returned");
|
|
210
|
+
return sess.id;
|
|
211
|
+
}
|
|
212
|
+
function relPath(p, worktree) {
|
|
213
|
+
if (!p)
|
|
214
|
+
return "";
|
|
215
|
+
try {
|
|
216
|
+
if (path.isAbsolute(p)) {
|
|
217
|
+
const rel = path.relative(worktree, p);
|
|
218
|
+
if (!rel.startsWith(".."))
|
|
219
|
+
return rel.replace(/\\/g, "/");
|
|
220
|
+
}
|
|
221
|
+
return p.replace(/\\/g, "/");
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return p;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function describeTool(toolName, input, worktree) {
|
|
228
|
+
const n = (toolName || "").toLowerCase();
|
|
229
|
+
const inp = input || {};
|
|
230
|
+
// Map BoneCode's tool names AND opencode's names
|
|
231
|
+
const isWrite = n === "write" || n === "write_file";
|
|
232
|
+
const isEdit = n === "edit" || n === "edit_file";
|
|
233
|
+
const isRead = n === "read" || n === "read_file";
|
|
234
|
+
const isShell = n === "bash" || n === "shell" || n === "run_command";
|
|
235
|
+
const isGlob = n === "glob";
|
|
236
|
+
const isGrep = n === "grep" || n === "search_files";
|
|
237
|
+
const isList = n === "list_directory" || n === "list" || n === "ls";
|
|
238
|
+
const isWebfetch = n === "webfetch";
|
|
239
|
+
const isWebsearch = n === "websearch";
|
|
240
|
+
const isPatch = n === "apply_patch" || n === "patch";
|
|
241
|
+
const isTodo = n === "todo_write" || n === "todowrite" || n === "todo";
|
|
242
|
+
const isTask = n === "task" || n === "subagent";
|
|
243
|
+
if (isWrite) {
|
|
244
|
+
return { icon: ARROW_L, color: CYAN, title: "Write", detail: relPath(inp.path || inp.filePath, worktree) };
|
|
245
|
+
}
|
|
246
|
+
if (isEdit) {
|
|
247
|
+
return { icon: ARROW_L, color: CYAN, title: "Edit", detail: relPath(inp.path || inp.filePath, worktree) };
|
|
248
|
+
}
|
|
249
|
+
if (isPatch) {
|
|
250
|
+
return { icon: PCT, color: CYAN, title: "Patch", detail: inp.patch ? `${String(inp.patch).split("\n").length} lines` : "" };
|
|
251
|
+
}
|
|
252
|
+
if (isRead) {
|
|
253
|
+
let detail = relPath(inp.path || inp.filePath, worktree);
|
|
254
|
+
if (inp.start_line || inp.end_line) {
|
|
255
|
+
detail += ` ${GRAY}[${inp.start_line || 1}-${inp.end_line || ""}]${R}`;
|
|
256
|
+
}
|
|
257
|
+
return { icon: ARROW_R, color: GRAY, title: "Read", detail };
|
|
258
|
+
}
|
|
259
|
+
if (isShell) {
|
|
260
|
+
const cmd = String(inp.command || "").trim();
|
|
261
|
+
const short = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
262
|
+
return { icon: DOLLAR, color: YELLOW, title: short || "Shell" };
|
|
263
|
+
}
|
|
264
|
+
if (isGlob) {
|
|
265
|
+
return { icon: STAR, color: GRAY, title: "Glob", detail: inp.pattern || "" };
|
|
266
|
+
}
|
|
267
|
+
if (isGrep) {
|
|
268
|
+
const pattern = inp.pattern || "";
|
|
269
|
+
const glob = inp.glob ? ` in ${inp.glob}` : "";
|
|
270
|
+
return { icon: STAR, color: GRAY, title: "Grep", detail: `"${pattern}"${glob}` };
|
|
271
|
+
}
|
|
272
|
+
if (isList) {
|
|
273
|
+
return { icon: ARROW_R, color: GRAY, title: "List", detail: relPath(inp.path, worktree) };
|
|
274
|
+
}
|
|
275
|
+
if (isWebfetch) {
|
|
276
|
+
return { icon: PCT, color: BLUE, title: "WebFetch", detail: inp.url || "" };
|
|
277
|
+
}
|
|
278
|
+
if (isWebsearch) {
|
|
279
|
+
return { icon: PCT, color: BLUE, title: "WebSearch", detail: inp.query ? `"${inp.query}"` : "" };
|
|
280
|
+
}
|
|
281
|
+
if (isTodo) {
|
|
282
|
+
const todos = Array.isArray(inp.todos) ? inp.todos : [];
|
|
283
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
284
|
+
return { icon: HASH, color: GRAY, title: "Todos", detail: `${done}/${todos.length} done` };
|
|
285
|
+
}
|
|
286
|
+
if (isTask) {
|
|
287
|
+
return { icon: HASH, color: CYAN, title: "Task", detail: inp.description || "" };
|
|
288
|
+
}
|
|
289
|
+
// Fallback
|
|
290
|
+
return { icon: GEAR, color: GRAY, title: toolName };
|
|
291
|
+
}
|
|
292
|
+
function renderToolStart(d) {
|
|
293
|
+
const detail = d.detail ? ` ${GRAY}${d.detail}${R}` : "";
|
|
294
|
+
nl(` ${d.color}${d.icon}${R} ${WHITE}${d.title}${R}${detail}`);
|
|
295
|
+
}
|
|
296
|
+
function renderToolDone(d, ms) {
|
|
297
|
+
const detail = d.detail ? ` ${GRAY}${d.detail}${R}` : "";
|
|
298
|
+
const time = ms !== undefined ? ` ${GRAY}${(ms / 1000).toFixed(1)}s${R}` : "";
|
|
299
|
+
// Replace the "..." with a checkmark (or just write a new line if not interactive)
|
|
300
|
+
nl(` ${GREEN}✓${R} ${GRAY}${d.title}${R}${detail}${time}`);
|
|
301
|
+
}
|
|
302
|
+
function renderToolError(d, err) {
|
|
303
|
+
const detail = d.detail ? ` ${GRAY}${d.detail}${R}` : "";
|
|
304
|
+
nl(` ${RED}✗${R} ${GRAY}${d.title}${R}${detail} ${RED}${err}${R}`);
|
|
305
|
+
}
|
|
306
|
+
// ─── User message rendering ───────────────────────────────────────────────────
|
|
307
|
+
function renderUserMessage(text) {
|
|
308
|
+
nl();
|
|
309
|
+
for (const line of text.split("\n")) {
|
|
310
|
+
nl(`${GRAY}${VERT}${R} ${WHITE}${line}${R}`);
|
|
311
|
+
}
|
|
312
|
+
nl();
|
|
313
|
+
}
|
|
314
|
+
function renderTurnEnd(model, elapsedMs, interrupted) {
|
|
315
|
+
const elapsed = (elapsedMs / 1000).toFixed(1);
|
|
316
|
+
const dur = interrupted ? `${YELLOW}interrupted${R}` : `${GRAY}${elapsed}s${R}`;
|
|
317
|
+
nl();
|
|
318
|
+
nl(` ${CYAN}${BLOCK}${R} ${WHITE}Build${R}${GRAY} · ${model} · ${R}${dur}`);
|
|
319
|
+
}
|
|
320
|
+
// ─── Help / providers / commands menu ─────────────────────────────────────────
|
|
321
|
+
function printHelp() {
|
|
322
|
+
nl();
|
|
323
|
+
nl(`${CYAN}${BOLD}BoneCode${R} ${GRAY}commands${R}`);
|
|
324
|
+
nl();
|
|
325
|
+
for (const c of COMMANDS) {
|
|
326
|
+
const args = c.args ? ` ${GRAY}${c.args}${R}` : "";
|
|
327
|
+
nl(` ${CYAN}${c.name.padEnd(12)}${R}${args.padEnd(20)} ${GRAY}${c.description}${R}`);
|
|
328
|
+
}
|
|
329
|
+
nl();
|
|
330
|
+
nl(` ${CYAN}Ctrl+C${R} ${GRAY}Interrupt current request${R}`);
|
|
331
|
+
nl(` ${CYAN}Ctrl+D${R} ${GRAY}Exit BoneCode${R}`);
|
|
332
|
+
nl(` ${CYAN}↑ / ↓${R} ${GRAY}Prompt history${R}`);
|
|
333
|
+
nl(` ${CYAN}@<path>${R} ${GRAY}Attach a file (Tab completes)${R}`);
|
|
334
|
+
nl();
|
|
335
|
+
}
|
|
336
|
+
async function fetchProviders(port, token) {
|
|
337
|
+
try {
|
|
338
|
+
return await apiGet(`http://localhost:${port}/v2/provider`, token);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function printProviders(providers, currentProvider) {
|
|
345
|
+
const width = cols();
|
|
346
|
+
nl();
|
|
347
|
+
nl(`${CYAN}${BOLD}Providers${R}`);
|
|
348
|
+
nl(`${GRAY}${"─".repeat(Math.min(width, 56))}${R}`);
|
|
349
|
+
for (const p of providers) {
|
|
350
|
+
const active = p.id === currentProvider;
|
|
351
|
+
const dot = active ? `${GREEN}●${R}` : `${GRAY}○${R}`;
|
|
352
|
+
const free = p.free ? ` ${GREEN}free${R}` : "";
|
|
353
|
+
const local = p.id === "local" ? ` ${CYAN}local config${R}` : "";
|
|
354
|
+
nl(` ${dot} ${active ? CYAN + BOLD : WHITE}${p.id}${R}${free}${local} ${GRAY}${p.name}${R}`);
|
|
355
|
+
if (p.freeNote)
|
|
356
|
+
nl(` ${GRAY}${p.freeNote}${R}`);
|
|
357
|
+
if (p.models?.length)
|
|
358
|
+
nl(` ${GRAY}${p.models.slice(0, 3).join(" ")}${R}`);
|
|
359
|
+
if (p.keyEnv)
|
|
360
|
+
nl(` ${GRAY}${p.keyEnv}${R}`);
|
|
361
|
+
}
|
|
362
|
+
nl(`${GRAY}${"─".repeat(Math.min(width, 56))}${R}`);
|
|
363
|
+
nl(` ${GRAY}/provider <id> /model <id>${R}`);
|
|
364
|
+
nl();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Stateful code-fence collapser.
|
|
368
|
+
*
|
|
369
|
+
* The LLM streams text token-by-token, including ```python\n...\n``` blocks.
|
|
370
|
+
* We don't want to dump that raw to the terminal because:
|
|
371
|
+
* (a) The code is going to be saved via a tool call anyway, so showing it
|
|
372
|
+
* twice (raw stream + saved file path) is redundant.
|
|
373
|
+
* (b) It can be 100s of lines long and overwhelms the TUI.
|
|
374
|
+
*
|
|
375
|
+
* Strategy: buffer everything between ``` and ```. When the closing fence
|
|
376
|
+
* arrives, emit a one-line marker like [code: python, 42 lines]
|
|
377
|
+
* instead of the buffered content.
|
|
378
|
+
*
|
|
379
|
+
* State machine:
|
|
380
|
+
* "text" — pass deltas through verbatim (with partial-fence detection)
|
|
381
|
+
* "fence" — buffer everything; emit marker on close fence
|
|
382
|
+
*/
|
|
383
|
+
function makeCodeFenceCollapser() {
|
|
384
|
+
let mode = "text";
|
|
385
|
+
let lang = "";
|
|
386
|
+
let buffered = ""; // bytes accumulated inside a fence
|
|
387
|
+
let pending = ""; // partial-fence buffer (when we see backticks but don't know yet)
|
|
388
|
+
const lines = (s) => s.split("\n").length;
|
|
389
|
+
function flushPending() {
|
|
390
|
+
const out = pending;
|
|
391
|
+
pending = "";
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
feed(chunk) {
|
|
396
|
+
let result = "";
|
|
397
|
+
let i = 0;
|
|
398
|
+
const buf = pending + chunk;
|
|
399
|
+
pending = "";
|
|
400
|
+
while (i < buf.length) {
|
|
401
|
+
if (mode === "text") {
|
|
402
|
+
// Look for ``` to enter fence mode
|
|
403
|
+
const fenceIdx = buf.indexOf("```", i);
|
|
404
|
+
if (fenceIdx === -1) {
|
|
405
|
+
// No fence in remaining text — emit it all, but hold the last 2 chars
|
|
406
|
+
// in case they're part of an upcoming fence.
|
|
407
|
+
const safeEnd = Math.max(i, buf.length - 2);
|
|
408
|
+
result += buf.slice(i, safeEnd);
|
|
409
|
+
pending = buf.slice(safeEnd);
|
|
410
|
+
i = buf.length;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
// Emit text up to the fence
|
|
414
|
+
result += buf.slice(i, fenceIdx);
|
|
415
|
+
// Read the language (everything until newline)
|
|
416
|
+
const nlIdx = buf.indexOf("\n", fenceIdx + 3);
|
|
417
|
+
if (nlIdx === -1) {
|
|
418
|
+
// Don't have the full opening line yet — buffer
|
|
419
|
+
pending = buf.slice(fenceIdx);
|
|
420
|
+
i = buf.length;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
lang = buf.slice(fenceIdx + 3, nlIdx).trim() || "code";
|
|
424
|
+
i = nlIdx + 1;
|
|
425
|
+
mode = "fence";
|
|
426
|
+
buffered = "";
|
|
427
|
+
// Print a leading marker (will be amended when we close)
|
|
428
|
+
result += `${GRAY}┃ code: ${lang}…${R}`;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// mode === "fence" — look for closing ```
|
|
432
|
+
const closeIdx = buf.indexOf("```", i);
|
|
433
|
+
if (closeIdx === -1) {
|
|
434
|
+
buffered += buf.slice(i);
|
|
435
|
+
// Hold last 2 chars in case they're part of an upcoming close fence
|
|
436
|
+
const safeEnd = Math.max(i, buf.length - 2);
|
|
437
|
+
buffered = buffered.slice(0, buffered.length - (buf.length - safeEnd));
|
|
438
|
+
pending = buf.slice(safeEnd);
|
|
439
|
+
i = buf.length;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
// Closing fence found
|
|
443
|
+
buffered += buf.slice(i, closeIdx);
|
|
444
|
+
const lineCount = lines(buffered.replace(/\n+$/, ""));
|
|
445
|
+
// Replace the placeholder we already wrote with the final marker.
|
|
446
|
+
// Carriage return + clear line + reprint marker.
|
|
447
|
+
result += `\r${ESC}[2K ${GRAY}┃ code: ${lang}, ${lineCount} line${lineCount === 1 ? "" : "s"}${R}\n`;
|
|
448
|
+
i = closeIdx + 3;
|
|
449
|
+
// Skip optional newline after closing fence
|
|
450
|
+
if (buf[i] === "\n")
|
|
451
|
+
i++;
|
|
452
|
+
mode = "text";
|
|
453
|
+
lang = "";
|
|
454
|
+
buffered = "";
|
|
455
|
+
}
|
|
456
|
+
return result;
|
|
457
|
+
},
|
|
458
|
+
flush() {
|
|
459
|
+
const tail = flushPending();
|
|
460
|
+
if (mode === "fence") {
|
|
461
|
+
// Stream ended mid-fence — close it with an approximate count
|
|
462
|
+
const lineCount = lines(buffered.replace(/\n+$/, ""));
|
|
463
|
+
mode = "text";
|
|
464
|
+
return tail + `\r${ESC}[2K ${GRAY}┃ code: ${lang}, ${lineCount}+ lines${R}\n`;
|
|
465
|
+
}
|
|
466
|
+
return tail;
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async function streamPrompt(opts) {
|
|
471
|
+
const { port, token, sessionId, model, provider, message, worktree, abortSignal } = opts;
|
|
472
|
+
const t0 = Date.now();
|
|
473
|
+
let fullText = "";
|
|
474
|
+
let tokens = 0;
|
|
475
|
+
let inAssistantText = false;
|
|
476
|
+
let tools = new Map();
|
|
477
|
+
// Code-fence collapser — replaces ```...``` blocks with [code: lang, N lines]
|
|
478
|
+
// markers across delta boundaries.
|
|
479
|
+
const fence = makeCodeFenceCollapser();
|
|
480
|
+
const collapseCodeFences = (chunk) => fence.feed(chunk);
|
|
481
|
+
// Track which message the deltas belong to so we can detect tool boundaries
|
|
482
|
+
// The compat adapter emits these event types:
|
|
483
|
+
// part.delta — text chunk
|
|
484
|
+
// tool.requested — tool call started
|
|
485
|
+
// message.updated — message saved
|
|
486
|
+
// session.updated — session state changed
|
|
487
|
+
// error — error event
|
|
488
|
+
const flushTextLine = () => {
|
|
489
|
+
if (inAssistantText) {
|
|
490
|
+
// Make sure assistant text ends with a newline before the next thing
|
|
491
|
+
if (fullText && !fullText.endsWith("\n"))
|
|
492
|
+
out("\n");
|
|
493
|
+
inAssistantText = false;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
const r = await fetch(`http://localhost:${port}/v2/session/${sessionId}/prompt`, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
|
|
500
|
+
body: JSON.stringify({ content: message, modelID: model, providerID: provider }),
|
|
501
|
+
signal: abortSignal,
|
|
502
|
+
});
|
|
503
|
+
if (!r.ok) {
|
|
504
|
+
return { text: "", tokens: 0, elapsedMs: Date.now() - t0, error: `HTTP ${r.status}: ${await r.text()}`, interrupted: false };
|
|
505
|
+
}
|
|
506
|
+
const reader = r.body.getReader();
|
|
507
|
+
const dec = new TextDecoder();
|
|
508
|
+
let buf = "";
|
|
509
|
+
while (true) {
|
|
510
|
+
const { value, done } = await reader.read();
|
|
511
|
+
if (done)
|
|
512
|
+
break;
|
|
513
|
+
buf += dec.decode(value, { stream: true });
|
|
514
|
+
const lines = buf.split("\n");
|
|
515
|
+
buf = lines.pop() || "";
|
|
516
|
+
for (const raw of lines) {
|
|
517
|
+
if (!raw.startsWith("data: "))
|
|
518
|
+
continue;
|
|
519
|
+
const json = raw.slice(6).trim();
|
|
520
|
+
if (!json || json === "[DONE]")
|
|
521
|
+
continue;
|
|
522
|
+
try {
|
|
523
|
+
const ev = JSON.parse(json);
|
|
524
|
+
// Text delta — assistant is generating prose
|
|
525
|
+
if (ev.type === "part.delta" && ev.delta?.type === "text") {
|
|
526
|
+
const text = ev.delta.text || "";
|
|
527
|
+
if (!text)
|
|
528
|
+
continue;
|
|
529
|
+
if (!inAssistantText) {
|
|
530
|
+
// Start of assistant text — print the indent prefix once
|
|
531
|
+
out(` `);
|
|
532
|
+
inAssistantText = true;
|
|
533
|
+
}
|
|
534
|
+
// Process the delta through the code-fence collapser so streaming
|
|
535
|
+
// code blocks appear as "[code: lang]" placeholders instead of
|
|
536
|
+
// dumping raw source.
|
|
537
|
+
const piece = collapseCodeFences(text);
|
|
538
|
+
// Print with leading-newline indenting (so each new line gets the 3-space prefix)
|
|
539
|
+
const indented = piece.replace(/\n/g, `\n `);
|
|
540
|
+
out(indented);
|
|
541
|
+
fullText += text;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
// Tool requested — show concise activity line
|
|
545
|
+
if (ev.type === "tool.requested") {
|
|
546
|
+
flushTextLine();
|
|
547
|
+
const callId = ev.tool_call_id || ev.id || `${ev.tool_name}-${Date.now()}`;
|
|
548
|
+
const display = describeTool(ev.tool_name || "tool", ev.tool_input || {}, worktree);
|
|
549
|
+
tools.set(callId, { callId, display, startedAt: Date.now() });
|
|
550
|
+
renderToolStart(display);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
// Tool completed
|
|
554
|
+
if (ev.type === "tool.completed" || ev.type === "tool.success") {
|
|
555
|
+
flushTextLine();
|
|
556
|
+
const callId = ev.tool_call_id || ev.id || "";
|
|
557
|
+
const tracked = tools.get(callId);
|
|
558
|
+
if (tracked) {
|
|
559
|
+
const ms = Date.now() - tracked.startedAt;
|
|
560
|
+
renderToolDone(tracked.display, ms);
|
|
561
|
+
tools.delete(callId);
|
|
562
|
+
}
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
// Tool failed
|
|
566
|
+
if (ev.type === "tool.failed" || ev.type === "tool.error") {
|
|
567
|
+
flushTextLine();
|
|
568
|
+
const callId = ev.tool_call_id || ev.id || "";
|
|
569
|
+
const tracked = tools.get(callId);
|
|
570
|
+
const err = ev.error || ev.message || "failed";
|
|
571
|
+
if (tracked) {
|
|
572
|
+
renderToolError(tracked.display, err);
|
|
573
|
+
tools.delete(callId);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
nl(` ${RED}✗ ${err}${R}`);
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
// Retry notification
|
|
581
|
+
if (ev.type === "session.retry") {
|
|
582
|
+
flushTextLine();
|
|
583
|
+
nl(` ${YELLOW}⟳ Retry ${ev.attempt || ""}: ${ev.message || ""}${R}`);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
// Server-side error
|
|
587
|
+
if (ev.type === "error") {
|
|
588
|
+
flushTextLine();
|
|
589
|
+
return {
|
|
590
|
+
text: fullText,
|
|
591
|
+
tokens,
|
|
592
|
+
elapsedMs: Date.now() - t0,
|
|
593
|
+
error: ev.properties?.message || "error",
|
|
594
|
+
interrupted: false,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
// Compaction
|
|
598
|
+
if (ev.type === "session.compacted") {
|
|
599
|
+
flushTextLine();
|
|
600
|
+
nl(` ${BLUE}⊕ Context compacted${R}`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// Ignore malformed events
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
flushTextLine();
|
|
610
|
+
// Drain any tools that didn't get a completion event
|
|
611
|
+
for (const tracked of tools.values()) {
|
|
612
|
+
const ms = Date.now() - tracked.startedAt;
|
|
613
|
+
renderToolDone(tracked.display, ms);
|
|
614
|
+
}
|
|
615
|
+
tools.clear();
|
|
616
|
+
// Flush any unclosed code fence
|
|
617
|
+
const tail = fence.flush();
|
|
618
|
+
if (tail)
|
|
619
|
+
out(tail);
|
|
620
|
+
// Fetch the final message to get token totals
|
|
621
|
+
try {
|
|
622
|
+
const msgs = await apiGet(`http://localhost:${port}/v2/session/${sessionId}/message`, token);
|
|
623
|
+
const last = msgs.filter((m) => m.role === "assistant").slice(-1)[0];
|
|
624
|
+
tokens = (last?.tokens?.input || 0) + (last?.tokens?.output || 0);
|
|
625
|
+
// If we didn't get any text deltas but the stored message has text, render it now
|
|
626
|
+
if (!fullText && last?.parts) {
|
|
627
|
+
for (const p of last.parts) {
|
|
628
|
+
if (p.type === "text" && p.text) {
|
|
629
|
+
out(` ${p.text.replace(/\n/g, `\n `)}\n`);
|
|
630
|
+
fullText = p.text;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
catch { }
|
|
637
|
+
return { text: fullText, tokens, elapsedMs: Date.now() - t0, interrupted: false };
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
flushTextLine();
|
|
641
|
+
if (e.name === "AbortError") {
|
|
642
|
+
return { text: fullText, tokens, elapsedMs: Date.now() - t0, interrupted: true };
|
|
643
|
+
}
|
|
644
|
+
return { text: fullText, tokens, elapsedMs: Date.now() - t0, error: e.message, interrupted: false };
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function filterCommands(query) {
|
|
648
|
+
if (!query || query === "/")
|
|
649
|
+
return COMMANDS;
|
|
650
|
+
const q = query.toLowerCase();
|
|
651
|
+
return COMMANDS.filter(c => c.name.toLowerCase().startsWith(q));
|
|
652
|
+
}
|
|
653
|
+
function renderCommandMenu(state) {
|
|
654
|
+
if (!state.visible || state.options.length === 0)
|
|
655
|
+
return 0;
|
|
656
|
+
const max = Math.min(state.options.length, 8);
|
|
657
|
+
for (let i = 0; i < max; i++) {
|
|
658
|
+
const c = state.options[i];
|
|
659
|
+
const selected = i === state.selected;
|
|
660
|
+
const prefix = selected ? `${CYAN}▌${R} ` : ` `;
|
|
661
|
+
const name = selected ? `${CYAN}${BOLD}${c.name}${R}` : `${WHITE}${c.name}${R}`;
|
|
662
|
+
const args = c.args ? ` ${GRAY}${c.args}${R}` : "";
|
|
663
|
+
const desc = `${GRAY}${c.description}${R}`;
|
|
664
|
+
nl(`${prefix}${name}${args} ${desc}`);
|
|
665
|
+
}
|
|
666
|
+
if (state.options.length > max) {
|
|
667
|
+
nl(` ${GRAY}... ${state.options.length - max} more (keep typing)${R}`);
|
|
668
|
+
return max + 1;
|
|
669
|
+
}
|
|
670
|
+
return max;
|
|
671
|
+
}
|
|
672
|
+
function clearMenu(rows) {
|
|
673
|
+
if (rows <= 0)
|
|
674
|
+
return;
|
|
675
|
+
// Move up `rows` lines and clear each one
|
|
676
|
+
for (let i = 0; i < rows; i++) {
|
|
677
|
+
out(`${ESC}[1A${ESC}[2K`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// ─── Main TUI loop ────────────────────────────────────────────────────────────
|
|
681
|
+
async function runTUI(opts) {
|
|
682
|
+
let { model, provider } = opts;
|
|
683
|
+
const { port, token, worktree } = opts;
|
|
684
|
+
let sessionId = opts.sessionId || null;
|
|
685
|
+
const history = [];
|
|
686
|
+
let abort = null;
|
|
687
|
+
let streaming = false;
|
|
688
|
+
// Initial session
|
|
689
|
+
if (!sessionId) {
|
|
690
|
+
try {
|
|
691
|
+
sessionId = await createSession(port, token, worktree, "BoneCode Session");
|
|
692
|
+
}
|
|
693
|
+
catch (e) {
|
|
694
|
+
process.stderr.write(`${RED}Failed to create session: ${e.message}${R}\n`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Header
|
|
699
|
+
printLogo();
|
|
700
|
+
nl(` ${GRAY}v${getVersion()} · ${model} · ${path.basename(worktree)}${R}`);
|
|
701
|
+
nl(` ${GRAY}session ${sessionId.slice(0, 8)}${R}`);
|
|
702
|
+
nl();
|
|
703
|
+
nl(` ${GRAY}Type ${R}${CYAN}/${R}${GRAY} to see commands · Ctrl+C interrupt · Ctrl+D exit${R}`);
|
|
704
|
+
nl();
|
|
705
|
+
// ─── Slash command menu state ──────────────────────────────────────────────
|
|
706
|
+
const menu = {
|
|
707
|
+
visible: false,
|
|
708
|
+
options: [],
|
|
709
|
+
selected: 0,
|
|
710
|
+
rowsRendered: 0,
|
|
711
|
+
};
|
|
712
|
+
// Set up readline AFTER setting up SIGINT so we control it
|
|
713
|
+
const rl = readline.createInterface({
|
|
714
|
+
input: process.stdin,
|
|
715
|
+
output: process.stdout,
|
|
716
|
+
terminal: true,
|
|
717
|
+
historySize: 200,
|
|
718
|
+
completer: buildCompleter(worktree),
|
|
719
|
+
});
|
|
720
|
+
const promptStr = () => `${CYAN}${BOLD}>${R} `;
|
|
721
|
+
// ─── Ctrl+C handling ──────────────────────────────────────────────────────
|
|
722
|
+
// When streaming: abort the request AND notify server
|
|
723
|
+
// When idle: clear menu/input or hint to use /exit
|
|
724
|
+
const onSigint = async () => {
|
|
725
|
+
if (streaming && abort) {
|
|
726
|
+
abort.abort();
|
|
727
|
+
// Also tell the server to cancel the agent loop
|
|
728
|
+
try {
|
|
729
|
+
await fetch(`http://localhost:${port}/v2/session/${sessionId}/cancel`, {
|
|
730
|
+
method: "POST",
|
|
731
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
catch { /* server may not have the endpoint, abort is enough */ }
|
|
735
|
+
// Don't reprompt here — the streamPrompt finally block will handle UI
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
// Idle: clear menu if visible, else hint
|
|
739
|
+
if (menu.visible) {
|
|
740
|
+
clearMenu(menu.rowsRendered);
|
|
741
|
+
menu.visible = false;
|
|
742
|
+
menu.rowsRendered = 0;
|
|
743
|
+
menu.selected = 0;
|
|
744
|
+
}
|
|
745
|
+
out(`\n${GRAY}(Ctrl+D or /exit to quit)${R}\n`);
|
|
746
|
+
rl.setPrompt(promptStr());
|
|
747
|
+
rl.prompt();
|
|
748
|
+
};
|
|
749
|
+
// Detach readline's default SIGINT (which closes the line buffer)
|
|
750
|
+
// and route it to our handler.
|
|
751
|
+
rl.on("SIGINT", onSigint);
|
|
752
|
+
rl.on("close", () => {
|
|
753
|
+
if (streaming && abort)
|
|
754
|
+
abort.abort();
|
|
755
|
+
nl(`\n${GRAY}Goodbye.${R}`);
|
|
756
|
+
process.exit(0);
|
|
757
|
+
});
|
|
758
|
+
// ─── Live menu update on every keystroke ──────────────────────────────────
|
|
759
|
+
// We hook into the keypress events of stdin to redraw the menu.
|
|
760
|
+
// The menu sits ABOVE the prompt line.
|
|
761
|
+
const stdin = rl.input;
|
|
762
|
+
const updateMenu = () => {
|
|
763
|
+
if (streaming)
|
|
764
|
+
return;
|
|
765
|
+
const line = rl.line || "";
|
|
766
|
+
const startsWithSlash = line.startsWith("/");
|
|
767
|
+
const shouldShow = startsWithSlash && !line.includes(" ");
|
|
768
|
+
if (!shouldShow) {
|
|
769
|
+
if (menu.visible) {
|
|
770
|
+
// Need to clear the menu — but we need to preserve the current input line.
|
|
771
|
+
// Strategy: save current line content & cursor, clear menu lines above,
|
|
772
|
+
// then restore the prompt and content.
|
|
773
|
+
const cursor = rl.cursor || 0;
|
|
774
|
+
out(`\r${ESC}[2K`); // clear current prompt line
|
|
775
|
+
clearMenu(menu.rowsRendered); // clear menu lines above
|
|
776
|
+
menu.visible = false;
|
|
777
|
+
menu.rowsRendered = 0;
|
|
778
|
+
menu.selected = 0;
|
|
779
|
+
// Redraw prompt + line
|
|
780
|
+
out(promptStr() + line);
|
|
781
|
+
// Restore cursor position
|
|
782
|
+
const drawn = line.length;
|
|
783
|
+
if (cursor < drawn) {
|
|
784
|
+
out(`${ESC}[${drawn - cursor}D`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
// Filter and reset selection if list changed
|
|
790
|
+
const newOptions = filterCommands(line);
|
|
791
|
+
if (newOptions.length === 0) {
|
|
792
|
+
// Clear menu if visible
|
|
793
|
+
if (menu.visible) {
|
|
794
|
+
const cursor = rl.cursor || 0;
|
|
795
|
+
out(`\r${ESC}[2K`);
|
|
796
|
+
clearMenu(menu.rowsRendered);
|
|
797
|
+
menu.visible = false;
|
|
798
|
+
menu.rowsRendered = 0;
|
|
799
|
+
out(promptStr() + line);
|
|
800
|
+
if (cursor < line.length)
|
|
801
|
+
out(`${ESC}[${line.length - cursor}D`);
|
|
802
|
+
}
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Save current input line
|
|
806
|
+
const cursor = rl.cursor || 0;
|
|
807
|
+
out(`\r${ESC}[2K`); // clear prompt line
|
|
808
|
+
clearMenu(menu.rowsRendered); // clear old menu
|
|
809
|
+
menu.options = newOptions;
|
|
810
|
+
menu.selected = Math.min(menu.selected, newOptions.length - 1);
|
|
811
|
+
if (menu.selected < 0)
|
|
812
|
+
menu.selected = 0;
|
|
813
|
+
menu.visible = true;
|
|
814
|
+
// Render the menu (each item ends in newline)
|
|
815
|
+
menu.rowsRendered = renderCommandMenu(menu);
|
|
816
|
+
// Redraw prompt + line
|
|
817
|
+
out(promptStr() + line);
|
|
818
|
+
if (cursor < line.length) {
|
|
819
|
+
out(`${ESC}[${line.length - cursor}D`);
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
// Capture special keys for menu navigation
|
|
823
|
+
const onKeypress = (_chunk, key) => {
|
|
824
|
+
if (!key)
|
|
825
|
+
return;
|
|
826
|
+
if (streaming)
|
|
827
|
+
return;
|
|
828
|
+
if (menu.visible && menu.options.length > 0) {
|
|
829
|
+
// Arrow up/down navigate the menu
|
|
830
|
+
if (key.name === "up") {
|
|
831
|
+
menu.selected = (menu.selected - 1 + menu.options.length) % menu.options.length;
|
|
832
|
+
updateMenu();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (key.name === "down") {
|
|
836
|
+
menu.selected = (menu.selected + 1) % menu.options.length;
|
|
837
|
+
updateMenu();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
// Tab: replace input with the selected command
|
|
841
|
+
if (key.name === "tab") {
|
|
842
|
+
const sel = menu.options[menu.selected];
|
|
843
|
+
if (sel) {
|
|
844
|
+
// Replace the line buffer with the command + space
|
|
845
|
+
rl.line = sel.name + " ";
|
|
846
|
+
rl.cursor = sel.name.length + 1;
|
|
847
|
+
updateMenu();
|
|
848
|
+
out(`\r${ESC}[2K`);
|
|
849
|
+
out(promptStr() + rl.line);
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// Default: trigger menu update on the next tick (after readline updates the buffer)
|
|
855
|
+
setImmediate(updateMenu);
|
|
856
|
+
};
|
|
857
|
+
stdin.on("keypress", onKeypress);
|
|
858
|
+
rl.setPrompt(promptStr());
|
|
859
|
+
rl.prompt();
|
|
860
|
+
// ─── Main input loop ──────────────────────────────────────────────────────
|
|
861
|
+
for await (const rawLine of rl) {
|
|
862
|
+
const text = rawLine.trim();
|
|
863
|
+
// Clear any visible menu before processing
|
|
864
|
+
if (menu.visible) {
|
|
865
|
+
menu.visible = false;
|
|
866
|
+
menu.rowsRendered = 0;
|
|
867
|
+
menu.selected = 0;
|
|
868
|
+
}
|
|
869
|
+
if (!text) {
|
|
870
|
+
rl.setPrompt(promptStr());
|
|
871
|
+
rl.prompt();
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
history.push(text);
|
|
875
|
+
if (history.length > 200)
|
|
876
|
+
history.shift();
|
|
877
|
+
// ── Slash commands ──────────────────────────────────────────────────────
|
|
878
|
+
if (text.startsWith("/")) {
|
|
879
|
+
const parts = text.slice(1).trim().split(/\s+/);
|
|
880
|
+
const cmd = parts[0]?.toLowerCase() || "";
|
|
881
|
+
const args = parts.slice(1);
|
|
882
|
+
switch (cmd) {
|
|
883
|
+
case "help":
|
|
884
|
+
case "h":
|
|
885
|
+
printHelp();
|
|
886
|
+
break;
|
|
887
|
+
case "new":
|
|
888
|
+
try {
|
|
889
|
+
sessionId = await createSession(port, token, worktree, "New Session");
|
|
890
|
+
nl(`${GREEN}✓${R} ${GRAY}session ${sessionId.slice(0, 8)}${R}`);
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
nl(`${RED}✗ ${e.message}${R}`);
|
|
894
|
+
}
|
|
895
|
+
break;
|
|
896
|
+
case "session":
|
|
897
|
+
nl(`${GRAY}${sessionId}${R}`);
|
|
898
|
+
break;
|
|
899
|
+
case "sessions": {
|
|
900
|
+
try {
|
|
901
|
+
const list = await apiGet(`http://localhost:${port}/v2/session?limit=10`, token);
|
|
902
|
+
if (!list.length) {
|
|
903
|
+
nl(`${GRAY}No sessions${R}`);
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
nl();
|
|
907
|
+
for (const s of list) {
|
|
908
|
+
const active = s.id === sessionId;
|
|
909
|
+
const dot = active ? `${GREEN}●${R}` : `${GRAY}○${R}`;
|
|
910
|
+
const t = new Date(s.time?.updated || Date.now()).toLocaleString();
|
|
911
|
+
nl(` ${dot} ${WHITE}${(s.title || "untitled").slice(0, 50).padEnd(50)}${R} ${GRAY}${t}${R}`);
|
|
912
|
+
}
|
|
913
|
+
nl();
|
|
914
|
+
}
|
|
915
|
+
catch (e) {
|
|
916
|
+
nl(`${RED}✗ ${e.message}${R}`);
|
|
917
|
+
}
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
case "model":
|
|
921
|
+
if (args[0]) {
|
|
922
|
+
if (args[0] === "local") {
|
|
923
|
+
provider = process.env.DEFAULT_PROVIDER || "openai_compatible";
|
|
924
|
+
model = process.env.DEFAULT_MODEL || "local-model";
|
|
925
|
+
}
|
|
926
|
+
else if (args[0].includes("/")) {
|
|
927
|
+
const i = args[0].indexOf("/");
|
|
928
|
+
provider = args[0].slice(0, i);
|
|
929
|
+
model = args[0].slice(i + 1);
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
model = args[0];
|
|
933
|
+
}
|
|
934
|
+
nl(`${GREEN}✓${R} ${WHITE}${provider}/${model}${R}`);
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
nl(`${GRAY}${provider}/${model}${R}`);
|
|
938
|
+
}
|
|
939
|
+
break;
|
|
940
|
+
case "provider":
|
|
941
|
+
if (args[0]) {
|
|
942
|
+
if (args[0] === "local") {
|
|
943
|
+
provider = process.env.DEFAULT_PROVIDER || "openai_compatible";
|
|
944
|
+
model = process.env.DEFAULT_MODEL || "local-model";
|
|
945
|
+
nl(`${GREEN}✓${R} ${WHITE}${provider}/${model}${R}`);
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
provider = args[0];
|
|
949
|
+
nl(`${GREEN}✓${R} ${WHITE}${provider}${R} ${GRAY}use /model <id> to set model${R}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
nl(`${GRAY}${provider}${R}`);
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
956
|
+
case "providers": {
|
|
957
|
+
const list = await fetchProviders(port, token);
|
|
958
|
+
if (list.length)
|
|
959
|
+
printProviders(list, provider);
|
|
960
|
+
else
|
|
961
|
+
nl(`${YELLOW}Could not fetch providers${R}`);
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
case "clear":
|
|
965
|
+
process.stdout.write(`${ESC}[2J${ESC}[H`);
|
|
966
|
+
printLogo();
|
|
967
|
+
break;
|
|
968
|
+
case "history":
|
|
969
|
+
if (!history.length)
|
|
970
|
+
nl(`${GRAY}No history${R}`);
|
|
971
|
+
else
|
|
972
|
+
history.slice(-10).forEach((h, i) => nl(` ${GRAY}${i + 1}.${R} ${h.slice(0, 80)}`));
|
|
973
|
+
break;
|
|
974
|
+
case "exit":
|
|
975
|
+
case "quit":
|
|
976
|
+
case "q":
|
|
977
|
+
nl(`\n${GRAY}Goodbye.${R}`);
|
|
978
|
+
process.exit(0);
|
|
979
|
+
break;
|
|
980
|
+
default:
|
|
981
|
+
nl(`${YELLOW}Unknown: /${cmd}${R} ${GRAY}Type /help for available commands${R}`);
|
|
982
|
+
}
|
|
983
|
+
rl.setPrompt(promptStr());
|
|
984
|
+
rl.prompt();
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
// ── Prompt to the agent ─────────────────────────────────────────────────
|
|
988
|
+
if (!sessionId) {
|
|
989
|
+
try {
|
|
990
|
+
sessionId = await createSession(port, token, worktree, text.slice(0, 50));
|
|
991
|
+
}
|
|
992
|
+
catch (e) {
|
|
993
|
+
nl(`${RED}✗ ${e.message}${R}`);
|
|
994
|
+
rl.setPrompt(promptStr());
|
|
995
|
+
rl.prompt();
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// Resolve @file mentions to absolute paths
|
|
1000
|
+
const resolved = text.replace(/@([\w./\-]+)/g, (match, fp) => {
|
|
1001
|
+
const abs = path.isAbsolute(fp) ? fp : path.resolve(worktree, fp);
|
|
1002
|
+
return fs.existsSync(abs) ? `@${abs}` : match;
|
|
1003
|
+
});
|
|
1004
|
+
renderUserMessage(text);
|
|
1005
|
+
// Show "..." spinner so the user knows something is happening
|
|
1006
|
+
out(` ${GRAY}thinking...${R}`);
|
|
1007
|
+
rl.pause();
|
|
1008
|
+
streaming = true;
|
|
1009
|
+
abort = new AbortController();
|
|
1010
|
+
const t0 = Date.now();
|
|
1011
|
+
const result = await streamPrompt({
|
|
1012
|
+
port, token, sessionId,
|
|
1013
|
+
model, provider,
|
|
1014
|
+
message: resolved,
|
|
1015
|
+
worktree,
|
|
1016
|
+
abortSignal: abort.signal,
|
|
1017
|
+
});
|
|
1018
|
+
streaming = false;
|
|
1019
|
+
abort = null;
|
|
1020
|
+
// Clear the thinking spinner if no output happened
|
|
1021
|
+
if (!result.text && !result.error) {
|
|
1022
|
+
clearLine();
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
// Make sure the next render starts on a clean line
|
|
1026
|
+
const cur = process.stdout.cursor;
|
|
1027
|
+
if (cur && cur.col !== 0)
|
|
1028
|
+
out("\n");
|
|
1029
|
+
}
|
|
1030
|
+
renderTurnEnd(model, result.elapsedMs, result.interrupted);
|
|
1031
|
+
if (result.error && !result.interrupted) {
|
|
1032
|
+
nl(` ${RED}✗ ${result.error}${R}`);
|
|
1033
|
+
}
|
|
1034
|
+
nl();
|
|
1035
|
+
rl.resume();
|
|
1036
|
+
rl.setPrompt(promptStr());
|
|
1037
|
+
rl.prompt();
|
|
1038
|
+
}
|
|
1039
|
+
stdin.removeListener("keypress", onKeypress);
|
|
1040
|
+
}
|
|
1041
|
+
exports.runTUI = runTUI;
|
|
1042
|
+
// ─── Entry point ──────────────────────────────────────────────────────────────
|
|
1043
|
+
async function startInteractiveTUI(opts) {
|
|
1044
|
+
const ready = await waitForServer(opts.port, 30000);
|
|
1045
|
+
if (!ready) {
|
|
1046
|
+
process.stderr.write(`${RED}BoneCode server not responding on port ${opts.port}${R}\n`);
|
|
1047
|
+
process.stderr.write(`${GRAY}Start with: bonecode serve${R}\n`);
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
await runTUI(opts);
|
|
1051
|
+
}
|
|
1052
|
+
exports.startInteractiveTUI = startInteractiveTUI;
|
|
1053
|
+
//# sourceMappingURL=tui.js.map
|