Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci-watch/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
20 changes: 15 additions & 5 deletions ci-watch/hooks/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 [<Text key={w.id} color={w.isFailed ? 'red' : w.result.startsWith('✓') ? 'green' : undefined}>{w.result}</Text>]
if (!w.runs.length) return [<Text key={w.id} dimColor>{`⟳ ${w.label} · waiting for workflows…`}</Text>]
return w.runs.map((r, i) => (
<Text key={`${w.id}:${i}`}>
{`⟳ ${w.label} · ${r.name} ${bar(r.elapsedMs, r.expectedMs)} ${duration(r.elapsedMs)}${r.expectedMs ? ` / ~${duration(r.expectedMs)}` : ''}`}
</Text>
))
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 <Text key={`${w.id}:${i}`}>{name + progress}</Text>
// Off the terminal a narrow row cuts the name and keeps the bar and its time on the same line.
return (
<Box key={`${w.id}:${i}`}>
<Text wrap="truncate-end">{name}</Text>
<Box flexShrink={0}>
<Text>{progress}</Text>
</Box>
</Box>
)
})
})
for (const b of list) {
rows.push(<Text key={b.url} color="red">{`✗ scheduled "${b.workflow}" failed (${b.at}) ${b.url}`}</Text>)
Expand Down
20 changes: 20 additions & 0 deletions ci-watch/tests/ci-watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
})
Loading