agent-state-machine 2.0.10 → 2.0.12
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.
- package/README.md +4 -15
- package/bin/cli.js +47 -11
- package/lib/llm.js +3 -3
- package/lib/runtime/runtime.js +59 -6
- package/lib/setup.js +25 -17
- package/package.json +1 -1
- package/vercel-server/ui/index.html +37 -3
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Workflows live in:
|
|
|
56
56
|
```text
|
|
57
57
|
workflows/<name>/
|
|
58
58
|
├── workflow.js # Native JS workflow (async/await)
|
|
59
|
+
├── config.js # Model/API key configuration
|
|
59
60
|
├── package.json # Sets "type": "module" for this workflow folder
|
|
60
61
|
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
61
62
|
├── interactions/ # Human-in-the-loop files (auto-created)
|
|
@@ -67,6 +68,8 @@ workflows/<name>/
|
|
|
67
68
|
|
|
68
69
|
## Writing workflows (native JS)
|
|
69
70
|
|
|
71
|
+
Edit `config.js` to set models and API keys for the workflow.
|
|
72
|
+
|
|
70
73
|
```js
|
|
71
74
|
/**
|
|
72
75
|
/**
|
|
@@ -83,20 +86,6 @@ workflows/<name>/
|
|
|
83
86
|
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
84
87
|
import { notify } from './scripts/mac-notification.js';
|
|
85
88
|
|
|
86
|
-
// Model configuration (also supports models in a separate config export)
|
|
87
|
-
export const config = {
|
|
88
|
-
models: {
|
|
89
|
-
low: "gemini",
|
|
90
|
-
med: "codex --model gpt-5.2",
|
|
91
|
-
high: "claude -m claude-opus-4-20250514 -p",
|
|
92
|
-
},
|
|
93
|
-
apiKeys: {
|
|
94
|
-
gemini: process.env.GEMINI_API_KEY,
|
|
95
|
-
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
96
|
-
openai: process.env.OPENAI_API_KEY,
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
89
|
export default async function() {
|
|
101
90
|
console.log('Starting project-builder workflow...');
|
|
102
91
|
|
|
@@ -298,7 +287,7 @@ export const config = {
|
|
|
298
287
|
};
|
|
299
288
|
```
|
|
300
289
|
|
|
301
|
-
The runtime captures the fully-built prompt in `state/history.jsonl`, viewable in the browser with live updates when running with the `--local` flag or via the remote URL. Remote follow links persist across runs (stored in `
|
|
290
|
+
The runtime captures the fully-built prompt in `state/history.jsonl`, viewable in the browser with live updates when running with the `--local` flag or via the remote URL. Remote follow links persist across runs (stored in `config.js`) unless you pass `-n`/`--new` to regenerate.
|
|
302
291
|
|
|
303
292
|
---
|
|
304
293
|
|
package/bin/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import fs from 'fs';
|
|
5
|
+
import readline from 'readline';
|
|
5
6
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
6
7
|
import { WorkflowRuntime } from '../lib/index.js';
|
|
7
8
|
import { setup } from '../lib/setup.js';
|
|
@@ -62,6 +63,7 @@ Environment Variables:
|
|
|
62
63
|
Workflow Structure:
|
|
63
64
|
workflows/<name>/
|
|
64
65
|
├── workflow.js # Native JS workflow (async/await)
|
|
66
|
+
├── config.js # Model/API key configuration
|
|
65
67
|
├── package.json # Sets "type": "module" for this workflow folder
|
|
66
68
|
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
67
69
|
├── interactions/ # Human-in-the-loop files (auto-created)
|
|
@@ -70,6 +72,28 @@ Workflow Structure:
|
|
|
70
72
|
`);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
async function confirmHardReset(workflowName) {
|
|
76
|
+
if (!process.stdin.isTTY) {
|
|
77
|
+
console.error('Error: Hard reset requires confirmation in a TTY.');
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
82
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
83
|
+
try {
|
|
84
|
+
const answer = String(
|
|
85
|
+
await ask(
|
|
86
|
+
`Hard reset deletes history, interactions, and memory for '${workflowName}'. Type 'y' to continue: `
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
.trim()
|
|
90
|
+
.toLowerCase();
|
|
91
|
+
return answer === 'y' || answer === 'yes';
|
|
92
|
+
} finally {
|
|
93
|
+
rl.close();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
function workflowsRoot() {
|
|
74
98
|
return path.join(process.cwd(), 'workflows');
|
|
75
99
|
}
|
|
@@ -162,8 +186,8 @@ function findConfigObjectRange(source) {
|
|
|
162
186
|
return null;
|
|
163
187
|
}
|
|
164
188
|
|
|
165
|
-
function
|
|
166
|
-
const source = fs.readFileSync(
|
|
189
|
+
function readRemotePathFromConfig(configFile) {
|
|
190
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
167
191
|
const range = findConfigObjectRange(source);
|
|
168
192
|
if (!range) return null;
|
|
169
193
|
const configSource = source.slice(range.start, range.end + 1);
|
|
@@ -171,19 +195,19 @@ function readRemotePathFromWorkflow(workflowFile) {
|
|
|
171
195
|
return match ? match[2] : null;
|
|
172
196
|
}
|
|
173
197
|
|
|
174
|
-
function
|
|
175
|
-
const source = fs.readFileSync(
|
|
198
|
+
function writeRemotePathToConfig(configFile, remotePath) {
|
|
199
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
176
200
|
const range = findConfigObjectRange(source);
|
|
177
201
|
const remoteLine = `remotePath: "${remotePath}"`;
|
|
178
202
|
|
|
179
203
|
if (!range) {
|
|
180
204
|
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
181
205
|
if (hasConfigExport) {
|
|
182
|
-
throw new Error('
|
|
206
|
+
throw new Error('Config export is not an object literal; add remotePath manually.');
|
|
183
207
|
}
|
|
184
208
|
const trimmed = source.replace(/\s*$/, '');
|
|
185
209
|
const appended = `${trimmed}\n\nexport const config = {\n ${remoteLine}\n};\n`;
|
|
186
|
-
fs.writeFileSync(
|
|
210
|
+
fs.writeFileSync(configFile, appended);
|
|
187
211
|
return;
|
|
188
212
|
}
|
|
189
213
|
|
|
@@ -224,15 +248,15 @@ function writeRemotePathToWorkflow(workflowFile, remotePath) {
|
|
|
224
248
|
source.slice(0, range.start) +
|
|
225
249
|
updatedConfigSource +
|
|
226
250
|
source.slice(range.end + 1);
|
|
227
|
-
fs.writeFileSync(
|
|
251
|
+
fs.writeFileSync(configFile, updatedSource);
|
|
228
252
|
}
|
|
229
253
|
|
|
230
|
-
function ensureRemotePath(
|
|
231
|
-
const existing =
|
|
254
|
+
function ensureRemotePath(configFile, { forceNew = false } = {}) {
|
|
255
|
+
const existing = readRemotePathFromConfig(configFile);
|
|
232
256
|
if (existing && !forceNew) return existing;
|
|
233
257
|
|
|
234
258
|
const remotePath = generateSessionToken();
|
|
235
|
-
|
|
259
|
+
writeRemotePathToConfig(configFile, remotePath);
|
|
236
260
|
return remotePath;
|
|
237
261
|
}
|
|
238
262
|
|
|
@@ -253,6 +277,7 @@ function summarizeStatus(state) {
|
|
|
253
277
|
if (s === 'COMPLETED') return ' [completed]';
|
|
254
278
|
if (s === 'FAILED') return ' [failed - can resume]';
|
|
255
279
|
if (s === 'PAUSED') return ' [paused - can resume]';
|
|
280
|
+
if (s === 'STOPPED') return ' [stopped - can resume]';
|
|
256
281
|
if (s === 'RUNNING') return ' [running]';
|
|
257
282
|
if (s === 'IDLE') return ' [idle]';
|
|
258
283
|
return state.status ? ` [${state.status}]` : '';
|
|
@@ -327,12 +352,18 @@ async function runOrResume(
|
|
|
327
352
|
|
|
328
353
|
const runtime = new WorkflowRuntime(workflowDir);
|
|
329
354
|
if (preResetHard) {
|
|
355
|
+
const confirmed = await confirmHardReset(workflowName);
|
|
356
|
+
if (!confirmed) {
|
|
357
|
+
console.log('Hard reset cancelled.');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
330
360
|
runtime.resetHard();
|
|
331
361
|
} else if (preReset) {
|
|
332
362
|
runtime.reset();
|
|
333
363
|
}
|
|
334
364
|
|
|
335
365
|
const workflowUrl = pathToFileURL(entry).href;
|
|
366
|
+
const configFile = path.join(workflowDir, 'config.js');
|
|
336
367
|
|
|
337
368
|
let localServer = null;
|
|
338
369
|
let remoteUrl = null;
|
|
@@ -354,7 +385,7 @@ async function runOrResume(
|
|
|
354
385
|
|
|
355
386
|
// Enable remote follow mode if we have a URL
|
|
356
387
|
if (remoteUrl) {
|
|
357
|
-
const sessionToken = ensureRemotePath(
|
|
388
|
+
const sessionToken = ensureRemotePath(configFile, { forceNew: forceNewRemotePath });
|
|
358
389
|
await runtime.enableRemote(remoteUrl, { sessionToken });
|
|
359
390
|
}
|
|
360
391
|
|
|
@@ -478,6 +509,11 @@ async function main() {
|
|
|
478
509
|
{
|
|
479
510
|
const workflowDir = resolveWorkflowDir(workflowName);
|
|
480
511
|
const runtime = new WorkflowRuntime(workflowDir);
|
|
512
|
+
const confirmed = await confirmHardReset(workflowName);
|
|
513
|
+
if (!confirmed) {
|
|
514
|
+
console.log('Hard reset cancelled.');
|
|
515
|
+
process.exit(0);
|
|
516
|
+
}
|
|
481
517
|
runtime.resetHard();
|
|
482
518
|
}
|
|
483
519
|
break;
|
package/lib/llm.js
CHANGED
|
@@ -314,7 +314,7 @@ async function executeAPI(provider, model, prompt, apiKey, options = {}) {
|
|
|
314
314
|
*
|
|
315
315
|
* @param {object} context - The workflow context (contains _config, _steering, etc.)
|
|
316
316
|
* @param {object} options - Options for the LLM call
|
|
317
|
-
* @param {string} options.model - Model key from
|
|
317
|
+
* @param {string} options.model - Model key from config.js models config
|
|
318
318
|
* @param {string} options.prompt - The prompt to send
|
|
319
319
|
* @param {boolean} options.includeContext - Whether to include context in prompt (default: true)
|
|
320
320
|
* @param {number} options.maxTokens - Max tokens for API calls (default: 4096)
|
|
@@ -359,7 +359,7 @@ export async function llm(context, options) {
|
|
|
359
359
|
|
|
360
360
|
if (!apiKey) {
|
|
361
361
|
throw new Error(
|
|
362
|
-
`No API key found for ${provider}. Set in
|
|
362
|
+
`No API key found for ${provider}. Set in config.js apiKeys or ${provider.toUpperCase()}_API_KEY env var`
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
365
|
|
|
@@ -459,4 +459,4 @@ export async function llmJSON(context, options) {
|
|
|
459
459
|
...response,
|
|
460
460
|
data: parseJSON(response.text)
|
|
461
461
|
};
|
|
462
|
-
}
|
|
462
|
+
}
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import readline from 'readline';
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
12
13
|
import { createMemoryProxy } from './memory.js';
|
|
13
14
|
import { RemoteClient } from '../remote/client.js';
|
|
14
15
|
|
|
@@ -218,6 +219,50 @@ export class WorkflowRuntime {
|
|
|
218
219
|
async runWorkflow(workflowPath) {
|
|
219
220
|
setCurrentRuntime(this);
|
|
220
221
|
|
|
222
|
+
// Handle Ctrl+C and termination signals to update status before exit
|
|
223
|
+
const handleShutdown = async (signal) => {
|
|
224
|
+
this.status = 'STOPPED';
|
|
225
|
+
this._error = `Workflow interrupted by ${signal}`;
|
|
226
|
+
this.persist();
|
|
227
|
+
|
|
228
|
+
// Log to history (local file)
|
|
229
|
+
const historyEntry = {
|
|
230
|
+
timestamp: new Date().toISOString(),
|
|
231
|
+
event: 'WORKFLOW_STOPPED',
|
|
232
|
+
reason: signal
|
|
233
|
+
};
|
|
234
|
+
const line = JSON.stringify(historyEntry) + '\n';
|
|
235
|
+
let existing = '';
|
|
236
|
+
if (fs.existsSync(this.historyFile)) {
|
|
237
|
+
existing = fs.readFileSync(this.historyFile, 'utf-8');
|
|
238
|
+
}
|
|
239
|
+
fs.writeFileSync(this.historyFile, line + existing);
|
|
240
|
+
|
|
241
|
+
// Send to remote and wait for it to complete before exiting
|
|
242
|
+
if (this.remoteClient && this.remoteEnabled) {
|
|
243
|
+
try {
|
|
244
|
+
await this.remoteClient.sendEvent(historyEntry);
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore errors during shutdown
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(`\n${C.yellow}⚠ Workflow '${this.workflowName}' stopped (${signal})${C.reset}`);
|
|
251
|
+
cleanupSignalHandlers();
|
|
252
|
+
clearCurrentRuntime();
|
|
253
|
+
process.exit(130); // 128 + SIGINT (2)
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const sigintHandler = () => handleShutdown('SIGINT');
|
|
257
|
+
const sigtermHandler = () => handleShutdown('SIGTERM');
|
|
258
|
+
process.on('SIGINT', sigintHandler);
|
|
259
|
+
process.on('SIGTERM', sigtermHandler);
|
|
260
|
+
|
|
261
|
+
const cleanupSignalHandlers = () => {
|
|
262
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
263
|
+
process.removeListener('SIGTERM', sigtermHandler);
|
|
264
|
+
};
|
|
265
|
+
|
|
221
266
|
try {
|
|
222
267
|
this.status = 'RUNNING';
|
|
223
268
|
this._error = null;
|
|
@@ -226,18 +271,24 @@ export class WorkflowRuntime {
|
|
|
226
271
|
|
|
227
272
|
this.prependHistory({ event: 'WORKFLOW_STARTED' });
|
|
228
273
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
274
|
+
const configPath = path.join(this.workflowDir, 'config.js');
|
|
275
|
+
if (!fs.existsSync(configPath)) {
|
|
276
|
+
throw new Error(`config.js not found in ${this.workflowDir}`);
|
|
277
|
+
}
|
|
278
|
+
const configUrl = pathToFileURL(configPath);
|
|
279
|
+
configUrl.searchParams.set('t', Date.now().toString());
|
|
280
|
+
const configModule = await import(configUrl.href);
|
|
281
|
+
const cfg = configModule.config || configModule.default || {};
|
|
235
282
|
this.workflowConfig = {
|
|
236
283
|
models: cfg.models || {},
|
|
237
284
|
apiKeys: cfg.apiKeys || {},
|
|
238
285
|
description: cfg.description || ''
|
|
239
286
|
};
|
|
240
287
|
|
|
288
|
+
// Import workflow module
|
|
289
|
+
const workflowModule = await import(workflowPath);
|
|
290
|
+
const runFn = workflowModule.default || workflowModule.run || workflowModule;
|
|
291
|
+
|
|
241
292
|
if (typeof runFn !== 'function') {
|
|
242
293
|
throw new Error('Workflow module must export a default async function');
|
|
243
294
|
}
|
|
@@ -263,6 +314,7 @@ export class WorkflowRuntime {
|
|
|
263
314
|
console.error(`\n${C.red}✗ Workflow '${this.workflowName}' failed: ${err.message}${C.reset}`);
|
|
264
315
|
throw err;
|
|
265
316
|
} finally {
|
|
317
|
+
cleanupSignalHandlers();
|
|
266
318
|
clearCurrentRuntime();
|
|
267
319
|
}
|
|
268
320
|
}
|
|
@@ -406,6 +458,7 @@ export class WorkflowRuntime {
|
|
|
406
458
|
let statusColor = C.reset;
|
|
407
459
|
if (this.status === 'COMPLETED') statusColor = C.green;
|
|
408
460
|
if (this.status === 'FAILED') statusColor = C.red;
|
|
461
|
+
if (this.status === 'STOPPED') statusColor = C.yellow;
|
|
409
462
|
if (this.status === 'RUNNING') statusColor = C.blue;
|
|
410
463
|
if (this.status === 'IDLE') statusColor = C.gray;
|
|
411
464
|
|
package/lib/setup.js
CHANGED
|
@@ -62,20 +62,6 @@ async function setup(workflowName) {
|
|
|
62
62
|
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
63
63
|
import { notify } from './scripts/mac-notification.js';
|
|
64
64
|
|
|
65
|
-
// Model configuration (also supports models in a separate config export)
|
|
66
|
-
export const config = {
|
|
67
|
-
models: {
|
|
68
|
-
low: "gemini",
|
|
69
|
-
med: "codex --model gpt-5.2",
|
|
70
|
-
high: "claude -m claude-opus-4-20250514 -p",
|
|
71
|
-
},
|
|
72
|
-
apiKeys: {
|
|
73
|
-
gemini: process.env.GEMINI_API_KEY,
|
|
74
|
-
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
75
|
-
openai: process.env.OPENAI_API_KEY,
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
65
|
export default async function() {
|
|
80
66
|
console.log('Starting ${workflowName} workflow...');
|
|
81
67
|
|
|
@@ -119,6 +105,24 @@ export default async function() {
|
|
|
119
105
|
fs.writeFileSync(workflowFile, workflowJs);
|
|
120
106
|
console.log(` Created: ${path.relative(process.cwd(), workflowFile)}`);
|
|
121
107
|
|
|
108
|
+
const configJs = `export const config = {
|
|
109
|
+
models: {
|
|
110
|
+
low: "gemini",
|
|
111
|
+
med: "codex --model gpt-5.2",
|
|
112
|
+
high: "claude -m claude-opus-4-20250514 -p",
|
|
113
|
+
},
|
|
114
|
+
apiKeys: {
|
|
115
|
+
gemini: process.env.GEMINI_API_KEY,
|
|
116
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
117
|
+
openai: process.env.OPENAI_API_KEY,
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const configFile = path.join(workflowDir, 'config.js');
|
|
123
|
+
fs.writeFileSync(configFile, configJs);
|
|
124
|
+
console.log(` Created: ${path.relative(process.cwd(), configFile)}`);
|
|
125
|
+
|
|
122
126
|
// Create example JS agent (ESM)
|
|
123
127
|
// Create example JS agent (ESM)
|
|
124
128
|
const exampleAgent = `/**
|
|
@@ -138,7 +142,7 @@ export default async function handler(context) {
|
|
|
138
142
|
console.log('[Agent: example] Steering loaded (' + context._steering.global.length + ' chars)');
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
// Example: Call an LLM (configure models in
|
|
145
|
+
// Example: Call an LLM (configure models in config.js)
|
|
142
146
|
// const response = await llm(context, {
|
|
143
147
|
// model: 'smart',
|
|
144
148
|
// prompt: 'Say hello and describe what you can help with.'
|
|
@@ -295,6 +299,7 @@ A workflow created with agent-state-machine (native JS format).
|
|
|
295
299
|
\\\`\\\`\\\`
|
|
296
300
|
${workflowName}/
|
|
297
301
|
├── workflow.js # Native JS workflow (async/await)
|
|
302
|
+
├── config.js # Model/API key configuration
|
|
298
303
|
├── package.json # Sets "type": "module" for this workflow folder
|
|
299
304
|
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
300
305
|
├── interactions/ # Human-in-the-loop inputs (created at runtime)
|
|
@@ -304,6 +309,8 @@ ${workflowName}/
|
|
|
304
309
|
|
|
305
310
|
## Usage
|
|
306
311
|
|
|
312
|
+
Edit \`config.js\` to set models and API keys for this workflow.
|
|
313
|
+
|
|
307
314
|
Run the workflow (or resume if interrupted):
|
|
308
315
|
\\\`\\\`\\\`bash
|
|
309
316
|
state-machine run ${workflowName}
|
|
@@ -411,8 +418,9 @@ Generate a greeting for {{name}}.
|
|
|
411
418
|
console.log(`\n✓ Workflow '${workflowName}' created successfully!\n`);
|
|
412
419
|
console.log('Next steps:');
|
|
413
420
|
console.log(` 1. Edit workflows/${workflowName}/workflow.js to implement your flow`);
|
|
414
|
-
console.log(` 2.
|
|
415
|
-
console.log(` 3.
|
|
421
|
+
console.log(` 2. Edit workflows/${workflowName}/config.js to set models/API keys`);
|
|
422
|
+
console.log(` 3. Add custom agents in workflows/${workflowName}/agents/`);
|
|
423
|
+
console.log(` 4. Run: state-machine run ${workflowName}\n`);
|
|
416
424
|
}
|
|
417
425
|
|
|
418
426
|
export { setup };
|
package/package.json
CHANGED
|
@@ -486,11 +486,24 @@
|
|
|
486
486
|
|
|
487
487
|
useEffect(() => localStorage.setItem("rf_theme", theme), [theme]);
|
|
488
488
|
|
|
489
|
+
// Helper to check if workflow is currently running based on history
|
|
490
|
+
const isWorkflowRunning = (entries) => {
|
|
491
|
+
// Find the most recent workflow lifecycle event (history is newest-first)
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
if (entry.event === "WORKFLOW_STARTED") return true;
|
|
494
|
+
if (entry.event === "WORKFLOW_STOPPED" ||
|
|
495
|
+
entry.event === "WORKFLOW_COMPLETED" ||
|
|
496
|
+
entry.event === "WORKFLOW_FAILED") return false;
|
|
497
|
+
}
|
|
498
|
+
return false; // No lifecycle events found
|
|
499
|
+
};
|
|
500
|
+
|
|
489
501
|
useEffect(() => {
|
|
490
502
|
if (history.length === 0) { setPendingInteraction(null); return; }
|
|
491
503
|
|
|
492
504
|
const resolvedSlugs = new Set();
|
|
493
505
|
let pending = null;
|
|
506
|
+
const workflowRunning = isWorkflowRunning(history);
|
|
494
507
|
|
|
495
508
|
for (const entry of history) {
|
|
496
509
|
const isResolution =
|
|
@@ -510,7 +523,8 @@
|
|
|
510
523
|
}
|
|
511
524
|
}
|
|
512
525
|
|
|
513
|
-
|
|
526
|
+
// Only show pending interaction if workflow is running
|
|
527
|
+
setPendingInteraction(workflowRunning ? pending : null);
|
|
514
528
|
}, [history]);
|
|
515
529
|
|
|
516
530
|
const fetchData = async () => {
|
|
@@ -520,8 +534,18 @@
|
|
|
520
534
|
if (data.entries) setHistory(data.entries);
|
|
521
535
|
if (data.workflowName) setWorkflowName(data.workflowName);
|
|
522
536
|
|
|
523
|
-
if
|
|
524
|
-
|
|
537
|
+
// Check if workflow is currently running based on most recent lifecycle event
|
|
538
|
+
const workflowRunning = isWorkflowRunning(data.entries || []);
|
|
539
|
+
|
|
540
|
+
if (workflowRunning) {
|
|
541
|
+
setStatus("connected");
|
|
542
|
+
} else if (token && data.cliConnected !== undefined) {
|
|
543
|
+
setStatus(data.cliConnected ? "connected" : "disconnected");
|
|
544
|
+
} else if (!token) {
|
|
545
|
+
setStatus("connected");
|
|
546
|
+
} else {
|
|
547
|
+
setStatus("disconnected");
|
|
548
|
+
}
|
|
525
549
|
|
|
526
550
|
setLoading(false);
|
|
527
551
|
return true;
|
|
@@ -569,6 +593,8 @@
|
|
|
569
593
|
break;
|
|
570
594
|
case "history":
|
|
571
595
|
setHistory(data.entries || []);
|
|
596
|
+
// Update status based on workflow lifecycle
|
|
597
|
+
setStatus(isWorkflowRunning(data.entries || []) ? "connected" : "disconnected");
|
|
572
598
|
break;
|
|
573
599
|
case "event":
|
|
574
600
|
setHistory((prev) => {
|
|
@@ -578,6 +604,14 @@
|
|
|
578
604
|
}
|
|
579
605
|
return [data, ...prev];
|
|
580
606
|
});
|
|
607
|
+
// Update status based on workflow lifecycle events
|
|
608
|
+
if (data.event === "WORKFLOW_STARTED") {
|
|
609
|
+
setStatus("connected");
|
|
610
|
+
} else if (data.event === "WORKFLOW_STOPPED" ||
|
|
611
|
+
data.event === "WORKFLOW_COMPLETED" ||
|
|
612
|
+
data.event === "WORKFLOW_FAILED") {
|
|
613
|
+
setStatus("disconnected");
|
|
614
|
+
}
|
|
581
615
|
break;
|
|
582
616
|
case "cli_connected":
|
|
583
617
|
case "cli_reconnected":
|