@bahulam/code 0.1.7 → 0.1.9
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 +1 -1
- package/package.json +3 -3
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +1 -1
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/settings.mjs +2 -2
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/approval.mjs +1 -1
- package/src/core/attachments.mjs +1 -1
- package/src/core/backend-url.mjs +11 -12
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +4 -4
- package/src/core/jsonl-writer.mjs +15 -15
- package/src/core/local-agent.mjs +26 -5
- package/src/core/local-store.mjs +172 -24
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +1 -1
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +4 -4
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +79 -61
- package/src/local-service/agent-relay.mjs +8 -8
- package/src/local-service/server.mjs +2 -2
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +1 -1
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/analytics.mjs +4 -1
- package/src/terminal/main.mjs +6 -6
- package/src/terminal/repl-render.mjs +26 -4
- package/src/terminal/repl-resume.mjs +3 -3
- package/src/terminal/repl.mjs +18 -18
- package/src/terminal/tool-display.mjs +1 -1
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +2 -2
- package/src/tools/generate-image.mjs +4 -4
- package/src/tools/project-overview.mjs +12 -12
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +74 -3
package/src/terminal/repl.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { execSync as _execSync } from 'node:child_process';
|
|
|
22
22
|
import { Writable as _WritableStream } from 'node:stream';
|
|
23
23
|
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
24
24
|
import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import { BahulamStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
26
26
|
import { AgentHistoryTurnBuilder } from '../core/agent-history.mjs';
|
|
27
27
|
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
28
28
|
import { tapSseEvent, registerBroadcaster } from '../daemon/event-tap.mjs';
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
MODES as V_MODES,
|
|
54
54
|
} from '../state/verbosity.mjs';
|
|
55
55
|
import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
56
|
-
import {
|
|
56
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
57
57
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
58
58
|
import * as telemetry from '../telemetry/index.mjs';
|
|
59
59
|
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
@@ -1586,8 +1586,8 @@ function foldSubAgentToolResult(data = {}) {
|
|
|
1586
1586
|
fold.entries.push(entry);
|
|
1587
1587
|
}
|
|
1588
1588
|
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
1589
|
-
const summary = summarizeResult(tool, data);
|
|
1590
1589
|
entry.args = entry.args && Object.keys(entry.args).length ? entry.args : args;
|
|
1590
|
+
const summary = summarizeResult(tool, data, entry.args);
|
|
1591
1591
|
entry.result = data;
|
|
1592
1592
|
entry.durationMs = durationMs;
|
|
1593
1593
|
entry.outcome = summary.text || '';
|
|
@@ -2358,7 +2358,7 @@ function renderEvent(event) {
|
|
|
2358
2358
|
break;
|
|
2359
2359
|
|
|
2360
2360
|
// Live steering ack events (PRD-081 §5.2). renderEvent is called from
|
|
2361
|
-
// the SSE loop (see `for await ... jsonlWriter.
|
|
2361
|
+
// the SSE loop (see `for await ... jsonlWriter.writeBahulamEvent(event)`
|
|
2362
2362
|
// in the /execute path) so every event already lands in the transcript.
|
|
2363
2363
|
// Here we only render the user-visible surface — no direct jsonlWriter
|
|
2364
2364
|
// calls (it's out of scope in this top-level dispatcher anyway).
|
|
@@ -3684,7 +3684,7 @@ async function handleCommand(input, ctx) {
|
|
|
3684
3684
|
}
|
|
3685
3685
|
|
|
3686
3686
|
// 5. Show continuity context. Non-summary modes use the captured
|
|
3687
|
-
//
|
|
3687
|
+
// bahulam_event stream when available so the terminal replay matches
|
|
3688
3688
|
// the original styled interaction; older sessions fall back to
|
|
3689
3689
|
// reconstructed text.
|
|
3690
3690
|
if (mode === 'summary' && resumed.summary) {
|
|
@@ -3776,7 +3776,7 @@ export async function startTerminalRepl() {
|
|
|
3776
3776
|
safeCwd(); // prime the cache in repl-utils.mjs for later recovery
|
|
3777
3777
|
|
|
3778
3778
|
const cliArgs = parseArgs(process.argv.slice(2));
|
|
3779
|
-
const auth = new
|
|
3779
|
+
const auth = new BahulamAuth();
|
|
3780
3780
|
|
|
3781
3781
|
// Projects are registered and indexed on demand through get_project_overview.
|
|
3782
3782
|
// CheckpointManager records per-file snapshots before edits so /undo works.
|
|
@@ -4123,7 +4123,7 @@ export async function startTerminalRepl() {
|
|
|
4123
4123
|
&& richHistory.summary
|
|
4124
4124
|
&& Number(richHistory.summaryCoveredMessageCount) > Number(richHistory.summaryCheckpointMessageCount || 0)
|
|
4125
4125
|
) {
|
|
4126
|
-
jsonlWriter.
|
|
4126
|
+
jsonlWriter.writeBahulamEvent({
|
|
4127
4127
|
type: 'resume_summary',
|
|
4128
4128
|
data: {
|
|
4129
4129
|
session_id: sessionId,
|
|
@@ -4138,7 +4138,7 @@ export async function startTerminalRepl() {
|
|
|
4138
4138
|
},
|
|
4139
4139
|
});
|
|
4140
4140
|
}
|
|
4141
|
-
jsonlWriter.
|
|
4141
|
+
jsonlWriter.writeBahulamEvent({
|
|
4142
4142
|
type: 'resume_context',
|
|
4143
4143
|
data: {
|
|
4144
4144
|
session_id: sessionId,
|
|
@@ -4424,7 +4424,7 @@ export async function startTerminalRepl() {
|
|
|
4424
4424
|
// Proxy stream: swallows writes when the dock owns the input row so
|
|
4425
4425
|
// readline's echoes and _refreshLine cursor moves can't fight the dock's
|
|
4426
4426
|
// absolute cursor placement (PRD-081 §5.1 — cursor race). When the dock
|
|
4427
|
-
// is not mounted (plain-mode fallback,
|
|
4427
|
+
// is not mounted (plain-mode fallback, BAHULAM_FIXED_INPUT=0, non-TTY),
|
|
4428
4428
|
// writes pass through so history navigation / backspace still redraw.
|
|
4429
4429
|
const readlineOutputProxy = new _WritableStream({
|
|
4430
4430
|
write(chunk, _enc, cb) {
|
|
@@ -4471,7 +4471,7 @@ export async function startTerminalRepl() {
|
|
|
4471
4471
|
// 3 rows fits comfortably in the dock's reservation without leaking
|
|
4472
4472
|
// past the safety row.
|
|
4473
4473
|
if (isInputDockMounted()) return 3;
|
|
4474
|
-
const raw = process.env.
|
|
4474
|
+
const raw = process.env.BAHULAM_PROMPT_BOTTOM_PADDING ?? '5';
|
|
4475
4475
|
const n = Number.parseInt(raw, 10);
|
|
4476
4476
|
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
4477
4477
|
return Math.min(8, n);
|
|
@@ -4744,14 +4744,14 @@ export async function startTerminalRepl() {
|
|
|
4744
4744
|
// 1. If the terminal supports bracketed paste, we hold flushing until we
|
|
4745
4745
|
// see the ESC[201~ end marker — reliable regardless of paste latency.
|
|
4746
4746
|
// 2. Otherwise we fall back to a short timer that merges bursts arriving
|
|
4747
|
-
// within
|
|
4747
|
+
// within BAHULAM_PASTE_FLUSH_MS.
|
|
4748
4748
|
let _lineInFlight = false;
|
|
4749
4749
|
const _queuedLines = [];
|
|
4750
4750
|
let _pasteLines = [];
|
|
4751
4751
|
let _pasteFlushTimer = null;
|
|
4752
4752
|
|
|
4753
4753
|
function pasteFlushDelayMs() {
|
|
4754
|
-
const raw = Number.parseInt(process.env.
|
|
4754
|
+
const raw = Number.parseInt(process.env.BAHULAM_PASTE_FLUSH_MS || '35', 10);
|
|
4755
4755
|
return Number.isFinite(raw) && raw >= 0 ? Math.min(250, raw) : 35;
|
|
4756
4756
|
}
|
|
4757
4757
|
|
|
@@ -4897,7 +4897,7 @@ export async function startTerminalRepl() {
|
|
|
4897
4897
|
// Create or reuse stream client — sessionId persists across turns.
|
|
4898
4898
|
// The same client also owns the authenticated vision-analysis preflight.
|
|
4899
4899
|
if (!streamClient || streamClient.baseUrl !== creds.backendUrl || streamClient.token !== creds.token) {
|
|
4900
|
-
streamClient = new
|
|
4900
|
+
streamClient = new BahulamStreamClient({
|
|
4901
4901
|
baseUrl: creds.backendUrl,
|
|
4902
4902
|
token: creds.token,
|
|
4903
4903
|
toolExecutor,
|
|
@@ -4921,7 +4921,7 @@ export async function startTerminalRepl() {
|
|
|
4921
4921
|
` ${c.brand('◇')} ${c.dim(`attached ${docPrep.documents.length} document${docPrep.documents.length === 1 ? '' : 's'}:`)} ` +
|
|
4922
4922
|
`${docPrep.documents.map(documentSummaryLine).join(c.dim(' · '))}\n`
|
|
4923
4923
|
);
|
|
4924
|
-
jsonlWriter.
|
|
4924
|
+
jsonlWriter.writeBahulamEvent({
|
|
4925
4925
|
type: 'attachments',
|
|
4926
4926
|
data: { documents: docPrep.documents.map(publicDocumentMetadata) },
|
|
4927
4927
|
});
|
|
@@ -4937,7 +4937,7 @@ export async function startTerminalRepl() {
|
|
|
4937
4937
|
});
|
|
4938
4938
|
if (prepared.attachments.length) {
|
|
4939
4939
|
process.stderr.write(` ${c.brand('◇')} ${c.dim(`attached ${prepared.attachments.length} image${prepared.attachments.length === 1 ? '' : 's'}:`)} ${prepared.attachments.map(attachmentSummaryLine).join(c.dim(' · '))}\n`);
|
|
4940
|
-
jsonlWriter.
|
|
4940
|
+
jsonlWriter.writeBahulamEvent({
|
|
4941
4941
|
type: 'attachments',
|
|
4942
4942
|
data: { attachments: prepared.attachments.map(publicAttachmentMetadata) },
|
|
4943
4943
|
});
|
|
@@ -4954,7 +4954,7 @@ export async function startTerminalRepl() {
|
|
|
4954
4954
|
});
|
|
4955
4955
|
process.stderr.write(`\r${' '.repeat(80)}\r`);
|
|
4956
4956
|
process.stderr.write(` ${c.green('✓')} ${c.dim(`Vision analysis completed for ${prepared.attachments.length} image${prepared.attachments.length === 1 ? '' : 's'}`)}${analysis.model ? c.dim(` · ${analysis.model}`) : ''}\n`);
|
|
4957
|
-
jsonlWriter.
|
|
4957
|
+
jsonlWriter.writeBahulamEvent({
|
|
4958
4958
|
type: 'vision_analysis',
|
|
4959
4959
|
data: {
|
|
4960
4960
|
model: analysis.model || '',
|
|
@@ -5140,7 +5140,7 @@ export async function startTerminalRepl() {
|
|
|
5140
5140
|
// Persist the local record regardless of outcome so the transcript
|
|
5141
5141
|
// reflects what the user typed. Delivered/queued follow-ups will
|
|
5142
5142
|
// get their SSE ack events written separately by the event handler.
|
|
5143
|
-
jsonlWriter.
|
|
5143
|
+
jsonlWriter.writeBahulamEvent({
|
|
5144
5144
|
type: 'user_intervention',
|
|
5145
5145
|
data: {
|
|
5146
5146
|
instruction,
|
|
@@ -5511,7 +5511,7 @@ export async function startTerminalRepl() {
|
|
|
5511
5511
|
_turnIterable = client.execute(input, execContext, session.agentHistory);
|
|
5512
5512
|
}
|
|
5513
5513
|
for await (const event of _turnIterable) {
|
|
5514
|
-
jsonlWriter.
|
|
5514
|
+
jsonlWriter.writeBahulamEvent(event);
|
|
5515
5515
|
// . daemon event log. Env-var gated (off by default) —
|
|
5516
5516
|
// when BAHULAM_DAEMON_EVENTLOG=1, mirror each SSE frame that maps
|
|
5517
5517
|
// to a first-class type into ~/.bahulam/sessions/<id>/events.jsonl.
|
|
@@ -90,7 +90,7 @@ function currentWorkingDirectory() {
|
|
|
90
90
|
// handles the display width from here)
|
|
91
91
|
//
|
|
92
92
|
// The point: keep the basename + parent visible even when the full path
|
|
93
|
-
// is long, so users don't see "Editing /Users/sree/Sites
|
|
93
|
+
// is long, so users don't see "Editing /Users/sree/Sites/.../
|
|
94
94
|
// codekepler-ba…" with the actual filename lost off the right edge.
|
|
95
95
|
function shortPath(filePath, cwd = currentWorkingDirectory()) {
|
|
96
96
|
const value = String(filePath || '');
|
package/src/tools/agent.mjs
CHANGED
|
@@ -87,10 +87,10 @@ export const AgentTool = {
|
|
|
87
87
|
settings: {
|
|
88
88
|
stream: false,
|
|
89
89
|
stagnationDetection: !['0', 'false', 'no', 'off'].includes(
|
|
90
|
-
(process.env.
|
|
90
|
+
(process.env.BAHULAM_STAGNATION_DETECTION ?? '0').toLowerCase(),
|
|
91
91
|
),
|
|
92
92
|
stagnationThreshold: Number.parseInt(
|
|
93
|
-
process.env.
|
|
93
|
+
process.env.BAHULAM_STAGNATION_THRESHOLD
|
|
94
94
|
?? '3',
|
|
95
95
|
10,
|
|
96
96
|
),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* sends the same bounded attachment shape used by the preflight vision path to
|
|
6
6
|
* the authenticated backend `/api/vision/analyze` endpoint.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
9
9
|
import {
|
|
10
10
|
loadImageAttachment,
|
|
11
11
|
publicAttachmentMetadata,
|
|
@@ -88,7 +88,7 @@ export const AnalyzeImageTool = {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const creds = new
|
|
91
|
+
const creds = new BahulamAuth().loadCredentials();
|
|
92
92
|
if (!creds.backendUrl || !creds.token) {
|
|
93
93
|
return {
|
|
94
94
|
success: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* generate_image — text-to-image generation for
|
|
2
|
+
* generate_image — text-to-image generation for Bahulam Code.
|
|
3
3
|
*
|
|
4
4
|
* Uses the OpenRouter-compatible image API shape (`POST /v1/images`) and saves
|
|
5
5
|
* generated bytes to local files so the agent can use the assets in code.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from 'node:fs/promises';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
11
11
|
|
|
12
12
|
const DEFAULT_IMAGE_GENERATION_MODEL = 'google/gemini-3-pro-image';
|
|
13
13
|
const MAX_GENERATED_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
@@ -97,7 +97,7 @@ export const GenerateImageTool = {
|
|
|
97
97
|
output_path: optionalString(input.output_path),
|
|
98
98
|
filename: optionalString(input.filename),
|
|
99
99
|
};
|
|
100
|
-
const creds = new
|
|
100
|
+
const creds = new BahulamAuth().loadCredentials();
|
|
101
101
|
const apiKey = gatewayApiKey(creds);
|
|
102
102
|
let result;
|
|
103
103
|
if (creds.backendUrl && creds.token) {
|
|
@@ -241,7 +241,7 @@ async function callImageApi({ apiKey, model, prompt, n, aspect_ratio, resolution
|
|
|
241
241
|
Authorization: `Bearer ${apiKey}`,
|
|
242
242
|
'Content-Type': 'application/json',
|
|
243
243
|
'X-Product': process.env.BAHULAM_PRODUCT || 'kepler',
|
|
244
|
-
'X-Bahulam-Client': '
|
|
244
|
+
'X-Bahulam-Client': 'bahulam-code-image-generation',
|
|
245
245
|
},
|
|
246
246
|
body: JSON.stringify(body),
|
|
247
247
|
});
|
|
@@ -322,8 +322,8 @@ function _readIfExists(dir, filename, maxChars = 8000) {
|
|
|
322
322
|
} catch { return ''; }
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
function _scanSkills(
|
|
326
|
-
const skillsDir = path.join(
|
|
325
|
+
function _scanSkills(bahulamDir) {
|
|
326
|
+
const skillsDir = path.join(bahulamDir, 'skills');
|
|
327
327
|
if (!fs.existsSync(skillsDir)) return [];
|
|
328
328
|
try {
|
|
329
329
|
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
@@ -402,15 +402,15 @@ export class ProjectRegistry {
|
|
|
402
402
|
// .bahulam/KEPLER.md, goal/plan/style, skills, AGENTS.md, etc. changes.
|
|
403
403
|
_attachLiveContext(resource, root) {
|
|
404
404
|
// Resolver — prefers .bahulam/, falls back to .kepler/ for legacy projects.
|
|
405
|
-
const
|
|
405
|
+
const bahulamDir = projectConfigDir(root);
|
|
406
406
|
resource.environment = detectEnvironment();
|
|
407
|
-
resource.project_context = _readIfExists(
|
|
407
|
+
resource.project_context = _readIfExists(bahulamDir, 'KEPLER.md', 10000) ||
|
|
408
408
|
_readIfExists(root, 'KEPLER.md', 10000) ||
|
|
409
|
-
_readIfExists(
|
|
410
|
-
resource.style = _readIfExists(
|
|
411
|
-
resource.goal = _readIfExists(
|
|
412
|
-
resource.plan = _readIfExists(
|
|
413
|
-
resource.skills_index = _scanSkills(
|
|
409
|
+
_readIfExists(bahulamDir, 'project.md', 8000);
|
|
410
|
+
resource.style = _readIfExists(bahulamDir, 'style.md', 4000);
|
|
411
|
+
resource.goal = _readIfExists(bahulamDir, 'goal.md', 2000);
|
|
412
|
+
resource.plan = _readIfExists(bahulamDir, 'plan.md', 6000);
|
|
413
|
+
resource.skills_index = _scanSkills(bahulamDir);
|
|
414
414
|
|
|
415
415
|
if (!resource.project_context) {
|
|
416
416
|
for (const name of ['.bahulam.md', 'AGENTS.md', 'CLAUDE.md']) {
|
|
@@ -426,7 +426,7 @@ export class ProjectRegistry {
|
|
|
426
426
|
throw new Error('get_project_overview requires a project path');
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
// LLM sometimes passes shell-escaped paths ("
|
|
429
|
+
// LLM sometimes passes shell-escaped paths ("Bahulam") or paths
|
|
430
430
|
// beginning with "~". Normalize defensively so the tool does not bounce
|
|
431
431
|
// back a "not found" error on a path that's correct apart from quoting.
|
|
432
432
|
rawPath = normalizePathInput(rawPath);
|
|
@@ -600,7 +600,7 @@ export class ProjectRegistry {
|
|
|
600
600
|
}
|
|
601
601
|
|
|
602
602
|
// LLM frequently passes shell-quoted paths copied from a terminal,
|
|
603
|
-
// e.g. "
|
|
603
|
+
// e.g. "Bahulam/src/app/\(kepler\)/page.tsx". Normalize here so
|
|
604
604
|
// every tool benefits, not just get_project_overview.
|
|
605
605
|
rawPath = normalizePathInput(rawPath);
|
|
606
606
|
|
|
@@ -633,7 +633,7 @@ export class ProjectRegistry {
|
|
|
633
633
|
let containingScratchRoot = containingProject ? null : findScratchRoot(candidate);
|
|
634
634
|
|
|
635
635
|
// Two reasons to try the unescaped variant:
|
|
636
|
-
// (1) candidate is outside every project root (literal "
|
|
636
|
+
// (1) candidate is outside every project root (literal "Bahulam"
|
|
637
637
|
// does not contain a real project), or
|
|
638
638
|
// (2) candidate is inside a root but does not exist on disk because
|
|
639
639
|
// a path segment like "\(kepler\)" only resolves once unescaped.
|
package/src/ui/formatter.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Formatter — Clean terminal output for
|
|
2
|
+
* Formatter — Clean terminal output for Bahulam events.
|
|
3
3
|
*
|
|
4
4
|
* Style reference: Claude Code screenshot
|
|
5
5
|
* ◐ Cyan spinner + cyan text for working/thinking
|
|
@@ -135,7 +135,7 @@ export class EventFormatter {
|
|
|
135
135
|
if (!this._lastBlock) return;
|
|
136
136
|
if (compactSame && this._lastBlock === nextBlock) return;
|
|
137
137
|
|
|
138
|
-
const mode = String(process.env.
|
|
138
|
+
const mode = String(process.env.BAHULAM_BLOCK_SEPARATOR || 'space').toLowerCase();
|
|
139
139
|
if (mode === 'off' || mode === 'none') return;
|
|
140
140
|
if (mode === 'dotted' || mode === 'dots') {
|
|
141
141
|
const cols = Math.max(24, process.stderr.columns || process.stdout.columns || 80);
|
package/src/ui/input-dock.mjs
CHANGED
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
* ... ← up to MAX_INPUT_ROWS
|
|
24
24
|
* ← spacer
|
|
25
25
|
* ══════════════════════════════════════════════════════ ← bottom rule (colored)
|
|
26
|
-
* deepseek-chat-v3 ·
|
|
26
|
+
* deepseek-chat-v3 · Bahulam ⎇ main · turn 4 ← meta row
|
|
27
27
|
* [Enter] send · [/] commands … ← tips row
|
|
28
28
|
* (one blank row at very bottom for cursor safety)
|
|
29
29
|
*
|
|
30
30
|
* Config:
|
|
31
|
-
* BAHULAM_INPUT_ROWS_MAX
|
|
32
|
-
* BAHULAM_TTY_MODE=stable
|
|
33
|
-
* BAHULAM_PLAIN=1
|
|
34
|
-
* BAHULAM_FIXED_INPUT=0
|
|
31
|
+
* BAHULAM_INPUT_ROWS_MAX 1..12 hard cap on input row growth (default 6)
|
|
32
|
+
* BAHULAM_TTY_MODE=stable scrollback-safe transcript, no fixed dock
|
|
33
|
+
* BAHULAM_PLAIN=1 deterministic no-ANSI output for automation
|
|
34
|
+
* BAHULAM_FIXED_INPUT=0 disable dock entirely (fallback readline)
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { paint, width as visibleWidth } from './palette.mjs';
|
|
@@ -214,7 +214,7 @@ function inputTextBudget() {
|
|
|
214
214
|
function resolveMaxInputRows(requested) {
|
|
215
215
|
const raw = requested != null
|
|
216
216
|
? requested
|
|
217
|
-
:
|
|
217
|
+
: process.env.BAHULAM_INPUT_ROWS_MAX;
|
|
218
218
|
const n = Number.parseInt(String(raw), 10);
|
|
219
219
|
if (!Number.isFinite(n)) return DEFAULT_MAX_INPUT_ROWS;
|
|
220
220
|
return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
|
|
@@ -530,7 +530,7 @@ export function mountInputDock({
|
|
|
530
530
|
const t = term();
|
|
531
531
|
if (!t.isTTY || t.plain) return false;
|
|
532
532
|
if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
|
|
533
|
-
if (process.env.BAHULAM_FIXED_INPUT === '0'
|
|
533
|
+
if (process.env.BAHULAM_FIXED_INPUT === '0') return false;
|
|
534
534
|
if (mounted) return true;
|
|
535
535
|
|
|
536
536
|
inputRowsMax = resolveMaxInputRows(requestedMax);
|
package/src/ui/spinner.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Behavior:
|
|
18
18
|
* - 120ms per frame.
|
|
19
|
-
* - Suppressed entirely when stdout is not a TTY or when
|
|
19
|
+
* - Suppressed entirely when stdout is not a TTY or when BAHULAM_PLAIN=1.
|
|
20
20
|
* The `start`/`stop` API is still safe to call (no-op).
|
|
21
21
|
* - Color follows the current orbit (orchestrated by the caller via the
|
|
22
22
|
* palette token). Default: brand.primary.
|
package/src/ui/term.mjs
CHANGED
|
@@ -34,9 +34,7 @@ function readEnv() {
|
|
|
34
34
|
NO_COLOR: env.NO_COLOR,
|
|
35
35
|
FORCE_COLOR: env.FORCE_COLOR,
|
|
36
36
|
BAHULAM_PLAIN: env.BAHULAM_PLAIN,
|
|
37
|
-
KEPLER_PLAIN: env.KEPLER_PLAIN,
|
|
38
37
|
BAHULAM_TTY_MODE: env.BAHULAM_TTY_MODE,
|
|
39
|
-
KEPLER_TTY_MODE: env.KEPLER_TTY_MODE,
|
|
40
38
|
COLORTERM: (env.COLORTERM || '').toLowerCase(),
|
|
41
39
|
TERM: (env.TERM || '').toLowerCase(),
|
|
42
40
|
TERM_PROGRAM: env.TERM_PROGRAM || '',
|
|
@@ -48,7 +46,6 @@ function detectColorLevel(env, isTTY) {
|
|
|
48
46
|
// Hard opt-out (https://no-color.org). Honored even on TTYs.
|
|
49
47
|
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '') return 'none';
|
|
50
48
|
if (env.BAHULAM_PLAIN === '1') return 'none';
|
|
51
|
-
if (env.KEPLER_PLAIN === '1') return 'none';
|
|
52
49
|
|
|
53
50
|
// Hard opt-in. FORCE_COLOR=1|2|3 maps to ansi16|ansi256|truecolor.
|
|
54
51
|
// FORCE_COLOR with no value or =true falls through to detection.
|
|
@@ -84,7 +81,6 @@ function detectColorLevel(env, isTTY) {
|
|
|
84
81
|
|
|
85
82
|
function detectUnicode(env) {
|
|
86
83
|
if (env.BAHULAM_PLAIN === '1') return false;
|
|
87
|
-
if (env.KEPLER_PLAIN === '1') return false;
|
|
88
84
|
// Most modern terminals on macOS/Linux handle UTF-8.
|
|
89
85
|
// Windows ConEmu / older terminals are the main holdouts; conservative
|
|
90
86
|
// fallback when LANG and LC_* are missing or explicitly POSIX.
|
|
@@ -95,8 +91,8 @@ function detectUnicode(env) {
|
|
|
95
91
|
}
|
|
96
92
|
|
|
97
93
|
function detectTtyMode(env) {
|
|
98
|
-
const raw = String(env.BAHULAM_TTY_MODE ||
|
|
99
|
-
if (env.BAHULAM_PLAIN === '1'
|
|
94
|
+
const raw = String(env.BAHULAM_TTY_MODE || '').trim().toLowerCase();
|
|
95
|
+
if (env.BAHULAM_PLAIN === '1') return 'plain';
|
|
100
96
|
if (raw === 'stable' || raw === 'simple' || raw === 'static' || raw === 'transcript') return 'stable';
|
|
101
97
|
return 'rich';
|
|
102
98
|
}
|
package/src/ui/tool-card.mjs
CHANGED
|
@@ -78,12 +78,15 @@ const paintShellAdapter = {
|
|
|
78
78
|
*
|
|
79
79
|
* @returns {{ text: string, tone: 'success'|'warn'|'danger'|'dim' }}
|
|
80
80
|
*/
|
|
81
|
-
export function summarizeResult(tool, data) {
|
|
81
|
+
export function summarizeResult(tool, data, args = {}) {
|
|
82
82
|
if (!data) return { text: '', tone: 'dim' };
|
|
83
83
|
|
|
84
84
|
if (data._blocked) {
|
|
85
85
|
return { text: firstOutputLine(data) || 'blocked', tone: 'danger' };
|
|
86
86
|
}
|
|
87
|
+
if (isNoChangeEditResult(tool, data)) {
|
|
88
|
+
return { text: 'no changes', tone: 'warn' };
|
|
89
|
+
}
|
|
87
90
|
if (data._observation_timeout) {
|
|
88
91
|
const ms = data._observation_timeout_ms;
|
|
89
92
|
const duration = typeof ms === 'number' ? formatDuration(ms) : '';
|
|
@@ -111,7 +114,8 @@ export function summarizeResult(tool, data) {
|
|
|
111
114
|
|
|
112
115
|
switch (tool) {
|
|
113
116
|
case 'read_file': {
|
|
114
|
-
const lines =
|
|
117
|
+
const lines = readFileLineCount(data, args);
|
|
118
|
+
if (lines == null) return { text: 'read', tone: 'success' };
|
|
115
119
|
return { text: `${lines} line${lines === 1 ? '' : 's'}`, tone: 'success' };
|
|
116
120
|
}
|
|
117
121
|
case 'read_files':
|
|
@@ -136,6 +140,9 @@ export function summarizeResult(tool, data) {
|
|
|
136
140
|
case 'write_file':
|
|
137
141
|
case 'write_project': {
|
|
138
142
|
const delta = diffDelta(data);
|
|
143
|
+
if (tool === 'edit_file' && delta === '+0 −0') {
|
|
144
|
+
return { text: 'no changes', tone: 'warn' };
|
|
145
|
+
}
|
|
139
146
|
if (delta) return { text: delta, tone: 'success' };
|
|
140
147
|
return { text: 'updated', tone: 'success' };
|
|
141
148
|
}
|
|
@@ -448,6 +455,64 @@ function lineCount(s) {
|
|
|
448
455
|
return String(s).split('\n').filter(Boolean).length;
|
|
449
456
|
}
|
|
450
457
|
|
|
458
|
+
function physicalLineCount(text) {
|
|
459
|
+
const value = String(text ?? '').replace(/\r\n?/g, '\n');
|
|
460
|
+
if (!value) return 0;
|
|
461
|
+
const lines = value.split('\n');
|
|
462
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
463
|
+
return lines.length;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function readFileLineCount(data = {}, args = {}) {
|
|
467
|
+
const explicit = explicitReadLineCount(data);
|
|
468
|
+
if (explicit != null) return explicit;
|
|
469
|
+
|
|
470
|
+
const text = firstReadTextPayload(data);
|
|
471
|
+
if (text != null) return physicalLineCount(text);
|
|
472
|
+
|
|
473
|
+
const range = requestedRangeLineCount(args || data?.args || {});
|
|
474
|
+
if (range != null) return range;
|
|
475
|
+
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function explicitReadLineCount(data = {}) {
|
|
480
|
+
if (!data || typeof data !== 'object') return null;
|
|
481
|
+
for (const key of ['_total_lines', 'line_count', 'lines_count', 'total_lines', 'lineCount', 'totalLines']) {
|
|
482
|
+
const value = data[key];
|
|
483
|
+
if (value == null || value === '') continue;
|
|
484
|
+
const number = Number(value);
|
|
485
|
+
if (Number.isFinite(number) && number >= 0) return Math.floor(number);
|
|
486
|
+
}
|
|
487
|
+
if (Array.isArray(data.lines)) return data.lines.length;
|
|
488
|
+
if (data.result && typeof data.result === 'object' && data.result !== data) {
|
|
489
|
+
return explicitReadLineCount(data.result);
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function firstReadTextPayload(data = {}) {
|
|
495
|
+
if (!data || typeof data !== 'object') return null;
|
|
496
|
+
for (const key of ['content', 'text', 'output', 'output_preview', 'message']) {
|
|
497
|
+
if (typeof data[key] === 'string') return data[key];
|
|
498
|
+
}
|
|
499
|
+
if (data.result && typeof data.result === 'object' && data.result !== data) {
|
|
500
|
+
return firstReadTextPayload(data.result);
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function requestedRangeLineCount(args = {}) {
|
|
506
|
+
const start = Number(args.start_line ?? args.startLine);
|
|
507
|
+
const end = Number(args.end_line ?? args.endLine);
|
|
508
|
+
if (Number.isFinite(start) && Number.isFinite(end) && start > 0 && end >= start) {
|
|
509
|
+
return Math.floor(end - start + 1);
|
|
510
|
+
}
|
|
511
|
+
const limit = Number(args.limit ?? args.max_lines ?? args.maxLines);
|
|
512
|
+
if (Number.isFinite(limit) && limit >= 0) return Math.floor(limit);
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
451
516
|
function countMatches(data) {
|
|
452
517
|
if (typeof data?.match_count === 'number') return data.match_count;
|
|
453
518
|
return lineCount(data?.output);
|
|
@@ -474,6 +539,12 @@ function diffDelta(data) {
|
|
|
474
539
|
return `+${a} −${r}`;
|
|
475
540
|
}
|
|
476
541
|
|
|
542
|
+
function isNoChangeEditResult(tool, data = {}) {
|
|
543
|
+
if (tool !== 'edit_file') return false;
|
|
544
|
+
if (data._no_change || data.no_change) return true;
|
|
545
|
+
return data.success !== false && data.lines_added === 0 && data.lines_removed === 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
477
548
|
function tone(text, t) {
|
|
478
549
|
switch (t) {
|
|
479
550
|
case 'success': return paint.state.success(text);
|
|
@@ -577,7 +648,7 @@ export function formatCard({ tool, args, result, durationMs, indent, columns, cw
|
|
|
577
648
|
const cols = columns || term().columns || 120;
|
|
578
649
|
const head = formatCardHead(tool, args, { indent, columns: cols, cwd });
|
|
579
650
|
|
|
580
|
-
const summary = summarizeResult(tool, result);
|
|
651
|
+
const summary = summarizeResult(tool, result, args);
|
|
581
652
|
const duration = formatDuration(durationMs ?? result?.duration_ms ?? (result?.duration_s != null ? result.duration_s * 1000 : null));
|
|
582
653
|
|
|
583
654
|
if (!summary.text && !duration) return head;
|