@bahulam/code 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +9 -4
  21. package/src/core/tool-executor.mjs +105 -29
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +231 -53
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/registry.mjs +4 -0
  51. package/src/ui/icons.mjs +2 -0
  52. package/src/ui/palette.mjs +1 -1
  53. package/src/ui/slash-commands.mjs +4 -6
  54. package/src/ui/tool-card.mjs +7 -3
  55. package/pulse/app/activity/page.tsx +0 -190
  56. package/pulse/app/api/activity/route.ts +0 -138
  57. package/pulse/app/api/benchmark/route.ts +0 -113
  58. package/pulse/app/api/benchmarks/route.ts +0 -195
  59. package/pulse/app/api/costs/route.ts +0 -88
  60. package/pulse/app/api/export/route.ts +0 -77
  61. package/pulse/app/api/history/route.ts +0 -11
  62. package/pulse/app/api/import/route.ts +0 -31
  63. package/pulse/app/api/memory/route.ts +0 -50
  64. package/pulse/app/api/plans/route.ts +0 -9
  65. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  66. package/pulse/app/api/projects/route.ts +0 -121
  67. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  68. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  69. package/pulse/app/api/sessions/route.ts +0 -112
  70. package/pulse/app/api/settings/route.ts +0 -14
  71. package/pulse/app/api/stats/route.ts +0 -143
  72. package/pulse/app/api/todos/route.ts +0 -9
  73. package/pulse/app/api/tools/route.ts +0 -160
  74. package/pulse/app/benchmarks/page.tsx +0 -224
  75. package/pulse/app/costs/page.tsx +0 -179
  76. package/pulse/app/export/page.tsx +0 -465
  77. package/pulse/app/favicon.ico +0 -0
  78. package/pulse/app/globals.css +0 -263
  79. package/pulse/app/help/page.tsx +0 -143
  80. package/pulse/app/history/page.tsx +0 -157
  81. package/pulse/app/layout.tsx +0 -46
  82. package/pulse/app/memory/page.tsx +0 -365
  83. package/pulse/app/overview-client.tsx +0 -393
  84. package/pulse/app/page.tsx +0 -14
  85. package/pulse/app/plans/page.tsx +0 -308
  86. package/pulse/app/projects/[slug]/page.tsx +0 -390
  87. package/pulse/app/projects/page.tsx +0 -110
  88. package/pulse/app/sessions/[id]/page.tsx +0 -243
  89. package/pulse/app/sessions/page.tsx +0 -39
  90. package/pulse/app/settings/page.tsx +0 -188
  91. package/pulse/app/todos/page.tsx +0 -211
  92. package/pulse/app/tools/page.tsx +0 -249
  93. package/pulse/cli.js +0 -164
  94. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  95. package/pulse/components/activity/streak-card.tsx +0 -36
  96. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  97. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  98. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  99. package/pulse/components/costs/model-token-table.tsx +0 -60
  100. package/pulse/components/global-search.tsx +0 -193
  101. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  102. package/pulse/components/layout/bottom-nav.tsx +0 -53
  103. package/pulse/components/layout/client-layout.tsx +0 -31
  104. package/pulse/components/layout/sidebar-context.tsx +0 -50
  105. package/pulse/components/layout/sidebar.tsx +0 -183
  106. package/pulse/components/layout/top-bar.tsx +0 -121
  107. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  108. package/pulse/components/overview/conversation-table.tsx +0 -148
  109. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  110. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  111. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  112. package/pulse/components/overview/stat-card.tsx +0 -102
  113. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  114. package/pulse/components/projects/project-card.tsx +0 -175
  115. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  116. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  117. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  118. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  119. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  120. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  121. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  122. package/pulse/components/sessions/session-badges.tsx +0 -49
  123. package/pulse/components/sessions/session-table.tsx +0 -299
  124. package/pulse/components/theme-provider.tsx +0 -44
  125. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  126. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  127. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  128. package/pulse/components/tools/version-history-table.tsx +0 -32
  129. package/pulse/components/ui/alert.tsx +0 -66
  130. package/pulse/components/ui/badge.tsx +0 -48
  131. package/pulse/components/ui/breadcrumb.tsx +0 -109
  132. package/pulse/components/ui/button.tsx +0 -64
  133. package/pulse/components/ui/calendar.tsx +0 -220
  134. package/pulse/components/ui/card.tsx +0 -92
  135. package/pulse/components/ui/command.tsx +0 -158
  136. package/pulse/components/ui/dialog.tsx +0 -158
  137. package/pulse/components/ui/input.tsx +0 -21
  138. package/pulse/components/ui/popover.tsx +0 -89
  139. package/pulse/components/ui/progress.tsx +0 -31
  140. package/pulse/components/ui/select.tsx +0 -190
  141. package/pulse/components/ui/separator.tsx +0 -28
  142. package/pulse/components/ui/sheet.tsx +0 -143
  143. package/pulse/components/ui/skeleton.tsx +0 -13
  144. package/pulse/components/ui/table.tsx +0 -116
  145. package/pulse/components/ui/tabs.tsx +0 -91
  146. package/pulse/components/ui/tooltip.tsx +0 -57
  147. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  148. package/pulse/components.json +0 -23
  149. package/pulse/eslint.config.mjs +0 -18
  150. package/pulse/lib/bahulam-paths.ts +0 -23
  151. package/pulse/lib/claude-reader.ts +0 -592
  152. package/pulse/lib/decode.ts +0 -129
  153. package/pulse/lib/pricing.ts +0 -102
  154. package/pulse/lib/replay-parser.ts +0 -165
  155. package/pulse/lib/tool-categories.ts +0 -140
  156. package/pulse/lib/utils.ts +0 -6
  157. package/pulse/next-env.d.ts +0 -6
  158. package/pulse/next.config.ts +0 -16
  159. package/pulse/package.json +0 -45
  160. package/pulse/postcss.config.mjs +0 -7
  161. package/pulse/public/activity.png +0 -0
  162. package/pulse/public/cc-lens.png +0 -0
  163. package/pulse/public/command-k.png +0 -0
  164. package/pulse/public/costs.png +0 -0
  165. package/pulse/public/dashboard-dark.png +0 -0
  166. package/pulse/public/dashboard-white.png +0 -0
  167. package/pulse/public/export.png +0 -0
  168. package/pulse/public/file.svg +0 -1
  169. package/pulse/public/globe.svg +0 -1
  170. package/pulse/public/next.svg +0 -1
  171. package/pulse/public/projects.png +0 -0
  172. package/pulse/public/session-chat.png +0 -0
  173. package/pulse/public/todos.png +0 -0
  174. package/pulse/public/tools.png +0 -0
  175. package/pulse/public/vercel.svg +0 -1
  176. package/pulse/public/window.svg +0 -1
  177. package/pulse/tsconfig.json +0 -34
  178. package/pulse/types/claude.ts +0 -294
@@ -21,6 +21,7 @@ import { paint } from '../ui/palette.mjs';
21
21
  import { runtime, session } from './repl-state.mjs';
22
22
  import { fitAnsiLine } from './repl-format.mjs';
23
23
  import { exploreCategory, isExploreTool } from './repl-explore.mjs';
24
+ import { watchState } from './watch-state.mjs';
24
25
  import {
25
26
  clearPinnedStatus,
26
27
  drawPinnedStatus,
@@ -36,21 +37,50 @@ import * as queue from '../ui/render-queue.mjs';
36
37
  // Max inner-tool lines shown under the spinner during a sub-agent run.
37
38
  const SUB_AGENT_WINDOW_ROWS = 7;
38
39
 
40
+ function statusWidth() {
41
+ return Math.max(8, (process.stderr.columns || process.stdout.columns || 120) - 1);
42
+ }
43
+
44
+ function fitStatusLine(line) {
45
+ return fitAnsiLine(String(line ?? '').replace(/[\r\n]+/g, ' '), statusWidth());
46
+ }
47
+
48
+ function fitStatusLines(lines) {
49
+ return (Array.isArray(lines) ? lines : [lines]).map(fitStatusLine);
50
+ }
51
+
39
52
  function presentStatus(rendered) {
53
+ // Watch panel override: when active, render the watch panel entries as a
54
+ // multi-line status block instead of the normal spinner/status line.
55
+ if (watchState.active) {
56
+ const lines = watchState.visible();
57
+ if (lines.length) {
58
+ const fitted = fitStatusLines(lines);
59
+ if (queue.isActive()) {
60
+ queue.statusBlock(fitted);
61
+ } else if (isInputDockMounted()) {
62
+ drawPinnedStatus(fitted.join('\n'));
63
+ } else {
64
+ inPlace(fitted.join('\n'));
65
+ }
66
+ return;
67
+ }
68
+ }
69
+ const fitted = fitStatusLine(rendered);
40
70
  if (queue.isActive()) {
41
71
  const win = runtime.subAgentWindow;
42
72
  if (win?.active && win.lines.length) {
43
73
  queue.statusBlock([
44
- rendered,
45
- ...win.lines.slice(-SUB_AGENT_WINDOW_ROWS).map(l => ` ${c.dim(l)}`),
74
+ fitted,
75
+ ...fitStatusLines(win.lines.slice(-SUB_AGENT_WINDOW_ROWS).map(l => ` ${c.dim(l)}`)),
46
76
  ]);
47
77
  return;
48
78
  }
49
- queue.status(rendered);
79
+ queue.status(fitted);
50
80
  return;
51
81
  }
52
- if (isInputDockMounted()) { drawPinnedStatus(rendered); return; }
53
- inPlace(rendered);
82
+ if (isInputDockMounted()) { drawPinnedStatus(fitted); return; }
83
+ inPlace(fitted);
54
84
  }
55
85
 
56
86
  /** Push a line into the live sub-agent tool window (dedup consecutive). */
@@ -62,6 +62,10 @@ export const runtime = {
62
62
  // N lines under the spinner) instead of appending to the transcript.
63
63
  // Full detail stays on the recorded cards (/expand, /last, `d`).
64
64
  subAgentWindow: { active: false, lines: [] },
65
+
66
+ // PRD-092: Watch panel — toggled by /watch. When active, the spinner
67
+ // area renders a compact agent-activity summary instead of the spinner.
68
+ watchPanelActive: false,
65
69
  };
66
70
 
67
71
  // Full session state for the current CLI process. Set by the REPL loop
@@ -73,6 +77,13 @@ export const session = {
73
77
  startTime: Date.now(),
74
78
  inputTokens: 0,
75
79
  outputTokens: 0,
80
+ // Per-turn context indicator for the dock strip. Updated at each
81
+ // AGENT_COMPLETE from the backend's usage.total_input_tokens (the
82
+ // cumulative input the primary agent sent this turn across all its
83
+ // internal inferences). Rough proxy for "how big is the prompt right
84
+ // now" — approximates what the NEXT user turn will pay before any
85
+ // growth. Reset by /new.
86
+ lastTurnInputTokens: 0,
76
87
  toolCalls: 0, // primary-agent tool calls in the current turn
77
88
  subAgentToolCalls: 0,// forwarded internal sub-agent tool calls in the current turn
78
89
  totalToolCalls: 0, // across all turns
@@ -85,6 +96,7 @@ export const session = {
85
96
  user: null, // { github_username, email, role }
86
97
  model: null, // from backend user profile
87
98
  modelLimits: {}, // role -> {model, context_length, max_output, source}
99
+ subAgentModels: {}, // role -> model from backend session_info
88
100
  blockedOps: 0, // safety guardrail blocks
89
101
  delegations: [], // agent delegation events: { from, to, time }
90
102
  phases: [], // phase history: { name, time }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Kepler REPL — Full Claude-like terminal UX.
2
+ * Bahulam REPL — Full Claude-like terminal UX.
3
3
  *
4
4
  * Pure ANSI. No React. No Ink. No flickering.
5
5
  *
@@ -67,6 +67,15 @@ import { createAgentFile, isVsCodeTerminal, listLocalAgents, openAgentFile, sync
67
67
  import { SessionManager } from '../core/session-manager.mjs';
68
68
  import { parseArgs } from '../config/cli-args.mjs';
69
69
  import { pickModelOverridesForm } from './repl-model-form.mjs';
70
+ import {
71
+ MODEL_CATEGORY_ORDER,
72
+ formatCategoryBadge,
73
+ formatCategoryHeading,
74
+ modelCategory,
75
+ modelDisplayCategory,
76
+ modelMatchesCatalogFilter,
77
+ normalizeCatalogCategory,
78
+ } from './model-catalog-display.mjs';
70
79
  import { loadEffectivePolicy, formatPolicySourceRows } from '../core/policy-resolver.mjs';
71
80
  import { loadProjectContext } from '../core/project-context-loader.mjs';
72
81
  import { buildContextEnvelope } from '../core/context-envelope.mjs';
@@ -92,6 +101,7 @@ import { exploreCategory, exploreCollapseEnabled, isExploreTool } from './repl-e
92
101
  import { session, orbitRef, sessionMgrRef, runtime } from './repl-state.mjs';
93
102
  import { safeCwd } from './repl-utils.mjs';
94
103
  import * as rqueue from '../ui/render-queue.mjs';
104
+ import { watchState } from './watch-state.mjs';
95
105
  import {
96
106
  appendContent,
97
107
  bumpSpinnerProgress,
@@ -291,14 +301,15 @@ function renderKeyboardHelp() {
291
301
 
292
302
  const MODEL_ROLE_ALIASES = new Map([
293
303
  ['reasoning', 'reasoning'],
294
- ['main', 'reasoning'],
295
304
  ['coder', 'reasoning'],
296
305
  ['coding', 'reasoning'],
297
306
  ['smart', 'reasoning'],
298
307
  ['fast', 'fast'],
299
308
  ['explorer', 'fast'],
300
- ['orchestrator', 'orchestrator'],
301
- ['planner', 'orchestrator'],
309
+ ['main', 'reasoning'],
310
+ ['orchestrator', 'plan'],
311
+ ['planner', 'plan'],
312
+ ['planning', 'plan'],
302
313
  ['local', 'local'],
303
314
  ['worker', 'worker'],
304
315
  ['explore', 'explore'],
@@ -306,25 +317,52 @@ const MODEL_ROLE_ALIASES = new Map([
306
317
  ['verify', 'verify'],
307
318
  ['debug', 'debug'],
308
319
  ['refactor', 'refactor'],
320
+ ['vision', 'image_analysis'],
321
+ ['image', 'image_analysis'],
322
+ ['image-analysis', 'image_analysis'],
323
+ ['image_analysis', 'image_analysis'],
324
+ ['analyze-image', 'image_analysis'],
325
+ ['analyze_image', 'image_analysis'],
326
+ ['image-generation', 'image_generation'],
327
+ ['image_generation', 'image_generation'],
328
+ ['image-gen', 'image_generation'],
329
+ ['generate-image', 'image_generation'],
330
+ ['generate_image', 'image_generation'],
309
331
  ]);
310
332
 
311
333
  const MODEL_ROLE_LABELS = {
312
334
  reasoning: 'coding',
313
335
  fast: 'fast',
314
- orchestrator: 'orchestrator',
336
+ orchestrator: 'planning',
315
337
  local: 'local',
316
338
  worker: 'worker',
317
339
  explore: 'explore',
318
- plan: 'plan',
340
+ plan: 'planning',
319
341
  verify: 'verify',
320
342
  debug: 'debug',
321
343
  refactor: 'refactor',
344
+ image_analysis: 'image analysis',
345
+ image_generation: 'image generation',
346
+ };
347
+
348
+ const MODEL_ROLE_DESCRIPTIONS = {
349
+ reasoning: 'coding model',
350
+ fast: 'fast low-cost work',
351
+ orchestrator: 'planning model (legacy role name)',
352
+ local: 'local fallback role',
353
+ worker: 'background worker role',
354
+ explore: 'read/search sub-agent',
355
+ plan: 'planning sub-agent',
356
+ verify: 'verification sub-agent',
357
+ debug: 'debugging sub-agent',
358
+ refactor: 'refactor sub-agent',
359
+ image_analysis: 'inspect screenshots and uploads',
360
+ image_generation: 'create visual assets',
322
361
  };
323
362
 
324
363
  const MODEL_ROLE_ORDER = [
325
364
  'reasoning',
326
365
  'fast',
327
- 'orchestrator',
328
366
  'local',
329
367
  'worker',
330
368
  'explore',
@@ -332,6 +370,8 @@ const MODEL_ROLE_ORDER = [
332
370
  'verify',
333
371
  'debug',
334
372
  'refactor',
373
+ 'image_analysis',
374
+ 'image_generation',
335
375
  ];
336
376
 
337
377
  function normalizeModelRole(value) {
@@ -349,7 +389,8 @@ function printModelCommandUsage() {
349
389
  process.stderr.write(` /model <model> set coding model directly\n`);
350
390
  process.stderr.write(` /model <role> <model>\n`);
351
391
  process.stderr.write(` /model ${NAMED_MODEL_MODES_LIST.join('|')}\n`);
352
- process.stderr.write(` /model list · status · refresh · clear [role]\n`);
392
+ process.stderr.write(` /model list [text|image|image-analysis|image-generation|multimodal]\n`);
393
+ process.stderr.write(` /model status · refresh · clear [role]\n`);
353
394
  process.stderr.write(` ${c.gray('Roles:')} ${MODEL_ROLE_ORDER.map(role => MODEL_ROLE_LABELS[role]).join(', ')}\n`);
354
395
  }
355
396
 
@@ -461,6 +502,15 @@ function modelCreditBadge(model) {
461
502
  return `~${credits < 10 ? credits.toFixed(1) : String(Math.round(credits))} cr/M in`;
462
503
  }
463
504
 
505
+ function printModelCatalogRows(rows) {
506
+ for (const m of rows) {
507
+ const badge = modelCreditBadge(m);
508
+ const category = formatCategoryBadge(modelDisplayCategory(m));
509
+ const price = badge ? c.dim(badge.padEnd(16)) : ''.padEnd(16);
510
+ process.stderr.write(` ${c.brand(String(m.id || '').padEnd(38))} ${price} ${category}\n`);
511
+ }
512
+ }
513
+
464
514
  async function warnIfNotCurated(model, ctx) {
465
515
  if (isByokModelRoute()) return;
466
516
  const catalog = await fetchModelCatalog(ctx);
@@ -470,28 +520,41 @@ async function warnIfNotCurated(model, ctx) {
470
520
  process.stderr.write(` ${c.yellow('!')} ${c.dim(`${model} is not in the platform catalog — the backend may reject it. See /model list.`)}\n`);
471
521
  } else if (row.harness_validated === false) {
472
522
  process.stderr.write(` ${c.yellow('!')} ${c.dim(`${model} is not harness-validated for the platform route — cost/quality untuned. See /model list.`)}\n`);
523
+ } else if (modelCategory(row) !== 'text') {
524
+ process.stderr.write(` ${c.yellow('!')} ${c.dim(`${model} is categorized as ${modelCategory(row)}; it is listed for media tooling, not coding-model overrides.`)}\n`);
473
525
  }
474
526
  }
475
527
 
476
- async function printModelCatalog(ctx) {
528
+ async function printModelCatalog(ctx, filterCategory = null) {
477
529
  const catalog = await fetchModelCatalog(ctx);
478
530
  if (!catalog) {
479
531
  process.stderr.write(` ${c.yellow('!')} ${c.dim(`Model catalog unavailable — ${_modelCatalogError || 'unknown error'}.`)}\n`);
480
532
  return;
481
533
  }
482
534
  const curated = catalog.filter(m => m?.harness_validated);
535
+ const filter = filterCategory ? String(filterCategory).trim().toLowerCase() : null;
536
+ const visible = filter
537
+ ? curated.filter(m => modelMatchesCatalogFilter(m, filter))
538
+ : curated;
539
+
483
540
  process.stderr.write(`\n ${c.bold('Platform catalog')} ${c.dim('(harness-validated, credit-priced)')}\n`);
484
541
  process.stderr.write(` ${c.gray('─'.repeat(64))}\n`);
485
- if (!curated.length) {
542
+ if (!visible.length) {
486
543
  process.stderr.write(` ${c.dim('(none published yet)')}\n`);
487
544
  }
488
- for (const m of curated) {
489
- const badge = modelCreditBadge(m);
490
- const tiers = Array.isArray(m.platform_access_tier) && m.platform_access_tier.length
491
- ? c.dim(` [${m.platform_access_tier.join(', ')}]`)
492
- : '';
493
- process.stderr.write(` ${c.brand(String(m.id || '').padEnd(38))} ${badge ? c.dim(badge.padEnd(16)) : ''.padEnd(16)}${tiers}\n`);
545
+
546
+ const categories = [...new Set(visible.map(modelDisplayCategory))].sort((a, b) => {
547
+ return (MODEL_CATEGORY_ORDER.indexOf(a) === -1 ? 999 : MODEL_CATEGORY_ORDER.indexOf(a)) -
548
+ (MODEL_CATEGORY_ORDER.indexOf(b) === -1 ? 999 : MODEL_CATEGORY_ORDER.indexOf(b)) ||
549
+ a.localeCompare(b);
550
+ });
551
+ for (const category of categories) {
552
+ const rows = visible.filter(m => modelDisplayCategory(m) === category);
553
+ if (!rows.length) continue;
554
+ process.stderr.write(`\n ${formatCategoryHeading(category, rows.length)}\n`);
555
+ printModelCatalogRows(rows);
494
556
  }
557
+
495
558
  const rest = catalog.length - curated.length;
496
559
  if (rest > 0) {
497
560
  process.stderr.write(` ${c.dim(`+${rest} more models available on the BYOK route (--route byok, own API key)`)}\n`);
@@ -552,10 +615,12 @@ function printModelStatus() {
552
615
  }
553
616
 
554
617
  const limits = session.modelLimits || {};
618
+ const subAgentModels = session.subAgentModels || {};
619
+ const planningModel = subAgentModels.plan || limits.planning?.model || limits.orchestrator?.model;
555
620
  const rows = [
556
- ['coder', limits.coder?.model],
557
- ['explorer', limits.explorer?.model],
558
- ['orchestrator', limits.orchestrator?.model],
621
+ ['coding', limits.coder?.model],
622
+ ['explore', subAgentModels.explore || limits.explorer?.model],
623
+ ['planning', planningModel],
559
624
  ].filter(([, model]) => model);
560
625
  if (rows.length) {
561
626
  process.stderr.write(`\n ${c.bold('Backend roles')}\n`);
@@ -577,20 +642,28 @@ function printModelStatus() {
577
642
  printModelCommandUsage();
578
643
  }
579
644
 
580
- const MODEL_FORM_ROLES = ['reasoning', 'fast', 'orchestrator', 'explore', 'plan'];
645
+ const MODEL_FORM_ROLES = ['reasoning', 'fast', 'explore', 'plan'];
646
+ const MODEL_MEDIA_FORM_ROLES = ['image_analysis', 'image_generation'];
581
647
 
582
648
  async function openModelForm(ctx) {
583
649
  const limits = session.modelLimits || {};
650
+ const subAgentModels = session.subAgentModels || {};
651
+ const planningModel = subAgentModels.plan || limits.planning?.model || limits.orchestrator?.model;
584
652
  const defaultsByRole = {
585
653
  reasoning: limits.coder?.model,
586
654
  fast: limits.explorer?.model,
587
- orchestrator: limits.orchestrator?.model,
588
- explore: limits.explorer?.model,
589
- plan: limits.orchestrator?.model,
655
+ explore: subAgentModels.explore || limits.explorer?.model,
656
+ plan: planningModel,
657
+ image_analysis: 'backend tier default',
658
+ image_generation: 'backend tier default',
590
659
  };
591
- const roles = MODEL_FORM_ROLES.map(role => ({
660
+ const roles = [...MODEL_FORM_ROLES, ...MODEL_MEDIA_FORM_ROLES].map(role => ({
592
661
  role,
593
662
  label: MODEL_ROLE_LABELS[role] || role,
663
+ description: MODEL_ROLE_DESCRIPTIONS[role] || '',
664
+ optionGroup: role === 'image_analysis'
665
+ ? 'image_analysis'
666
+ : (role === 'image_generation' ? 'image_generation' : 'text'),
594
667
  current: (session.modelOverrides || {})[role] || null,
595
668
  defaultLabel: defaultsByRole[role] || null,
596
669
  }));
@@ -616,7 +689,7 @@ async function openModelForm(ctx) {
616
689
  // Merge: form rows replace their roles; overrides on roles the form
617
690
  // doesn't show (verify/debug/…) are left untouched.
618
691
  const next = { ...(session.modelOverrides || {}) };
619
- for (const role of MODEL_FORM_ROLES) delete next[role];
692
+ for (const role of [...MODEL_FORM_ROLES, ...MODEL_MEDIA_FORM_ROLES]) delete next[role];
620
693
  Object.assign(next, result.overrides);
621
694
  session.modelOverrides = next;
622
695
  if (result.overrides.reasoning) session.model = result.overrides.reasoning;
@@ -653,7 +726,14 @@ async function handleModelCommand(rest = '', ctx) {
653
726
  }
654
727
 
655
728
  if (parts[0] === 'list' || parts[0] === 'catalog') {
656
- await printModelCatalog(ctx);
729
+ const category = parts[1]?.toLowerCase();
730
+ const normalizedCategory = normalizeCatalogCategory(category);
731
+ if (normalizedCategory && !['text', 'image', 'image_analysis', 'image_generation', 'multimodal', 'embedding', 'audio', 'video', 'other'].includes(normalizedCategory)) {
732
+ process.stderr.write(` ${c.yellow('!')} ${c.dim(`Unknown model category: ${category}`)}\n`);
733
+ printModelCommandUsage();
734
+ return;
735
+ }
736
+ await printModelCatalog(ctx, normalizedCategory || null);
657
737
  return;
658
738
  }
659
739
 
@@ -1248,12 +1328,17 @@ function buildContextStrip() {
1248
1328
  // Cache hit % lives under /cache — keep the always-on strip focused on
1249
1329
  // volume + elapsed. Historical rate calc was double-counting the cache tokens
1250
1330
  // vs OpenRouter's convention (see computeCacheTotals) which was misleading.
1251
- const right = [
1252
- c.dim(`${formatTokens(totalTokens)} tok`),
1253
- c.dim(elapsed),
1254
- ].join(c.dim(' · '));
1255
-
1256
- return right;
1331
+ const parts = [];
1332
+ // ctx: last turn's cumulative input tokens — approximates the CURRENT
1333
+ // prompt size. Only shown once we've completed at least one turn (so
1334
+ // the initial banner doesn't read '0 ctx'). This is the number the
1335
+ // 160k summarize threshold compares against on the next turn.
1336
+ if (session.lastTurnInputTokens > 0) {
1337
+ parts.push(c.dim(`ctx ${formatTokens(session.lastTurnInputTokens)}`));
1338
+ }
1339
+ parts.push(c.dim(`${formatTokens(totalTokens)} tok`));
1340
+ parts.push(c.dim(elapsed));
1341
+ return parts.join(c.dim(' · '));
1257
1342
  }
1258
1343
 
1259
1344
  // ── Dock meta line (model · cwd ⎇ branch · turn N) ─────────────────────
@@ -1784,6 +1869,9 @@ function renderEvent(event) {
1784
1869
 
1785
1870
  case 'tool_call':
1786
1871
  case 'tool_request': {
1872
+ if (watchState.active) {
1873
+ watchState.addEntry('tool', { label: data?.tool, detail: data?.args?.file_path || data?.args?.path || data?.args?.pattern || data?.args?.query || '' });
1874
+ }
1787
1875
  const isInternal = Boolean(data?.internal || data?.sub_agent);
1788
1876
  if (isInternal) {
1789
1877
  session.subAgentToolCalls++;
@@ -1841,6 +1929,10 @@ function renderEvent(event) {
1841
1929
 
1842
1930
  case 'tool_result':
1843
1931
  case 'tool_done': {
1932
+ if (watchState.active) {
1933
+ const success = data?.success !== false;
1934
+ watchState.addEntry('done', { label: data?.tool, detail: success ? '✓' : '✗' });
1935
+ }
1844
1936
  if (shouldFoldSubAgentTool(data)) {
1845
1937
  foldSubAgentToolResult(data);
1846
1938
  break;
@@ -1936,6 +2028,9 @@ function renderEvent(event) {
1936
2028
  }
1937
2029
 
1938
2030
  case 'delegation': {
2031
+ if (watchState.active) {
2032
+ watchState.addEntry('handoff', { label: `${data?.from || '?'} → ${data?.to || '?'}` });
2033
+ }
1939
2034
  stopSpinner();
1940
2035
  clearPendingHead();
1941
2036
  const from = data?.from || '';
@@ -1954,6 +2049,9 @@ function renderEvent(event) {
1954
2049
  // ── Sub-Agent Activity ──
1955
2050
 
1956
2051
  case 'sub_agent_start': {
2052
+ if (watchState.active) {
2053
+ watchState.addEntry('spawn', { type: data?.type, label: (data?.query || '').slice(0, 60) });
2054
+ }
1957
2055
  stopSpinner();
1958
2056
  clearPendingHead();
1959
2057
  flushFoldedSubAgentTools();
@@ -1992,7 +2090,7 @@ function renderEvent(event) {
1992
2090
  // is visible by emitting a small dim transcript line too, dedup'd
1993
2091
  // per (agentType, tool). Queue-active mode keeps its clean spinner
1994
2092
  // + window; the fallback line is only for the "otherwise blind"
1995
- // case that surfaced in 2.6.17 reports.
2093
+ // case that surfaced in earlier local terminal reports.
1996
2094
  if (!rqueue.isActive()) {
1997
2095
  const label = data?.label || '';
1998
2096
  const hint = label ? ` · ${label}` : '';
@@ -2014,6 +2112,12 @@ function renderEvent(event) {
2014
2112
  }
2015
2113
 
2016
2114
  case 'sub_agent_complete': {
2115
+ if (watchState.active) {
2116
+ const agentType = data?.type || 'sub-agent';
2117
+ const toolCalls = data?.tool_calls || 0;
2118
+ const durationS = data?.duration_s || 0;
2119
+ watchState.addEntry('done', { type: agentType, detail: `${toolCalls} tools · ${durationS.toFixed(1)}s`, status: 'done' });
2120
+ }
2017
2121
  setSubAgentWindowActive(false);
2018
2122
  stopSpinner();
2019
2123
  clearPendingHead();
@@ -2215,6 +2319,9 @@ function renderEvent(event) {
2215
2319
  if (data?.model_limits && typeof data.model_limits === 'object') {
2216
2320
  session.modelLimits = data.model_limits;
2217
2321
  }
2322
+ if (data?.sub_agent_models && typeof data.sub_agent_models === 'object') {
2323
+ session.subAgentModels = data.sub_agent_models;
2324
+ }
2218
2325
  if (data?.user) session.user = { ...session.user, ...data.user };
2219
2326
  // BYOK users pay their model provider directly; the platform does not
2220
2327
  // charge them credits. Hide cost + credits when this flag is set.
@@ -2309,6 +2416,10 @@ function renderEvent(event) {
2309
2416
  const out = usage.total_output_tokens || usage.output_tokens || 0;
2310
2417
  session.inputTokens += inp;
2311
2418
  session.outputTokens += out;
2419
+ // Remember THIS turn's cumulative input for the dock strip's ctx
2420
+ // indicator — approximates how big the prompt currently is (vs
2421
+ // session.inputTokens which sums every turn since launch).
2422
+ session.lastTurnInputTokens = inp;
2312
2423
 
2313
2424
  // Model-aware cost calculation
2314
2425
  const costResult = calculateCost(usage);
@@ -2827,6 +2938,19 @@ async function handleCommand(input, ctx) {
2827
2938
  return;
2828
2939
  }
2829
2940
 
2941
+ case '/watch': {
2942
+ const nowActive = !watchState.active;
2943
+ watchState.active = nowActive;
2944
+ runtime.watchPanelActive = nowActive;
2945
+ watchState.clear();
2946
+ process.stderr.write(` ${c.green(nowActive ? '✓' : '✗')} ${c.dim(`Watch panel ${nowActive ? 'on' : 'off'}`)}\n`);
2947
+ if (!nowActive) {
2948
+ // Clear any watch panel status lines
2949
+ if (rqueue.isActive()) rqueue.clearStatus();
2950
+ else if (isInputDockMounted()) { clearPinnedStatus(); moveToContent(); }
2951
+ }
2952
+ return;
2953
+ }
2830
2954
  case '/login':
2831
2955
  process.stderr.write(`${c.brand('Starting login flow...')}\n`);
2832
2956
  telemetry.track('login_shown', { method: 'repl_command' });
@@ -3328,40 +3452,48 @@ async function handleCommand(input, ctx) {
3328
3452
  return;
3329
3453
  }
3330
3454
 
3331
- case '/revoke': {
3332
- const wasActive = ctx.approval.revoke();
3333
- if (wasActive) {
3334
- process.stderr.write(` ${c.green('✓')} ${c.dim('Auto-approvals revoked. All tool calls will prompt again.')}\n`);
3335
- } else {
3336
- process.stderr.write(` ${c.gray('No auto-approvals were active.')}\n`);
3337
- }
3338
- return;
3339
- }
3340
3455
 
3341
3456
  case '/auto': {
3342
- // Session autopilot for long-running jobs: auto-approve routine
3343
- // writes/shell while STILL prompting for dangerous tiers (rm,
3344
- // force-push, command substitution, …) and never overriding hard
3345
- // safety blocks. Distinct from the launch-time
3346
- // --dangerously-skip-permissions flag, which approves everything
3347
- // including dangerous tiers.
3457
+ // Session autopilot for long-running jobs. Three levels:
3458
+ // /auto (or /auto on) routine writes/shell auto-approve;
3459
+ // dangerous tiers (rm/force-push/
3460
+ // command substitution/protected
3461
+ // files) still prompt.
3462
+ // /auto full approves EVERYTHING short of the
3463
+ // unbypassable hard-safety blocks —
3464
+ // mid-session equivalent of the
3465
+ // --dangerously-skip-permissions
3466
+ // launch flag. Long autonomous runs
3467
+ // won't stall on a dangerous tier.
3468
+ // /auto off all approvals prompt again.
3348
3469
  const sub = (rest || '').trim().toLowerCase();
3349
3470
  if (sub === 'off') {
3350
3471
  ctx.approval.approveAll = false;
3472
+ ctx.approval.autoApprove = false;
3351
3473
  process.stderr.write(` ${c.green('✓')} ${c.dim('Auto mode off — approvals prompt again.')}\n`);
3352
3474
  return;
3353
3475
  }
3476
+ if (sub === 'full' || sub === 'dangerous' || sub === 'yolo') {
3477
+ ctx.approval.autoApprove = true;
3478
+ ctx.approval.approveAll = true;
3479
+ process.stderr.write(` ${c.yellow('⚠')} ${c.bold('Auto FULL')} ${c.dim('— EVERY tool call auto-approves this session.')}\n`);
3480
+ process.stderr.write(` ${c.dim('Includes dangerous shell (rm/force-push/substitution) and protected files.')}\n`);
3481
+ process.stderr.write(` ${c.dim('Hard safety blocks are still enforced by the CLI safety layer.')}\n`);
3482
+ process.stderr.write(` ${c.dim('Disable with /auto off · this is the runtime equivalent of --dangerously-skip-permissions.')}\n`);
3483
+ return;
3484
+ }
3354
3485
  if (sub === '' || sub === 'on') {
3486
+ ctx.approval.autoApprove = false; // clear any prior /auto full
3355
3487
  ctx.approval.approveAll = true;
3356
3488
  process.stderr.write(` ${c.green('✓')} ${c.bold('Auto mode on')} ${c.dim('— routine tool calls auto-approve this session.')}\n`);
3357
3489
  process.stderr.write(` ${c.dim('Still prompts: dangerous shell (rm/force-push/substitution), protected files.')}\n`);
3358
3490
  process.stderr.write(` ${c.dim('Hard safety blocks stay enforced. Disable with /auto off · inspect with /approvals.')}\n`);
3359
- process.stderr.write(` ${c.dim('Tip: start your message with #auto to switch the backend agent into autonomous mode too.')}\n`);
3491
+ process.stderr.write(` ${c.dim('Need full autopilot mid-run? /auto full same effect as --dangerously-skip-permissions.')}\n`);
3360
3492
  return;
3361
3493
  }
3362
3494
  // status / anything else → show current mode
3363
3495
  process.stderr.write(` ${c.dim('Approval mode:')} ${ctx.approval.getModeLabel()}\n`);
3364
- process.stderr.write(` ${c.dim('Usage: /auto [on|off|status]')}\n`);
3496
+ process.stderr.write(` ${c.dim('Usage: /auto [on|off|full|status]')}\n`);
3365
3497
  return;
3366
3498
  }
3367
3499
 
@@ -3632,7 +3764,7 @@ async function fetchUser(ctx) {
3632
3764
  });
3633
3765
  if (resp.ok) {
3634
3766
  session.user = await resp.json();
3635
- session.model = session.user.default_reasoning_model || session.user.default_orchestrator_model || null;
3767
+ session.model = session.user.default_reasoning_model || null;
3636
3768
  }
3637
3769
  } catch {}
3638
3770
  }
@@ -3696,7 +3828,7 @@ export async function startTerminalRepl() {
3696
3828
  });
3697
3829
  }
3698
3830
 
3699
- let toolExecutor = makeToolExecutor({ showIndexStatus: true });
3831
+ let toolExecutor = null;
3700
3832
  const skipPerms = cliArgs.skipPermissions;
3701
3833
  let approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
3702
3834
 
@@ -3710,7 +3842,7 @@ export async function startTerminalRepl() {
3710
3842
  // Persistent stream client — session_id captured from backend on first turn
3711
3843
  let streamClient = null;
3712
3844
 
3713
- const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
3845
+ const ctx = { auth, toolExecutor: null, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
3714
3846
 
3715
3847
  let startupOutputRow = 1;
3716
3848
  let startupOutputCol = 1;
@@ -3773,6 +3905,7 @@ export async function startTerminalRepl() {
3773
3905
  user: session.user,
3774
3906
  model: session.model,
3775
3907
  modelLimits: session.modelLimits,
3908
+ subAgentModels: session.subAgentModels,
3776
3909
  modelOverrides: session.modelOverrides,
3777
3910
  modelMode: session.modelMode,
3778
3911
  routePreference: session.routePreference,
@@ -3805,6 +3938,7 @@ export async function startTerminalRepl() {
3805
3938
  startTime: Date.now(),
3806
3939
  inputTokens: 0,
3807
3940
  outputTokens: 0,
3941
+ lastTurnInputTokens: 0,
3808
3942
  toolCalls: 0,
3809
3943
  subAgentToolCalls: 0,
3810
3944
  totalToolCalls: 0,
@@ -3817,6 +3951,7 @@ export async function startTerminalRepl() {
3817
3951
  user: preserved.user,
3818
3952
  model: preserved.model,
3819
3953
  modelLimits: preserved.modelLimits,
3954
+ subAgentModels: preserved.subAgentModels,
3820
3955
  modelOverrides: preserved.modelOverrides,
3821
3956
  modelMode: preserved.modelMode,
3822
3957
  routePreference: preserved.routePreference,
@@ -4095,6 +4230,11 @@ export async function startTerminalRepl() {
4095
4230
  catch { /* preflight is best-effort */ }
4096
4231
  }
4097
4232
 
4233
+ // Create the auto-indexing executor after the startup header is visible so
4234
+ // the indexing status appears in the user's line of sight.
4235
+ toolExecutor = makeToolExecutor({ showIndexStatus: true });
4236
+ ctx.toolExecutor = toolExecutor;
4237
+
4098
4238
  // ── Initialization ──
4099
4239
  process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
4100
4240
  await fetchUser(ctx);
@@ -4163,9 +4303,23 @@ export async function startTerminalRepl() {
4163
4303
  initialContentRow: dockCursor.row,
4164
4304
  initialContentCol: dockCursor.col,
4165
4305
  });
4306
+ // 1 Hz live-tick for the elapsed clock in the dock's top strip.
4307
+ // Only fires when the user is idle (inputActive === true) — while the
4308
+ // agent is streaming content, skipping the tick avoids ANSI writes
4309
+ // interleaving with the stream. Cheap: one renderIdleDockInput per
4310
+ // second, only if mounted + idle. `unref()` so the timer never blocks
4311
+ // process exit.
4312
+ let _dockTickTimer = null;
4166
4313
  if (inputDockActive) {
4167
4314
  process.on('beforeExit', unmountInputDock);
4168
4315
  process.on('exit', unmountInputDock);
4316
+ _dockTickTimer = setInterval(() => {
4317
+ if (!isInputDockMounted()) return;
4318
+ if (!inputActive) return;
4319
+ try { renderIdleDockInput(); } catch { /* one bad tick is not fatal */ }
4320
+ }, 1000);
4321
+ _dockTickTimer.unref?.();
4322
+ process.on('exit', () => { if (_dockTickTimer) clearInterval(_dockTickTimer); });
4169
4323
  }
4170
4324
 
4171
4325
  // ── Bracketed paste (DEC private mode 2004) ──────────────────────────────
@@ -4922,6 +5076,11 @@ export async function startTerminalRepl() {
4922
5076
  runtime.lastRenderedBlock = 'user';
4923
5077
  }
4924
5078
 
5079
+ function isExecutionSlashCommand(instruction) {
5080
+ const command = String(instruction || '').trim().split(/\s+/, 1)[0].toLowerCase();
5081
+ return command === '/watch' || command === '/auto';
5082
+ }
5083
+
4925
5084
  async function submitExecutionInstruction() {
4926
5085
  const instruction = executionInputBuffer.trim();
4927
5086
  executionInputBuffer = '';
@@ -4940,6 +5099,25 @@ export async function startTerminalRepl() {
4940
5099
  executionInputVisible = false;
4941
5100
  return;
4942
5101
  }
5102
+ if (isExecutionSlashCommand(instruction)) {
5103
+ if (isInputDockMounted()) {
5104
+ clearInputPrompt();
5105
+ moveToContent();
5106
+ } else if (executionInputVisible) {
5107
+ process.stderr.write('\n');
5108
+ }
5109
+ executionInputVisible = false;
5110
+ await handleCommand(instruction, ctx);
5111
+ if (isInputDockMounted()) {
5112
+ renderDockInput(executionInputPrefix(), '', {
5113
+ context: buildContextStrip(),
5114
+ meta: buildDockMeta(),
5115
+ tips: executionInputTips(),
5116
+ });
5117
+ moveToContent();
5118
+ }
5119
+ return;
5120
+ }
4943
5121
  printExecutionInstruction(instruction);
4944
5122
  if (isInputDockMounted()) {
4945
5123
  renderDockInput(executionInputPrefix(), '', {