@bahulam/code 2.6.13 → 2.6.15

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.
@@ -35,6 +35,7 @@ import { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '
35
35
  import {
36
36
  getVerbosity,
37
37
  setVerbosity,
38
+ showSubAgentTools,
38
39
  label as verbosityLabel,
39
40
  MODES as V_MODES,
40
41
  } from '../state/verbosity.mjs';
@@ -44,7 +45,7 @@ import { ApprovalManager } from '../core/approval.mjs';
44
45
  import { resolveBackendUrl } from '../core/backend-url.mjs';
45
46
  import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
46
47
  import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
47
- import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
48
+ import { BUILTIN_AGENTS, findBuiltinAgent, localAgentMatches, runAgent, runAgentDefinition } from './agents.mjs';
48
49
  import { createAgentFile, isVsCodeTerminal, listLocalAgents, openAgentFile, syncAgentsToBackend } from '../agents/scaffold.mjs';
49
50
  import { SessionManager } from '../core/session-manager.mjs';
50
51
  import { parseArgs } from '../config/cli-args.mjs';
@@ -83,12 +84,14 @@ import {
83
84
  isInlineOutcomeTool,
84
85
  renderBlockBoundary,
85
86
  renderExploreRun,
87
+ renderFileDiffEvent,
86
88
  renderStagnation,
87
89
  renderToolCall,
88
90
  renderToolResult,
89
91
  startContentStream,
90
92
  startSpinner,
91
93
  stopSpinner,
94
+ transcriptRenderableLines,
92
95
  thinkingPrefix,
93
96
  updateSpinner,
94
97
  } from './repl-render.mjs';
@@ -134,6 +137,7 @@ import {
134
137
  } from '../ui/slash-commands.mjs';
135
138
  import { createOrbit } from '../state/orbit.mjs';
136
139
  import {
140
+ clearPinnedStatus,
137
141
  clearInputPrompt,
138
142
  focusDockInput,
139
143
  isInputDockMounted,
@@ -258,7 +262,7 @@ function renderHelp(topic = '') {
258
262
  function renderKeyboardHelp() {
259
263
  process.stderr.write(`\n ${c.bold('Keyboard')}\n`);
260
264
  process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n`);
261
- process.stderr.write(` ${c.gray('Ctrl+D')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
265
+ process.stderr.write(` ${c.gray('F2')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
262
266
  }
263
267
 
264
268
  const MODEL_ROLE_ALIASES = new Map([
@@ -849,6 +853,164 @@ function updateStatusBar() {
849
853
 
850
854
  // ── Event Renderer ──
851
855
 
856
+ function isDeniedStatusMessage(message = '') {
857
+ return /^(?:Denied\s+\S+|Blocked by safety policy)\b/i.test(String(message || '').trim());
858
+ }
859
+
860
+ function toolCallId(data = {}, tool = 'tool') {
861
+ return data.call_id || data._callId || data.request_id || data.id ||
862
+ `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
863
+ }
864
+
865
+ function isSubAgentToolEvent(data = {}) {
866
+ return Boolean(data?.internal || data?.sub_agent);
867
+ }
868
+
869
+ function shouldFoldSubAgentTool(data = {}) {
870
+ return isSubAgentToolEvent(data) && !showSubAgentTools(getVerbosity());
871
+ }
872
+
873
+ function foldedSubAgentName(data = {}) {
874
+ return data?.sub_agent || data?.agent || data?.type || 'sub-agent';
875
+ }
876
+
877
+ function ensureFoldedSubAgentTools(agentType) {
878
+ const current = runtime.foldedSubAgentTools;
879
+ if (current && current.agentType === agentType) return current;
880
+ if (current?.entries?.length) flushFoldedSubAgentTools();
881
+ runtime.foldedSubAgentTools = {
882
+ agentType,
883
+ entries: [],
884
+ startedAt: Date.now(),
885
+ };
886
+ return runtime.foldedSubAgentTools;
887
+ }
888
+
889
+ function findFoldedToolEntry(fold, callId, tool) {
890
+ if (!fold) return null;
891
+ if (callId) {
892
+ const exact = fold.entries.find(entry => entry.callId === callId);
893
+ if (exact) return exact;
894
+ }
895
+ for (let i = fold.entries.length - 1; i >= 0; i--) {
896
+ const entry = fold.entries[i];
897
+ if (entry.tool === tool && !entry.result) return entry;
898
+ }
899
+ return null;
900
+ }
901
+
902
+ function foldSubAgentToolCall(data = {}) {
903
+ const tool = data?.tool || 'unknown';
904
+ const args = data?.args || {};
905
+ const callId = toolCallId(data, tool);
906
+ const agentType = foldedSubAgentName(data);
907
+ const fold = ensureFoldedSubAgentTools(agentType);
908
+ const existing = findFoldedToolEntry(fold, callId, tool);
909
+ const entry = existing || {
910
+ callId,
911
+ tool,
912
+ args,
913
+ summary: toolDisplaySummary(tool, args, { cwd: safeCwd() }),
914
+ startedAt: Date.now(),
915
+ result: null,
916
+ durationMs: null,
917
+ outcome: '',
918
+ tone: 'dim',
919
+ };
920
+ if (!existing) fold.entries.push(entry);
921
+ recordCard({ id: callId, tool, args, startedAt: entry.startedAt });
922
+ session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
923
+ startSpinner(`${agentType} → ${tool}`);
924
+ }
925
+
926
+ function foldSubAgentToolResult(data = {}) {
927
+ const tool = data?.tool || data?._tool || 'unknown';
928
+ const args = data?.args || {};
929
+ const callId = toolCallId(data, tool);
930
+ const agentType = foldedSubAgentName(data);
931
+ const fold = ensureFoldedSubAgentTools(agentType);
932
+ let entry = findFoldedToolEntry(fold, callId, tool);
933
+ if (!entry) {
934
+ entry = {
935
+ callId,
936
+ tool,
937
+ args,
938
+ summary: toolDisplaySummary(tool, args, { cwd: safeCwd() }),
939
+ startedAt: Date.now(),
940
+ result: null,
941
+ durationMs: null,
942
+ outcome: '',
943
+ tone: 'dim',
944
+ };
945
+ fold.entries.push(entry);
946
+ }
947
+ const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
948
+ const summary = summarizeResult(tool, data);
949
+ entry.args = entry.args && Object.keys(entry.args).length ? entry.args : args;
950
+ entry.result = data;
951
+ entry.durationMs = durationMs;
952
+ entry.outcome = summary.text || '';
953
+ entry.tone = summary.tone || 'dim';
954
+ if (data._blocked) session.blockedOps++;
955
+ recordCard({ id: callId, tool, args: entry.args, result: data, durationMs, startedAt: entry.startedAt });
956
+ }
957
+
958
+ function foldedOutcome(entry) {
959
+ if (!entry?.outcome) return '';
960
+ const painter = entry.tone === 'success' ? paint.state.success
961
+ : entry.tone === 'warn' ? paint.state.warn
962
+ : entry.tone === 'danger' ? paint.state.danger
963
+ : paint.text.muted;
964
+ return `${paint.text.dim('—')} ${painter(entry.outcome)}`;
965
+ }
966
+
967
+ function foldedToolLine(entry, indent, columns) {
968
+ const head = formatCardHead(entry.tool, entry.args || {}, {
969
+ cwd: safeCwd(),
970
+ columns: Math.max(40, columns - indent.length - 2),
971
+ indent: '',
972
+ }).split('\n')[0];
973
+ const line = `${indent}${paint.text.dim('•')} ${head}${entry.outcome ? ` ${foldedOutcome(entry)}` : ''}`;
974
+ return fitAnsiLine(line, Math.max(32, columns));
975
+ }
976
+
977
+ function flushFoldedSubAgentTools() {
978
+ const fold = runtime.foldedSubAgentTools;
979
+ if (!fold) return;
980
+ const entries = Array.isArray(fold.entries) ? fold.entries : [];
981
+ runtime.foldedSubAgentTools = null;
982
+ if (!entries.length) return;
983
+
984
+ renderBlockBoundary('tool', { compactSame: true });
985
+ const indent = subAgentIndent();
986
+ const cols = process.stderr.columns || 120;
987
+ const shown = entries.slice(0, 4);
988
+ const extra = Math.max(0, entries.length - shown.length);
989
+ const header = `${indent}${paint.text.dim('⎿')} ${paint.text.dim(`${fold.agentType} tools · ${entries.length} tool use${entries.length === 1 ? '' : 's'}`)}`;
990
+ process.stderr.write(`${fitAnsiLine(header, cols)}\n`);
991
+ for (const entry of shown) {
992
+ process.stderr.write(`${foldedToolLine(entry, `${indent} `, cols)}\n`);
993
+ }
994
+ if (extra > 0) {
995
+ process.stderr.write(`${indent} ${paint.text.dim(`… +${extra} tool use${extra === 1 ? '' : 's'} · /last expands this batch`)}\n`);
996
+ } else {
997
+ process.stderr.write(`${indent} ${paint.text.dim('/last expands this batch')}\n`);
998
+ }
999
+ recordCard({
1000
+ id: `sub-agent-tools:${fold.agentType}:${Date.now()}`,
1001
+ tool: 'sub_agent_tools',
1002
+ args: { agent: fold.agentType, total: entries.length },
1003
+ result: {
1004
+ success: true,
1005
+ output: `${entries.length} folded sub-agent tool use${entries.length === 1 ? '' : 's'}`,
1006
+ tools: entries,
1007
+ },
1008
+ durationMs: Date.now() - (fold.startedAt || Date.now()),
1009
+ startedAt: fold.startedAt || Date.now(),
1010
+ });
1011
+ runtime.lastRenderedBlock = 'tool';
1012
+ }
1013
+
852
1014
  function renderEvent(event) {
853
1015
  const { type, data } = event;
854
1016
 
@@ -890,6 +1052,10 @@ function renderEvent(event) {
890
1052
  renderStagnation(data);
891
1053
  break;
892
1054
  }
1055
+ if (isDeniedStatusMessage(msg)) {
1056
+ stopSpinner();
1057
+ break;
1058
+ }
893
1059
  startSpinner(msg);
894
1060
  break;
895
1061
  }
@@ -927,6 +1093,7 @@ function renderEvent(event) {
927
1093
  if (text) {
928
1094
  flushContent();
929
1095
  stopSpinner();
1096
+ flushFoldedSubAgentTools();
930
1097
  if (runtime.streamedPartialText && text.startsWith(runtime.streamedPartialText)) {
931
1098
  text = text.slice(runtime.streamedPartialText.length);
932
1099
  } else if (runtime.streamedPartialText.includes(text)) {
@@ -934,17 +1101,20 @@ function renderEvent(event) {
934
1101
  }
935
1102
  }
936
1103
  if (text) {
937
- renderBlockBoundary('content');
938
- if (!runtime.contentHeaderPrinted) {
939
- process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
940
- runtime.contentHeaderPrinted = true;
941
- }
942
1104
  const rendered = renderMarkdown(text);
943
- for (const line of rendered.split('\n')) {
944
- process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
1105
+ const lines = transcriptRenderableLines(rendered);
1106
+ if (lines.length) {
1107
+ renderBlockBoundary('content', { compactSame: true });
1108
+ if (!runtime.contentHeaderPrinted) {
1109
+ process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
1110
+ runtime.contentHeaderPrinted = true;
1111
+ }
1112
+ for (const line of lines) {
1113
+ process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
1114
+ }
1115
+ runtime.renderedContentThisTurn = true;
1116
+ runtime.lastRenderedBlock = 'content';
945
1117
  }
946
- runtime.renderedContentThisTurn = true;
947
- runtime.lastRenderedBlock = 'content';
948
1118
  }
949
1119
  break;
950
1120
  }
@@ -1009,6 +1179,10 @@ function renderEvent(event) {
1009
1179
  session.totalToolCalls++;
1010
1180
  stopSpinner();
1011
1181
  flushContent();
1182
+ if (shouldFoldSubAgentTool(data)) {
1183
+ foldSubAgentToolCall(data);
1184
+ break;
1185
+ }
1012
1186
  renderToolCall(data);
1013
1187
  break;
1014
1188
  }
@@ -1039,6 +1213,7 @@ function renderEvent(event) {
1039
1213
  }
1040
1214
 
1041
1215
  case 'approval_denied': {
1216
+ stopSpinner();
1042
1217
  const reason = data?.reason || 'User denied';
1043
1218
  const toolName = data?.tool || '';
1044
1219
  const indent = subAgentIndent();
@@ -1051,10 +1226,22 @@ function renderEvent(event) {
1051
1226
  case 'tool_result':
1052
1227
  case 'tool_done': {
1053
1228
  stopSpinner();
1229
+ if (shouldFoldSubAgentTool(data)) {
1230
+ foldSubAgentToolResult(data);
1231
+ break;
1232
+ }
1054
1233
  renderToolResult(data, type);
1055
1234
  break;
1056
1235
  }
1057
1236
 
1237
+ case 'file_diff': {
1238
+ stopSpinner();
1239
+ flushContent();
1240
+ flushPendingHead();
1241
+ renderFileDiffEvent(data);
1242
+ break;
1243
+ }
1244
+
1058
1245
  case 'plan': {
1059
1246
  stopSpinner();
1060
1247
  flushContent();
@@ -1153,6 +1340,7 @@ function renderEvent(event) {
1153
1340
  case 'sub_agent_start': {
1154
1341
  stopSpinner();
1155
1342
  clearPendingHead();
1343
+ flushFoldedSubAgentTools();
1156
1344
  const agentType = data?.type || 'sub-agent';
1157
1345
  const query = data?.query || '';
1158
1346
  renderBlockBoundary('subagent');
@@ -1182,6 +1370,7 @@ function renderEvent(event) {
1182
1370
  case 'sub_agent_complete': {
1183
1371
  stopSpinner();
1184
1372
  clearPendingHead();
1373
+ flushFoldedSubAgentTools();
1185
1374
  const agentType = data?.type || 'sub-agent';
1186
1375
  const usage = data?.usage || {};
1187
1376
  const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
@@ -1250,6 +1439,7 @@ function renderEvent(event) {
1250
1439
  case 'error':
1251
1440
  stopSpinner();
1252
1441
  flushContent();
1442
+ flushFoldedSubAgentTools();
1253
1443
  {
1254
1444
  const guidance = formatAgentErrorGuidance(data || {});
1255
1445
  renderBlockBoundary('status', { compactSame: true });
@@ -1291,22 +1481,26 @@ function renderEvent(event) {
1291
1481
  case 'complete': {
1292
1482
  stopSpinner();
1293
1483
  flushContent();
1484
+ flushFoldedSubAgentTools();
1294
1485
  resetSubAgents();
1295
1486
  session.inSubAgent = false;
1296
1487
 
1297
1488
  const summary = data?.summary || '';
1298
1489
  if (summary && !runtime.renderedContentThisTurn) {
1299
- renderBlockBoundary('content');
1300
- if (!runtime.contentHeaderPrinted) {
1301
- process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
1302
- runtime.contentHeaderPrinted = true;
1303
- }
1304
1490
  const rendered = renderMarkdown(summary);
1305
- for (const line of rendered.split('\n')) {
1306
- process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
1491
+ const lines = transcriptRenderableLines(rendered);
1492
+ if (lines.length) {
1493
+ renderBlockBoundary('content', { compactSame: true });
1494
+ if (!runtime.contentHeaderPrinted) {
1495
+ process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
1496
+ runtime.contentHeaderPrinted = true;
1497
+ }
1498
+ for (const line of lines) {
1499
+ process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
1500
+ }
1501
+ runtime.renderedContentThisTurn = true;
1502
+ runtime.lastRenderedBlock = 'content';
1307
1503
  }
1308
- runtime.renderedContentThisTurn = true;
1309
- runtime.lastRenderedBlock = 'content';
1310
1504
  }
1311
1505
 
1312
1506
  // Update session token counts
@@ -1649,6 +1843,111 @@ function handleTasksCommand(rest, ctx) {
1649
1843
  }
1650
1844
  }
1651
1845
 
1846
+ async function prepareDirectAgentRunContext(ctx, instruction = '') {
1847
+ const cwd = safeCwd();
1848
+ const registered = await ctx.toolExecutor.registerProjectRoots([cwd]);
1849
+ const failed = registered.find(result => result?.success === false);
1850
+ if (failed) {
1851
+ throw new Error(`Could not register project root for sub-agent: ${failed.error || failed.root || cwd}`);
1852
+ }
1853
+
1854
+ const effectivePolicy = loadEffectivePolicy({ cwd });
1855
+ if (ctx.approval) {
1856
+ ctx.approval.policy = effectivePolicy.policy;
1857
+ if (ctx.approval.trustStore) ctx.approval.trustStore.policy = effectivePolicy.policy;
1858
+ }
1859
+ const projectContext = loadProjectContext({ cwd, previous: ctx.latestProjectContext || null });
1860
+ const projectResources = ctx.toolExecutor.getProjectResources();
1861
+ const envelope = buildContextEnvelope({
1862
+ cwd,
1863
+ effectivePolicy,
1864
+ projectContext,
1865
+ activeHints: [],
1866
+ projectResources,
1867
+ agentContext: ctx.toolExecutor.getAgentContext(),
1868
+ });
1869
+
1870
+ ctx.effectivePolicy = effectivePolicy;
1871
+ ctx.latestProjectContext = projectContext;
1872
+ ctx.latestEnvelope = envelope;
1873
+
1874
+ const execContext = {
1875
+ ...envelope,
1876
+ cwd,
1877
+ project_root: cwd,
1878
+ project_resources: projectResources,
1879
+ work_scope: buildWorkScope({
1880
+ instruction: instruction || 'Run sub-agent task',
1881
+ cwd,
1882
+ projectResources,
1883
+ }),
1884
+ };
1885
+ const modelOverrides = Object.fromEntries(sessionModelOverrideEntries());
1886
+ if (Object.keys(modelOverrides).length > 0) {
1887
+ execContext.model_overrides = modelOverrides;
1888
+ if (modelOverrides.reasoning) execContext.model_override = modelOverrides.reasoning;
1889
+ }
1890
+ return execContext;
1891
+ }
1892
+
1893
+ async function handleRunCommand(rest = '', ctx) {
1894
+ const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
1895
+ const target = parts.shift();
1896
+ const instruction = parts.join(' ');
1897
+
1898
+ if (!target) {
1899
+ process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
1900
+ return;
1901
+ }
1902
+
1903
+ const localAgent = listLocalAgents(safeCwd()).find(agent => localAgentMatches(agent, target));
1904
+ const builtinAgent = findBuiltinAgent(target);
1905
+ const runnableAgent = localAgent || builtinAgent;
1906
+ if (runnableAgent) {
1907
+ try {
1908
+ const execContext = await prepareDirectAgentRunContext(ctx, instruction || target);
1909
+ return await runAgentDefinition(runnableAgent, instruction, ctx, session, renderEvent, {
1910
+ cwd: execContext.cwd,
1911
+ execContext,
1912
+ });
1913
+ } catch (err) {
1914
+ process.stderr.write(` ${c.red('✗')} ${err.message || String(err)}\n`);
1915
+ return;
1916
+ }
1917
+ }
1918
+
1919
+ try {
1920
+ process.stderr.write(` ${c.dim(`Running workflow '${target}'...`)}\n`);
1921
+ const result = await ctx.toolExecutor.execute('workflow_run_multi', {
1922
+ name: target,
1923
+ instruction,
1924
+ });
1925
+
1926
+ if (result?.success === false) {
1927
+ process.stderr.write(` ${c.red('✗')} ${result.output || `Workflow '${target}' failed.`}\n`);
1928
+ return;
1929
+ }
1930
+
1931
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`Workflow '${target}' complete`)}\n`);
1932
+ const details = [];
1933
+ if (result?.run_id) details.push(`run ${result.run_id}`);
1934
+ if (result?.duration_s) details.push(`${result.duration_s}s`);
1935
+ if (result?.total_tokens) details.push(`${formatTokens(result.total_tokens)} tok`);
1936
+ if (result?.total_cost) details.push(formatCostValue(result.total_cost));
1937
+ if (details.length) process.stderr.write(` ${c.dim(details.join(' · '))}\n`);
1938
+
1939
+ const output = result?.result || result?.output || '';
1940
+ if (output) {
1941
+ process.stderr.write('\n');
1942
+ process.stderr.write(renderMarkdown(String(output), { width: process.stderr.columns || 96 }));
1943
+ process.stderr.write('\n');
1944
+ }
1945
+ } catch (err) {
1946
+ process.stderr.write(` ${c.red('✗')} ${err.message || String(err)}\n`);
1947
+ process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
1948
+ }
1949
+ }
1950
+
1652
1951
  async function handleCommand(input, ctx) {
1653
1952
  const { cmd, rest, aliasTarget } = normalizeCommandInput(input);
1654
1953
  if (aliasTarget) {
@@ -1686,6 +1985,10 @@ async function handleCommand(input, ctx) {
1686
1985
  handleTasksCommand(rest, ctx);
1687
1986
  return;
1688
1987
 
1988
+ case '/run':
1989
+ await handleRunCommand(rest, ctx);
1990
+ return;
1991
+
1689
1992
  case '/attach':
1690
1993
  handleAttachCommand(rest, ctx);
1691
1994
  return;
@@ -2182,6 +2485,7 @@ async function handleCommand(input, ctx) {
2182
2485
  session.agentHistory.length = 0;
2183
2486
  session.toolCalls = 0;
2184
2487
  session.subAgentToolCalls = 0;
2488
+ runtime.foldedSubAgentTools = null;
2185
2489
  clearCards();
2186
2490
  process.stderr.write(` ${c.gray('Conversation cleared.')}\n`);
2187
2491
  return;
@@ -2496,11 +2800,60 @@ export async function startTerminalRepl() {
2496
2800
 
2497
2801
  const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
2498
2802
 
2803
+ let startupOutputRow = 1;
2804
+ let startupOutputCol = 1;
2805
+
2806
+ function trackStartupOutput(chunk) {
2807
+ if (!process.stderr.isTTY || term().plain) return;
2808
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
2809
+ if (!text) return;
2810
+ const clean = text
2811
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
2812
+ .replace(/\x1b[()][A-Za-z0-9]/g, '');
2813
+ const width = Math.max(1, process.stderr.columns || process.stdout.columns || 80);
2814
+ for (const ch of clean) {
2815
+ if (ch === '\r') {
2816
+ startupOutputCol = 1;
2817
+ continue;
2818
+ }
2819
+ if (ch === '\n') {
2820
+ startupOutputRow++;
2821
+ startupOutputCol = 1;
2822
+ continue;
2823
+ }
2824
+ startupOutputCol++;
2825
+ if (startupOutputCol > width) {
2826
+ startupOutputRow++;
2827
+ startupOutputCol = 1;
2828
+ }
2829
+ }
2830
+ }
2831
+
2832
+ function startStartupOutputTracking() {
2833
+ if (!process.stderr.isTTY || term().plain) return () => {};
2834
+ const originalWrite = process.stderr.write;
2835
+ function trackedStartupWrite(chunk, ...args) {
2836
+ trackStartupOutput(chunk);
2837
+ return originalWrite.call(this, chunk, ...args);
2838
+ }
2839
+ process.stderr.write = trackedStartupWrite;
2840
+ return () => {
2841
+ if (process.stderr.write === trackedStartupWrite) {
2842
+ process.stderr.write = originalWrite;
2843
+ }
2844
+ };
2845
+ }
2846
+
2847
+ function startupCursorSeed() {
2848
+ return { row: startupOutputRow, col: startupOutputCol };
2849
+ }
2850
+
2499
2851
  async function startNewSession({ announce = true } = {}) {
2500
2852
  stopSpinner();
2501
2853
  flushContent();
2502
2854
  flushPendingHead();
2503
2855
  flushCompactReadRun();
2856
+ runtime.foldedSubAgentTools = null;
2504
2857
  clearCards();
2505
2858
 
2506
2859
  const preserved = {
@@ -2814,57 +3167,67 @@ export async function startTerminalRepl() {
2814
3167
  // ── Print banner + preflight + init BEFORE mounting the status bar ──
2815
3168
  // The status bar shrinks the scroll region; if it mounts first, the
2816
3169
  // banner scrolls off-screen before the user ever sees it.
2817
- printBanner(auth);
2818
-
2819
- // Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
2820
- // KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
2821
- if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.freeswim) {
2822
- try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
2823
- catch { /* preflight is best-effort */ }
2824
- }
2825
-
2826
- // ── Initialization ──
2827
- process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
2828
- await fetchUser(ctx);
2829
-
2830
- // Clear the spinner line
2831
- process.stderr.write(`\r${' '.repeat(60)}\r`);
2832
- process.stderr.write(` ${c.green('✓')} ${c.dim('Ready; projects will be indexed on demand')}\n`);
2833
- if (session.user) {
2834
- process.stderr.write(` ${c.green('')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
2835
- }
2836
- // ── Resume previous session ──
2837
- if (cliArgs.resume) {
2838
- const lastSession = cliArgs.resumeSessionId
2839
- ? { sessionId: cliArgs.resumeSessionId }
2840
- : sessionMgr.getLastSession();
2841
-
2842
- if (lastSession) {
2843
- const resumed = await activateResumedSession(lastSession.sessionId, 'startup');
2844
- if (resumed.ok) {
2845
- process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed session: ${messageCountLabel(resumed.messages)}`)}`);
2846
- process.stderr.write(` ${c.dim('· project')} ${c.brand(path.basename(safeCwd()))}`);
2847
- process.stderr.write(` ${c.dim(`· agent ${resumed.historyMode}`)}`);
2848
- if (resumed.switchedProject) process.stderr.write(` ${c.dim('(cwd restored)')}`);
2849
- if (resumed.projectMissing) process.stderr.write(` ${c.yellow('(saved project path unavailable; using current cwd)')}`);
2850
- if (resumed.instruction) process.stderr.write(` ${c.dim('—')} ${c.dim(resumed.instruction.slice(0, 50))}`);
2851
- process.stderr.write('\n');
2852
- renderResumePreview(resumed, { renderEvent });
3170
+ const stopStartupOutputTracking = startStartupOutputTracking();
3171
+ let dockCursor = startupCursorSeed();
3172
+ try {
3173
+ printBanner(auth);
3174
+
3175
+ // Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
3176
+ // KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
3177
+ if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.freeswim) {
3178
+ try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
3179
+ catch { /* preflight is best-effort */ }
3180
+ }
3181
+
3182
+ // ── Initialization ──
3183
+ process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
3184
+ await fetchUser(ctx);
3185
+
3186
+ // Clear the spinner line
3187
+ process.stderr.write(`\r${' '.repeat(60)}\r`);
3188
+ process.stderr.write(` ${c.green('✓')} ${c.dim('Ready; projects will be indexed on demand')}\n`);
3189
+ if (session.user) {
3190
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
3191
+ }
3192
+ // ── Resume previous session ──
3193
+ if (cliArgs.resume) {
3194
+ const lastSession = cliArgs.resumeSessionId
3195
+ ? { sessionId: cliArgs.resumeSessionId }
3196
+ : sessionMgr.getLastSession();
3197
+
3198
+ if (lastSession) {
3199
+ const resumed = await activateResumedSession(lastSession.sessionId, 'startup');
3200
+ if (resumed.ok) {
3201
+ process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed session: ${messageCountLabel(resumed.messages)}`)}`);
3202
+ process.stderr.write(` ${c.dim('· project')} ${c.brand(path.basename(safeCwd()))}`);
3203
+ process.stderr.write(` ${c.dim( agent ${resumed.historyMode}`)}`);
3204
+ if (resumed.switchedProject) process.stderr.write(` ${c.dim('(cwd restored)')}`);
3205
+ if (resumed.projectMissing) process.stderr.write(` ${c.yellow('(saved project path unavailable; using current cwd)')}`);
3206
+ if (resumed.instruction) process.stderr.write(` ${c.dim('—')} ${c.dim(resumed.instruction.slice(0, 50))}`);
3207
+ process.stderr.write('\n');
3208
+ renderResumePreview(resumed, { renderEvent });
3209
+ } else {
3210
+ process.stderr.write(` ${c.yellow('!')} ${c.dim(resumed.reason || 'No conversation found for session ' + lastSession.sessionId)}\n`);
3211
+ }
2853
3212
  } else {
2854
- process.stderr.write(` ${c.yellow('!')} ${c.dim(resumed.reason || 'No conversation found for session ' + lastSession.sessionId)}\n`);
3213
+ process.stderr.write(` ${c.yellow('!')} ${c.dim('No previous session to resume')}\n`);
2855
3214
  }
2856
- } else {
2857
- process.stderr.write(` ${c.yellow('!')} ${c.dim('No previous session to resume')}\n`);
2858
3215
  }
2859
- }
2860
3216
 
2861
- process.stderr.write(`\n ${c.dim('Press')} ${c.brand('Enter')} ${c.dim('to start, or type a prompt below.')}\n`);
3217
+ process.stderr.write(`\n ${c.dim('Press')} ${c.brand('Enter')} ${c.dim('to start, or type a prompt below.')}\n`);
3218
+ } finally {
3219
+ dockCursor = startupCursorSeed();
3220
+ stopStartupOutputTracking();
3221
+ }
2862
3222
 
2863
3223
  // Keep one bottom-reserved UI surface: the fixed input dock. The older
2864
3224
  // status bar used the same terminal scroll-region primitive, so mounting
2865
3225
  // both would make prompt placement unpredictable.
2866
3226
  orbitRef.current = createOrbit();
2867
- const inputDockActive = mountInputDock();
3227
+ const inputDockActive = mountInputDock({
3228
+ initialContentRow: dockCursor.row,
3229
+ initialContentCol: dockCursor.col,
3230
+ });
2868
3231
  if (inputDockActive) {
2869
3232
  process.on('beforeExit', unmountInputDock);
2870
3233
  process.on('exit', unmountInputDock);
@@ -2877,11 +3240,17 @@ export async function startTerminalRepl() {
2877
3240
  // terminals that ignore the request.
2878
3241
  const PASTE_BEGIN = '\x1b[200~';
2879
3242
  const PASTE_END = '\x1b[201~';
3243
+ const F2_SEQUENCES = new Set(['\x1bOQ', '\x1b[12~']);
2880
3244
  let _inBracketedPaste = false;
2881
3245
  let _bracketedPasteBuffer = '';
3246
+ let _suppressBracketedPasteLines = false;
3247
+ let _bracketedPasteStartLine = '';
3248
+ let _bracketedPasteStartCursor = 0;
3249
+ let _promptHasInsertedPaste = false;
2882
3250
  const _pasteEndListeners = new Set();
2883
3251
  function onBracketedPasteEnd(cb) { _pasteEndListeners.add(cb); return () => _pasteEndListeners.delete(cb); }
2884
3252
  function isInBracketedPaste() { return _inBracketedPaste; }
3253
+ function isF2Sequence(text) { return F2_SEQUENCES.has(String(text || '')); }
2885
3254
 
2886
3255
  if (process.stdin.isTTY) {
2887
3256
  try { process.stderr.write('\x1b[?2004h'); } catch {}
@@ -2900,6 +3269,11 @@ export async function startTerminalRepl() {
2900
3269
  if (start === -1) return;
2901
3270
  _inBracketedPaste = true;
2902
3271
  _bracketedPasteBuffer = '';
3272
+ _suppressBracketedPasteLines = true;
3273
+ _bracketedPasteStartLine = String(rl?.line || '');
3274
+ _bracketedPasteStartCursor = typeof rl?.cursor === 'number'
3275
+ ? rl.cursor
3276
+ : _bracketedPasteStartLine.length;
2903
3277
  i = start + PASTE_BEGIN.length;
2904
3278
  } else {
2905
3279
  const end = s.indexOf(PASTE_END, i);
@@ -2914,7 +3288,10 @@ export async function startTerminalRepl() {
2914
3288
  // Notify subscribers on next tick so readline finishes emitting its
2915
3289
  // synchronous `line` events for the buffered content first.
2916
3290
  const cbs = [..._pasteEndListeners];
2917
- setImmediate(() => { for (const cb of cbs) { try { cb(payload); } catch {} } });
3291
+ setImmediate(() => {
3292
+ for (const cb of cbs) { try { cb(payload); } catch {} }
3293
+ _suppressBracketedPasteLines = false;
3294
+ });
2918
3295
  i = end + PASTE_END.length;
2919
3296
  }
2920
3297
  }
@@ -2937,8 +3314,10 @@ export async function startTerminalRepl() {
2937
3314
 
2938
3315
  function printInputBottomRule() {
2939
3316
  if (isInputDockMounted()) {
3317
+ clearPinnedStatus();
2940
3318
  clearInputPrompt();
2941
3319
  moveToContent();
3320
+ if (process.stderr.isTTY && !term().plain) process.stderr.write('\r\x1b[2K');
2942
3321
  return;
2943
3322
  }
2944
3323
  if (term().plain) return;
@@ -2946,11 +3325,11 @@ export async function startTerminalRepl() {
2946
3325
  }
2947
3326
 
2948
3327
  function idleInputTips() {
2949
- return '[Enter] send [/] commands [Tab] complete [Ctrl+D] details';
3328
+ return '[Enter] send [/] commands [Tab] complete [F2] details';
2950
3329
  }
2951
3330
 
2952
3331
  function executionInputTips() {
2953
- return 'type any extra context (paths, corrections, follow-ups) · [Enter] send · [Esc] cancel · [Ctrl+P] pause';
3332
+ return 'type extra context · [Enter] send · [Esc] cancel · [Ctrl+P] pause · [F2] details';
2954
3333
  }
2955
3334
 
2956
3335
  // Proxy stream: swallows writes when the dock owns the input row so
@@ -3088,10 +3467,10 @@ export async function startTerminalRepl() {
3088
3467
  slashHintLine = '';
3089
3468
  }
3090
3469
 
3091
- function replaceReadlineLine(value) {
3470
+ function replaceReadlineLine(value, cursor = null) {
3092
3471
  const next = String(value || '');
3093
3472
  rl.line = next;
3094
- rl.cursor = next.length;
3473
+ rl.cursor = cursor == null ? next.length : Math.max(0, Math.min(next.length, Number(cursor) || 0));
3095
3474
  if (typeof rl._refreshLine === 'function') {
3096
3475
  rl._refreshLine();
3097
3476
  } else {
@@ -3101,6 +3480,17 @@ export async function startTerminalRepl() {
3101
3480
  }
3102
3481
  }
3103
3482
 
3483
+ function insertPromptText(text, { baseLine = rl.line || '', baseCursor = rl.cursor, fromPaste = false } = {}) {
3484
+ const payload = String(text || '');
3485
+ if (!payload) return;
3486
+ const line = String(baseLine || '');
3487
+ const cursor = typeof baseCursor === 'number' ? Math.max(0, Math.min(line.length, baseCursor)) : line.length;
3488
+ const next = `${line.slice(0, cursor)}${payload}${line.slice(cursor)}`;
3489
+ if (fromPaste) _promptHasInsertedPaste = true;
3490
+ replaceReadlineLine(next, cursor + payload.length);
3491
+ renderIdleDockInput();
3492
+ }
3493
+
3104
3494
  function acceptSlashHint() {
3105
3495
  const item = slashHintItems[slashHintSelected];
3106
3496
  if (!item) return false;
@@ -3190,8 +3580,18 @@ export async function startTerminalRepl() {
3190
3580
  readline.emitKeypressEvents(process.stdin, rl);
3191
3581
  process.stdin.on('keypress', (_str, key = {}) => {
3192
3582
  if (!inputActive) return;
3583
+ if (_inBracketedPaste || _suppressBracketedPasteLines) return;
3584
+ if (key.name === 'return' || key.name === 'enter') return;
3585
+ if (key.name === 'f2') {
3586
+ clearSlashHint();
3587
+ if (isInputDockMounted()) moveToContent();
3588
+ expandLast();
3589
+ renderIdleDockInput();
3590
+ return;
3591
+ }
3193
3592
  setImmediate(() => {
3194
3593
  if (!inputActive) return;
3594
+ if (_inBracketedPaste || _suppressBracketedPasteLines) return;
3195
3595
  if (slashHintVisible && key.name === 'tab' && acceptSlashHint()) return;
3196
3596
  if (slashHintVisible && key.name === 'down' && moveSlashHintSelection(1)) return;
3197
3597
  if (slashHintVisible && key.name === 'up' && moveSlashHintSelection(-1)) return;
@@ -3247,8 +3647,18 @@ export async function startTerminalRepl() {
3247
3647
  _pasteFlushTimer = null;
3248
3648
  }
3249
3649
  if (!_pasteLines.length) return;
3250
- const line = _pasteLines.join('\n');
3650
+ const trailing = String(rl.line || '');
3651
+ const pastedLines = _pasteLines.slice();
3251
3652
  _pasteLines = [];
3653
+ if (pastedLines.length > 1 || trailing) {
3654
+ const text = [...pastedLines, trailing].join('\n');
3655
+ _promptHasInsertedPaste = true;
3656
+ replaceReadlineLine(text);
3657
+ renderIdleDockInput();
3658
+ return;
3659
+ }
3660
+ const line = pastedLines.join('\n');
3661
+ _promptHasInsertedPaste = false;
3252
3662
  queueOrRunLine(line);
3253
3663
  }
3254
3664
 
@@ -3258,8 +3668,29 @@ export async function startTerminalRepl() {
3258
3668
  // or the user pressed Enter normally), the debounce falls back to old
3259
3669
  // behavior — a single Enter flushes almost instantly.
3260
3670
  rl.on('line', async (line) => {
3671
+ if (_suppressBracketedPasteLines) {
3672
+ _pasteLines = [];
3673
+ if (_pasteFlushTimer) {
3674
+ clearTimeout(_pasteFlushTimer);
3675
+ _pasteFlushTimer = null;
3676
+ }
3677
+ return;
3678
+ }
3679
+ if (!inputActive) {
3680
+ _pasteLines = [];
3681
+ if (_pasteFlushTimer) {
3682
+ clearTimeout(_pasteFlushTimer);
3683
+ _pasteFlushTimer = null;
3684
+ }
3685
+ return;
3686
+ }
3687
+ const submitInsertedPaste = _promptHasInsertedPaste || String(line || '').includes('\n');
3261
3688
  _pasteLines.push(line);
3262
3689
  if (_pasteFlushTimer) clearTimeout(_pasteFlushTimer);
3690
+ if (submitInsertedPaste) {
3691
+ flushPastedLines();
3692
+ return;
3693
+ }
3263
3694
  if (isInBracketedPaste()) {
3264
3695
  _pasteFlushTimer = null;
3265
3696
  } else {
@@ -3267,10 +3698,19 @@ export async function startTerminalRepl() {
3267
3698
  }
3268
3699
  });
3269
3700
 
3270
- onBracketedPasteEnd(() => {
3271
- // Readline has finished emitting synchronous line events for the pasted
3272
- // content by the time this fires (setImmediate in the pre-listener).
3273
- flushPastedLines();
3701
+ onBracketedPasteEnd((payload) => {
3702
+ _pasteLines = [];
3703
+ if (_pasteFlushTimer) {
3704
+ clearTimeout(_pasteFlushTimer);
3705
+ _pasteFlushTimer = null;
3706
+ }
3707
+ // Readline has finished emitting synchronous `line` events by now.
3708
+ // Treat paste as editing the prompt buffer; Enter remains the submit.
3709
+ insertPromptText(payload || '', {
3710
+ baseLine: _bracketedPasteStartLine,
3711
+ baseCursor: _bracketedPasteStartCursor,
3712
+ fromPaste: true,
3713
+ });
3274
3714
  });
3275
3715
 
3276
3716
  async function _handleLine(line) {
@@ -3485,6 +3925,21 @@ export async function startTerminalRepl() {
3485
3925
  }
3486
3926
  runtime.afterContentFlush = focusExecutionInput;
3487
3927
 
3928
+ function printExecutionInstruction(instruction) {
3929
+ if (isInputDockMounted()) {
3930
+ clearInputPrompt();
3931
+ moveToContent();
3932
+ } else if (executionInputVisible) {
3933
+ process.stderr.write('\n');
3934
+ }
3935
+ renderBlockBoundary('user', { compactSame: true });
3936
+ process.stderr.write(`${transcriptHeader('you', { tone: 'user' })} ${paint.text.dim('follow-up')}\n`);
3937
+ for (const line of String(instruction || '').split('\n')) {
3938
+ process.stderr.write(`${transcriptLine(line, { tone: 'user' })}\n`);
3939
+ }
3940
+ runtime.lastRenderedBlock = 'user';
3941
+ }
3942
+
3488
3943
  async function submitExecutionInstruction() {
3489
3944
  const instruction = executionInputBuffer.trim();
3490
3945
  executionInputBuffer = '';
@@ -3503,18 +3958,14 @@ export async function startTerminalRepl() {
3503
3958
  executionInputVisible = false;
3504
3959
  return;
3505
3960
  }
3961
+ printExecutionInstruction(instruction);
3506
3962
  if (isInputDockMounted()) {
3507
- clearInputPrompt();
3508
- moveToContent();
3509
- process.stderr.write(`${executionInputPrefix()}${instruction}\n`);
3510
3963
  renderDockInput(executionInputPrefix(), '', {
3511
3964
  context: buildContextStrip(),
3512
3965
  meta: buildDockMeta(),
3513
3966
  tips: executionInputTips(),
3514
3967
  });
3515
3968
  moveToContent();
3516
- } else if (executionInputVisible) {
3517
- process.stderr.write('\n');
3518
3969
  }
3519
3970
  executionInputVisible = false;
3520
3971
  // Live steering (PRD-081 §5.2): submit through the dedicated
@@ -3540,18 +3991,26 @@ export async function startTerminalRepl() {
3540
3991
  });
3541
3992
 
3542
3993
  if (status === 'accepted') {
3994
+ renderBlockBoundary('status', { compactSame: true });
3543
3995
  process.stderr.write(` ${c.green('↳')} ${c.dim('sent to running agent')}\n`);
3996
+ runtime.lastRenderedBlock = 'status';
3544
3997
  } else if (status === 'duplicate') {
3998
+ renderBlockBoundary('status', { compactSame: true });
3545
3999
  process.stderr.write(` ${c.dim('↳ already sent (idempotent)')}\n`);
4000
+ runtime.lastRenderedBlock = 'status';
3546
4001
  } else if (status === 'queued_next_turn') {
3547
4002
  _queuedLines.push(instruction);
4003
+ renderBlockBoundary('status', { compactSame: true });
3548
4004
  process.stderr.write(` ${c.yellow('↳')} ${c.dim('task ended — queued for next turn')}\n`);
4005
+ runtime.lastRenderedBlock = 'status';
3549
4006
  } else {
3550
4007
  // no_task, error, or unknown — fall back to next-turn queue so the
3551
4008
  // user's text is never silently lost.
3552
4009
  _queuedLines.push(instruction);
3553
4010
  const errBits = result && result.error ? ` ${c.dim(`(${String(result.error).slice(0, 80)})`)}` : '';
4011
+ renderBlockBoundary('status', { compactSame: true });
3554
4012
  process.stderr.write(` ${c.yellow('↳')} ${c.dim('queued for next turn')}${errBits}\n`);
4013
+ runtime.lastRenderedBlock = 'status';
3555
4014
  }
3556
4015
  }
3557
4016
 
@@ -3637,6 +4096,14 @@ export async function startTerminalRepl() {
3637
4096
  const bytes2 = [...data];
3638
4097
  const text2 = data.toString('utf8');
3639
4098
 
4099
+ if (isF2Sequence(text2)) {
4100
+ stopSpinner();
4101
+ if (isInputDockMounted()) moveToContent();
4102
+ expandLast();
4103
+ if (isInputDockMounted()) redrawExecutionInput();
4104
+ return;
4105
+ }
4106
+
3640
4107
  // Esc key (single byte 0x1b, not part of arrow sequence)
3641
4108
  if (bytes2.length === 1 && bytes2[0] === 0x1b) {
3642
4109
  if (executionInputVisible || executionInputBuffer) {
@@ -3729,15 +4196,6 @@ export async function startTerminalRepl() {
3729
4196
  return;
3730
4197
  }
3731
4198
 
3732
- // Ctrl+D — expand last tool card (Mission Control §6.2). Only when
3733
- // there's no in-progress follow-up input.
3734
- if (!executionInputBuffer && bytes2.length === 1 && bytes2[0] === 0x04) {
3735
- stopSpinner();
3736
- if (isInputDockMounted()) moveToContent();
3737
- expandLast();
3738
- return;
3739
- }
3740
-
3741
4199
  // Any safe text (unicode, tabs, spaces, symbols) becomes a live
3742
4200
  // follow-up instruction. Enter sends it via resume(instruction).
3743
4201
  if (isSafeFollowUpText(text2)) {
@@ -3752,6 +4210,15 @@ export async function startTerminalRepl() {
3752
4210
  approval.setExecutionHooks({
3753
4211
  onPause: () => { execListenerActive = false; },
3754
4212
  onResume: () => { execListenerActive = true; },
4213
+ onApprovalPromptEnd: () => {
4214
+ if (!isInputDockMounted()) return;
4215
+ renderDockInput(executionInputPrefix(), executionInputBuffer, {
4216
+ context: buildContextStrip(),
4217
+ meta: buildDockMeta(),
4218
+ tips: executionInputTips(),
4219
+ });
4220
+ moveToContent();
4221
+ },
3755
4222
  });
3756
4223
 
3757
4224
  keypressCleanup = () => {
@@ -3773,8 +4240,6 @@ export async function startTerminalRepl() {
3773
4240
  moveToContent();
3774
4241
  }
3775
4242
  startContentStream();
3776
- process.stderr.write(`\n${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
3777
- runtime.contentHeaderPrinted = true;
3778
4243
 
3779
4244
  // Immediate feedback so the screen isn't blank between submit and the
3780
4245
  // first backend event. The first `status`, `thinking`, or `content_*`