@bahulam/code 0.1.5 → 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 (179) 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 +112 -16
  21. package/src/core/tool-executor.mjs +123 -31
  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 +255 -55
  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/project-overview.mjs +5 -2
  51. package/src/tools/registry.mjs +4 -0
  52. package/src/ui/icons.mjs +2 -0
  53. package/src/ui/palette.mjs +1 -1
  54. package/src/ui/slash-commands.mjs +4 -6
  55. package/src/ui/tool-card.mjs +7 -3
  56. package/pulse/app/activity/page.tsx +0 -190
  57. package/pulse/app/api/activity/route.ts +0 -138
  58. package/pulse/app/api/benchmark/route.ts +0 -113
  59. package/pulse/app/api/benchmarks/route.ts +0 -195
  60. package/pulse/app/api/costs/route.ts +0 -88
  61. package/pulse/app/api/export/route.ts +0 -77
  62. package/pulse/app/api/history/route.ts +0 -11
  63. package/pulse/app/api/import/route.ts +0 -31
  64. package/pulse/app/api/memory/route.ts +0 -50
  65. package/pulse/app/api/plans/route.ts +0 -9
  66. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  67. package/pulse/app/api/projects/route.ts +0 -121
  68. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  69. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  70. package/pulse/app/api/sessions/route.ts +0 -112
  71. package/pulse/app/api/settings/route.ts +0 -14
  72. package/pulse/app/api/stats/route.ts +0 -143
  73. package/pulse/app/api/todos/route.ts +0 -9
  74. package/pulse/app/api/tools/route.ts +0 -160
  75. package/pulse/app/benchmarks/page.tsx +0 -224
  76. package/pulse/app/costs/page.tsx +0 -179
  77. package/pulse/app/export/page.tsx +0 -465
  78. package/pulse/app/favicon.ico +0 -0
  79. package/pulse/app/globals.css +0 -263
  80. package/pulse/app/help/page.tsx +0 -143
  81. package/pulse/app/history/page.tsx +0 -157
  82. package/pulse/app/layout.tsx +0 -46
  83. package/pulse/app/memory/page.tsx +0 -365
  84. package/pulse/app/overview-client.tsx +0 -393
  85. package/pulse/app/page.tsx +0 -14
  86. package/pulse/app/plans/page.tsx +0 -308
  87. package/pulse/app/projects/[slug]/page.tsx +0 -390
  88. package/pulse/app/projects/page.tsx +0 -110
  89. package/pulse/app/sessions/[id]/page.tsx +0 -243
  90. package/pulse/app/sessions/page.tsx +0 -39
  91. package/pulse/app/settings/page.tsx +0 -188
  92. package/pulse/app/todos/page.tsx +0 -211
  93. package/pulse/app/tools/page.tsx +0 -249
  94. package/pulse/cli.js +0 -164
  95. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  96. package/pulse/components/activity/streak-card.tsx +0 -36
  97. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  98. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  99. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  100. package/pulse/components/costs/model-token-table.tsx +0 -60
  101. package/pulse/components/global-search.tsx +0 -193
  102. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  103. package/pulse/components/layout/bottom-nav.tsx +0 -53
  104. package/pulse/components/layout/client-layout.tsx +0 -31
  105. package/pulse/components/layout/sidebar-context.tsx +0 -50
  106. package/pulse/components/layout/sidebar.tsx +0 -183
  107. package/pulse/components/layout/top-bar.tsx +0 -121
  108. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  109. package/pulse/components/overview/conversation-table.tsx +0 -148
  110. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  111. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  112. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  113. package/pulse/components/overview/stat-card.tsx +0 -102
  114. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  115. package/pulse/components/projects/project-card.tsx +0 -175
  116. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  117. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  118. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  119. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  120. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  121. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  122. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  123. package/pulse/components/sessions/session-badges.tsx +0 -49
  124. package/pulse/components/sessions/session-table.tsx +0 -299
  125. package/pulse/components/theme-provider.tsx +0 -44
  126. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  127. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  128. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  129. package/pulse/components/tools/version-history-table.tsx +0 -32
  130. package/pulse/components/ui/alert.tsx +0 -66
  131. package/pulse/components/ui/badge.tsx +0 -48
  132. package/pulse/components/ui/breadcrumb.tsx +0 -109
  133. package/pulse/components/ui/button.tsx +0 -64
  134. package/pulse/components/ui/calendar.tsx +0 -220
  135. package/pulse/components/ui/card.tsx +0 -92
  136. package/pulse/components/ui/command.tsx +0 -158
  137. package/pulse/components/ui/dialog.tsx +0 -158
  138. package/pulse/components/ui/input.tsx +0 -21
  139. package/pulse/components/ui/popover.tsx +0 -89
  140. package/pulse/components/ui/progress.tsx +0 -31
  141. package/pulse/components/ui/select.tsx +0 -190
  142. package/pulse/components/ui/separator.tsx +0 -28
  143. package/pulse/components/ui/sheet.tsx +0 -143
  144. package/pulse/components/ui/skeleton.tsx +0 -13
  145. package/pulse/components/ui/table.tsx +0 -116
  146. package/pulse/components/ui/tabs.tsx +0 -91
  147. package/pulse/components/ui/tooltip.tsx +0 -57
  148. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  149. package/pulse/components.json +0 -23
  150. package/pulse/eslint.config.mjs +0 -18
  151. package/pulse/lib/bahulam-paths.ts +0 -23
  152. package/pulse/lib/claude-reader.ts +0 -592
  153. package/pulse/lib/decode.ts +0 -129
  154. package/pulse/lib/pricing.ts +0 -102
  155. package/pulse/lib/replay-parser.ts +0 -165
  156. package/pulse/lib/tool-categories.ts +0 -140
  157. package/pulse/lib/utils.ts +0 -6
  158. package/pulse/next-env.d.ts +0 -6
  159. package/pulse/next.config.ts +0 -16
  160. package/pulse/package.json +0 -45
  161. package/pulse/postcss.config.mjs +0 -7
  162. package/pulse/public/activity.png +0 -0
  163. package/pulse/public/cc-lens.png +0 -0
  164. package/pulse/public/command-k.png +0 -0
  165. package/pulse/public/costs.png +0 -0
  166. package/pulse/public/dashboard-dark.png +0 -0
  167. package/pulse/public/dashboard-white.png +0 -0
  168. package/pulse/public/export.png +0 -0
  169. package/pulse/public/file.svg +0 -1
  170. package/pulse/public/globe.svg +0 -1
  171. package/pulse/public/next.svg +0 -1
  172. package/pulse/public/projects.png +0 -0
  173. package/pulse/public/session-chat.png +0 -0
  174. package/pulse/public/todos.png +0 -0
  175. package/pulse/public/tools.png +0 -0
  176. package/pulse/public/vercel.svg +0 -1
  177. package/pulse/public/window.svg +0 -1
  178. package/pulse/tsconfig.json +0 -34
  179. package/pulse/types/claude.ts +0 -294
@@ -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';
@@ -74,6 +83,7 @@ import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessi
74
83
  import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
75
84
  import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
76
85
  import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
86
+ import { startSpinner as startInlineSpinner } from '../ui/spinner.mjs';
77
87
  import {
78
88
  appendVisionAnalysisToInstruction,
79
89
  appendDocumentsToInstruction,
@@ -91,6 +101,7 @@ import { exploreCategory, exploreCollapseEnabled, isExploreTool } from './repl-e
91
101
  import { session, orbitRef, sessionMgrRef, runtime } from './repl-state.mjs';
92
102
  import { safeCwd } from './repl-utils.mjs';
93
103
  import * as rqueue from '../ui/render-queue.mjs';
104
+ import { watchState } from './watch-state.mjs';
94
105
  import {
95
106
  appendContent,
96
107
  bumpSpinnerProgress,
@@ -290,14 +301,15 @@ function renderKeyboardHelp() {
290
301
 
291
302
  const MODEL_ROLE_ALIASES = new Map([
292
303
  ['reasoning', 'reasoning'],
293
- ['main', 'reasoning'],
294
304
  ['coder', 'reasoning'],
295
305
  ['coding', 'reasoning'],
296
306
  ['smart', 'reasoning'],
297
307
  ['fast', 'fast'],
298
308
  ['explorer', 'fast'],
299
- ['orchestrator', 'orchestrator'],
300
- ['planner', 'orchestrator'],
309
+ ['main', 'reasoning'],
310
+ ['orchestrator', 'plan'],
311
+ ['planner', 'plan'],
312
+ ['planning', 'plan'],
301
313
  ['local', 'local'],
302
314
  ['worker', 'worker'],
303
315
  ['explore', 'explore'],
@@ -305,25 +317,52 @@ const MODEL_ROLE_ALIASES = new Map([
305
317
  ['verify', 'verify'],
306
318
  ['debug', 'debug'],
307
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'],
308
331
  ]);
309
332
 
310
333
  const MODEL_ROLE_LABELS = {
311
334
  reasoning: 'coding',
312
335
  fast: 'fast',
313
- orchestrator: 'orchestrator',
336
+ orchestrator: 'planning',
314
337
  local: 'local',
315
338
  worker: 'worker',
316
339
  explore: 'explore',
317
- plan: 'plan',
340
+ plan: 'planning',
318
341
  verify: 'verify',
319
342
  debug: 'debug',
320
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',
321
361
  };
322
362
 
323
363
  const MODEL_ROLE_ORDER = [
324
364
  'reasoning',
325
365
  'fast',
326
- 'orchestrator',
327
366
  'local',
328
367
  'worker',
329
368
  'explore',
@@ -331,6 +370,8 @@ const MODEL_ROLE_ORDER = [
331
370
  'verify',
332
371
  'debug',
333
372
  'refactor',
373
+ 'image_analysis',
374
+ 'image_generation',
334
375
  ];
335
376
 
336
377
  function normalizeModelRole(value) {
@@ -348,7 +389,8 @@ function printModelCommandUsage() {
348
389
  process.stderr.write(` /model <model> set coding model directly\n`);
349
390
  process.stderr.write(` /model <role> <model>\n`);
350
391
  process.stderr.write(` /model ${NAMED_MODEL_MODES_LIST.join('|')}\n`);
351
- 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`);
352
394
  process.stderr.write(` ${c.gray('Roles:')} ${MODEL_ROLE_ORDER.map(role => MODEL_ROLE_LABELS[role]).join(', ')}\n`);
353
395
  }
354
396
 
@@ -460,6 +502,15 @@ function modelCreditBadge(model) {
460
502
  return `~${credits < 10 ? credits.toFixed(1) : String(Math.round(credits))} cr/M in`;
461
503
  }
462
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
+
463
514
  async function warnIfNotCurated(model, ctx) {
464
515
  if (isByokModelRoute()) return;
465
516
  const catalog = await fetchModelCatalog(ctx);
@@ -469,28 +520,41 @@ async function warnIfNotCurated(model, ctx) {
469
520
  process.stderr.write(` ${c.yellow('!')} ${c.dim(`${model} is not in the platform catalog — the backend may reject it. See /model list.`)}\n`);
470
521
  } else if (row.harness_validated === false) {
471
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`);
472
525
  }
473
526
  }
474
527
 
475
- async function printModelCatalog(ctx) {
528
+ async function printModelCatalog(ctx, filterCategory = null) {
476
529
  const catalog = await fetchModelCatalog(ctx);
477
530
  if (!catalog) {
478
531
  process.stderr.write(` ${c.yellow('!')} ${c.dim(`Model catalog unavailable — ${_modelCatalogError || 'unknown error'}.`)}\n`);
479
532
  return;
480
533
  }
481
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
+
482
540
  process.stderr.write(`\n ${c.bold('Platform catalog')} ${c.dim('(harness-validated, credit-priced)')}\n`);
483
541
  process.stderr.write(` ${c.gray('─'.repeat(64))}\n`);
484
- if (!curated.length) {
542
+ if (!visible.length) {
485
543
  process.stderr.write(` ${c.dim('(none published yet)')}\n`);
486
544
  }
487
- for (const m of curated) {
488
- const badge = modelCreditBadge(m);
489
- const tiers = Array.isArray(m.platform_access_tier) && m.platform_access_tier.length
490
- ? c.dim(` [${m.platform_access_tier.join(', ')}]`)
491
- : '';
492
- 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);
493
556
  }
557
+
494
558
  const rest = catalog.length - curated.length;
495
559
  if (rest > 0) {
496
560
  process.stderr.write(` ${c.dim(`+${rest} more models available on the BYOK route (--route byok, own API key)`)}\n`);
@@ -551,10 +615,12 @@ function printModelStatus() {
551
615
  }
552
616
 
553
617
  const limits = session.modelLimits || {};
618
+ const subAgentModels = session.subAgentModels || {};
619
+ const planningModel = subAgentModels.plan || limits.planning?.model || limits.orchestrator?.model;
554
620
  const rows = [
555
- ['coder', limits.coder?.model],
556
- ['explorer', limits.explorer?.model],
557
- ['orchestrator', limits.orchestrator?.model],
621
+ ['coding', limits.coder?.model],
622
+ ['explore', subAgentModels.explore || limits.explorer?.model],
623
+ ['planning', planningModel],
558
624
  ].filter(([, model]) => model);
559
625
  if (rows.length) {
560
626
  process.stderr.write(`\n ${c.bold('Backend roles')}\n`);
@@ -576,20 +642,28 @@ function printModelStatus() {
576
642
  printModelCommandUsage();
577
643
  }
578
644
 
579
- 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'];
580
647
 
581
648
  async function openModelForm(ctx) {
582
649
  const limits = session.modelLimits || {};
650
+ const subAgentModels = session.subAgentModels || {};
651
+ const planningModel = subAgentModels.plan || limits.planning?.model || limits.orchestrator?.model;
583
652
  const defaultsByRole = {
584
653
  reasoning: limits.coder?.model,
585
654
  fast: limits.explorer?.model,
586
- orchestrator: limits.orchestrator?.model,
587
- explore: limits.explorer?.model,
588
- 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',
589
659
  };
590
- const roles = MODEL_FORM_ROLES.map(role => ({
660
+ const roles = [...MODEL_FORM_ROLES, ...MODEL_MEDIA_FORM_ROLES].map(role => ({
591
661
  role,
592
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'),
593
667
  current: (session.modelOverrides || {})[role] || null,
594
668
  defaultLabel: defaultsByRole[role] || null,
595
669
  }));
@@ -615,7 +689,7 @@ async function openModelForm(ctx) {
615
689
  // Merge: form rows replace their roles; overrides on roles the form
616
690
  // doesn't show (verify/debug/…) are left untouched.
617
691
  const next = { ...(session.modelOverrides || {}) };
618
- 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];
619
693
  Object.assign(next, result.overrides);
620
694
  session.modelOverrides = next;
621
695
  if (result.overrides.reasoning) session.model = result.overrides.reasoning;
@@ -652,7 +726,14 @@ async function handleModelCommand(rest = '', ctx) {
652
726
  }
653
727
 
654
728
  if (parts[0] === 'list' || parts[0] === 'catalog') {
655
- 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);
656
737
  return;
657
738
  }
658
739
 
@@ -1247,12 +1328,17 @@ function buildContextStrip() {
1247
1328
  // Cache hit % lives under /cache — keep the always-on strip focused on
1248
1329
  // volume + elapsed. Historical rate calc was double-counting the cache tokens
1249
1330
  // vs OpenRouter's convention (see computeCacheTotals) which was misleading.
1250
- const right = [
1251
- c.dim(`${formatTokens(totalTokens)} tok`),
1252
- c.dim(elapsed),
1253
- ].join(c.dim(' · '));
1254
-
1255
- 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(' · '));
1256
1342
  }
1257
1343
 
1258
1344
  // ── Dock meta line (model · cwd ⎇ branch · turn N) ─────────────────────
@@ -1783,6 +1869,9 @@ function renderEvent(event) {
1783
1869
 
1784
1870
  case 'tool_call':
1785
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
+ }
1786
1875
  const isInternal = Boolean(data?.internal || data?.sub_agent);
1787
1876
  if (isInternal) {
1788
1877
  session.subAgentToolCalls++;
@@ -1840,6 +1929,10 @@ function renderEvent(event) {
1840
1929
 
1841
1930
  case 'tool_result':
1842
1931
  case 'tool_done': {
1932
+ if (watchState.active) {
1933
+ const success = data?.success !== false;
1934
+ watchState.addEntry('done', { label: data?.tool, detail: success ? '✓' : '✗' });
1935
+ }
1843
1936
  if (shouldFoldSubAgentTool(data)) {
1844
1937
  foldSubAgentToolResult(data);
1845
1938
  break;
@@ -1935,6 +2028,9 @@ function renderEvent(event) {
1935
2028
  }
1936
2029
 
1937
2030
  case 'delegation': {
2031
+ if (watchState.active) {
2032
+ watchState.addEntry('handoff', { label: `${data?.from || '?'} → ${data?.to || '?'}` });
2033
+ }
1938
2034
  stopSpinner();
1939
2035
  clearPendingHead();
1940
2036
  const from = data?.from || '';
@@ -1953,6 +2049,9 @@ function renderEvent(event) {
1953
2049
  // ── Sub-Agent Activity ──
1954
2050
 
1955
2051
  case 'sub_agent_start': {
2052
+ if (watchState.active) {
2053
+ watchState.addEntry('spawn', { type: data?.type, label: (data?.query || '').slice(0, 60) });
2054
+ }
1956
2055
  stopSpinner();
1957
2056
  clearPendingHead();
1958
2057
  flushFoldedSubAgentTools();
@@ -1991,7 +2090,7 @@ function renderEvent(event) {
1991
2090
  // is visible by emitting a small dim transcript line too, dedup'd
1992
2091
  // per (agentType, tool). Queue-active mode keeps its clean spinner
1993
2092
  // + window; the fallback line is only for the "otherwise blind"
1994
- // case that surfaced in 2.6.17 reports.
2093
+ // case that surfaced in earlier local terminal reports.
1995
2094
  if (!rqueue.isActive()) {
1996
2095
  const label = data?.label || '';
1997
2096
  const hint = label ? ` · ${label}` : '';
@@ -2013,6 +2112,12 @@ function renderEvent(event) {
2013
2112
  }
2014
2113
 
2015
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
+ }
2016
2121
  setSubAgentWindowActive(false);
2017
2122
  stopSpinner();
2018
2123
  clearPendingHead();
@@ -2214,6 +2319,9 @@ function renderEvent(event) {
2214
2319
  if (data?.model_limits && typeof data.model_limits === 'object') {
2215
2320
  session.modelLimits = data.model_limits;
2216
2321
  }
2322
+ if (data?.sub_agent_models && typeof data.sub_agent_models === 'object') {
2323
+ session.subAgentModels = data.sub_agent_models;
2324
+ }
2217
2325
  if (data?.user) session.user = { ...session.user, ...data.user };
2218
2326
  // BYOK users pay their model provider directly; the platform does not
2219
2327
  // charge them credits. Hide cost + credits when this flag is set.
@@ -2308,6 +2416,10 @@ function renderEvent(event) {
2308
2416
  const out = usage.total_output_tokens || usage.output_tokens || 0;
2309
2417
  session.inputTokens += inp;
2310
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;
2311
2423
 
2312
2424
  // Model-aware cost calculation
2313
2425
  const costResult = calculateCost(usage);
@@ -2826,6 +2938,19 @@ async function handleCommand(input, ctx) {
2826
2938
  return;
2827
2939
  }
2828
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
+ }
2829
2954
  case '/login':
2830
2955
  process.stderr.write(`${c.brand('Starting login flow...')}\n`);
2831
2956
  telemetry.track('login_shown', { method: 'repl_command' });
@@ -3327,40 +3452,48 @@ async function handleCommand(input, ctx) {
3327
3452
  return;
3328
3453
  }
3329
3454
 
3330
- case '/revoke': {
3331
- const wasActive = ctx.approval.revoke();
3332
- if (wasActive) {
3333
- process.stderr.write(` ${c.green('✓')} ${c.dim('Auto-approvals revoked. All tool calls will prompt again.')}\n`);
3334
- } else {
3335
- process.stderr.write(` ${c.gray('No auto-approvals were active.')}\n`);
3336
- }
3337
- return;
3338
- }
3339
3455
 
3340
3456
  case '/auto': {
3341
- // Session autopilot for long-running jobs: auto-approve routine
3342
- // writes/shell while STILL prompting for dangerous tiers (rm,
3343
- // force-push, command substitution, …) and never overriding hard
3344
- // safety blocks. Distinct from the launch-time
3345
- // --dangerously-skip-permissions flag, which approves everything
3346
- // 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.
3347
3469
  const sub = (rest || '').trim().toLowerCase();
3348
3470
  if (sub === 'off') {
3349
3471
  ctx.approval.approveAll = false;
3472
+ ctx.approval.autoApprove = false;
3350
3473
  process.stderr.write(` ${c.green('✓')} ${c.dim('Auto mode off — approvals prompt again.')}\n`);
3351
3474
  return;
3352
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
+ }
3353
3485
  if (sub === '' || sub === 'on') {
3486
+ ctx.approval.autoApprove = false; // clear any prior /auto full
3354
3487
  ctx.approval.approveAll = true;
3355
3488
  process.stderr.write(` ${c.green('✓')} ${c.bold('Auto mode on')} ${c.dim('— routine tool calls auto-approve this session.')}\n`);
3356
3489
  process.stderr.write(` ${c.dim('Still prompts: dangerous shell (rm/force-push/substitution), protected files.')}\n`);
3357
3490
  process.stderr.write(` ${c.dim('Hard safety blocks stay enforced. Disable with /auto off · inspect with /approvals.')}\n`);
3358
- 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`);
3359
3492
  return;
3360
3493
  }
3361
3494
  // status / anything else → show current mode
3362
3495
  process.stderr.write(` ${c.dim('Approval mode:')} ${ctx.approval.getModeLabel()}\n`);
3363
- process.stderr.write(` ${c.dim('Usage: /auto [on|off|status]')}\n`);
3496
+ process.stderr.write(` ${c.dim('Usage: /auto [on|off|full|status]')}\n`);
3364
3497
  return;
3365
3498
  }
3366
3499
 
@@ -3631,7 +3764,7 @@ async function fetchUser(ctx) {
3631
3764
  });
3632
3765
  if (resp.ok) {
3633
3766
  session.user = await resp.json();
3634
- session.model = session.user.default_reasoning_model || session.user.default_orchestrator_model || null;
3767
+ session.model = session.user.default_reasoning_model || null;
3635
3768
  }
3636
3769
  } catch {}
3637
3770
  }
@@ -3674,7 +3807,28 @@ export async function startTerminalRepl() {
3674
3807
  return res;
3675
3808
  };
3676
3809
 
3677
- let toolExecutor = createToolExecutor({ checkpoints, hookRunner, interactionHandler: askUserInteraction });
3810
+ function makeToolExecutor({ showIndexStatus = false } = {}) {
3811
+ const shouldShowIndexStatus = showIndexStatus && process.stderr.isTTY && !term().plain;
3812
+ let stopIndexSpinner = null;
3813
+ return createToolExecutor({
3814
+ checkpoints,
3815
+ hookRunner,
3816
+ interactionHandler: askUserInteraction,
3817
+ onAutoRegisterStart: shouldShowIndexStatus ? (root) => {
3818
+ const name = path.basename(root || safeCwd()) || root || 'project';
3819
+ stopIndexSpinner?.();
3820
+ stopIndexSpinner = startInlineSpinner(
3821
+ `Indexing ${name} so tools can read and search this project...`
3822
+ );
3823
+ } : null,
3824
+ onAutoRegisterDone: shouldShowIndexStatus ? () => {
3825
+ stopIndexSpinner?.();
3826
+ stopIndexSpinner = null;
3827
+ } : null,
3828
+ });
3829
+ }
3830
+
3831
+ let toolExecutor = null;
3678
3832
  const skipPerms = cliArgs.skipPermissions;
3679
3833
  let approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
3680
3834
 
@@ -3688,7 +3842,7 @@ export async function startTerminalRepl() {
3688
3842
  // Persistent stream client — session_id captured from backend on first turn
3689
3843
  let streamClient = null;
3690
3844
 
3691
- 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: [] };
3692
3846
 
3693
3847
  let startupOutputRow = 1;
3694
3848
  let startupOutputCol = 1;
@@ -3751,6 +3905,7 @@ export async function startTerminalRepl() {
3751
3905
  user: session.user,
3752
3906
  model: session.model,
3753
3907
  modelLimits: session.modelLimits,
3908
+ subAgentModels: session.subAgentModels,
3754
3909
  modelOverrides: session.modelOverrides,
3755
3910
  modelMode: session.modelMode,
3756
3911
  routePreference: session.routePreference,
@@ -3768,7 +3923,7 @@ export async function startTerminalRepl() {
3768
3923
  checkpoints = new CheckpointManager(safeCwd());
3769
3924
  effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
3770
3925
  hookRunner = new HookRunner({ cwd: safeCwd() });
3771
- toolExecutor = createToolExecutor({ checkpoints, hookRunner, interactionHandler: askUserInteraction });
3926
+ toolExecutor = makeToolExecutor();
3772
3927
  approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
3773
3928
  if (ctx._rl) approval.setReadline(ctx._rl);
3774
3929
  sessionMgr = new SessionManager(safeCwd());
@@ -3783,6 +3938,7 @@ export async function startTerminalRepl() {
3783
3938
  startTime: Date.now(),
3784
3939
  inputTokens: 0,
3785
3940
  outputTokens: 0,
3941
+ lastTurnInputTokens: 0,
3786
3942
  toolCalls: 0,
3787
3943
  subAgentToolCalls: 0,
3788
3944
  totalToolCalls: 0,
@@ -3795,6 +3951,7 @@ export async function startTerminalRepl() {
3795
3951
  user: preserved.user,
3796
3952
  model: preserved.model,
3797
3953
  modelLimits: preserved.modelLimits,
3954
+ subAgentModels: preserved.subAgentModels,
3798
3955
  modelOverrides: preserved.modelOverrides,
3799
3956
  modelMode: preserved.modelMode,
3800
3957
  routePreference: preserved.routePreference,
@@ -3948,7 +4105,7 @@ export async function startTerminalRepl() {
3948
4105
  checkpoints = new CheckpointManager(safeCwd());
3949
4106
  effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
3950
4107
  hookRunner = new HookRunner({ cwd: safeCwd(), sessionId });
3951
- toolExecutor = createToolExecutor({ checkpoints, hookRunner, interactionHandler: askUserInteraction });
4108
+ toolExecutor = makeToolExecutor();
3952
4109
  approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
3953
4110
  if (ctx._rl) approval.setReadline(ctx._rl);
3954
4111
  sessionMgr = new SessionManager(safeCwd());
@@ -4073,6 +4230,11 @@ export async function startTerminalRepl() {
4073
4230
  catch { /* preflight is best-effort */ }
4074
4231
  }
4075
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
+
4076
4238
  // ── Initialization ──
4077
4239
  process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
4078
4240
  await fetchUser(ctx);
@@ -4141,9 +4303,23 @@ export async function startTerminalRepl() {
4141
4303
  initialContentRow: dockCursor.row,
4142
4304
  initialContentCol: dockCursor.col,
4143
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;
4144
4313
  if (inputDockActive) {
4145
4314
  process.on('beforeExit', unmountInputDock);
4146
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); });
4147
4323
  }
4148
4324
 
4149
4325
  // ── Bracketed paste (DEC private mode 2004) ──────────────────────────────
@@ -4900,6 +5076,11 @@ export async function startTerminalRepl() {
4900
5076
  runtime.lastRenderedBlock = 'user';
4901
5077
  }
4902
5078
 
5079
+ function isExecutionSlashCommand(instruction) {
5080
+ const command = String(instruction || '').trim().split(/\s+/, 1)[0].toLowerCase();
5081
+ return command === '/watch' || command === '/auto';
5082
+ }
5083
+
4903
5084
  async function submitExecutionInstruction() {
4904
5085
  const instruction = executionInputBuffer.trim();
4905
5086
  executionInputBuffer = '';
@@ -4918,6 +5099,25 @@ export async function startTerminalRepl() {
4918
5099
  executionInputVisible = false;
4919
5100
  return;
4920
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
+ }
4921
5121
  printExecutionInstruction(instruction);
4922
5122
  if (isInputDockMounted()) {
4923
5123
  renderDockInput(executionInputPrefix(), '', {
@@ -24,6 +24,8 @@ const TOOL_LABELS = Object.freeze({
24
24
  git_diff: 'Reviewing changes',
25
25
  git_status: 'Checking git',
26
26
  analyze_code: 'Analyzing',
27
+ analyze_image: 'Analyzing image',
28
+ generate_image: 'Generating image',
27
29
  get_project_overview: 'Indexing project',
28
30
  skills_list: 'Listing skills',
29
31
  skill_view: 'Loading skill',
@@ -76,9 +78,39 @@ function currentWorkingDirectory() {
76
78
  }
77
79
  }
78
80
 
81
+ // Shortens a path for tool-card display without dropping the meaningful
82
+ // tail. Strategy, in priority order:
83
+ //
84
+ // 1. INSIDE cwd /repo/foo/bar.py (cwd = /repo) → foo/bar.py
85
+ // 2. SIBLING under cwd's /work/repo-a/x.py (cwd = /work/repo-b)
86
+ // parent → …/repo-a/x.py
87
+ // (typical mono-repo / adjacent-clone case)
88
+ // 3. UNDER $HOME /Users/sree/Sites/…/x.py → ~/Sites/…/x.py
89
+ // 4. Absolute (fallback — printed as-is; card truncator
90
+ // handles the display width from here)
91
+ //
92
+ // The point: keep the basename + parent visible even when the full path
93
+ // is long, so users don't see "Editing /Users/sree/Sites/Tarang-Orca/
94
+ // codekepler-ba…" with the actual filename lost off the right edge.
79
95
  function shortPath(filePath, cwd = currentWorkingDirectory()) {
80
96
  const value = String(filePath || '');
97
+ if (!value) return value;
98
+
99
+ // 1. inside cwd
81
100
  if (cwd && value.startsWith(`${cwd}/`)) return value.slice(cwd.length + 1);
101
+
102
+ // 2. sibling under cwd's parent (e.g. sister repo in the same workspace)
103
+ if (cwd) {
104
+ const cwdParent = cwd.slice(0, cwd.lastIndexOf('/'));
105
+ if (cwdParent && value.startsWith(`${cwdParent}/`)) {
106
+ return `…/${value.slice(cwdParent.length + 1)}`;
107
+ }
108
+ }
109
+
110
+ // 3. under $HOME
111
+ const home = process.env.HOME || process.env.USERPROFILE || '';
112
+ if (home && value.startsWith(`${home}/`)) return `~/${value.slice(home.length + 1)}`;
113
+
82
114
  return value;
83
115
  }
84
116