@webinson/peer 0.4.15 → 0.4.16

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.
package/README.md CHANGED
@@ -19,12 +19,12 @@ In a Claude Code thread:
19
19
  - `/peer stop` ends sharing. Quitting or restarting Claude Code only pauses it: the thread shows as stopped and
20
20
  is shared again when the session is resumed. `/clear` and signing out of Claude Code end it.
21
21
  - A band above the prompt shows the thread's memories (`17 memories`), `Use Elephant` and `Use Jellyfish`, and
22
- `● Active` (shared) or `○ Inactive` at the right; hovering a control explains it. The modes decide what memory
23
- the thread uses:
24
- - **Use Elephant** (shared): your OpenViking memory, recalled with each prompt and saved as the thread goes, on
25
- top of Claude Code's own memory. It saves only the turns from the moment it was chosen.
26
- - **Inactive** (not shared): Claude Code's own memory alone; OpenViking stays off.
27
- - **Use Jellyfish** (shared): no memory at all, neither OpenViking nor Claude Code's own (`MEMORY.md`).
22
+ `● Active` (shared) or `○ Inactive` at the right; a press says in a toast what the thread now uses. The modes
23
+ decide what memory the thread uses:
24
+ - **Use Elephant** (shared): PeerMemory (your personal OpenViking), recalled with each prompt and saved as the
25
+ thread goes, on top of Claude Code's own memory. It saves only the turns from the moment it was chosen.
26
+ - **Inactive** (not shared): Claude Code's own memory alone; PeerMemory stays off.
27
+ - **Use Jellyfish** (shared): no memory at all, neither PeerMemory nor Claude Code's own (`MEMORY.md`).
28
28
  `CLAUDE.md` instructions stay.
29
29
 
30
30
  Pressing the lit mode again stops sharing (Inactive); pressing either while Inactive shares again. Every thread
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peer-thread-mark",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
4
4
  "description": "Marks a Claude Code thread shared with Peer (/peer) with a band above the prompt, with /peer stop and /peer start; panes for /peer-list, /peer-thread and /peer-all (search your memories)",
5
5
  "license": "MIT",
6
6
  "types": "./types/index.d.ts"
@@ -453,7 +453,8 @@ function projects(people: { email: string; threads: Thread[] }[]) {
453
453
 
454
454
  // A press does what typing the command does: the CLI's own hook shares or stops the thread and says so.
455
455
  // (A plugin may not submit a prompt that starts with `/`, so the hook is run directly.)
456
- async function send($: EngineInterface, prompt: string) {
456
+ // `said`, when given, is the toast in place of the CLI's own line, once the press is done; a failure still says why.
457
+ async function send($: EngineInterface, prompt: string, said?: () => Promise<string>) {
457
458
  try {
458
459
  const argv = await promptHook($)
459
460
  if (!argv) {
@@ -463,11 +464,23 @@ async function send($: EngineInterface, prompt: string) {
463
464
  const input = { session_id: await $.session.id(), cwd: await $.env.get('PWD'), prompt }
464
465
  const { stdout } = await $.process.run(argv, { stdin: JSON.stringify(input), timeoutMs: 10_000 })
465
466
  const reason = JSON.parse(stdout.trim().split('\n').pop() || '{}').reason
466
- if (reason) $.ui.toast(String(reason))
467
+ if (reason && !said) $.ui.toast(String(reason))
467
468
  } catch (error) {
468
469
  $.ui.toast(`Peer: ${error instanceof Error ? error.message : String(error)}`)
470
+ await refresh($)
471
+ return
469
472
  }
470
473
  await refresh($)
474
+ if (said) $.ui.toast(await said())
475
+ }
476
+
477
+ // What a press of the band leaves the thread with, in a toast that stands in for a tooltip: the desktop does not
478
+ // follow the pointer over the band, so a hover hint cannot show there.
479
+ async function memoryNote($: EngineInterface) {
480
+ const use = await memoryUse($)
481
+ if (use === 'openviking') return 'Elephant: PeerMemory recalls and saves from now on.'
482
+ if (use === 'none') return 'Jellyfish: no memory in this thread.'
483
+ return 'Inactive: not shared, Claude Code memory only.'
471
484
  }
472
485
 
473
486
  export const register: Register = on => {
@@ -519,7 +532,7 @@ export const register: Register = on => {
519
532
 
520
533
  on('tool.call', async ($, e, next) => {
521
534
  if (e.tool.startsWith(OPENVIKING_TOOL) && (await memoryUse($)) !== 'openviking') {
522
- return { deny: 'OpenViking is off in this thread: only Use Elephant in the Peer band turns it on.' }
535
+ return { deny: 'PeerMemory is off in this thread. Use Elephant turns it on.' }
523
536
  }
524
537
  return next(e)
525
538
  })
@@ -672,7 +685,7 @@ export const register: Register = on => {
672
685
  {nearHere.map((memory, index) => savedCard(memory, fromHere.length + index))}
673
686
  {recalled.map(recallCard)}
674
687
  {asked.map((use, index) =>
675
- card(`a-${e.requestId}-${index}`, 'Asked', <Text bold wrap="truncate-end">{use.items[0] ?? use.kind}</Text>, clock(use.at), `OpenViking ${use.kind}`),
688
+ card(`a-${e.requestId}-${index}`, 'Asked', <Text bold wrap="truncate-end">{use.items[0] ?? use.kind}</Text>, clock(use.at), `PeerMemory ${use.kind}`),
676
689
  )}
677
690
  </Box>
678
691
  )}
@@ -696,9 +709,12 @@ export const register: Register = on => {
696
709
  const kind = await read($, mode)
697
710
 
698
711
  const { Box, Button, Text } = $.ui.resolve(e)
699
- // A hint is the band's tooltip: a line under the controls, hidden until the pointer is on its control's scope.
712
+ // A hint is the band's tooltip: one line in the free space of the band's own row, between the modes and the
713
+ // status, hidden until the pointer is on its control, so the band never has to grow for it. The control's zone
714
+ // Box and its Button both join the hint's scope, as a desktop's native button may take the pointer from the Box
715
+ // around it; the zones carry keys of their own, apart from their Buttons'.
700
716
  const hint = (scope: string, text: string) => (
701
- <Box key={`hint-${scope}`} display="none" hover={{ scope, display: 'flex' }}>
717
+ <Box key={`hint-${scope}`} display="none" flexGrow={1} hover={{ scope, display: 'flex' }}>
702
718
  <Text dimColor size="footnote" wrap="truncate-end">{text}</Text>
703
719
  </Box>
704
720
  )
@@ -707,26 +723,34 @@ export const register: Register = on => {
707
723
  // A mode is lit only while the thread is shared: pressing the lit one stops sharing (Inactive), pressing
708
724
  // either while Inactive picks it and shares again (Active), pressing the other while Active only switches.
709
725
  const pick = async (key: Mode) => {
710
- if (isShared && kind === key) return send($, '/peer stop')
726
+ const said = () => memoryNote($)
727
+ if (isShared && kind === key) return send($, '/peer stop', said)
711
728
  await update($, mode, () => key)
712
- if (isShared) await applyMemory($)
713
- else await send($, '/peer')
729
+ if (!isShared) return send($, '/peer', said)
730
+ await applyMemory($)
731
+ $.ui.toast(await said())
714
732
  }
715
733
  // A desktop draws a Button's label as its own native button and leaves Text inside it unstyled, so the lit
716
734
  // mode is the primary button and the other the secondary; the terminal draws the primary in the accent color.
717
735
  const animal = (key: Mode, name: string) => (
718
- <Box key={key} hover={{ scope: `peer-${key}` }}>
719
- <Button key={key} label={name} variant={isShared && kind === key ? 'primary' : 'secondary'} onPress={() => void pick(key)} />
736
+ <Box key={`zone-${key}`} gap={1} alignItems="center" hover={{ scope: `peer-${key}` }}>
737
+ <Button key={key} label={name} variant={isShared && kind === key ? 'primary' : 'secondary'} hover={{ scope: `peer-${key}` }} onPress={() => void pick(key)} />
720
738
  </Box>
721
739
  )
722
740
  const modeHint = (key: Mode, about: string) =>
723
- `${about} ${isShared && kind === key ? 'Press again to turn Peer off for this thread.' : isShared ? 'Press to switch.' : 'Press to turn Peer on with it.'}`
741
+ `${about} ${isShared && kind === key ? 'Press again to stop.' : isShared ? 'Press to switch.' : 'Press to share with it.'}`
742
+ const hints = {
743
+ memory: total ? 'What this thread saved.' : 'Nothing saved yet.',
744
+ elephant: modeHint('elephant', 'Recall and save with PeerMemory.'),
745
+ jellyfish: modeHint('jellyfish', 'No memory at all.'),
746
+ share: isShared ? 'Shared with your workspace. Press to stop.' : 'Not shared. Press to share.',
747
+ }
724
748
  return (
725
749
  <Box flexDirection="column">
726
750
  <Box width="100%" justifyContent="space-between" gap={2}>
727
751
  <Box gap={2}>
728
- <Box key="memory" hover={{ scope: 'peer-memory' }}>
729
- <Button key="memory" label={`${count} ${noun}`} plain onPress={async () => { memoryOpen = await openThread($) }}>
752
+ <Box key="zone-memory" gap={1} alignItems="center" hover={{ scope: 'peer-memory' }}>
753
+ <Button key="memory" label={`${count} ${noun}`} plain hover={{ scope: 'peer-memory' }} onPress={async () => { memoryOpen = await openThread($) }}>
730
754
  <Text bold tabular>{count}</Text>
731
755
  <Text>{` ${noun}`}</Text>
732
756
  </Button>
@@ -736,16 +760,18 @@ export const register: Register = on => {
736
760
  {animal('jellyfish', 'Use Jellyfish')}
737
761
  </Box>
738
762
  </Box>
763
+ <Box flexGrow={1} flexShrink={1} paddingX={2}>
764
+ {e.surface === 'terminal' && hint('peer-memory', hints.memory)}
765
+ {e.surface === 'terminal' && hint('peer-elephant', hints.elephant)}
766
+ {e.surface === 'terminal' && hint('peer-jellyfish', hints.jellyfish)}
767
+ {e.surface === 'terminal' && hint('peer-share', hints.share)}
768
+ </Box>
739
769
  {/* The dot is Text beside the Button, where both surfaces keep its color. */}
740
- <Box key="toggle" gap={1} alignItems="center" hover={{ scope: 'peer-share' }}>
770
+ <Box key="zone-toggle" gap={1} alignItems="center" hover={{ scope: 'peer-share' }}>
741
771
  <Text color={isShared ? 'success' : 'inactive'}>{isShared ? '●' : '○'}</Text>
742
- <Button key="toggle" label={isShared ? 'Active' : 'Inactive'} plain dimColor={!isShared} onPress={() => send($, isShared ? '/peer stop' : '/peer')} />
772
+ <Button key="toggle" label={isShared ? 'Active' : 'Inactive'} plain dimColor={!isShared} hover={{ scope: 'peer-share' }} onPress={() => send($, isShared ? '/peer stop' : '/peer', () => memoryNote($))} />
743
773
  </Box>
744
774
  </Box>
745
- {hint('peer-memory', total ? `What this thread has saved to your memory. Open the list.` : 'Nothing saved from this thread yet. Open the list.')}
746
- {hint('peer-elephant', modeHint('elephant', 'Elephant never forgets: this thread saves what it learns and recalls your memories.'))}
747
- {hint('peer-jellyfish', modeHint('jellyfish', 'Jellyfish lives in the moment: this thread saves nothing and recalls nothing.'))}
748
- {hint('peer-share', isShared ? 'Shared with your Peer workspace: teammates see this thread. Press to stop sharing (/peer stop).' : 'Not shared. Press to share this thread with your Peer workspace (/peer).')}
749
775
  </Box>
750
776
  )
751
777
  })
@@ -901,7 +927,7 @@ export const register: Register = on => {
901
927
  return status(now?.error ? <Text color="error">{`Peer: your memories did not load (${now.error})`}</Text> : <Text dimColor>Loading…</Text>)
902
928
  }
903
929
  if (now.error) return status(<Text color="error">{`Peer: your memories did not load (${now.error})`}</Text>)
904
- if (!found.connected) return status(<Text dimColor>{`No personal OpenViking here (${found.reason}).`}</Text>)
930
+ if (!found.connected) return status(<Text dimColor>{`No PeerMemory here (${found.reason}).`}</Text>)
905
931
  if (found.total === 0) return status(<Text dimColor>{ask.text ? 'Nothing has those words.' : 'No memories yet.'}</Text>)
906
932
 
907
933
  const words = searchWords(ask.text)
@@ -1005,7 +1031,7 @@ export const register: Register = on => {
1005
1031
  </Box>
1006
1032
  )
1007
1033
  }
1008
- if (!now.connected) return <Text dimColor>{`No personal OpenViking here (${now.reason}).`}</Text>
1034
+ if (!now.connected) return <Text dimColor>{`No PeerMemory here (${now.reason}).`}</Text>
1009
1035
 
1010
1036
  const groups = sections(now.saves)
1011
1037
  const total = groups.check.length + groups.added.length + groups.updated.length + groups.removed.length
@@ -1074,7 +1100,7 @@ export const register: Register = on => {
1074
1100
  // Where memory went into the thread, oldest first: one line each, what and when; a press opens which memories,
1075
1101
  // and Show in thread where it came with a message.
1076
1102
  const uses = now.uses ?? []
1077
- const USE_WORDS: Record<string, string> = { start: 'At the start', resume: 'On resume', prompt: 'With your message', tool: 'Asked OpenViking' }
1103
+ const USE_WORDS: Record<string, string> = { start: 'At the start', resume: 'On resume', prompt: 'With your message', tool: 'Asked PeerMemory' }
1078
1104
  const useRow = (use: MemoryUse, index: number) => {
1079
1105
  const id = `use-${index}`
1080
1106
  const isOpen = open === id
@@ -1122,7 +1148,7 @@ export const register: Register = on => {
1122
1148
  {uses.map(useRow)}
1123
1149
  </Box>
1124
1150
  )}
1125
- {total === 0 && <Text dimColor>Nothing saved yet. OpenViking saves after longer turns.</Text>}
1151
+ {total === 0 && <Text dimColor>Nothing saved yet. PeerMemory saves after longer turns.</Text>}
1126
1152
  {section('Check', groups.check, true)}
1127
1153
  {section('New', groups.added)}
1128
1154
  {section('Updated', groups.updated)}
@@ -45,6 +45,8 @@ test('the band counts the thread\'s memories, names no workspace, and opens the
45
45
  on('session.id', () => ({ value: 's1' }) as never)
46
46
  on('env.set', () => ({ value: {} }) as never)
47
47
  on('ui.invalidate', () => ({ value: undefined }) as never)
48
+ const toasts: string[] = []
49
+ on('ui.toast', (_, e) => { toasts.push(String((e as { text?: string }).text ?? '')); return { value: undefined } as never })
48
50
  on('clock.now', () => ({ value: 1 }) as never)
49
51
  const opened: string[] = []
50
52
  on('ui.open', (_, e) => { opened.push((e as { id: string }).id); return { value: { isPlaced: true } } as never })
@@ -93,5 +95,8 @@ test('the band counts the thread\'s memories, names no workspace, and opens the
93
95
  texts = (await modes.findAll({ type: 'Text' })).map(t => t.text)
94
96
  expect(texts).toContain('●')
95
97
  expect(await lit(modes as never)).toEqual(['Use Jellyfish'])
98
+ // The desktop does not follow the pointer over the band, so each press says what the thread now uses, as a toast.
99
+ expect(toasts.at(-1)).toBe('Jellyfish: no memory in this thread.')
100
+ expect((await modes.findAll({ type: 'Box' } as never) as { key?: string }[]).some(b => b.key === 'hint-peer-jellyfish')).toBe(false)
96
101
  await modes.unmount()
97
102
  })
@@ -40,6 +40,7 @@ test('Elephant turns OpenViking on, Inactive keeps Claude Code memory alone, Jel
40
40
  on('command.register', (_, e) => ({ value: { command: (e as { name: string }).name } }) as never)
41
41
  on('ui.status', () => ({ value: undefined }) as never)
42
42
  on('ui.invalidate', () => ({ value: undefined }) as never)
43
+ on('ui.toast', () => ({ value: undefined }) as never)
43
44
  on('session.start', (_, e) => ({ cwd: e.cwd }))
44
45
  // Beneath the mod: OpenViking's recall and a hook of the person's own, a tool that answers, core's memory.
45
46
  on('classic.UserPromptSubmit', () => ({ additionalContext: ['<openviking-context>- [memory 80%] x</openviking-context>', 'kept'] }) as never)
@@ -118,7 +118,7 @@ test('a message shows under it what OpenViking saved from it, and what it only u
118
118
  olderTexts = (await older.findAll({ type: 'Text' })).map(t => t.text)
119
119
  expect(olderTexts).toContain('The partner API takes at most 100 items')
120
120
  expect(olderTexts).toContain(' Asked ')
121
- expect(olderTexts).toContain('OpenViking search')
121
+ expect(olderTexts).toContain('PeerMemory search')
122
122
  await older.unmount()
123
123
 
124
124
  // The /peer-thread pane lists where memory went into the thread; one with a message can show it.
@@ -130,7 +130,7 @@ test('a message shows under it what OpenViking saved from it, and what it only u
130
130
  expect(usedTexts).toContain('Used')
131
131
  expect(usedTexts).toContain('At the start · 2 memories listed')
132
132
  expect(usedTexts).toContain("With your message · 1 teammate's page")
133
- expect(usedTexts).toContain('Asked OpenViking: search')
133
+ expect(usedTexts).toContain('Asked PeerMemory: search')
134
134
  await usedPane.press({ key: 'use-0' } as never)
135
135
  expect((await usedPane.findAll({ type: 'Text' })).map(t => t.text)).toContain('· This Mac')
136
136
  expect((await usedPane.findAll({ type: 'Button' })).some(b => b.key === 'use-0-show')).toBe(false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webinson/peer",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
4
4
  "description": "Share chosen Claude Code threads with your team: who has agents running and on what.",
5
5
  "license": "MIT",
6
6
  "type": "module",