backstep 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backstep",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Press ctrl+z to rewind everything Claude did in its last turn: file edits, shell commands, and database and tool changes.",
5
5
  "types": "./types/index.d.ts",
6
6
  "userConfig": {
@@ -132,6 +132,8 @@ test('with nothing to undo the band still holds the key, and a press says there
132
132
 
133
133
  const ui = await $.ui.mount({ ...BAND, surface: 'terminal' })
134
134
  expect((await ui.find({ type: 'Text', text: /nothing to undo yet/ }))).toBeTruthy()
135
+ // The mark: the front triangle in the terminal's own color, the one behind in the logo's pink.
136
+ expect((await ui.find({ type: 'Text', text: '◀', props: { color: '#ee7bb6' } }))).toBeTruthy()
135
137
  await ui.press({ key: 'undo' })
136
138
  expect(toasts[0]).toContain('Nothing to undo yet')
137
139
  await ui.unmount()
@@ -13,6 +13,12 @@ const turns = atom({ plugin: 'backstep', key: 'turns' } as const, [] as Turn[])
13
13
  const rewind = atom({ plugin: 'backstep', key: 'rewind' } as const, null as Rewind | null)
14
14
  const note = atom({ plugin: 'backstep', key: 'note' } as const, null as string | null)
15
15
  const armed = atom({ plugin: 'backstep', key: 'armed' } as const, null as number | null)
16
+ // The keys bound to undo, read as a session starts and when the person changes
17
+ // them. Kept in state, not a variable, so the band redraws with the new key.
18
+ const keys = atom({ plugin: 'backstep', key: 'keys' } as const, ['ctrl+u'] as string[])
19
+ // Backstep's mark, as on the logo: the front rewind triangle in the terminal's
20
+ // own black or white, the one behind it in the logo's pink.
21
+ const PINK = '#ee7bb6'
16
22
  // How long the "press again to undo" question waits before it lets go.
17
23
  const CONFIRM_MS = 5000
18
24
 
@@ -186,8 +192,9 @@ async function undoLastTurn($: EngineInterface) {
186
192
 
187
193
  if (done) {
188
194
  const list = items.filter((i) => i.ok).map((i) => `- ${i.label}`).join('\n')
195
+ const key = await keysLabel($)
189
196
  await update($, note, () =>
190
- `[Backstep] The user pressed undo (${keysLabel()}) on your previous turn. These changes were reverted:\n${list}\n` +
197
+ `[Backstep] The user pressed undo (${key}) on your previous turn. These changes were reverted:\n${list}\n` +
191
198
  (lost ? `${plural(lost, 'action')} from that turn couldn't be undone (they reached outside this machine with no way back, or the project was too big to snapshot).\n` : '') +
192
199
  `Treat them as not having happened, re-read files before editing them, and don't redo them unless asked.`,
193
200
  )
@@ -275,10 +282,8 @@ type ShellPlan = { kind: 'read' | 'write' | 'outbound'; reason?: string; id?: st
275
282
 
276
283
  // Set from the plugin's options each time the module registers.
277
284
  let cli = 'backstep'
278
- // The keys bound to undo, read as a session starts and when the person changes them.
279
- let undoKeys = ['ctrl+u']
280
285
  const keyLabel = (k: string) => (k.startsWith('cmd+') ? `⌘${k.slice(4).toUpperCase()}` : k)
281
- const keysLabel = () => undoKeys.map(keyLabel).join(' / ')
286
+ const keysLabel = async ($: EngineInterface) => (await read($, keys)).map(keyLabel).join(' / ')
282
287
 
283
288
  function backstep(args: string[]): string[] {
284
289
  return cli === 'backstep' ? ['backstep', ...args] : ['node', '--disable-warning=ExperimentalWarning', cli, ...args]
@@ -441,10 +446,12 @@ async function changeKey($: EngineInterface, key: string): Promise<string> {
441
446
  r = { ok: false, detail: ran.stderr.trim() || 'it failed' }
442
447
  }
443
448
  if (!r.ok || !r.key) return `Couldn't change the undo key: ${r.detail ?? 'it failed'}`
444
- undoKeys = r.keys?.length ? r.keys : [r.key]
445
- $.ui.toast(`Backstep: undo is now ${keysLabel()}.`)
446
- const was = r.previous && !undoKeys.includes(r.previous) ? ` (it was ${keyLabel(r.previous)})` : ''
447
- return `Undo is now ${keysLabel()}${was}. It works right away.`
449
+ const now = r.keys?.length ? r.keys : [r.key]
450
+ await update($, keys, () => now)
451
+ const label = await keysLabel($)
452
+ $.ui.toast(`Backstep: undo is now ${label}.`)
453
+ const was = r.previous && !now.includes(r.previous) ? ` (it was ${keyLabel(r.previous)})` : ''
454
+ return `Undo is now ${label}${was}. It works right away.`
448
455
  }
449
456
 
450
457
  const CONNECT = 'mcp__backstep__connect'
@@ -499,15 +506,15 @@ export const register: Register = (on, options) => {
499
506
  await $.tool.register(CONNECT_TOOL)
500
507
  await $.tool.register(KEY_TOOL)
501
508
  const r = await next(e)
502
- const bound = await $.process.run(backstep(['key', '--json'])).catch(() => null)
509
+ const boundOut = await $.process.run(backstep(['key', '--json'])).catch(() => null)
503
510
  try {
504
- const keys = (JSON.parse(bound?.stdout || '{}') as { keys?: string[] }).keys
505
- if (keys?.length) undoKeys = keys
511
+ const bound = (JSON.parse(boundOut?.stdout || '{}') as { keys?: string[] }).keys
512
+ if (bound?.length) await update($, keys, () => bound)
506
513
  } catch {
507
514
  // keep the default
508
515
  }
509
516
  // Said first thing, so nobody has to wonder whether it's running.
510
- $.ui.log(`✓ Backstep connected · press ${keysLabel()} to undo Claude's last turn`)
517
+ $.ui.log(`✓ Backstep connected · press ${await keysLabel($)} to undo Claude's last turn`)
511
518
  // In the background: a session never waits on either.
512
519
  void wrapNewServers($, e.cwd)
513
520
  // One question at a time: Supabase first, then any update.
@@ -592,15 +599,17 @@ export const register: Register = (on, options) => {
592
599
  if (r?.phase === 'running') {
593
600
  return (
594
601
  <Box>
595
- <Text color="success" bold>↺ Rewinding </Text>
596
- <Text color="success">{bar(r.frame)}</Text>
602
+ <Text bold>◀</Text>
603
+ <Text color={PINK} bold>◀</Text>
604
+ <Text bold> Rewinding </Text>
605
+ <Text>{bar(r.frame)}</Text>
597
606
  <Text dimColor> undoing the last turn…</Text>
598
607
  </Box>
599
608
  )
600
609
  }
601
610
 
602
611
  if (r) {
603
- const tone = r.phase === 'failed' ? 'error' : r.phase === 'empty' ? 'warning' : 'success'
612
+ const tone = r.phase === 'failed' ? 'error' : r.phase === 'empty' ? 'warning' : undefined
604
613
  const glyph = r.phase === 'failed' ? '✗' : r.phase === 'empty' ? '!' : '✓'
605
614
  const visible = r.items.slice(0, Math.min(r.shown, 6))
606
615
  const more = r.items.length - 6
@@ -612,7 +621,7 @@ export const register: Register = (on, options) => {
612
621
  {visible.map((i, n) => (
613
622
  <Text key={`i${n}`} color={i.ok ? undefined : 'error'} dimColor={i.ok}>
614
623
  {' '}
615
- {i.ok ? '↺ ' : '✗ '}
624
+ {i.ok ? '◀ ' : '✗ '}
616
625
  {i.label}
617
626
  </Text>
618
627
  ))}
@@ -622,13 +631,16 @@ export const register: Register = (on, options) => {
622
631
  }
623
632
 
624
633
  const target = (await read($, turns)).findLast(hasWork)
634
+ const label = (await read($, keys)).map(keyLabel).join(' / ')
625
635
  if (target && (await read($, armed)) !== null) {
626
636
  return (
627
637
  <Box>
628
- <Text color="warning" bold>↺ Undo Claude's last turn? </Text>
638
+ <Text bold>◀</Text>
639
+ <Text color={PINK} bold>◀</Text>
640
+ <Text color="warning" bold> Undo Claude's last turn? </Text>
629
641
  <Text dimColor>{summary(target)} </Text>
630
642
  <Button key="undo" action={UNDO_ACTION} plain onPress={() => void pressUndo($)}>
631
- {`${keysLabel()} again to undo`}
643
+ {`${label} again to undo`}
632
644
  </Button>
633
645
  <Text dimColor> · </Text>
634
646
  <Button key="cancel" plain dimColor onPress={() => void update($, armed, () => null)}>
@@ -643,10 +655,12 @@ export const register: Register = (on, options) => {
643
655
  // with nothing to undo, a press says so rather than doing nothing.
644
656
  return (
645
657
  <Box>
646
- <Text color="success">↺ </Text>
658
+ <Text bold>◀</Text>
659
+ <Text color={PINK} bold>◀</Text>
660
+ <Text> </Text>
647
661
  <Text dimColor>{target ? `Last turn: ${summary(target)} ` : 'Backstep is on · nothing to undo yet '}</Text>
648
662
  <Button key="undo" action={UNDO_ACTION} plain dimColor onPress={() => void pressUndo($)}>
649
- {`${keysLabel()} to undo`}
663
+ {`${label} to undo`}
650
664
  </Button>
651
665
  </Box>
652
666
  )
@@ -26,6 +26,7 @@ export type Rewind = {
26
26
  declare module 'claude-code' {
27
27
  interface PluginState {
28
28
  // armed: when the undo key was first pressed; a second press within a few seconds undoes.
29
- backstep: { turns: Turn[]; rewind: Rewind | null; note: string | null; armed: number | null }
29
+ // keys: the keys bound to undo, kept here so the band redraws when they change.
30
+ backstep: { turns: Turn[]; rewind: Rewind | null; note: string | null; armed: number | null; keys: string[] }
30
31
  }
31
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backstep",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Undo for everything your agents touch: an MCP proxy that makes agent actions reversible.",
5
5
  "scripts": {
6
6
  "dev": "node src/cli.ts",