@outputai/cli 0.7.1-next.d67ad85.0 → 0.7.1-next.db8ddd7.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 (34) hide show
  1. package/dist/api/generated/api.d.ts +2 -2
  2. package/dist/api/generated/api.js +2 -2
  3. package/dist/api/workflow_catalog.d.ts +7 -0
  4. package/dist/api/workflow_catalog.js +11 -0
  5. package/dist/api/workflow_catalog.spec.d.ts +1 -0
  6. package/dist/api/workflow_catalog.spec.js +30 -0
  7. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  8. package/dist/commands/workflow/list.d.ts +2 -0
  9. package/dist/commands/workflow/list.js +23 -21
  10. package/dist/commands/workflow/list.spec.js +57 -0
  11. package/dist/commands/workflow/status.js +6 -1
  12. package/dist/generated/framework_version.json +1 -1
  13. package/dist/services/cost_calculator.d.ts +1 -5
  14. package/dist/services/cost_calculator.js +214 -102
  15. package/dist/services/cost_calculator.spec.js +329 -253
  16. package/dist/services/workflow_runs.js +6 -1
  17. package/dist/types/cost.d.ts +64 -23
  18. package/dist/types/cost.js +4 -0
  19. package/dist/utils/cost_formatter.js +65 -43
  20. package/dist/utils/format_workflow_result.js +4 -2
  21. package/dist/utils/format_workflow_result.spec.js +13 -3
  22. package/dist/utils/normalize_workflow_status.d.ts +8 -0
  23. package/dist/utils/normalize_workflow_status.js +8 -0
  24. package/dist/utils/normalize_workflow_status.spec.d.ts +1 -0
  25. package/dist/utils/normalize_workflow_status.spec.js +13 -0
  26. package/dist/utils/scenario_resolver.js +3 -11
  27. package/dist/utils/scenario_resolver.spec.js +5 -9
  28. package/dist/views/dev/components/workflow_status.js +1 -1
  29. package/dist/views/dev/hooks/use_run_detail.js +6 -1
  30. package/dist/views/dev/hooks/use_run_detail.spec.js +1 -1
  31. package/dist/views/dev/hooks/use_workflow_catalog.js +2 -6
  32. package/dist/views/dev/panels/runs_panel.js +1 -1
  33. package/oclif.manifest.json +19 -2
  34. package/package.json +4 -4
@@ -2,6 +2,7 @@
2
2
  * Workflow runs service for fetching workflow run data from the API
3
3
  */
4
4
  import { getWorkflowRuns } from '#api/generated/api.js';
5
+ import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
5
6
  export async function fetchWorkflowRuns(options = {}) {
6
7
  const params = {};
7
8
  if (options.limit) {
@@ -21,8 +22,12 @@ export async function fetchWorkflowRuns(options = {}) {
21
22
  throw new Error('API returned invalid response (missing data)');
22
23
  }
23
24
  const data = response.data;
25
+ const runs = (data.runs || []).map(run => ({
26
+ ...run,
27
+ status: normalizeWorkflowStatus(run.status)
28
+ }));
24
29
  return {
25
- runs: data.runs || [],
30
+ runs,
26
31
  count: data.count || 0
27
32
  };
28
33
  }
@@ -2,6 +2,10 @@
2
2
  * Cost Calculator Types
3
3
  *
4
4
  * TypeScript interfaces for trace parsing, pricing configuration, and cost reports.
5
+ *
6
+ * Costs are sourced from the trace events themselves (the as-charged "original"
7
+ * cost), with `costs.yml` applied as an optional override layer (the "adjusted"
8
+ * cost). Both figures are surfaced per model and per host.
5
9
  */
6
10
  export interface TokenUsage {
7
11
  inputTokens?: number;
@@ -9,6 +13,25 @@ export interface TokenUsage {
9
13
  cachedInputTokens?: number;
10
14
  reasoningTokens?: number;
11
15
  }
16
+ export type { LLMUsageEvent } from '@outputai/llm';
17
+ import type { LLMUsageEvent } from '@outputai/llm';
18
+ export type LLMUsageLine = LLMUsageEvent['usage'][number];
19
+ export interface HTTPCostEvent {
20
+ type: 'http:request:cost';
21
+ url: string;
22
+ requestId: string;
23
+ total: number;
24
+ }
25
+ export interface HTTPCountEvent {
26
+ type: 'http:request:count';
27
+ url: string;
28
+ requestId: string;
29
+ }
30
+ export interface NodeAttributes {
31
+ 'llm:usage'?: LLMUsageEvent;
32
+ 'http:request:cost'?: HTTPCostEvent;
33
+ 'http:request:count'?: HTTPCountEvent;
34
+ }
12
35
  export interface TraceNode {
13
36
  id?: string;
14
37
  kind: string;
@@ -17,15 +40,16 @@ export interface TraceNode {
17
40
  endedAt?: number;
18
41
  children?: TraceNode[];
19
42
  input?: Record<string, unknown>;
20
- output?: Record<string, unknown> & {
21
- usage?: TokenUsage;
22
- };
43
+ output?: Record<string, unknown>;
44
+ attributes?: NodeAttributes;
23
45
  }
24
46
  export interface LLMCall {
25
47
  stepName: string;
26
48
  llmName: string;
27
49
  model: string;
28
50
  usage: TokenUsage;
51
+ originalCost: number;
52
+ lines: LLMUsageLine[];
29
53
  }
30
54
  export interface HTTPCall {
31
55
  stepName: string;
@@ -34,6 +58,8 @@ export interface HTTPCall {
34
58
  input: Record<string, unknown>;
35
59
  output: Record<string, unknown>;
36
60
  status?: number;
61
+ host: string;
62
+ originalCost?: number;
37
63
  }
38
64
  export interface ModelPricing {
39
65
  provider: string;
@@ -80,47 +106,60 @@ export interface LLMCostResult {
80
106
  output: number;
81
107
  cached: number;
82
108
  reasoning: number;
83
- cost: number;
84
- warning?: string;
109
+ originalCost: number;
110
+ adjustedCost: number;
85
111
  }
86
112
  export interface ServiceCostResult {
87
113
  step: string;
88
114
  cost: number;
89
115
  usage: string;
116
+ kind: 'computed' | 'estimated' | 'failed';
90
117
  model?: string;
91
118
  endpoint?: string;
92
119
  warning?: string;
93
120
  details?: Record<string, unknown>;
94
121
  }
95
- export interface ServiceCostSummary {
96
- serviceName: string;
97
- calls: ServiceCostResult[];
98
- totalCost: number;
122
+ export interface HTTPCostResult {
123
+ step: string;
124
+ host: string;
125
+ usage: string;
126
+ originalCost: number;
127
+ adjustedCost: number;
128
+ }
129
+ export interface HostCostSummary {
130
+ host: string;
131
+ calls: HTTPCostResult[];
132
+ originalTotalCost: number;
133
+ adjustedTotalCost: number;
99
134
  }
100
135
  export interface CostReport {
101
136
  traceFile: string;
102
137
  workflowName: string;
103
138
  durationMs: number | null;
104
139
  llmCalls: LLMCostResult[];
105
- llmTotalCost: number;
140
+ llmOriginalCost: number;
141
+ llmAdjustedCost: number;
106
142
  totalInputTokens: number;
107
143
  totalOutputTokens: number;
108
144
  totalCachedTokens: number;
109
145
  totalReasoningTokens: number;
110
- unknownModels: string[];
111
- services: ServiceCostSummary[];
112
- serviceTotalCost: number;
146
+ httpCosts: HostCostSummary[];
147
+ httpOriginalCost: number;
148
+ httpAdjustedCost: number;
149
+ originalTotalCost: number;
113
150
  totalCost: number;
114
151
  }
115
152
  export interface LLMModelSummary {
116
153
  model: string;
117
154
  count: number;
118
- cost: number;
155
+ originalCost: number;
156
+ adjustedCost: number;
119
157
  }
120
- export interface ServiceSummary {
121
- serviceName: string;
158
+ export interface HostSummary {
159
+ host: string;
122
160
  callCount: number;
123
- cost: number;
161
+ originalCost: number;
162
+ adjustedCost: number;
124
163
  }
125
164
  export interface VerboseFlags {
126
165
  hasReasoning: boolean;
@@ -132,18 +171,20 @@ export interface ParsedCostData {
132
171
  duration: string;
133
172
  llmModels: LLMModelSummary[];
134
173
  llmTotalCalls: number;
135
- llmTotalCost: number;
136
- services: ServiceSummary[];
137
- serviceTotalCalls: number;
138
- serviceTotalCost: number;
174
+ llmOriginalCost: number;
175
+ llmAdjustedCost: number;
176
+ hosts: HostSummary[];
177
+ httpTotalCalls: number;
178
+ httpOriginalCost: number;
179
+ httpAdjustedCost: number;
139
180
  verbose: VerboseFlags;
140
181
  llmCalls: LLMCostResult[];
141
- serviceDetails: ServiceCostSummary[];
182
+ httpDetails: HostCostSummary[];
142
183
  totalInputTokens: number;
143
184
  totalOutputTokens: number;
144
185
  totalCachedTokens: number;
145
186
  totalReasoningTokens: number;
187
+ originalTotalCost: number;
146
188
  totalCost: number;
147
- unknownModels: string[];
148
189
  isEmpty: boolean;
149
190
  }
@@ -2,5 +2,9 @@
2
2
  * Cost Calculator Types
3
3
  *
4
4
  * TypeScript interfaces for trace parsing, pricing configuration, and cost reports.
5
+ *
6
+ * Costs are sourced from the trace events themselves (the as-charged "original"
7
+ * cost), with `costs.yml` applied as an optional override layer (the "adjusted"
8
+ * cost). Both figures are surfaced per model and per host.
5
9
  */
6
10
  export {};
@@ -29,76 +29,96 @@ export function parseCostData(report) {
29
29
  const byModel = {};
30
30
  for (const r of report.llmCalls) {
31
31
  if (!byModel[r.model]) {
32
- byModel[r.model] = { count: 0, cost: 0 };
32
+ byModel[r.model] = { count: 0, originalCost: 0, adjustedCost: 0 };
33
33
  }
34
34
  byModel[r.model].count++;
35
- byModel[r.model].cost += r.cost;
35
+ byModel[r.model].originalCost += r.originalCost;
36
+ byModel[r.model].adjustedCost += r.adjustedCost;
36
37
  }
37
38
  const llmModels = Object.entries(byModel)
38
- .sort((a, b) => b[1].cost - a[1].cost)
39
- .map(([model, stats]) => ({ model, count: stats.count, cost: stats.cost }));
40
- const services = [...report.services]
41
- .sort((a, b) => b.totalCost - a.totalCost)
42
- .map(s => ({ serviceName: s.serviceName, callCount: s.calls.length, cost: s.totalCost }));
43
- const serviceTotalCalls = services.reduce((sum, s) => sum + s.callCount, 0);
39
+ .sort((a, b) => b[1].adjustedCost - a[1].adjustedCost)
40
+ .map(([model, s]) => ({ model, ...s }));
41
+ const hosts = [...report.httpCosts]
42
+ .sort((a, b) => b.adjustedTotalCost - a.adjustedTotalCost)
43
+ .map(h => ({
44
+ host: h.host,
45
+ callCount: h.calls.length,
46
+ originalCost: h.originalTotalCost,
47
+ adjustedCost: h.adjustedTotalCost
48
+ }));
49
+ const httpTotalCalls = hosts.reduce((sum, h) => sum + h.callCount, 0);
44
50
  return {
45
51
  traceFile: report.traceFile,
46
52
  workflowName: report.workflowName,
47
53
  duration: report.durationMs ? `${(report.durationMs / 1000).toFixed(1)}s` : 'N/A',
48
54
  llmModels,
49
55
  llmTotalCalls: report.llmCalls.length,
50
- llmTotalCost: report.llmTotalCost,
51
- services,
52
- serviceTotalCalls,
53
- serviceTotalCost: report.serviceTotalCost,
56
+ llmOriginalCost: report.llmOriginalCost,
57
+ llmAdjustedCost: report.llmAdjustedCost,
58
+ hosts,
59
+ httpTotalCalls,
60
+ httpOriginalCost: report.httpOriginalCost,
61
+ httpAdjustedCost: report.httpAdjustedCost,
54
62
  verbose: {
55
63
  hasReasoning: report.totalReasoningTokens > 0,
56
64
  hasCached: report.totalCachedTokens > 0
57
65
  },
58
66
  llmCalls: report.llmCalls,
59
- serviceDetails: report.services,
67
+ httpDetails: report.httpCosts,
60
68
  totalInputTokens: report.totalInputTokens,
61
69
  totalOutputTokens: report.totalOutputTokens,
62
70
  totalCachedTokens: report.totalCachedTokens,
63
71
  totalReasoningTokens: report.totalReasoningTokens,
72
+ originalTotalCost: report.originalTotalCost,
64
73
  totalCost: report.totalCost,
65
- unknownModels: report.unknownModels,
66
- isEmpty: report.llmCalls.length === 0 && report.services.length === 0
74
+ isEmpty: report.llmCalls.length === 0 && report.httpCosts.length === 0
67
75
  };
68
76
  }
69
77
  function formatSummary(data) {
70
78
  const lines = [];
71
79
  if (data.llmModels.length > 0) {
72
80
  const table = new Table({
73
- head: ['Model', 'Calls', 'Cost'],
81
+ head: ['Model', 'Calls', 'Original', 'Adjusted'],
74
82
  style: { head: ['cyan'] },
75
- colAligns: ['left', 'right', 'right']
83
+ colAligns: ['left', 'right', 'right', 'right']
76
84
  });
77
85
  for (const m of data.llmModels) {
78
- table.push([m.model, pluralize(m.count, 'call'), formatCurrency(m.cost)]);
86
+ table.push([
87
+ m.model,
88
+ pluralize(m.count, 'call'),
89
+ formatCurrency(m.originalCost),
90
+ formatCurrency(m.adjustedCost)
91
+ ]);
79
92
  }
80
93
  table.push([
81
94
  'Subtotal',
82
95
  pluralize(data.llmTotalCalls, 'call'),
83
- formatCurrency(data.llmTotalCost)
96
+ formatCurrency(data.llmOriginalCost),
97
+ formatCurrency(data.llmAdjustedCost)
84
98
  ]);
85
99
  lines.push('LLM Costs:');
86
100
  lines.push(table.toString());
87
101
  lines.push('');
88
102
  }
89
- if (data.services.length > 0) {
103
+ if (data.hosts.length > 0) {
90
104
  const table = new Table({
91
- head: ['Service', 'Calls', 'Cost'],
105
+ head: ['Host', 'Calls', 'Original', 'Adjusted'],
92
106
  style: { head: ['cyan'] },
93
- colAligns: ['left', 'right', 'right']
107
+ colAligns: ['left', 'right', 'right', 'right']
94
108
  });
95
- for (const s of data.services) {
96
- table.push([s.serviceName, pluralize(s.callCount, 'call'), formatCurrency(s.cost)]);
109
+ for (const h of data.hosts) {
110
+ table.push([
111
+ h.host,
112
+ pluralize(h.callCount, 'call'),
113
+ formatCurrency(h.originalCost),
114
+ formatCurrency(h.adjustedCost)
115
+ ]);
97
116
  }
98
117
  table.push([
99
118
  'Subtotal',
100
- pluralize(data.serviceTotalCalls, 'call'),
101
- formatCurrency(data.serviceTotalCost)
119
+ pluralize(data.httpTotalCalls, 'call'),
120
+ formatCurrency(data.httpOriginalCost),
121
+ formatCurrency(data.httpAdjustedCost)
102
122
  ]);
103
123
  lines.push('API Costs:');
104
124
  lines.push(table.toString());
@@ -119,8 +139,8 @@ function formatVerbose(data) {
119
139
  head.push('Reasoning');
120
140
  colAligns.push('right');
121
141
  }
122
- head.push('Cost');
123
- colAligns.push('right');
142
+ head.push('Original', 'Adjusted');
143
+ colAligns.push('right', 'right');
124
144
  const table = new Table({
125
145
  head,
126
146
  style: { head: ['cyan'] },
@@ -139,7 +159,7 @@ function formatVerbose(data) {
139
159
  if (data.verbose.hasReasoning) {
140
160
  row.push(formatNumber(r.reasoning));
141
161
  }
142
- row.push(formatCurrency(r.cost) + (r.warning ? ` (${r.warning})` : ''));
162
+ row.push(formatCurrency(r.originalCost), formatCurrency(r.adjustedCost));
143
163
  table.push(row);
144
164
  }
145
165
  const totalRow = [
@@ -154,29 +174,34 @@ function formatVerbose(data) {
154
174
  if (data.verbose.hasReasoning) {
155
175
  totalRow.push(formatNumber(data.totalReasoningTokens));
156
176
  }
157
- totalRow.push(formatCurrency(data.llmTotalCost));
177
+ totalRow.push(formatCurrency(data.llmOriginalCost), formatCurrency(data.llmAdjustedCost));
158
178
  table.push(totalRow);
159
179
  lines.push('LLM Calls:');
160
180
  lines.push(table.toString());
161
181
  lines.push('');
162
182
  }
163
- if (data.serviceDetails.length > 0) {
183
+ if (data.httpDetails.length > 0) {
164
184
  const table = new Table({
165
- head: ['Service', 'Step', 'Usage', 'Cost'],
185
+ head: ['Host', 'Step', 'Usage', 'Original', 'Adjusted'],
166
186
  style: { head: ['cyan'] },
167
- colAligns: ['left', 'left', 'right', 'right']
187
+ colAligns: ['left', 'left', 'right', 'right', 'right']
168
188
  });
169
- for (const service of data.serviceDetails) {
170
- for (const call of service.calls) {
189
+ for (const host of data.httpDetails) {
190
+ for (const call of host.calls) {
171
191
  table.push([
172
- service.serviceName,
192
+ host.host,
173
193
  call.step,
174
194
  call.usage,
175
- formatCurrency(call.cost)
195
+ formatCurrency(call.originalCost),
196
+ formatCurrency(call.adjustedCost)
176
197
  ]);
177
198
  }
178
199
  }
179
- table.push(['Subtotal', '', '', formatCurrency(data.serviceTotalCost)]);
200
+ table.push([
201
+ 'Subtotal', '', '',
202
+ formatCurrency(data.httpOriginalCost),
203
+ formatCurrency(data.httpAdjustedCost)
204
+ ]);
180
205
  lines.push('API Calls:');
181
206
  lines.push(table.toString());
182
207
  lines.push('');
@@ -203,13 +228,10 @@ export function formatCostReport(report, options = {}) {
203
228
  colAligns: ['left', 'right'],
204
229
  colWidths: [36, 12]
205
230
  });
206
- totalTable.push(['TOTAL ESTIMATED COST', formatCurrency(data.totalCost)]);
231
+ totalTable.push(['TOTAL ESTIMATED COST (adjusted)', formatCurrency(data.totalCost)]);
232
+ totalTable.push(['As-charged (from trace)', formatCurrency(data.originalTotalCost)]);
207
233
  lines.push(totalTable.toString());
208
234
  }
209
- if (data.unknownModels.length > 0) {
210
- lines.push('');
211
- lines.push(`Warning: Unknown models (add to config/costs.yml): ${data.unknownModels.join(', ')}`);
212
- }
213
235
  if (data.isEmpty) {
214
236
  lines.push('No billable calls found in trace.');
215
237
  }
@@ -1,15 +1,17 @@
1
+ import { normalizeWorkflowStatus } from './normalize_workflow_status.js';
1
2
  export const ERROR_STATUSES = new Set(['failed', 'canceled', 'terminated', 'timed_out']);
2
3
  export function formatWorkflowResult(result) {
4
+ const status = normalizeWorkflowStatus(result.status);
3
5
  const lines = [
4
6
  `Workflow ID: ${result.workflowId || 'unknown'}`,
5
7
  ''
6
8
  ];
7
- if (result.status === 'completed') {
9
+ if (status === 'completed') {
8
10
  lines.push('Output:');
9
11
  lines.push(JSON.stringify(result.output, null, 2));
10
12
  }
11
13
  else {
12
- lines.push(`Status: ${result.status || 'unknown'}`);
14
+ lines.push(`Status: ${status || 'unknown'}`);
13
15
  if (result.error) {
14
16
  lines.push(`Error: ${result.error}`);
15
17
  }
@@ -46,16 +46,26 @@ describe('formatWorkflowResult', () => {
46
46
  expect(result).toContain('Status: canceled');
47
47
  expect(result).toContain('Error: Workflow was canceled');
48
48
  });
49
- it('should display status without error line for continued workflows', () => {
49
+ it('should display status without error line for continued_as_new workflows', () => {
50
50
  const result = formatWorkflowResult({
51
51
  workflowId: 'wf-cont',
52
- status: 'continued',
52
+ status: 'continued_as_new',
53
53
  output: null,
54
54
  error: null
55
55
  });
56
- expect(result).toContain('Status: continued');
56
+ expect(result).toContain('Status: continued_as_new');
57
57
  expect(result).not.toContain('Error:');
58
58
  });
59
+ it('temporarily normalizes legacy continued status to continued_as_new', () => {
60
+ const legacyResult = {
61
+ workflowId: 'wf-cont',
62
+ status: 'continued',
63
+ output: null,
64
+ error: null
65
+ };
66
+ const result = formatWorkflowResult(legacyResult);
67
+ expect(result).toContain('Status: continued_as_new');
68
+ });
59
69
  it('should omit error line when error is null on failed workflow', () => {
60
70
  const result = formatWorkflowResult({
61
71
  workflowId: 'wf-789',
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Temporary compatibility for API responses produced before CONTINUED_AS_NEW
3
+ * was exposed as `continued_as_new`.
4
+ *
5
+ * @param status - Workflow status from the API
6
+ * @returns Normalized workflow status
7
+ */
8
+ export declare const normalizeWorkflowStatus: <T extends string | null | undefined>(status: T) => T | "continued_as_new";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Temporary compatibility for API responses produced before CONTINUED_AS_NEW
3
+ * was exposed as `continued_as_new`.
4
+ *
5
+ * @param status - Workflow status from the API
6
+ * @returns Normalized workflow status
7
+ */
8
+ export const normalizeWorkflowStatus = (status) => status === 'continued' ? 'continued_as_new' : status;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeWorkflowStatus } from './normalize_workflow_status.js';
3
+ describe('normalizeWorkflowStatus', () => {
4
+ it('temporarily maps continued to continued_as_new', () => {
5
+ expect(normalizeWorkflowStatus('continued')).toBe('continued_as_new');
6
+ });
7
+ it('leaves other statuses and nullish values unchanged', () => {
8
+ expect(normalizeWorkflowStatus('completed')).toBe('completed');
9
+ expect(normalizeWorkflowStatus('continued_as_new')).toBe('continued_as_new');
10
+ expect(normalizeWorkflowStatus(null)).toBeNull();
11
+ expect(normalizeWorkflowStatus(undefined)).toBeUndefined();
12
+ });
13
+ });
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync } from 'node:fs';
2
2
  import { dirname, resolve } from 'node:path';
3
- import { getWorkflowCatalog } from '#api/generated/api.js';
3
+ import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
4
4
  import { getWorkflowsBasePath } from '#utils/paths.js';
5
5
  const SCENARIOS_DIR = 'scenarios';
6
6
  const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
@@ -30,17 +30,9 @@ export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkfl
30
30
  }
31
31
  async function fetchWorkflowPath(workflowName) {
32
32
  try {
33
- const response = await getWorkflowCatalog();
34
- const data = response?.data;
35
- const workflows = data?.workflows;
36
- if (!workflows) {
37
- return null;
38
- }
33
+ const workflows = await fetchWorkflowCatalog();
39
34
  const workflow = workflows.find(w => w.name === workflowName);
40
- if (!workflow) {
41
- return null;
42
- }
43
- return workflow.path ?? null;
35
+ return workflow?.path ?? null;
44
36
  }
45
37
  catch {
46
38
  return null;
@@ -1,23 +1,19 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { resolveScenarioPath, getScenarioNotFoundMessage, extractWorkflowRelativePath, findWorkflowDirectoryFromPath, listScenariosForWorkflow } from './scenario_resolver.js';
3
3
  import * as fs from 'node:fs';
4
- import * as api from '#api/generated/api.js';
4
+ import * as catalog from '#api/workflow_catalog.js';
5
5
  vi.mock('node:fs', () => ({
6
6
  existsSync: vi.fn(),
7
7
  readdirSync: vi.fn()
8
8
  }));
9
- vi.mock('#api/generated/api.js', () => ({
10
- getWorkflowCatalog: vi.fn()
9
+ vi.mock('#api/workflow_catalog.js', () => ({
10
+ fetchWorkflowCatalog: vi.fn()
11
11
  }));
12
12
  function mockCatalog(workflows) {
13
- vi.mocked(api.getWorkflowCatalog).mockResolvedValue({
14
- data: { workflows },
15
- status: 200,
16
- headers: new Headers()
17
- });
13
+ vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue(workflows);
18
14
  }
19
15
  function mockCatalogFailure() {
20
- vi.mocked(api.getWorkflowCatalog).mockRejectedValue(new Error('API unavailable'));
16
+ vi.mocked(catalog.fetchWorkflowCatalog).mockRejectedValue(new Error('API unavailable'));
21
17
  }
22
18
  describe('extractWorkflowRelativePath', () => {
23
19
  it('should extract relative path from workflow.js path', () => {
@@ -7,7 +7,7 @@ const WORKFLOW_STATUS_MAP = {
7
7
  canceled: { icon: '○', color: 'gray' },
8
8
  terminated: { icon: '✗', color: 'gray' },
9
9
  timed_out: { icon: '✗', color: 'red' },
10
- continued: { icon: '↻', color: 'blue' }
10
+ continued_as_new: { icon: '↻', color: 'blue' }
11
11
  };
12
12
  const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
13
13
  export const resolveWorkflowStatus = (status) => WORKFLOW_STATUS_MAP[status] ?? DEFAULT_DISPLAY;
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { getWorkflowIdResult, getWorkflowIdRunsRidResult, getWorkflowIdTraceLog, getWorkflowIdRunsRidTraceLog } from '#api/generated/api.js';
4
+ import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
4
5
  const EMPTY_DETAIL = {
5
6
  result: null,
6
7
  trace: null,
@@ -88,7 +89,11 @@ const fetchResult = async (workflowId, runId) => {
88
89
  const response = runId ?
89
90
  await getWorkflowIdRunsRidResult(workflowId, runId) :
90
91
  await getWorkflowIdResult(workflowId);
91
- return response.data;
92
+ const data = response.data;
93
+ return {
94
+ ...data,
95
+ status: normalizeWorkflowStatus(data.status)
96
+ };
92
97
  }
93
98
  catch {
94
99
  return null;
@@ -10,7 +10,7 @@ describe('isTerminalRunStatus', () => {
10
10
  });
11
11
  it('returns false for in-progress states', () => {
12
12
  expect(isTerminalRunStatus('running')).toBe(false);
13
- expect(isTerminalRunStatus('continued')).toBe(false);
13
+ expect(isTerminalRunStatus('continued_as_new')).toBe(false);
14
14
  });
15
15
  it('returns false for nullish input', () => {
16
16
  expect(isTerminalRunStatus(null)).toBe(false);
@@ -1,16 +1,12 @@
1
1
  import { useState } from 'react';
2
- import { getWorkflowCatalog } from '#api/generated/api.js';
2
+ import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
3
3
  import { usePoll } from '#views/dev/hooks/use_poll.js';
4
4
  const CATALOG_INTERVAL_MS = 10_000;
5
5
  export const useWorkflowCatalog = (enabled) => {
6
6
  const [workflows, setWorkflows] = useState([]);
7
7
  usePoll(enabled, CATALOG_INTERVAL_MS, async () => {
8
8
  try {
9
- const response = await getWorkflowCatalog();
10
- const data = response?.data;
11
- if (data?.workflows) {
12
- setWorkflows(data.workflows);
13
- }
9
+ setWorkflows(await fetchWorkflowCatalog());
14
10
  }
15
11
  catch {
16
12
  // API may not be ready yet
@@ -22,7 +22,7 @@ const STATUS_ORDER = {
22
22
  timed_out: 2,
23
23
  terminated: 3,
24
24
  canceled: 4,
25
- continued: 5,
25
+ continued_as_new: 5,
26
26
  completed: 6
27
27
  };
28
28
  const sortRuns = (runs) => [...runs].sort((a, b) => {
@@ -704,9 +704,26 @@
704
704
  "<%= config.bin %> <%= command.id %> --format table",
705
705
  "<%= config.bin %> <%= command.id %> --format json",
706
706
  "<%= config.bin %> <%= command.id %> --detailed",
707
- "<%= config.bin %> <%= command.id %> --filter simple"
707
+ "<%= config.bin %> <%= command.id %> --filter simple",
708
+ "<%= config.bin %> <%= command.id %> --catalog my-catalog"
708
709
  ],
709
710
  "flags": {
711
+ "catalog": {
712
+ "aliases": [
713
+ "task-queue"
714
+ ],
715
+ "char": "c",
716
+ "charAliases": [
717
+ "q"
718
+ ],
719
+ "deprecateAliases": true,
720
+ "description": "Catalog to list workflows from (defaults to OUTPUT_CATALOG_ID)",
721
+ "env": "OUTPUT_CATALOG_ID",
722
+ "name": "catalog",
723
+ "hasDynamicHelp": false,
724
+ "multiple": false,
725
+ "type": "option"
726
+ },
710
727
  "format": {
711
728
  "char": "f",
712
729
  "description": "Output format",
@@ -1424,5 +1441,5 @@
1424
1441
  ]
1425
1442
  }
1426
1443
  },
1427
- "version": "0.7.1-next.d67ad85.0"
1444
+ "version": "0.7.1-next.db8ddd7.0"
1428
1445
  }