diff --git a/ci-watch/.claude-plugin/plugin.json b/ci-watch/.claude-plugin/plugin.json index 4491e41..c1edeaa 100644 --- a/ci-watch/.claude-plugin/plugin.json +++ b/ci-watch/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ci-watch", - "version": "0.1.3", + "version": "0.1.4", "description": "Watches GitHub Actions in a band above the prompt after Claude pushes, opens a PR or pushes a tag: a progress bar per run, then pass or fail; optionally flags failed scheduled workflows at start", "author": { "name": "Mehmet Aras", diff --git a/ci-watch/hooks/register.tsx b/ci-watch/hooks/register.tsx index 9b5d840..dd3a667 100644 --- a/ci-watch/hooks/register.tsx +++ b/ci-watch/hooks/register.tsx @@ -242,14 +242,24 @@ export const register: Register = (on, options) => { if (e.props.hasSurvey || (!items.length && !list.length)) return next(e) const { Box, Button, Text } = $.ui.resolve(e) + const isTerminal = e.surface === 'terminal' const rows = items.flatMap(w => { if (w.result) return [{w.result}] if (!w.runs.length) return [{`⟳ ${w.label} · waiting for workflows…`}] - return w.runs.map((r, i) => ( - - {`⟳ ${w.label} · ${r.name} ${bar(r.elapsedMs, r.expectedMs)} ${duration(r.elapsedMs)}${r.expectedMs ? ` / ~${duration(r.expectedMs)}` : ''}`} - - )) + return w.runs.map((r, i) => { + const name = `⟳ ${w.label} · ${r.name}` + const progress = ` ${bar(r.elapsedMs, r.expectedMs)} ${duration(r.elapsedMs)}${r.expectedMs ? ` / ~${duration(r.expectedMs)}` : ''}` + if (isTerminal) return {name + progress} + // Off the terminal a narrow row cuts the name and keeps the bar and its time on the same line. + return ( + + {name} + + {progress} + + + ) + }) }) for (const b of list) { rows.push({`✗ scheduled "${b.workflow}" failed (${b.at}) ${b.url}`}) diff --git a/ci-watch/tests/ci-watch.test.ts b/ci-watch/tests/ci-watch.test.ts index ae307f6..26d670c 100644 --- a/ci-watch/tests/ci-watch.test.ts +++ b/ci-watch/tests/ci-watch.test.ts @@ -231,3 +231,23 @@ test('with scheduled off (the default), start runs no scan', async ($, on) => { expect(argvs).toEqual([]) expect(held.broken).toEqual([]) }) + +test('off the terminal a run row cuts its name and keeps the bar and time on its line', async ($, on) => { + const watch: CiWatchItem = { id: 'w1', label: 'push fix/a-branch-name-long-enough-to-crowd-the-row', runs: [{ name: 'CI', elapsedMs: 65_000, expectedMs: 120_000 }] } + on('state.get', (_$, e) => ({ value: { value: e.key === 'watches' ? [watch] : [], version: 1 } })) + // Beneath the plugin the test stands for the engine's own band. + on('ui.render', { component: 'AbovePrompt' }, () => ({ type: 'Box', children: [] })) + const props = { hasSurvey: false, isWorking: false, maxRows: 10, bodyColumns: 60, scroll: { offset: 0, bodyRows: 10 }, view: {} } + const draw = async (surface: 'terminal' | 'desktop') => { + const ui = await $.ui.mount({ plugin: 'ci-watch', surface, component: 'AbovePrompt', props }) + const texts = await ui.findAll({ type: 'Text' }) + const kept = (await ui.findAll({ type: 'Box' })).filter(b => b.props.flexShrink === 0).map(b => b.text) + await ui.unmount() + return { texts, kept } + } + const desktop = await draw('desktop') + expect(desktop.texts.find(t => t.text.startsWith('⟳ push'))?.props.wrap).toBe('truncate-end') + expect(desktop.kept).toEqual([` ${bar(65_000, 120_000)} 1m05s / ~2m00s`]) + // The terminal keeps its one line of text. + expect((await draw('terminal')).texts.map(t => t.text)).toContain(`⟳ ${watch.label} · CI ${bar(65_000, 120_000)} 1m05s / ~2m00s`) +})