backstep 0.1.6 → 0.1.7

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.7",
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": {
@@ -13,6 +13,11 @@ 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, two rewind triangles, in the terminal's own black or white.
20
+ const MARK = '◀◀'
16
21
  // How long the "press again to undo" question waits before it lets go.
17
22
  const CONFIRM_MS = 5000
18
23
 
@@ -186,8 +191,9 @@ async function undoLastTurn($: EngineInterface) {
186
191
 
187
192
  if (done) {
188
193
  const list = items.filter((i) => i.ok).map((i) => `- ${i.label}`).join('\n')
194
+ const key = await keysLabel($)
189
195
  await update($, note, () =>
190
- `[Backstep] The user pressed undo (${keysLabel()}) on your previous turn. These changes were reverted:\n${list}\n` +
196
+ `[Backstep] The user pressed undo (${key}) on your previous turn. These changes were reverted:\n${list}\n` +
191
197
  (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
198
  `Treat them as not having happened, re-read files before editing them, and don't redo them unless asked.`,
193
199
  )
@@ -275,10 +281,8 @@ type ShellPlan = { kind: 'read' | 'write' | 'outbound'; reason?: string; id?: st
275
281
 
276
282
  // Set from the plugin's options each time the module registers.
277
283
  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
284
  const keyLabel = (k: string) => (k.startsWith('cmd+') ? `⌘${k.slice(4).toUpperCase()}` : k)
281
- const keysLabel = () => undoKeys.map(keyLabel).join(' / ')
285
+ const keysLabel = async ($: EngineInterface) => (await read($, keys)).map(keyLabel).join(' / ')
282
286
 
283
287
  function backstep(args: string[]): string[] {
284
288
  return cli === 'backstep' ? ['backstep', ...args] : ['node', '--disable-warning=ExperimentalWarning', cli, ...args]
@@ -441,10 +445,12 @@ async function changeKey($: EngineInterface, key: string): Promise<string> {
441
445
  r = { ok: false, detail: ran.stderr.trim() || 'it failed' }
442
446
  }
443
447
  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.`
448
+ const now = r.keys?.length ? r.keys : [r.key]
449
+ await update($, keys, () => now)
450
+ const label = await keysLabel($)
451
+ $.ui.toast(`Backstep: undo is now ${label}.`)
452
+ const was = r.previous && !now.includes(r.previous) ? ` (it was ${keyLabel(r.previous)})` : ''
453
+ return `Undo is now ${label}${was}. It works right away.`
448
454
  }
449
455
 
450
456
  const CONNECT = 'mcp__backstep__connect'
@@ -499,15 +505,15 @@ export const register: Register = (on, options) => {
499
505
  await $.tool.register(CONNECT_TOOL)
500
506
  await $.tool.register(KEY_TOOL)
501
507
  const r = await next(e)
502
- const bound = await $.process.run(backstep(['key', '--json'])).catch(() => null)
508
+ const boundOut = await $.process.run(backstep(['key', '--json'])).catch(() => null)
503
509
  try {
504
- const keys = (JSON.parse(bound?.stdout || '{}') as { keys?: string[] }).keys
505
- if (keys?.length) undoKeys = keys
510
+ const bound = (JSON.parse(boundOut?.stdout || '{}') as { keys?: string[] }).keys
511
+ if (bound?.length) await update($, keys, () => bound)
506
512
  } catch {
507
513
  // keep the default
508
514
  }
509
515
  // 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`)
516
+ $.ui.log(`✓ Backstep connected · press ${await keysLabel($)} to undo Claude's last turn`)
511
517
  // In the background: a session never waits on either.
512
518
  void wrapNewServers($, e.cwd)
513
519
  // One question at a time: Supabase first, then any update.
@@ -592,15 +598,15 @@ export const register: Register = (on, options) => {
592
598
  if (r?.phase === 'running') {
593
599
  return (
594
600
  <Box>
595
- <Text color="success" bold>↺ Rewinding </Text>
596
- <Text color="success">{bar(r.frame)}</Text>
601
+ <Text bold>{MARK} Rewinding </Text>
602
+ <Text>{bar(r.frame)}</Text>
597
603
  <Text dimColor> undoing the last turn…</Text>
598
604
  </Box>
599
605
  )
600
606
  }
601
607
 
602
608
  if (r) {
603
- const tone = r.phase === 'failed' ? 'error' : r.phase === 'empty' ? 'warning' : 'success'
609
+ const tone = r.phase === 'failed' ? 'error' : r.phase === 'empty' ? 'warning' : undefined
604
610
  const glyph = r.phase === 'failed' ? '✗' : r.phase === 'empty' ? '!' : '✓'
605
611
  const visible = r.items.slice(0, Math.min(r.shown, 6))
606
612
  const more = r.items.length - 6
@@ -612,7 +618,7 @@ export const register: Register = (on, options) => {
612
618
  {visible.map((i, n) => (
613
619
  <Text key={`i${n}`} color={i.ok ? undefined : 'error'} dimColor={i.ok}>
614
620
  {' '}
615
- {i.ok ? '↺ ' : '✗ '}
621
+ {i.ok ? '◀ ' : '✗ '}
616
622
  {i.label}
617
623
  </Text>
618
624
  ))}
@@ -622,13 +628,14 @@ export const register: Register = (on, options) => {
622
628
  }
623
629
 
624
630
  const target = (await read($, turns)).findLast(hasWork)
631
+ const label = (await read($, keys)).map(keyLabel).join(' / ')
625
632
  if (target && (await read($, armed)) !== null) {
626
633
  return (
627
634
  <Box>
628
- <Text color="warning" bold>↺ Undo Claude's last turn? </Text>
635
+ <Text color="warning" bold>{MARK} Undo Claude's last turn? </Text>
629
636
  <Text dimColor>{summary(target)} </Text>
630
637
  <Button key="undo" action={UNDO_ACTION} plain onPress={() => void pressUndo($)}>
631
- {`${keysLabel()} again to undo`}
638
+ {`${label} again to undo`}
632
639
  </Button>
633
640
  <Text dimColor> · </Text>
634
641
  <Button key="cancel" plain dimColor onPress={() => void update($, armed, () => null)}>
@@ -643,10 +650,10 @@ export const register: Register = (on, options) => {
643
650
  // with nothing to undo, a press says so rather than doing nothing.
644
651
  return (
645
652
  <Box>
646
- <Text color="success">↺ </Text>
653
+ <Text bold>{MARK} </Text>
647
654
  <Text dimColor>{target ? `Last turn: ${summary(target)} ` : 'Backstep is on · nothing to undo yet '}</Text>
648
655
  <Button key="undo" action={UNDO_ACTION} plain dimColor onPress={() => void pressUndo($)}>
649
- {`${keysLabel()} to undo`}
656
+ {`${label} to undo`}
650
657
  </Button>
651
658
  </Box>
652
659
  )
@@ -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.7",
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",