@bahulam/code 2.6.15 → 2.6.16
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/package.json +5 -5
- package/src/auth/tarang-auth.mjs +6 -0
- package/src/config/cli-args.mjs +7 -1
- package/src/config/model-catalog.mjs +57 -0
- package/src/core/bundled-runtime.mjs +12 -0
- package/src/core/error-guidance.mjs +8 -4
- package/src/core/local-agent.mjs +3 -3
- package/src/index.mjs +7 -3
- package/src/permissions/command-classifier.mjs +50 -2
- package/src/telemetry/index.mjs +97 -71
- package/src/terminal/main.mjs +40 -0
- package/src/terminal/repl-format.mjs +64 -4
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +152 -26
- package/src/terminal/repl-resume.mjs +27 -12
- package/src/terminal/repl-state.mjs +15 -0
- package/src/terminal/repl.mjs +489 -21
- package/src/ui/approval.mjs +22 -5
- package/src/ui/input-dock.mjs +67 -5
- package/src/ui/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +2 -0
- package/src/ui/sub-agent.mjs +17 -2
package/src/terminal/repl.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
|
29
29
|
import { buildWorkScope, promptProjectRoots } from '../core/work-scope.mjs';
|
|
30
30
|
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
31
31
|
import { HookRunner } from '../config/hook-runner.mjs';
|
|
32
|
+
import { readShippedCatalog } from '../config/model-catalog.mjs';
|
|
32
33
|
import { runPreflight } from '../onboarding/preflight.mjs';
|
|
33
34
|
import { printBanner as printBrandedBanner } from '../ui/banner.mjs';
|
|
34
35
|
import { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '../ui/mission-report.mjs';
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
43
44
|
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
44
45
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
46
|
+
import * as telemetry from '../telemetry/index.mjs';
|
|
45
47
|
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
46
48
|
import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
|
|
47
49
|
import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
|
|
@@ -49,6 +51,7 @@ import { BUILTIN_AGENTS, findBuiltinAgent, localAgentMatches, runAgent, runAgent
|
|
|
49
51
|
import { createAgentFile, isVsCodeTerminal, listLocalAgents, openAgentFile, syncAgentsToBackend } from '../agents/scaffold.mjs';
|
|
50
52
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
51
53
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
54
|
+
import { pickModelOverridesForm } from './repl-model-form.mjs';
|
|
52
55
|
import { loadEffectivePolicy, formatPolicySourceRows } from '../core/policy-resolver.mjs';
|
|
53
56
|
import { loadProjectContext } from '../core/project-context-loader.mjs';
|
|
54
57
|
import { buildContextEnvelope } from '../core/context-envelope.mjs';
|
|
@@ -72,8 +75,10 @@ import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
|
|
|
72
75
|
import { exploreCategory, exploreCollapseEnabled, isExploreTool } from './repl-explore.mjs';
|
|
73
76
|
import { session, orbitRef, sessionMgrRef, runtime } from './repl-state.mjs';
|
|
74
77
|
import { safeCwd } from './repl-utils.mjs';
|
|
78
|
+
import * as rqueue from '../ui/render-queue.mjs';
|
|
75
79
|
import {
|
|
76
80
|
appendContent,
|
|
81
|
+
bumpSpinnerProgress,
|
|
77
82
|
clearPendingHead,
|
|
78
83
|
clippedThinking,
|
|
79
84
|
expandIndex,
|
|
@@ -82,12 +87,14 @@ import {
|
|
|
82
87
|
flushExploreRun,
|
|
83
88
|
flushPendingHead,
|
|
84
89
|
isInlineOutcomeTool,
|
|
90
|
+
pushSubAgentWindowLine,
|
|
85
91
|
renderBlockBoundary,
|
|
86
92
|
renderExploreRun,
|
|
87
93
|
renderFileDiffEvent,
|
|
88
94
|
renderStagnation,
|
|
89
95
|
renderToolCall,
|
|
90
96
|
renderToolResult,
|
|
97
|
+
setSubAgentWindowActive,
|
|
91
98
|
startContentStream,
|
|
92
99
|
startSpinner,
|
|
93
100
|
stopSpinner,
|
|
@@ -321,16 +328,211 @@ function sessionModelOverrideEntries() {
|
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
function printModelCommandUsage() {
|
|
324
|
-
process.stderr.write(` ${c.gray('Usage:')} /model
|
|
331
|
+
process.stderr.write(` ${c.gray('Usage:')} /model interactive per-role form\n`);
|
|
332
|
+
process.stderr.write(` /model <model> set coding model directly\n`);
|
|
325
333
|
process.stderr.write(` /model <role> <model>\n`);
|
|
326
|
-
process.stderr.write(` /model
|
|
334
|
+
process.stderr.write(` /model ${NAMED_MODEL_MODES_LIST.join('|')}\n`);
|
|
335
|
+
process.stderr.write(` /model list · status · refresh · clear [role]\n`);
|
|
327
336
|
process.stderr.write(` ${c.gray('Roles:')} ${MODEL_ROLE_ORDER.map(role => MODEL_ROLE_LABELS[role]).join(', ')}\n`);
|
|
328
337
|
}
|
|
329
338
|
|
|
339
|
+
// ── W7: curated platform catalog (PRD-076) ──
|
|
340
|
+
// Platform-route model overrides are validated against the backend's
|
|
341
|
+
// curated catalog (harness_validated models only). BYOK route skips
|
|
342
|
+
// validation entirely — the user's key, the user's models. Fail-open:
|
|
343
|
+
// if the catalog can't be fetched, the backend remains the enforcer.
|
|
344
|
+
|
|
345
|
+
const NAMED_MODEL_MODES_LIST = ['fast', 'thinking', 'extra', 'max'];
|
|
346
|
+
const NAMED_MODEL_MODES = new Set(NAMED_MODEL_MODES_LIST);
|
|
347
|
+
|
|
348
|
+
let _modelCatalogCache = null;
|
|
349
|
+
let _modelCatalogError = null;
|
|
350
|
+
let _modelCatalogSource = null; // 'snapshot' | 'backend'
|
|
351
|
+
let _backendRefreshInFlight = null;
|
|
352
|
+
|
|
353
|
+
async function fetchModelCatalog(ctx) {
|
|
354
|
+
// Seed from the shipped snapshot so /model list, /model form and the
|
|
355
|
+
// curation warnings work offline. The backend refresh below overlays
|
|
356
|
+
// any drift without blocking the UI.
|
|
357
|
+
if (!_modelCatalogCache) {
|
|
358
|
+
const shipped = readShippedCatalog();
|
|
359
|
+
if (shipped && shipped.length) {
|
|
360
|
+
_modelCatalogCache = shipped;
|
|
361
|
+
_modelCatalogSource = 'snapshot';
|
|
362
|
+
_modelCatalogError = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Kick off a background backend refresh at most once per REPL session.
|
|
367
|
+
// Snapshot already served the caller — this only matters for the *next*
|
|
368
|
+
// /model list. Skip entirely when offline mode is forced.
|
|
369
|
+
if (!_backendRefreshInFlight && _modelCatalogSource !== 'backend' &&
|
|
370
|
+
process.env.BAHULAM_MODEL_CATALOG_OFFLINE !== '1') {
|
|
371
|
+
_backendRefreshInFlight = refreshCatalogFromBackend(ctx).finally(() => {
|
|
372
|
+
_backendRefreshInFlight = null;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (_modelCatalogCache) return _modelCatalogCache;
|
|
377
|
+
|
|
378
|
+
// Snapshot missing (shouldn't happen in a shipped package) — fall back
|
|
379
|
+
// to whatever the backend refresh produces.
|
|
380
|
+
await _backendRefreshInFlight;
|
|
381
|
+
return _modelCatalogCache;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function refreshCatalogFromBackend(ctx) {
|
|
385
|
+
try {
|
|
386
|
+
const creds = ctx?.auth?.loadCredentials?.();
|
|
387
|
+
if (!creds?.backendUrl) {
|
|
388
|
+
if (!_modelCatalogCache) _modelCatalogError = 'not logged in (no backend url)';
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const headers = { 'X-Product': 'bahulam' };
|
|
392
|
+
if (creds.token) headers['Authorization'] = `Bearer ${creds.token}`;
|
|
393
|
+
const resp = await fetch(`${creds.backendUrl}/api/models`, {
|
|
394
|
+
headers,
|
|
395
|
+
signal: AbortSignal.timeout(3000),
|
|
396
|
+
});
|
|
397
|
+
if (!resp.ok) {
|
|
398
|
+
if (!_modelCatalogCache) _modelCatalogError = `backend returned ${resp.status}`;
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const data = await resp.json();
|
|
402
|
+
const models = Array.isArray(data?.models) ? data.models : null;
|
|
403
|
+
if (models && models.length) {
|
|
404
|
+
_modelCatalogCache = models;
|
|
405
|
+
_modelCatalogSource = 'backend';
|
|
406
|
+
_modelCatalogError = null;
|
|
407
|
+
} else if (!_modelCatalogCache) {
|
|
408
|
+
_modelCatalogError = models ? 'catalog is empty' : 'unexpected response shape';
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
if (!_modelCatalogCache) {
|
|
412
|
+
_modelCatalogError = err?.name === 'TimeoutError' ? 'backend timeout (3s)' : 'backend unreachable';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isByokModelRoute() {
|
|
418
|
+
return session.routePreference === 'byok' || (!session.routePreference && session.isByok);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// PRD-076 W7b: mirror session model state into ~/.bahulam/config.json so
|
|
422
|
+
// picks survive restarts and the bundled Python runtime — which already
|
|
423
|
+
// reads `model_config` via read_local_model_config() — sees them without
|
|
424
|
+
// requiring a live backend fetch.
|
|
425
|
+
function persistSessionModelState(ctx) {
|
|
426
|
+
try {
|
|
427
|
+
const auth = ctx?.auth;
|
|
428
|
+
if (!auth?.saveCredentials) return;
|
|
429
|
+
auth.saveCredentials({
|
|
430
|
+
model_config: { ...(session.modelOverrides || {}) },
|
|
431
|
+
model_mode: session.modelMode || null,
|
|
432
|
+
route_preference: session.routePreference || null,
|
|
433
|
+
});
|
|
434
|
+
} catch {
|
|
435
|
+
// Persistence is best-effort — never break the /model command over
|
|
436
|
+
// a config write failure.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function modelCreditBadge(model) {
|
|
441
|
+
const usd = Number(model?.input_cost_usd_per_m);
|
|
442
|
+
if (!Number.isFinite(usd) || usd <= 0) return '';
|
|
443
|
+
const credits = usd * 200; // credits = provider cost × 2 × 100/USD
|
|
444
|
+
return `~${credits < 10 ? credits.toFixed(1) : String(Math.round(credits))} cr/M in`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function warnIfNotCurated(model, ctx) {
|
|
448
|
+
if (isByokModelRoute()) return;
|
|
449
|
+
const catalog = await fetchModelCatalog(ctx);
|
|
450
|
+
if (!catalog) return;
|
|
451
|
+
const row = catalog.find(m => m?.id === model);
|
|
452
|
+
if (!row) {
|
|
453
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`${model} is not in the platform catalog — the backend may reject it. See /model list.`)}\n`);
|
|
454
|
+
} else if (row.harness_validated === false) {
|
|
455
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`${model} is not harness-validated for the platform route — cost/quality untuned. See /model list.`)}\n`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function printModelCatalog(ctx) {
|
|
460
|
+
const catalog = await fetchModelCatalog(ctx);
|
|
461
|
+
if (!catalog) {
|
|
462
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`Model catalog unavailable — ${_modelCatalogError || 'unknown error'}.`)}\n`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const curated = catalog.filter(m => m?.harness_validated);
|
|
466
|
+
process.stderr.write(`\n ${c.bold('Platform catalog')} ${c.dim('(harness-validated, credit-priced)')}\n`);
|
|
467
|
+
process.stderr.write(` ${c.gray('─'.repeat(64))}\n`);
|
|
468
|
+
if (!curated.length) {
|
|
469
|
+
process.stderr.write(` ${c.dim('(none published yet)')}\n`);
|
|
470
|
+
}
|
|
471
|
+
for (const m of curated) {
|
|
472
|
+
const badge = modelCreditBadge(m);
|
|
473
|
+
const tiers = Array.isArray(m.platform_access_tier) && m.platform_access_tier.length
|
|
474
|
+
? c.dim(` [${m.platform_access_tier.join(', ')}]`)
|
|
475
|
+
: '';
|
|
476
|
+
process.stderr.write(` ${c.brand(String(m.id || '').padEnd(38))} ${badge ? c.dim(badge.padEnd(16)) : ''.padEnd(16)}${tiers}\n`);
|
|
477
|
+
}
|
|
478
|
+
const rest = catalog.length - curated.length;
|
|
479
|
+
if (rest > 0) {
|
|
480
|
+
process.stderr.write(` ${c.dim(`+${rest} more models available on the BYOK route (--route byok, own API key)`)}\n`);
|
|
481
|
+
}
|
|
482
|
+
process.stderr.write('\n');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function applyLaunchModelArgs(cliArgs, ctx) {
|
|
486
|
+
const route = String(cliArgs.route || '').trim().toLowerCase();
|
|
487
|
+
if (route) {
|
|
488
|
+
if (route === 'platform' || route === 'byok') {
|
|
489
|
+
session.routePreference = route;
|
|
490
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Model route:')} ${c.brand(route)}\n`);
|
|
491
|
+
} else {
|
|
492
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`Unknown --route ${cliArgs.route} (expected platform|byok)`)}\n`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const value = String(cliArgs.model || '').trim();
|
|
497
|
+
if (!value) return Promise.resolve();
|
|
498
|
+
|
|
499
|
+
if (NAMED_MODEL_MODES.has(value.toLowerCase())) {
|
|
500
|
+
session.modelMode = value.toLowerCase();
|
|
501
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Session model mode:')} ${c.brand(session.modelMode)}\n`);
|
|
502
|
+
return Promise.resolve();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const pending = [];
|
|
506
|
+
for (const part of value.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
507
|
+
const eq = part.indexOf('=');
|
|
508
|
+
let role = 'reasoning';
|
|
509
|
+
let model = part;
|
|
510
|
+
if (eq > 0) {
|
|
511
|
+
const maybeRole = normalizeModelRole(part.slice(0, eq));
|
|
512
|
+
if (!maybeRole) {
|
|
513
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`Unknown model role in --model: ${part.slice(0, eq)}`)}\n`);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
role = maybeRole;
|
|
517
|
+
model = part.slice(eq + 1).trim();
|
|
518
|
+
}
|
|
519
|
+
if (!model) continue;
|
|
520
|
+
session.modelOverrides = { ...(session.modelOverrides || {}), [role]: model };
|
|
521
|
+
if (role === 'reasoning') session.model = model;
|
|
522
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Session ${MODEL_ROLE_LABELS[role] || role} model:`)} ${c.brand(model)}\n`);
|
|
523
|
+
pending.push(warnIfNotCurated(model, ctx));
|
|
524
|
+
}
|
|
525
|
+
return Promise.all(pending);
|
|
526
|
+
}
|
|
527
|
+
|
|
330
528
|
function printModelStatus() {
|
|
331
529
|
process.stderr.write(`\n ${c.bold('Models')}\n`);
|
|
332
530
|
process.stderr.write(` ${c.gray('─'.repeat(44))}\n`);
|
|
333
531
|
process.stderr.write(` ${c.gray('Active coding')} ${session.model || 'backend default'}\n`);
|
|
532
|
+
process.stderr.write(` ${c.gray('Route ')} ${session.routePreference || (session.isByok ? 'byok' : 'platform')}\n`);
|
|
533
|
+
if (session.modelMode) {
|
|
534
|
+
process.stderr.write(` ${c.gray('Mode ')} ${session.modelMode}\n`);
|
|
535
|
+
}
|
|
334
536
|
|
|
335
537
|
const limits = session.modelLimits || {};
|
|
336
538
|
const rows = [
|
|
@@ -358,16 +560,124 @@ function printModelStatus() {
|
|
|
358
560
|
printModelCommandUsage();
|
|
359
561
|
}
|
|
360
562
|
|
|
361
|
-
|
|
563
|
+
const MODEL_FORM_ROLES = ['reasoning', 'fast', 'orchestrator', 'explore', 'plan'];
|
|
564
|
+
|
|
565
|
+
async function openModelForm(ctx) {
|
|
566
|
+
const limits = session.modelLimits || {};
|
|
567
|
+
const defaultsByRole = {
|
|
568
|
+
reasoning: limits.coder?.model,
|
|
569
|
+
fast: limits.explorer?.model,
|
|
570
|
+
orchestrator: limits.orchestrator?.model,
|
|
571
|
+
explore: limits.explorer?.model,
|
|
572
|
+
plan: limits.orchestrator?.model,
|
|
573
|
+
};
|
|
574
|
+
const roles = MODEL_FORM_ROLES.map(role => ({
|
|
575
|
+
role,
|
|
576
|
+
label: MODEL_ROLE_LABELS[role] || role,
|
|
577
|
+
current: (session.modelOverrides || {})[role] || null,
|
|
578
|
+
defaultLabel: defaultsByRole[role] || null,
|
|
579
|
+
}));
|
|
580
|
+
const catalog = await fetchModelCatalog(ctx);
|
|
581
|
+
// No curated catalog (backend down, empty table, …): fall back to the
|
|
582
|
+
// distinct models the backend already reported for this session so the
|
|
583
|
+
// form is still navigable instead of a dead single-option row.
|
|
584
|
+
const fallbackIds = [...new Set([
|
|
585
|
+
...Object.values(limits).map(l => l?.model),
|
|
586
|
+
...Object.values(session.modelOverrides || {}),
|
|
587
|
+
].filter(Boolean))];
|
|
588
|
+
const result = await pickModelOverridesForm({
|
|
589
|
+
rl: ctx?._rl || null,
|
|
590
|
+
roles,
|
|
591
|
+
catalog: catalog || [],
|
|
592
|
+
fallbackIds,
|
|
593
|
+
unavailableNote: catalog ? null : (_modelCatalogError || 'backend unreachable'),
|
|
594
|
+
});
|
|
595
|
+
if (!result) {
|
|
596
|
+
process.stderr.write(` ${c.dim('No model changes.')}\n`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// Merge: form rows replace their roles; overrides on roles the form
|
|
600
|
+
// doesn't show (verify/debug/…) are left untouched.
|
|
601
|
+
const next = { ...(session.modelOverrides || {}) };
|
|
602
|
+
for (const role of MODEL_FORM_ROLES) delete next[role];
|
|
603
|
+
Object.assign(next, result.overrides);
|
|
604
|
+
session.modelOverrides = next;
|
|
605
|
+
if (result.overrides.reasoning) session.model = result.overrides.reasoning;
|
|
606
|
+
persistSessionModelState(ctx);
|
|
607
|
+
const chosen = Object.entries(result.overrides);
|
|
608
|
+
if (!chosen.length) {
|
|
609
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('All roles back to backend defaults.')}\n`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
for (const [role, model] of chosen) {
|
|
613
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`${MODEL_ROLE_LABELS[role] || role}:`)} ${c.brand(model)}\n`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function handleModelCommand(rest = '', ctx) {
|
|
362
618
|
const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
363
619
|
if (parts.length === 0) {
|
|
620
|
+
if (process.stdin.isTTY) {
|
|
621
|
+
await openModelForm(ctx);
|
|
622
|
+
} else {
|
|
623
|
+
printModelStatus();
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (parts[0] === 'status') {
|
|
364
629
|
printModelStatus();
|
|
365
630
|
return;
|
|
366
631
|
}
|
|
367
632
|
|
|
633
|
+
if (parts[0] === 'form') {
|
|
634
|
+
await openModelForm(ctx);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (parts[0] === 'list' || parts[0] === 'catalog') {
|
|
639
|
+
await printModelCatalog(ctx);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (parts[0] === 'refresh') {
|
|
644
|
+
// Bust the in-process catalog and pull a fresh /api/models. This only
|
|
645
|
+
// touches the in-memory cache — persisted /model picks in
|
|
646
|
+
// ~/.bahulam/config.json (model_config, model_mode, route_preference)
|
|
647
|
+
// are untouched, so session overrides survive the refresh.
|
|
648
|
+
_modelCatalogCache = null;
|
|
649
|
+
_modelCatalogError = null;
|
|
650
|
+
_modelCatalogSource = null;
|
|
651
|
+
_backendRefreshInFlight = null;
|
|
652
|
+
process.stderr.write(` ${c.dim('Refreshing model catalog…')}\n`);
|
|
653
|
+
await refreshCatalogFromBackend(ctx);
|
|
654
|
+
if (_modelCatalogSource !== 'backend') {
|
|
655
|
+
// Backend fetch failed — restore the shipped snapshot so subsequent
|
|
656
|
+
// /model commands still see a catalog.
|
|
657
|
+
const shipped = readShippedCatalog();
|
|
658
|
+
if (shipped && shipped.length) {
|
|
659
|
+
_modelCatalogCache = shipped;
|
|
660
|
+
_modelCatalogSource = 'snapshot';
|
|
661
|
+
}
|
|
662
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`Backend refresh failed — ${_modelCatalogError || 'unknown error'}. Showing shipped snapshot.`)}\n`);
|
|
663
|
+
}
|
|
664
|
+
await printModelCatalog(ctx);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (parts.length === 1 && NAMED_MODEL_MODES.has(parts[0].toLowerCase())) {
|
|
669
|
+
session.modelMode = parts[0].toLowerCase();
|
|
670
|
+
persistSessionModelState(ctx);
|
|
671
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Session model mode:')} ${c.brand(session.modelMode)}\n`);
|
|
672
|
+
process.stderr.write(` ${c.dim('The platform maps this mode to pinned models. Use /model clear to reset.')}\n`);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
368
676
|
if (parts[0] === 'clear' || parts[0] === 'reset') {
|
|
369
677
|
if (parts.length === 1) {
|
|
370
678
|
session.modelOverrides = {};
|
|
679
|
+
session.modelMode = null;
|
|
680
|
+
persistSessionModelState(ctx);
|
|
371
681
|
process.stderr.write(` ${c.green('✓')} ${c.dim('Cleared all session model overrides.')}\n`);
|
|
372
682
|
return;
|
|
373
683
|
}
|
|
@@ -378,6 +688,7 @@ function handleModelCommand(rest = '') {
|
|
|
378
688
|
return;
|
|
379
689
|
}
|
|
380
690
|
delete session.modelOverrides[role];
|
|
691
|
+
persistSessionModelState(ctx);
|
|
381
692
|
process.stderr.write(` ${c.green('✓')} ${c.dim(`Cleared ${MODEL_ROLE_LABELS[role] || role} model override.`)}\n`);
|
|
382
693
|
return;
|
|
383
694
|
}
|
|
@@ -397,8 +708,10 @@ function handleModelCommand(rest = '') {
|
|
|
397
708
|
|
|
398
709
|
session.modelOverrides = { ...(session.modelOverrides || {}), [role]: model };
|
|
399
710
|
if (role === 'reasoning') session.model = model;
|
|
711
|
+
persistSessionModelState(ctx);
|
|
400
712
|
process.stderr.write(` ${c.green('✓')} ${c.dim(`Session ${MODEL_ROLE_LABELS[role] || role} model override:`)} ${c.brand(model)}\n`);
|
|
401
713
|
process.stderr.write(` ${c.dim('Use /model clear or /model clear <role> to return to backend settings.')}\n`);
|
|
714
|
+
await warnIfNotCurated(model, ctx);
|
|
402
715
|
}
|
|
403
716
|
|
|
404
717
|
function stripPathQuotes(value) {
|
|
@@ -634,8 +947,10 @@ function commandCompletions(line) {
|
|
|
634
947
|
const top = ['/help', '/status', '/plan', '/tasks', '/history', '/settings', '/why'];
|
|
635
948
|
const namespaced = HELP_GROUPS.flatMap(g => g.commands.map(([name]) => name.split(/\s+/)[0]));
|
|
636
949
|
const all = [...new Set([...top, ...namespaced, ...Object.keys(COMMANDS), '/quit'])].sort();
|
|
637
|
-
|
|
638
|
-
|
|
950
|
+
// No fallback-to-all: a non-matching prefix ("/Users/...", a pasted
|
|
951
|
+
// path) must yield NOTHING so the hint overlay hides, not the full
|
|
952
|
+
// catalog. Bare "/" still matches every command via startsWith.
|
|
953
|
+
return all.filter(cmd => cmd.startsWith(line));
|
|
639
954
|
}
|
|
640
955
|
|
|
641
956
|
function slashCommandSuggestions(line, limit = 5) {
|
|
@@ -1081,7 +1396,10 @@ function renderEvent(event) {
|
|
|
1081
1396
|
runtime.lastRenderedBlock = 'thinking';
|
|
1082
1397
|
session._lastEmittedThinking = text;
|
|
1083
1398
|
}
|
|
1084
|
-
|
|
1399
|
+
// Shared 'thinking' phase — the elapsed clock keeps counting
|
|
1400
|
+
// across successive thinking events instead of resetting per
|
|
1401
|
+
// thought, so the user sees "… · 24s" during long reasoning.
|
|
1402
|
+
startSpinner(text.slice(0, 80), { phase: 'thinking' });
|
|
1085
1403
|
// Capture reasoning so /why can replay it.
|
|
1086
1404
|
session.lastReasoning = text;
|
|
1087
1405
|
}
|
|
@@ -1348,7 +1666,12 @@ function renderEvent(event) {
|
|
|
1348
1666
|
runtime.lastRenderedBlock = 'subagent';
|
|
1349
1667
|
session.inSubAgent = inSubAgentBlock(); // kept for legacy readers
|
|
1350
1668
|
session.subAgentCounts[agentType] = (session.subAgentCounts[agentType] || 0) + 1;
|
|
1351
|
-
|
|
1669
|
+
// Fixed-height live tool window under the spinner (queue mode):
|
|
1670
|
+
// inner tool calls stream here instead of flooding the transcript.
|
|
1671
|
+
setSubAgentWindowActive(true);
|
|
1672
|
+
// Phase per sub-agent run: the status line counts elapsed time and
|
|
1673
|
+
// tool calls live ("plan agent · 4 calls · 32s") for the whole run.
|
|
1674
|
+
startSpinner(`${agentType} agent`, { phase: `sub:${agentType}:${Date.now()}` });
|
|
1352
1675
|
break;
|
|
1353
1676
|
}
|
|
1354
1677
|
|
|
@@ -1358,16 +1681,24 @@ function renderEvent(event) {
|
|
|
1358
1681
|
const agentType = data?.type || 'sub-agent';
|
|
1359
1682
|
const tool = data?.tool || '';
|
|
1360
1683
|
if (!tool) break;
|
|
1684
|
+
// Feed the live window from THIS event — it always fires (55/55 in
|
|
1685
|
+
// observed runs), unlike the inner tool_call render path which
|
|
1686
|
+
// diverts for explore-category tools and folded verbosity modes.
|
|
1687
|
+
// Dedup-consecutive inside the push keeps repeat tools quiet.
|
|
1688
|
+
pushSubAgentWindowLine(`→ ${tool}`);
|
|
1361
1689
|
// Don't clobber an active explore-run spinner. "exploring · 5 read ·
|
|
1362
1690
|
// 2 searched" is more informative than "explore → search_code", and
|
|
1363
1691
|
// sub_agent_tool fires on every step of a sub-agent — otherwise the
|
|
1364
1692
|
// spinner would flip-flop between the two texts and read as blank.
|
|
1365
1693
|
if (runtime.exploreRun.lineActive && isExploreTool(tool)) break;
|
|
1694
|
+
// Same phase → clock keeps counting; the counter shows progress.
|
|
1695
|
+
bumpSpinnerProgress();
|
|
1366
1696
|
updateSpinner(`${agentType} → ${tool}`);
|
|
1367
1697
|
break;
|
|
1368
1698
|
}
|
|
1369
1699
|
|
|
1370
1700
|
case 'sub_agent_complete': {
|
|
1701
|
+
setSubAgentWindowActive(false);
|
|
1371
1702
|
stopSpinner();
|
|
1372
1703
|
clearPendingHead();
|
|
1373
1704
|
flushFoldedSubAgentTools();
|
|
@@ -1479,6 +1810,8 @@ function renderEvent(event) {
|
|
|
1479
1810
|
}
|
|
1480
1811
|
|
|
1481
1812
|
case 'complete': {
|
|
1813
|
+
// Fire first_answer on the first turn's completion
|
|
1814
|
+
if (session.turns === 1 && session.user) telemetry.track('first_answer', {});
|
|
1482
1815
|
stopSpinner();
|
|
1483
1816
|
flushContent();
|
|
1484
1817
|
flushFoldedSubAgentTools();
|
|
@@ -1887,6 +2220,8 @@ async function prepareDirectAgentRunContext(ctx, instruction = '') {
|
|
|
1887
2220
|
execContext.model_overrides = modelOverrides;
|
|
1888
2221
|
if (modelOverrides.reasoning) execContext.model_override = modelOverrides.reasoning;
|
|
1889
2222
|
}
|
|
2223
|
+
if (session.modelMode) execContext.model_mode = session.modelMode;
|
|
2224
|
+
if (session.routePreference) execContext.model_route = session.routePreference;
|
|
1890
2225
|
return execContext;
|
|
1891
2226
|
}
|
|
1892
2227
|
|
|
@@ -2029,8 +2364,10 @@ async function handleCommand(input, ctx) {
|
|
|
2029
2364
|
|
|
2030
2365
|
case '/login':
|
|
2031
2366
|
process.stderr.write(`${c.brand('Starting login flow...')}\n`);
|
|
2367
|
+
telemetry.track('login_shown', { method: 'repl_command' });
|
|
2032
2368
|
try {
|
|
2033
2369
|
await ctx.auth.login();
|
|
2370
|
+
telemetry.track('login_completed', { method: 'repl_command' });
|
|
2034
2371
|
process.stderr.write(`${c.green('✓ Login successful!')}\n`);
|
|
2035
2372
|
await fetchUser(ctx);
|
|
2036
2373
|
} catch (err) {
|
|
@@ -2471,7 +2808,7 @@ async function handleCommand(input, ctx) {
|
|
|
2471
2808
|
}
|
|
2472
2809
|
|
|
2473
2810
|
case '/model': {
|
|
2474
|
-
handleModelCommand(rest);
|
|
2811
|
+
await handleModelCommand(rest, ctx);
|
|
2475
2812
|
return;
|
|
2476
2813
|
}
|
|
2477
2814
|
|
|
@@ -2536,6 +2873,67 @@ async function handleCommand(input, ctx) {
|
|
|
2536
2873
|
return;
|
|
2537
2874
|
}
|
|
2538
2875
|
|
|
2876
|
+
case '/auto': {
|
|
2877
|
+
// Session autopilot for long-running jobs: auto-approve routine
|
|
2878
|
+
// writes/shell while STILL prompting for dangerous tiers (rm,
|
|
2879
|
+
// force-push, command substitution, …) and never overriding hard
|
|
2880
|
+
// safety blocks. Distinct from the launch-time --freeswim flag,
|
|
2881
|
+
// which approves everything including dangerous tiers.
|
|
2882
|
+
const sub = (rest || '').trim().toLowerCase();
|
|
2883
|
+
if (sub === 'off') {
|
|
2884
|
+
ctx.approval.approveAll = false;
|
|
2885
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Auto mode off — approvals prompt again.')}\n`);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (sub === '' || sub === 'on') {
|
|
2889
|
+
ctx.approval.approveAll = true;
|
|
2890
|
+
process.stderr.write(` ${c.green('✓')} ${c.bold('Auto mode on')} ${c.dim('— routine tool calls auto-approve this session.')}\n`);
|
|
2891
|
+
process.stderr.write(` ${c.dim('Still prompts: dangerous shell (rm/force-push/substitution), protected files.')}\n`);
|
|
2892
|
+
process.stderr.write(` ${c.dim('Hard safety blocks stay enforced. Disable with /auto off · inspect with /approvals.')}\n`);
|
|
2893
|
+
process.stderr.write(` ${c.dim('Tip: start your message with #auto to switch the backend agent into autonomous mode too.')}\n`);
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
// status / anything else → show current mode
|
|
2897
|
+
process.stderr.write(` ${c.dim('Approval mode:')} ${ctx.approval.getModeLabel()}\n`);
|
|
2898
|
+
process.stderr.write(` ${c.dim('Usage: /auto [on|off|status]')}\n`);
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
case '/approvals': {
|
|
2903
|
+
const parts = (rest || '').trim().split(/\s+/).filter(Boolean);
|
|
2904
|
+
const action = (parts[0] || 'list').toLowerCase();
|
|
2905
|
+
if (action === 'clear') {
|
|
2906
|
+
const wasActive = ctx.approval.revoke();
|
|
2907
|
+
process.stderr.write(wasActive
|
|
2908
|
+
? ` ${c.green('✓')} ${c.dim('All session approvals cleared.')}\n`
|
|
2909
|
+
: ` ${c.gray('No session approvals were active.')}\n`);
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (action === 'allow' && parts[1]) {
|
|
2913
|
+
const tool = parts[1];
|
|
2914
|
+
ctx.approval.approvedToolTypes.add(tool);
|
|
2915
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Auto-approving ${tool} for this session (dangerous tiers still prompt).`)}\n`);
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
if (action === 'remove' && parts[1]) {
|
|
2919
|
+
const removed = ctx.approval.approvedToolTypes.delete(parts[1]);
|
|
2920
|
+
process.stderr.write(removed
|
|
2921
|
+
? ` ${c.green('✓')} ${c.dim(`${parts[1]} will prompt again.`)}\n`
|
|
2922
|
+
: ` ${c.gray(`${parts[1]} had no session grant.`)}\n`);
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
// list (default)
|
|
2926
|
+
const s = ctx.approval.getSummary();
|
|
2927
|
+
process.stderr.write(`\n ${c.bold('Session approvals')}\n`);
|
|
2928
|
+
process.stderr.write(` ${c.dim('Mode')} ${ctx.approval.getModeLabel()}\n`);
|
|
2929
|
+
process.stderr.write(` ${c.dim('Allow-all')} ${s.autoApproveAll ? c.green('on') : c.dim('off')}\n`);
|
|
2930
|
+
process.stderr.write(` ${c.dim('Tool grants')} ${s.autoApprovedTypes.length ? s.autoApprovedTypes.join(', ') : c.dim('none')}\n`);
|
|
2931
|
+
process.stderr.write(` ${c.dim('Trust rules')} ${c.dim(`${s.trust.sessionRules || 0} session · ${s.trust.projectRules || 0} project`)}\n`);
|
|
2932
|
+
process.stderr.write(` ${c.dim('Decisions')} ${c.dim(`${s.approved} approved · ${s.denied} denied`)}\n`);
|
|
2933
|
+
process.stderr.write(` ${c.dim('Edit: /approvals allow <tool> · /approvals remove <tool> · /approvals clear · /auto [on|off]')}\n\n`);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2539
2937
|
case '/sessions': {
|
|
2540
2938
|
const resumable = await listResumableSessions();
|
|
2541
2939
|
if (resumable.length === 0) {
|
|
@@ -2862,6 +3260,8 @@ export async function startTerminalRepl() {
|
|
|
2862
3260
|
model: session.model,
|
|
2863
3261
|
modelLimits: session.modelLimits,
|
|
2864
3262
|
modelOverrides: session.modelOverrides,
|
|
3263
|
+
modelMode: session.modelMode,
|
|
3264
|
+
routePreference: session.routePreference,
|
|
2865
3265
|
isByok: session.isByok,
|
|
2866
3266
|
subscriptionTier: session.subscriptionTier,
|
|
2867
3267
|
creditsTotal: session.creditsTotal,
|
|
@@ -2904,6 +3304,8 @@ export async function startTerminalRepl() {
|
|
|
2904
3304
|
model: preserved.model,
|
|
2905
3305
|
modelLimits: preserved.modelLimits,
|
|
2906
3306
|
modelOverrides: preserved.modelOverrides,
|
|
3307
|
+
modelMode: preserved.modelMode,
|
|
3308
|
+
routePreference: preserved.routePreference,
|
|
2907
3309
|
blockedOps: 0,
|
|
2908
3310
|
delegations: [],
|
|
2909
3311
|
phases: [],
|
|
@@ -3186,6 +3588,25 @@ export async function startTerminalRepl() {
|
|
|
3186
3588
|
// Clear the spinner line
|
|
3187
3589
|
process.stderr.write(`\r${' '.repeat(60)}\r`);
|
|
3188
3590
|
process.stderr.write(` ${c.green('✓')} ${c.dim('Ready; projects will be indexed on demand')}\n`);
|
|
3591
|
+
|
|
3592
|
+
// PRD-076 W7b: restore persisted /model picks from ~/.bahulam/config.json
|
|
3593
|
+
// before applying CLI-flag overrides, so `bahulam-code` alone re-uses
|
|
3594
|
+
// last session's choices and `bahulam-code --model foo` still wins.
|
|
3595
|
+
try {
|
|
3596
|
+
const persisted = auth.loadCredentials();
|
|
3597
|
+
if (persisted?.modelConfig && Object.keys(persisted.modelConfig).length) {
|
|
3598
|
+
session.modelOverrides = { ...persisted.modelConfig };
|
|
3599
|
+
if (persisted.modelConfig.reasoning) session.model = persisted.modelConfig.reasoning;
|
|
3600
|
+
}
|
|
3601
|
+
if (persisted?.modelMode) session.modelMode = persisted.modelMode;
|
|
3602
|
+
if (persisted?.routePreference) session.routePreference = persisted.routePreference;
|
|
3603
|
+
} catch { /* best-effort restore */ }
|
|
3604
|
+
|
|
3605
|
+
// --model / --route launch overrides (after fetchUser so they win
|
|
3606
|
+
// over the profile default; catalog validation is fail-open).
|
|
3607
|
+
if (cliArgs.model || cliArgs.route) {
|
|
3608
|
+
try { await applyLaunchModelArgs(cliArgs, ctx); } catch {}
|
|
3609
|
+
}
|
|
3189
3610
|
if (session.user) {
|
|
3190
3611
|
process.stderr.write(` ${c.green('✓')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
|
|
3191
3612
|
}
|
|
@@ -3412,50 +3833,92 @@ export async function startTerminalRepl() {
|
|
|
3412
3833
|
readline.cursorTo(process.stderr, col);
|
|
3413
3834
|
}
|
|
3414
3835
|
|
|
3415
|
-
|
|
3836
|
+
// Hint frames must NOT go through the patched std streams — under the
|
|
3837
|
+
// render queue, redirected writes are sanitized (cursor CSI stripped)
|
|
3838
|
+
// and serialized into the transcript. That's exactly the "pasted a
|
|
3839
|
+
// path, command list flooded the message area" bug. queue.raw() is the
|
|
3840
|
+
// serialized trusted channel for cursor-addressed frame writes;
|
|
3841
|
+
// readline helpers are the legacy no-queue path.
|
|
3842
|
+
function writeHintFrame(frame) {
|
|
3843
|
+
if (rqueue.isActive()) {
|
|
3844
|
+
rqueue.raw(frame);
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
process.stderr.write(frame);
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
let slashHintTimer = null;
|
|
3851
|
+
|
|
3852
|
+
function renderSlashHintNow(line = '', { preserveSelection = false } = {}) {
|
|
3416
3853
|
if (!process.stderr.isTTY || term().plain || !inputActive || !promptBottomPaddingLines()) return;
|
|
3417
3854
|
const rows = promptBottomPaddingLines();
|
|
3418
3855
|
const suggestions = slashCommandSuggestions(line, Math.min(5, rows));
|
|
3856
|
+
// Nothing matches (pasted path, typo) → hide instead of rendering
|
|
3857
|
+
// an empty/robotic frame.
|
|
3858
|
+
if (!suggestions.length) {
|
|
3859
|
+
if (slashHintVisible) clearSlashHint();
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3419
3862
|
const cols = process.stdout.columns || 80;
|
|
3420
3863
|
if (!preserveSelection || line !== slashHintLine) slashHintSelected = 0;
|
|
3421
3864
|
slashHintItems = suggestions;
|
|
3422
3865
|
slashHintLine = line;
|
|
3423
3866
|
if (slashHintSelected >= slashHintItems.length) slashHintSelected = Math.max(0, slashHintItems.length - 1);
|
|
3424
3867
|
|
|
3425
|
-
|
|
3868
|
+
// Compose the whole frame as ONE write: down a row, paint each hint
|
|
3869
|
+
// row (clear + text), then back up to the input row.
|
|
3870
|
+
let frame = '\x1b[1B';
|
|
3426
3871
|
for (let i = 0; i < rows; i++) {
|
|
3427
|
-
|
|
3428
|
-
readline.cursorTo(process.stderr, 0);
|
|
3872
|
+
frame += '\x1b[2K\x1b[G';
|
|
3429
3873
|
const item = suggestions[i];
|
|
3430
3874
|
if (item) {
|
|
3431
3875
|
const marker = i === slashHintSelected ? c.brand('›') : c.dim(' ');
|
|
3432
3876
|
const command = item.command.padEnd(13);
|
|
3433
3877
|
const maxDesc = Math.max(0, cols - 21);
|
|
3434
3878
|
const desc = truncateHintText(item.description, maxDesc);
|
|
3435
|
-
|
|
3879
|
+
frame += ` ${marker} ${c.brand(command)}${desc ? c.dim(desc) : ''}`;
|
|
3436
3880
|
}
|
|
3437
|
-
if (i < rows - 1)
|
|
3881
|
+
if (i < rows - 1) frame += '\x1b[1B';
|
|
3438
3882
|
}
|
|
3439
|
-
|
|
3883
|
+
frame += `\x1b[${rows}A`;
|
|
3884
|
+
writeHintFrame(frame);
|
|
3440
3885
|
restoreReadlineCursor();
|
|
3441
|
-
slashHintVisible =
|
|
3886
|
+
slashHintVisible = true;
|
|
3442
3887
|
slashHintRowsVisible = rows;
|
|
3443
3888
|
}
|
|
3444
3889
|
|
|
3890
|
+
// Debounced entry point: a bracketed paste delivers the buffer as many
|
|
3891
|
+
// rapid input events — rendering per event is what turned one paste
|
|
3892
|
+
// into dozens of hint frames. Selection-preserving calls (arrow keys)
|
|
3893
|
+
// stay immediate for responsiveness.
|
|
3894
|
+
function renderSlashHint(line = '', opts = {}) {
|
|
3895
|
+
if (opts.preserveSelection) {
|
|
3896
|
+
if (slashHintTimer) { clearTimeout(slashHintTimer); slashHintTimer = null; }
|
|
3897
|
+
renderSlashHintNow(line, opts);
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
if (slashHintTimer) clearTimeout(slashHintTimer);
|
|
3901
|
+
slashHintTimer = setTimeout(() => {
|
|
3902
|
+
slashHintTimer = null;
|
|
3903
|
+
renderSlashHintNow(typeof rl?.line === 'string' ? rl.line : line, opts);
|
|
3904
|
+
}, 24);
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3445
3907
|
function clearSlashHint({ restoreCursor: shouldRestoreCursor = true } = {}) {
|
|
3908
|
+
if (slashHintTimer) { clearTimeout(slashHintTimer); slashHintTimer = null; }
|
|
3446
3909
|
if (!slashHintVisible || !process.stderr.isTTY || term().plain) {
|
|
3447
3910
|
slashHintVisible = false;
|
|
3448
3911
|
slashHintRowsVisible = 0;
|
|
3449
3912
|
return;
|
|
3450
3913
|
}
|
|
3451
3914
|
const rows = slashHintRowsVisible || promptBottomPaddingLines() || 1;
|
|
3452
|
-
|
|
3915
|
+
let frame = '\x1b[1B';
|
|
3453
3916
|
for (let i = 0; i < rows; i++) {
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
if (i < rows - 1) readline.moveCursor(process.stderr, 0, 1);
|
|
3917
|
+
frame += '\x1b[2K\x1b[G';
|
|
3918
|
+
if (i < rows - 1) frame += '\x1b[1B';
|
|
3457
3919
|
}
|
|
3458
|
-
|
|
3920
|
+
frame += `\x1b[${rows}A`;
|
|
3921
|
+
writeHintFrame(frame);
|
|
3459
3922
|
// The dock's bottom rule + tips row live in the rows we just cleared.
|
|
3460
3923
|
// Repaint the frame (input row untouched) so they reappear.
|
|
3461
3924
|
if (isInputDockMounted()) redrawDockFrame();
|
|
@@ -3852,6 +4315,9 @@ export async function startTerminalRepl() {
|
|
|
3852
4315
|
session.history.push(userMessage);
|
|
3853
4316
|
session.agentHistory.push(userMessage);
|
|
3854
4317
|
session.turns++;
|
|
4318
|
+
// Fire first_prompt on user's first turn
|
|
4319
|
+
if (session.turns === 1) telemetry.track('first_prompt', {});
|
|
4320
|
+
|
|
3855
4321
|
session.toolCalls = 0;
|
|
3856
4322
|
session.subAgentToolCalls = 0;
|
|
3857
4323
|
session.lastTask = originalInput;
|
|
@@ -4294,6 +4760,8 @@ export async function startTerminalRepl() {
|
|
|
4294
4760
|
execContext.model_overrides = modelOverrides;
|
|
4295
4761
|
if (modelOverrides.reasoning) execContext.model_override = modelOverrides.reasoning;
|
|
4296
4762
|
}
|
|
4763
|
+
if (session.modelMode) execContext.model_mode = session.modelMode;
|
|
4764
|
+
if (session.routePreference) execContext.model_route = session.routePreference;
|
|
4297
4765
|
// PRD-071: seed work_scope from CLI so the backend has a byte-stable
|
|
4298
4766
|
// scope block from turn 1. Uses projectResources already gathered by
|
|
4299
4767
|
// the envelope above.
|