diff --git a/change-ledger/.claude-plugin/plugin.json b/change-ledger/.claude-plugin/plugin.json
index 124ddc5..f8090fd 100644
--- a/change-ledger/.claude-plugin/plugin.json
+++ b/change-ledger/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "change-ledger",
- "version": "0.1.4",
+ "version": "0.1.5",
"description": "Lists the files this session edited in a pane (/changes), with line counts and who edited them, beside the git working tree",
"author": { "name": "Mehmet Aras", "url": "https://github.com/arasovic" },
"repository": "https://github.com/arasovic/claude-code-mods",
diff --git a/change-ledger/hooks/register.tsx b/change-ledger/hooks/register.tsx
index 88ee597..95e83e1 100644
--- a/change-ledger/hooks/register.tsx
+++ b/change-ledger/hooks/register.tsx
@@ -102,9 +102,9 @@ const refreshGit = async ($: EngineInterface) => {
// Rows are runs of styled text. The terminal draws them on a grid of cells, so frames pad and cut them exactly.
// Other surfaces draw text in a proportional font, where spaces line nothing up: there a row is a flex line.
-// `fill` stretches, `w` is a column that many characters wide (`right` aligns it), and a Bar draws `parts`
-// as real bars in place of its `cells`, `w` wide or stretching.
-type Seg = { t: string; c?: string; d?: boolean; b?: boolean; fill?: boolean; w?: number; right?: boolean }
+// `fill` stretches, `w` is a column that many characters wide (`right` aligns it), `keep` never gives way
+// when the line runs short, and a Bar draws `parts` as real bars in place of its `cells`, `w` wide or stretching.
+type Seg = { t: string; c?: string; d?: boolean; b?: boolean; fill?: boolean; w?: number; right?: boolean; keep?: boolean }
type Bar = { cells: Seg[]; parts: { n: number; c?: string }[]; w?: number }
type Row = (Seg | Bar)[]
@@ -195,7 +195,7 @@ export const register: Register = on => {
}
if (s.fill) return
const text = {!s.w ? s.t : s.right ? s.t.trim() : s.t.trimEnd()}
- return s.w ? {text} : text
+ return s.w || s.keep ? {text} : text
})
const line = (row: Row) => (row.length ? {items(row)} : )
const frame = (title: string, tone: string, rows: Row[], right = '') => {
@@ -203,9 +203,11 @@ export const register: Register = on => {
return (
- {title}
+
+ {title}
+
- {right ? {right} : null}
+ {right ? {right} : null}
{rows.map(line)}
@@ -241,9 +243,15 @@ export const register: Register = on => {
const added = list.reduce((n, f) => n + f.added, 0)
const removed = list.reduce((n, f) => n + f.removed, 0)
const glance: Row = list.length
- ? [{ t: `${list.length} ${list.length === 1 ? 'file' : 'files'} `, b: true }, { t: `+${added}`, c: 'success' }, { t: ` −${removed}`, c: 'error' }]
+ ? [{ t: `${list.length} ${list.length === 1 ? 'file' : 'files'} `, b: true, keep: true }, { t: `+${added}`, c: 'success', keep: true }, { t: ` −${removed}`, c: 'error', keep: true }]
: [{ t: 'No edits yet', d: true }]
- if (tree) glance.push({ t: ` ${tree.branch}`, c: 'suggestion' }, ...(tree.ahead ? [{ t: ` ↑${tree.ahead}`, d: true }] : []), ...(tree.behind ? [{ t: ` ↓${tree.behind}`, c: 'warning' }] : []))
+ // Off the grid only the branch name gives way.
+ if (tree)
+ glance.push(
+ { t: ` ${tree.branch}`, c: 'suggestion' },
+ ...(tree.ahead ? [{ t: ` ↑${tree.ahead}`, d: true, keep: true }] : []),
+ ...(tree.behind ? [{ t: ` ↓${tree.behind}`, c: 'warning', keep: true }] : []),
+ )
// The git frame lists every changed path: the ones this session edited get a filled dot, the rest (shell, you) an empty one.
const touched = new Set(list.map(f => f.path))
@@ -253,8 +261,8 @@ export const register: Register = on => {
? tree.files.slice(0, GIT_MAX).map(f => {
const mine = touched.has(`${tree.root}/${f.path}`)
const tone = f.status === '??' || f.status === 'A' ? 'success' : f.status === 'D' ? 'error' : 'warning'
- const right: Row = f.added === undefined ? [{ t: f.status === '??' ? 'new' : '', d: true }] : counts(f.added, f.removed ?? 0)
- return spread([{ t: mine ? '● ' : '○ ', c: mine ? 'suggestion' : undefined, d: !mine }, { t: f.status.padEnd(3), c: tone, w: 3 }, { t: f.path, d: !mine }], right, inner)
+ const right: Row = f.added === undefined ? [{ t: f.status === '??' ? 'new' : '', d: true, keep: true }] : counts(f.added, f.removed ?? 0)
+ return spread([{ t: mine ? '● ' : '○ ', c: mine ? 'suggestion' : undefined, d: !mine, keep: true }, { t: f.status.padEnd(3), c: tone, w: 3 }, { t: f.path, d: !mine }], right, inner)
})
: [[{ t: 'Working tree clean.', d: true }]]
: []
@@ -269,7 +277,7 @@ export const register: Register = on => {
? shown.flatMap((f, i) => {
const bar = statBar(f.added, f.removed, max, 8)
const top = spread(
- [{ t: name(f.path), b: true }, ...(f.created ? [{ t: ' new', c: 'success' }] : [])],
+ [{ t: name(f.path), b: true }, ...(f.created ? [{ t: ' new', c: 'success', keep: true }] : [])],
[
{
cells: [{ t: '■'.repeat(bar.plus), c: 'success' }, { t: '■'.repeat(bar.minus), c: 'error' }, { t: '·'.repeat(bar.rest), d: true }],
@@ -280,7 +288,7 @@ export const register: Register = on => {
],
inner,
)
- const sub = spread([{ t: ` ${folder(f.path, cwd)}`, d: true }], [{ t: `${f.agents.join(', ')} · ${f.edits}× · ${ago(now - f.at)}`, d: true }], inner)
+ const sub = spread([{ t: ` ${folder(f.path, cwd)}`, d: true }], [{ t: f.agents.join(', '), d: true }, { t: ` · ${f.edits}× · ${ago(now - f.at)}`, d: true, keep: true }], inner)
return i ? [[], top, sub] : [top, sub]
})
: [[{ t: 'Files Claude edits or writes show here.', d: true }]]
diff --git a/change-ledger/tests/change-ledger.test.ts b/change-ledger/tests/change-ledger.test.ts
index ea7d124..c83218a 100644
--- a/change-ledger/tests/change-ledger.test.ts
+++ b/change-ledger/tests/change-ledger.test.ts
@@ -1,4 +1,5 @@
-import { expect, test } from 'claude-code/testing'
+import { expect, mock, test } from 'claude-code/testing'
+import type { LedgerFile, LedgerGit } from '../types'
import { ago, folder, lineDelta, lines, parseNumstat, parseStatus, statBar } from '../hooks/register'
@@ -47,3 +48,29 @@ test('the stat bar scales to the largest change and keeps a cell for a tiny one'
expect(statBar(0, 0, 10, 8)).toEqual({ plus: 0, minus: 0, rest: 8 })
expect(ago(90_000)).toBe('1m ago')
})
+
+const PANE = { title: 'Changes', isFocused: false, bodyColumns: 40, placement: 'dock', scroll: { offset: 0, bodyRows: 40 }, view: {} } as const
+
+test('off the terminal only paths, folders, agents and the branch give way', async ($, on) => {
+ mock.clock(on, { now: 90_000 })
+ on('session.cwd', () => ({ value: '/repo' }))
+ const files: LedgerFile[] = [
+ { path: '/repo/src/components/settings/a-rather-long-file-name.tsx', added: 120, removed: 30, edits: 3, at: 0, agents: ['main', 'general-purpose'], created: true },
+ ]
+ const git: LedgerGit = {
+ root: '/repo',
+ branch: 'fix/a-branch-name-long-enough-to-run-past-the-pane',
+ ahead: 2,
+ behind: 1,
+ files: [{ status: '??', path: 'notes/an-untracked-file-with-a-long-name.md' }],
+ }
+ const held: Record = { files, git }
+ on('state.get', (_$, e) => ({ value: { value: held[e.key], version: 1 } }))
+ const ui = await $.ui.mount({ plugin: 'change-ledger', surface: 'desktop', component: 'Pane', props: PANE, requestId: 'changes' })
+ const texts = (await ui.findAll({ type: 'Text' })).map(r => r.text)
+ const kept = (await ui.findAll({ type: 'Box' })).filter(b => b.props.flexShrink === 0).map(b => b.text)
+ await ui.unmount()
+ // The long parts arrive whole for the surface to cut.
+ for (const t of [' fix/a-branch-name-long-enough-to-run-past-the-pane', 'notes/an-untracked-file-with-a-long-name.md', 'main, general-purpose']) expect(texts).toContain(t)
+ for (const t of ['1 file ', '+120', ' −30', ' ↑2', ' ↓1', 'Edits', ' new', ' · 3× · 1m ago', 'Working tree', 'new']) expect(kept).toContain(t)
+})