@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 +6 -6
- package/mod/peer-thread-mark/.claude-plugin/plugin.json +1 -1
- package/mod/peer-thread-mark/hooks/register.tsx +50 -24
- package/mod/peer-thread-mark/tests/band.test.tsx +5 -0
- package/mod/peer-thread-mark/tests/modes.test.tsx +1 -0
- package/mod/peer-thread-mark/tests/saved.test.tsx +2 -2
- package/package.json +1 -1
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;
|
|
23
|
-
the thread uses:
|
|
24
|
-
- **Use Elephant** (shared): your OpenViking
|
|
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;
|
|
27
|
-
- **Use Jellyfish** (shared): no memory at all, neither
|
|
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.
|
|
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
|
-
|
|
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: '
|
|
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), `
|
|
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:
|
|
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
|
-
|
|
726
|
+
const said = () => memoryNote($)
|
|
727
|
+
if (isShared && kind === key) return send($, '/peer stop', said)
|
|
711
728
|
await update($, mode, () => key)
|
|
712
|
-
if (isShared)
|
|
713
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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('
|
|
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
|
|
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)
|