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`)
+})