Skip to content

Commit 1d917e5

Browse files
committed
fix(cue): pipeline persistence, save UX, loader resilience, codex stdin banner
Fixes a cluster of Cue pipeline editor bugs around persistence and rendering: - Pipeline vanish after save: createPipeline assigned timestamp ids; on reload subscriptionsToPipelines regenerated name-based ids, leaving the saved selectedPipelineId stale and convertToReactFlowNodes skipping every pipeline. mergePipelinesWithSavedLayout now validates the saved selection against the live pipeline ids and falls back to the first pipeline; a live safety-net effect in usePipelineState resets selectedPipelineId to null whenever it points at a pipeline that no longer exists. - Save silently doing nothing: validatePipelines used to skip empty pipelines, so 'create N pipelines, click save' returned success without writing anything. Empty pipelines are now flagged ('add a trigger and an agent before saving') and handleSave refuses to no-op when the editor has pipelines but nothing partitions to a root. - Lost-on-save trust gap: handleSave now write-back-verifies every cue:writeYaml by reading the file and comparing bytes, throws on mismatch, preserves isDirty on failure, and fires explicit success/ error toast notifications so the 2-second in-button flash can no longer be missed. - Trigger config caught at load instead of save: pipelineToYaml was happy to write a time.scheduled subscription with no schedule_times (or time.heartbeat with no interval_minutes, etc.), the loader then rejected the entire YAML for every agent in that project root on Cue toggle. validatePipelines now mirrors the YAML schema's per-event requirements, blocking the bad save up front. - One bad subscription killing a whole project's config: extracted validateSubscription and added partitionValidSubscriptions; loader now drops individual invalid subs as warnings instead of failing the entire load. Config-level errors (missing subscriptions array, bad settings) remain fatal. - Codex 'Reading additional input from stdin...' in run output: cue child processes spawn with stdio[0]='ignore' in local mode so codex exec doesn't emit the stdin banner before observing EOF. SSH stdin script and SSH small-prompt paths still get a writable pipe. Also includes editor polish from the working branch: pendingSavedViewportRef threaded from usePipelineLayout to CuePipelineEditor so viewport restore waits for ReactFlow to measure nodes (no more empty canvas on first open), plus PipelineCanvas + AllPipelinesView locking tweaks and matching tests. Tests: 25,260 passing.
1 parent 4cd4787 commit 1d917e5

17 files changed

Lines changed: 1423 additions & 278 deletions

src/__tests__/main/cue/cue-executor.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,10 @@ describe('cue-executor', () => {
399399
expect.any(Array),
400400
expect.objectContaining({
401401
cwd: '/projects/test',
402-
stdio: ['pipe', 'pipe', 'pipe'],
402+
// Local mode uses 'ignore' for stdin so agents like Codex don't
403+
// emit "Reading additional input from stdin..." into the run
404+
// output before observing EOF.
405+
stdio: ['ignore', 'pipe', 'pipe'],
403406
})
404407
);
405408

src/__tests__/main/cue/cue-process-lifecycle.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,14 @@ describe('cue-process-lifecycle', () => {
115115
const resultPromise = runProcess('run-1', spec, createOptions());
116116
await vi.advanceTimersByTimeAsync(0);
117117

118+
// Local mode: stdin is `'ignore'` so agents like Codex don't print
119+
// "Reading additional input from stdin..." into the run output.
118120
expect(mockSpawn).toHaveBeenCalledWith(
119121
'claude',
120122
['--print', '--', 'test prompt'],
121123
expect.objectContaining({
122124
cwd: '/projects/test',
123-
stdio: ['pipe', 'pipe', 'pipe'],
125+
stdio: ['ignore', 'pipe', 'pipe'],
124126
})
125127
);
126128

src/__tests__/main/cue/cue-yaml-loader.test.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,23 +334,47 @@ subscriptions:
334334
}
335335
});
336336

337-
it('returns { ok: false, reason: "invalid", errors } when validation fails', () => {
337+
it('skips per-subscription validation errors and surfaces them as warnings', () => {
338+
// Lenient loader: a single broken subscription must not block valid
339+
// subs in the same YAML. The bad sub is dropped, others load, and
340+
// the failure is surfaced as a warning so the user can fix it.
338341
mockExistsSync.mockReturnValue(true);
339342
mockReadFileSync.mockReturnValue(`
340343
subscriptions:
341344
- name: bad-sub
342345
event: time.heartbeat
343346
prompt: Hi
347+
- name: good-sub
348+
event: time.heartbeat
349+
prompt: Check status
350+
interval_minutes: 5
351+
`);
352+
353+
const result = loadCueConfigDetailed('/projects/test');
354+
355+
expect(result.ok).toBe(true);
356+
if (result.ok) {
357+
expect(result.config.subscriptions.map((s) => s.name)).toEqual(['good-sub']);
358+
expect(result.warnings).toEqual(
359+
expect.arrayContaining([
360+
expect.stringMatching(/Skipped invalid subscription.*interval_minutes/),
361+
])
362+
);
363+
}
364+
});
365+
366+
it('returns { ok: false, reason: "invalid" } only for config-level errors', () => {
367+
mockExistsSync.mockReturnValue(true);
368+
mockReadFileSync.mockReturnValue(`
369+
subscriptions: not-an-array
344370
`);
345-
// Missing interval_minutes for time.heartbeat — validator rejects this.
346371

347372
const result = loadCueConfigDetailed('/projects/test');
348373

349374
expect(result.ok).toBe(false);
350375
if (!result.ok && result.reason === 'invalid') {
351-
expect(result.errors.length).toBeGreaterThan(0);
352376
expect(result.errors).toEqual(
353-
expect.arrayContaining([expect.stringMatching(/interval_minutes/)])
377+
expect.arrayContaining([expect.stringMatching(/subscriptions/)])
354378
);
355379
}
356380
});

0 commit comments

Comments
 (0)