@serkanalgur/opencode-nexus 2.4.1 → 2.5.0

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 (3) hide show
  1. package/dist/index.js +190 -40
  2. package/dist/tui.js +86 -59
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9920,33 +9920,86 @@ class CustomRoleManager {
9920
9920
  }
9921
9921
 
9922
9922
  // src/forecast.ts
9923
+ var CACHE_READ_MULTIPLE = 0.1;
9924
+ var CACHE_WRITE_MULTIPLE = 1.25;
9925
+ function per1kPricing(input, output) {
9926
+ return {
9927
+ input,
9928
+ output,
9929
+ cacheRead: input * CACHE_READ_MULTIPLE,
9930
+ cacheWrite: input * CACHE_WRITE_MULTIPLE
9931
+ };
9932
+ }
9933
+ var FALLBACK_PRICING_PER_1K = {
9934
+ "claude-sonnet-4-6": per1kPricing(0.015, 0.075),
9935
+ "claude-opus-4-7": per1kPricing(0.075, 0.375),
9936
+ "claude-haiku-4-5": per1kPricing(0.001, 0.005),
9937
+ "gpt-5-mini": per1kPricing(0.00015, 0.0006),
9938
+ "gpt-5": per1kPricing(0.0025, 0.01),
9939
+ "gemini-2.5-flash": per1kPricing(0.000075, 0.0003),
9940
+ "minimax-m2.5-free": per1kPricing(0, 0)
9941
+ };
9942
+ var UNKNOWN_PRICING_PER_1K = per1kPricing(0.01, 0.05);
9943
+ var per1k = (rate, tokens) => rate * tokens / 1000;
9944
+ function bareModelId(ref) {
9945
+ return ref.slice(ref.indexOf("/") + 1);
9946
+ }
9947
+ function totalTokens(usage) {
9948
+ return usage.input + usage.output + usage.reasoning + usage.cache.read + usage.cache.write;
9949
+ }
9950
+ function priceTokens(usage, pricing) {
9951
+ const inputCost = per1k(pricing.input, usage.input);
9952
+ const outputCost = per1k(pricing.output, usage.output + usage.reasoning);
9953
+ const cacheCost = per1k(pricing.cacheRead, usage.cache.read) + per1k(pricing.cacheWrite, usage.cache.write);
9954
+ return { inputCost, outputCost, cacheCost, total: inputCost + outputCost + cacheCost };
9955
+ }
9956
+
9923
9957
  class CostForecaster {
9924
- modelPricing = new Map;
9925
- constructor() {
9926
- this.modelPricing.set("claude-sonnet-4-6", { input: 0.000015, output: 0.000075 });
9927
- this.modelPricing.set("claude-opus-4-7", { input: 0.000075, output: 0.000375 });
9928
- this.modelPricing.set("claude-haiku-4-5", { input: 0.000001, output: 0.000005 });
9929
- this.modelPricing.set("gpt-5-mini", { input: 0.00000015, output: 0.0000006 });
9930
- this.modelPricing.set("gpt-5", { input: 0.0000025, output: 0.00001 });
9931
- this.modelPricing.set("gemini-2.5-flash", { input: 0.000000075, output: 0.0000003 });
9958
+ resolvePricing;
9959
+ constructor(pricing) {
9960
+ this.resolvePricing = pricing;
9961
+ }
9962
+ priceFor(model, provider) {
9963
+ const real = this.resolvePricing?.(model, provider);
9964
+ if (real)
9965
+ return { pricing: real, source: "model-costs" };
9966
+ const bare = bareModelId(model);
9967
+ const fallback = FALLBACK_PRICING_PER_1K[model] ?? FALLBACK_PRICING_PER_1K[bare];
9968
+ if (fallback)
9969
+ return { pricing: fallback, source: "fallback-table" };
9970
+ return { pricing: UNKNOWN_PRICING_PER_1K, source: "unknown-model" };
9971
+ }
9972
+ measureCost(usage, model, provider) {
9973
+ const { pricing, source } = this.priceFor(model, provider);
9974
+ return {
9975
+ cost: priceTokens(usage, pricing).total,
9976
+ tokens: totalTokens(usage),
9977
+ pricingSource: source,
9978
+ ...source === "model-costs" ? { confidence: 1 } : {}
9979
+ };
9932
9980
  }
9933
- updatePricing(modelId, input, output) {
9934
- this.modelPricing.set(modelId, { input, output });
9981
+ costOf(usage, model, provider) {
9982
+ return priceTokens(usage, this.priceFor(model, provider).pricing).total;
9935
9983
  }
9936
- estimateTokens(task, complexity) {
9984
+ estimateTokensFor(complexity, fileCount) {
9937
9985
  const baseInput = 700;
9938
9986
  const complexityMultiplier = 1 + complexity.overall / 100 * 3;
9939
- const fileMultiplier = 1 + task.files.include.length * 0.2;
9987
+ const fileMultiplier = 1 + fileCount * 0.2;
9940
9988
  const input = Math.round(baseInput * complexityMultiplier * fileMultiplier);
9941
9989
  const output = Math.round(input * 0.5);
9942
9990
  return { input, output };
9943
9991
  }
9992
+ estimateTokens(task, complexity) {
9993
+ return this.estimateTokensFor(complexity, task.files.include.length);
9994
+ }
9995
+ estimateCost(complexity, model, provider) {
9996
+ const { input, output } = this.estimateTokensFor(complexity, 0);
9997
+ return this.costOf({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, model, provider);
9998
+ }
9944
9999
  forecastTask(task, role, modelId, complexity) {
9945
10000
  const { input, output } = this.estimateTokens(task, complexity);
9946
- const pricing = this.modelPricing.get(modelId) || { input: 0.00001, output: 0.00005 };
9947
- const inputCost = input * pricing.input;
9948
- const outputCost = output * pricing.output;
9949
- const overhead = 0.0001;
10001
+ const { pricing, source } = this.priceFor(modelId);
10002
+ const breakdown = priceTokens({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, pricing);
9950
10003
  return {
9951
10004
  taskId: task.id,
9952
10005
  taskName: task.name,
@@ -9954,9 +10007,15 @@ class CostForecaster {
9954
10007
  model: modelId,
9955
10008
  estimatedInputTokens: input,
9956
10009
  estimatedOutputTokens: output,
9957
- estimatedCost: inputCost + outputCost + overhead,
9958
- confidence: 0.6 + Math.random() * 0.3,
9959
- breakdown: { inputCost, outputCost, overhead }
10010
+ estimatedCost: breakdown.total,
10011
+ source: "estimated",
10012
+ pricingSource: source,
10013
+ confidence: undefined,
10014
+ breakdown: {
10015
+ inputCost: breakdown.inputCost,
10016
+ outputCost: breakdown.outputCost,
10017
+ cacheCost: breakdown.cacheCost
10018
+ }
9960
10019
  };
9961
10020
  }
9962
10021
  forecastAll(tasks, budgetRemaining) {
@@ -10164,6 +10223,12 @@ function lastAssistantText(messages) {
10164
10223
  }
10165
10224
  return "";
10166
10225
  }
10226
+ function sumBy(records, select) {
10227
+ let total = 0;
10228
+ for (const record of records.values())
10229
+ total += select(record);
10230
+ return total;
10231
+ }
10167
10232
  var DEFAULT_ESCALATION = {
10168
10233
  maxRetries: 3,
10169
10234
  retryDelay: 1000,
@@ -10209,6 +10274,8 @@ class NexusOrchestrator {
10209
10274
  onStateChange = null;
10210
10275
  stateChangeTimer = null;
10211
10276
  costHistory = [];
10277
+ tokensByModel = new Map;
10278
+ costProvenance = new Map;
10212
10279
  cleanupInterval = null;
10213
10280
  get healthMonitor() {
10214
10281
  return this._healthMonitor;
@@ -10226,6 +10293,7 @@ class NexusOrchestrator {
10226
10293
  this.messageRouter = new MessageRouter;
10227
10294
  this.escalationPolicy = {
10228
10295
  ...DEFAULT_ESCALATION,
10296
+ fallbackModels: [...DEFAULT_ESCALATION.fallbackModels],
10229
10297
  maxRetries: this.config.selfHealing.maxRetries,
10230
10298
  retryDelay: this.config.selfHealing.retryDelay,
10231
10299
  enableRespawn: this.config.selfHealing.contextTransfer
@@ -10235,7 +10303,7 @@ class NexusOrchestrator {
10235
10303
  this.performanceTracker = new PerformanceTracker;
10236
10304
  this.executionHistory = new ExecutionHistory;
10237
10305
  this.customRoles = new CustomRoleManager;
10238
- this.forecaster = new CostForecaster;
10306
+ this.forecaster = new CostForecaster((model, provider) => this.getModelCost(model, provider));
10239
10307
  }
10240
10308
  async initialize(ctx, onStateChange) {
10241
10309
  this.ctx = ctx;
@@ -10590,14 +10658,17 @@ class NexusOrchestrator {
10590
10658
  await this.sleep(this.config.schedulerInterval);
10591
10659
  }
10592
10660
  }
10593
- async spawnAndExecute(node, transferContext) {
10661
+ selectQualifiedModel(node) {
10594
10662
  const complexity = this.analyzeComplexity(node.task);
10595
10663
  const model = this.selectModel(node.task.requiredRole, complexity);
10596
10664
  if (!model.provider || !model.model) {
10597
10665
  const missing = !model.provider && !model.model ? "provider and model" : !model.provider ? "provider" : "model";
10598
10666
  throw new Error(`Model selection for role "${node.task.requiredRole}" is missing its ${missing}: ` + `got { provider: ${JSON.stringify(model.provider)}, model: ${JSON.stringify(model.model)} }. ` + `Cannot build a "providerID/modelID" reference.`);
10599
10667
  }
10600
- const qualifiedModel = `${model.provider}/${model.model}`;
10668
+ return `${model.provider}/${model.model}`;
10669
+ }
10670
+ async spawnAndExecute(node, options = {}) {
10671
+ const qualifiedModel = options.modelOverride ?? this.selectQualifiedModel(node);
10601
10672
  const agent = await this.spawnAgent({
10602
10673
  role: node.task.requiredRole,
10603
10674
  task: node.task,
@@ -10607,7 +10678,7 @@ class NexusOrchestrator {
10607
10678
  node.status = "running";
10608
10679
  node.task.assignedAgent = agent.id;
10609
10680
  this.notifyStateChange();
10610
- await this.executeTask(agent, node, transferContext);
10681
+ await this.executeTask(agent, node, options.transferContext);
10611
10682
  }
10612
10683
  async executeTask(agent, node, transferContext) {
10613
10684
  if (!this.ctx || !agent.sessionID) {
@@ -10661,20 +10732,21 @@ Please continue from where the previous agent left off.`;
10661
10732
  const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
10662
10733
  const output = lastAssistantText(messages) || "Task completed (no output captured)";
10663
10734
  const duration = Date.now() - startTime;
10735
+ const cost = await this.safeAccountTaskCost(agent, node.task);
10664
10736
  const result = {
10665
10737
  success: true,
10666
10738
  output,
10667
10739
  duration,
10668
- tokensUsed: 0,
10669
- cost: this.estimateModelCost(agent.model.model, agent.model.provider)
10740
+ tokensUsed: cost.tokensUsed,
10741
+ cost: cost.cost,
10742
+ costProvenance: cost.provenance
10670
10743
  };
10671
10744
  this.dag.markComplete(node.id, result);
10672
10745
  this.todoEnforcer.completeTask(agent.id);
10673
10746
  agent.metrics.tasksCompleted++;
10674
10747
  agent.metrics.totalCost += result.cost;
10675
- this.totalSpent += result.cost;
10676
- this.costByAgent.set(agent.id, (this.costByAgent.get(agent.id) || 0) + result.cost);
10677
- this.checkBudget();
10748
+ agent.metrics.totalTokens += result.tokensUsed;
10749
+ this.trackCost(agent.id, `${agent.model.provider}/${agent.model.model}`, result.cost, result.tokensUsed, result.costProvenance);
10678
10750
  this.performanceTracker.record({
10679
10751
  model: agent.model.model,
10680
10752
  role: node.task.requiredRole,
@@ -10716,14 +10788,20 @@ Please continue from where the previous agent left off.`;
10716
10788
  } catch (error) {
10717
10789
  const duration = Date.now() - startTime;
10718
10790
  const errorMessage = error.message || "Task failed";
10791
+ const cost = await this.safeAccountTaskCost(agent, node.task);
10719
10792
  const result = {
10720
10793
  success: false,
10721
10794
  error: errorMessage,
10722
10795
  duration,
10723
- tokensUsed: 0,
10724
- cost: 0
10796
+ tokensUsed: cost.tokensUsed,
10797
+ cost: cost.cost,
10798
+ costProvenance: cost.provenance
10725
10799
  };
10726
10800
  this.dag.markFailed(node.id, new Error(errorMessage));
10801
+ node.result = result;
10802
+ this.trackCost(agent.id, `${agent.model.provider}/${agent.model.model}`, result.cost, result.tokensUsed, result.costProvenance);
10803
+ agent.metrics.totalCost += result.cost;
10804
+ agent.metrics.totalTokens += result.tokensUsed;
10727
10805
  this.todoEnforcer.completeTask(agent.id);
10728
10806
  agent.metrics.tasksFailed++;
10729
10807
  agent.status = "failed";
@@ -10834,7 +10912,7 @@ Please continue from where the previous agent left off.`;
10834
10912
  await this.terminateAgent(agent.id);
10835
10913
  node.status = "pending";
10836
10914
  this.notifyStateChange();
10837
- await this.spawnAndExecute(node, context);
10915
+ await this.spawnAndExecute(node, { transferContext: context });
10838
10916
  return;
10839
10917
  }
10840
10918
  if (policy.fallbackModels.length > 0) {
@@ -10843,7 +10921,7 @@ Please continue from where the previous agent left off.`;
10843
10921
  await this.terminateAgent(agent.id);
10844
10922
  node.status = "pending";
10845
10923
  this.notifyStateChange();
10846
- await this.spawnAgent({ role: node.task.requiredRole, model: fallbackModel });
10924
+ await this.spawnAndExecute(node, { modelOverride: fallbackModel });
10847
10925
  return;
10848
10926
  }
10849
10927
  }
@@ -11077,10 +11155,10 @@ Please continue from where the previous agent left off.`;
11077
11155
  if (qualified)
11078
11156
  return qualified;
11079
11157
  }
11080
- const bare = model.slice(model.indexOf("/") + 1);
11158
+ const bare = bareModelId(model);
11081
11159
  let best;
11082
11160
  for (const [key, cost] of this.modelCosts) {
11083
- if (key.slice(key.indexOf("/") + 1) !== bare)
11161
+ if (bareModelId(key) !== bare)
11084
11162
  continue;
11085
11163
  if (!best || cost.input < best.input)
11086
11164
  best = cost;
@@ -11162,28 +11240,96 @@ Please continue from where the previous agent left off.`;
11162
11240
  const scored = unique.map((m) => this.scoreModel(m, role, complexity));
11163
11241
  const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
11164
11242
  const affordable = scored.filter((s) => {
11165
- const cost = this.estimateModelCost(s.model, s.provider);
11166
- return cost <= budgetRemaining || cost === 0;
11243
+ const estimate = this.forecaster.estimateCost(complexity, s.model, s.provider);
11244
+ return estimate <= budgetRemaining || estimate === 0;
11167
11245
  });
11168
11246
  const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.overallScore - a.overallScore)[0];
11169
11247
  return {
11170
11248
  provider: best.provider,
11171
11249
  model: best.model,
11172
- estimatedCost: this.estimateModelCost(best.model, best.provider),
11250
+ estimatedCost: this.forecaster.estimateCost(complexity, best.model, best.provider),
11173
11251
  estimatedQuality: best.qualityScore,
11174
11252
  reasoning: best.reasoning
11175
11253
  };
11176
11254
  }
11177
- trackCost(agentId, model, cost, tokens) {
11255
+ trackCost(agentId, model, cost, tokens, provenance) {
11178
11256
  this.totalSpent += cost;
11179
11257
  const agentCost = this.costByAgent.get(agentId) || 0;
11180
11258
  this.costByAgent.set(agentId, agentCost + cost);
11181
11259
  const modelCost = this.costByModel.get(model) || 0;
11182
11260
  this.costByModel.set(model, modelCost + cost);
11183
- this.costHistory.push({ timestamp: Date.now(), cost, agentId });
11261
+ this.tokensByModel.set(model, (this.tokensByModel.get(model) || 0) + tokens);
11262
+ this.recordProvenance(model, cost, provenance);
11263
+ this.costHistory.push({ timestamp: Date.now(), cost, agentId, model, tokens, provenance });
11184
11264
  this.checkBudget();
11185
11265
  this.notifyStateChange();
11186
11266
  }
11267
+ recordProvenance(model, cost, provenance) {
11268
+ const measured = provenance.usage === "measured";
11269
+ const prior = this.costProvenance.get(model) ?? {
11270
+ usage: provenance.usage,
11271
+ pricing: provenance.pricing,
11272
+ measuredEntries: 0,
11273
+ estimatedEntries: 0,
11274
+ measuredSpend: 0,
11275
+ estimatedSpend: 0
11276
+ };
11277
+ prior.usage = provenance.usage;
11278
+ prior.pricing = provenance.pricing;
11279
+ if (measured) {
11280
+ prior.measuredEntries++;
11281
+ prior.measuredSpend += cost;
11282
+ } else {
11283
+ prior.estimatedEntries++;
11284
+ prior.estimatedSpend += cost;
11285
+ }
11286
+ this.costProvenance.set(model, prior);
11287
+ }
11288
+ safeAccountTaskCost(agent, task) {
11289
+ return this.accountTaskCost(agent, task).catch(() => ({
11290
+ cost: 0,
11291
+ tokensUsed: 0,
11292
+ provenance: { usage: "estimated", pricing: "unknown-model" }
11293
+ }));
11294
+ }
11295
+ async accountTaskCost(agent, task) {
11296
+ const model = `${agent.model.provider}/${agent.model.model}`;
11297
+ const read = agent.sessionID ? await this.readSessionTokens(agent.sessionID) : { read: false };
11298
+ if (read.read) {
11299
+ const measured = this.forecaster.measureCost(read.usage, model, agent.model.provider);
11300
+ return {
11301
+ cost: measured.cost,
11302
+ tokensUsed: measured.tokens,
11303
+ provenance: { usage: "measured", pricing: measured.pricingSource }
11304
+ };
11305
+ }
11306
+ const predicted = this.forecaster.forecastTask(task, task.requiredRole, model, task.complexity);
11307
+ return {
11308
+ cost: predicted.estimatedCost,
11309
+ tokensUsed: predicted.estimatedInputTokens + predicted.estimatedOutputTokens,
11310
+ provenance: { usage: "estimated", pricing: predicted.pricingSource }
11311
+ };
11312
+ }
11313
+ async readSessionTokens(sessionID) {
11314
+ const finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
11315
+ try {
11316
+ const session = await this.ctx?.session.get({ sessionID });
11317
+ const tokens = session?.tokens;
11318
+ if (!tokens)
11319
+ return { read: false };
11320
+ return {
11321
+ read: true,
11322
+ usage: {
11323
+ input: finite(tokens.input),
11324
+ output: finite(tokens.output),
11325
+ reasoning: finite(tokens.reasoning),
11326
+ cache: { read: finite(tokens.cache?.read), write: finite(tokens.cache?.write) }
11327
+ }
11328
+ };
11329
+ } catch {
11330
+ return { read: false };
11331
+ }
11332
+ }
11187
11333
  checkBudget() {
11188
11334
  const remaining = this.budget.maxTotalCost - this.totalSpent;
11189
11335
  const remainingPercent = remaining / this.budget.maxTotalCost;
@@ -11274,7 +11420,11 @@ Please continue from where the previous agent left off.`;
11274
11420
  totalSpent: state.totalSpent,
11275
11421
  budgetRemaining: state.budgetRemaining,
11276
11422
  byAgent: Object.fromEntries(this.costByAgent),
11277
- byModel: Object.fromEntries(this.costByModel)
11423
+ byModel: Object.fromEntries(this.costByModel),
11424
+ tokensByModel: Object.fromEntries(this.tokensByModel),
11425
+ provenance: Object.fromEntries(this.costProvenance),
11426
+ measuredEntries: sumBy(this.costProvenance, (p) => p.measuredEntries),
11427
+ estimatedEntries: sumBy(this.costProvenance, (p) => p.estimatedEntries)
11278
11428
  }, null, 2);
11279
11429
  }
11280
11430
  pause() {
package/dist/tui.js CHANGED
@@ -454,6 +454,78 @@ var PRESETS = {
454
454
 
455
455
  // src/tui.tsx
456
456
  import { jsxDEV, Fragment } from "@opentui/solid/jsx-dev-runtime";
457
+ var NEXUS_AGENT_PREFIX = "nexus-";
458
+ function metadataString(metadata, key) {
459
+ const value = metadata?.[key];
460
+ return typeof value === "string" && value.length > 0 ? value : undefined;
461
+ }
462
+ function roleFromAgent(agent) {
463
+ if (!agent)
464
+ return;
465
+ const role = agent.startsWith(NEXUS_AGENT_PREFIX) ? agent.slice(NEXUS_AGENT_PREFIX.length) : agent;
466
+ return role.length > 0 ? role : undefined;
467
+ }
468
+ function modelLabel(model) {
469
+ if (!model?.providerID || !model.id)
470
+ return;
471
+ return `${model.providerID}/${model.id}`;
472
+ }
473
+ function titleCase(value) {
474
+ return value.charAt(0).toUpperCase() + value.slice(1);
475
+ }
476
+ function sidebarAgentFor(session, status, now) {
477
+ const role = metadataString(session.metadata, "nexusRole") ?? roleFromAgent(session.agent);
478
+ return {
479
+ id: session.id,
480
+ name: role ? titleCase(role) : session.title || session.id.slice(0, 12),
481
+ role: role ?? "agent",
482
+ status,
483
+ model: metadataString(session.metadata, "nexusModel") ?? modelLabel(session.model) ?? "",
484
+ sessionID: session.id,
485
+ spawnedAt: now,
486
+ tasksCompleted: 0,
487
+ tasksFailed: 0
488
+ };
489
+ }
490
+ function sidebarChildSessionIDs(family, currentSessionID) {
491
+ if (!Array.isArray(family))
492
+ return [];
493
+ return family.filter((id) => id !== currentSessionID);
494
+ }
495
+ function collectSidebarAgents(source, currentSessionID, statusOf, now) {
496
+ const childIDs = sidebarChildSessionIDs(source.family(currentSessionID), currentSessionID);
497
+ if (childIDs.length === 0)
498
+ return [];
499
+ const agents = [];
500
+ for (const id of childIDs) {
501
+ try {
502
+ const session = source.get(id);
503
+ if (!session)
504
+ continue;
505
+ let status = "completed";
506
+ try {
507
+ status = statusOf(id);
508
+ } catch {}
509
+ agents.push(sidebarAgentFor(session, status, now));
510
+ } catch {}
511
+ }
512
+ return agents;
513
+ }
514
+ function mergeSidebarAgents(agents, polled, childIDs) {
515
+ const next = agents.map((agent) => ({ ...agent }));
516
+ for (const agent of polled) {
517
+ const existing = next.find((a) => a.sessionID === agent.sessionID);
518
+ if (existing) {
519
+ existing.status = agent.status;
520
+ existing.name = agent.name;
521
+ existing.role = agent.role;
522
+ existing.model = agent.model;
523
+ } else {
524
+ next.push(agent);
525
+ }
526
+ }
527
+ return next.filter((a) => a.sessionID && childIDs.includes(a.sessionID) || a.status === "completed" || a.status === "failed");
528
+ }
457
529
  var tui_default = define({
458
530
  id: "nexus.cli",
459
531
  setup(context) {
@@ -848,69 +920,20 @@ var tui_default = define({
848
920
  const currentSessionID = currentRoute.sessionID;
849
921
  if (!currentSessionID)
850
922
  return;
851
- const family = context.data.session.family(currentSessionID);
852
- if (!family || !Array.isArray(family))
853
- return;
854
- const childIDs = family.filter((id) => id !== currentSessionID);
855
- try {
856
- const allSessions = context.data.session.list();
857
- if (allSessions && Array.isArray(allSessions)) {
858
- for (const s of allSessions) {
859
- if (!s || !s.id)
860
- continue;
861
- const meta = s.metadata;
862
- if (meta?.nexusRole && !childIDs.includes(s.id) && s.id !== currentSessionID) {
863
- childIDs.push(s.id);
864
- }
865
- }
866
- }
867
- } catch {}
923
+ const source = context.data.session;
924
+ const childIDs = sidebarChildSessionIDs(source.family(currentSessionID), currentSessionID);
868
925
  if (childIDs.length === 0) {
869
926
  setSidebarState((draft) => {
870
- draft.agents = draft.agents.filter((a) => a.status === "completed" || a.status === "failed");
927
+ draft.agents = mergeSidebarAgents(draft.agents, [], []);
871
928
  });
872
929
  return;
873
930
  }
874
- const newAgents = childIDs.map((id) => {
875
- try {
876
- const session = context.data.session.get(id);
877
- if (!session)
878
- return null;
879
- const meta = session.metadata || {};
880
- let status = "completed";
881
- try {
882
- status = context.data.session.status(id) === "running" ? "working" : "completed";
883
- } catch {}
884
- return {
885
- id,
886
- name: meta.nexusRole ? `${meta.nexusRole.charAt(0).toUpperCase() + meta.nexusRole.slice(1)}` : session?.title || id.slice(0, 12),
887
- role: meta.nexusRole || "agent",
888
- status,
889
- model: meta.nexusModel || "",
890
- sessionID: id,
891
- spawnedAt: new Date().toISOString(),
892
- tasksCompleted: 0,
893
- tasksFailed: 0
894
- };
895
- } catch {
896
- return null;
897
- }
898
- }).filter(Boolean);
899
- if (newAgents.length > 0) {
900
- setSidebarState((draft) => {
901
- for (const agent of newAgents) {
902
- const existing = draft.agents.find((a) => a.sessionID === agent.sessionID);
903
- if (existing) {
904
- existing.status = agent.status;
905
- existing.name = agent.name;
906
- existing.model = agent.model;
907
- } else {
908
- draft.agents.push(agent);
909
- }
910
- }
911
- draft.agents = draft.agents.filter((a) => a.sessionID && childIDs.includes(a.sessionID) || a.status === "completed" || a.status === "failed");
912
- });
913
- }
931
+ const newAgents = collectSidebarAgents(source, currentSessionID, (id) => context.data.session.status(id) === "running" ? "working" : "completed", new Date().toISOString());
932
+ if (newAgents.length === 0)
933
+ return;
934
+ setSidebarState((draft) => {
935
+ draft.agents = mergeSidebarAgents(draft.agents, newAgents, childIDs);
936
+ });
914
937
  } catch {}
915
938
  };
916
939
  const unsubSessionSucceeded = context.data.on("session.execution.succeeded", (event) => {
@@ -1039,5 +1062,9 @@ var tui_default = define({
1039
1062
  }
1040
1063
  });
1041
1064
  export {
1042
- tui_default as default
1065
+ collectSidebarAgents,
1066
+ tui_default as default,
1067
+ mergeSidebarAgents,
1068
+ sidebarAgentFor,
1069
+ sidebarChildSessionIDs
1043
1070
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "description": "Adaptive Multi-Agent Orchestration with Cost Intelligence for OpenCode V2",
5
5
  "keywords": [
6
6
  "opencode",