@webinson/peer 0.4.2 → 0.4.5

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.
@@ -0,0 +1,107 @@
1
+ import { expect, test } from 'claude-code/testing'
2
+
3
+ const PANE = {
4
+ plugin: 'peer-thread-mark',
5
+ component: 'Pane',
6
+ requestId: 'peer-all-memories',
7
+ props: { title: 'Your memory', isFocused: false, bodyColumns: 44, placement: 'dock', scroll: { offset: 0, bodyRows: 40 }, view: {} },
8
+ } as const
9
+
10
+ const row = (n: number) => ({
11
+ uri: `viking://user/ana/peers/github.com-acme-shop/memories/events/day_${n}.md`,
12
+ repo: 'github.com-acme-shop',
13
+ type: 'events',
14
+ title: `day ${n}`,
15
+ at: '2026-11-03',
16
+ lines: n === 1 ? ['The batch limit is 100.'] : [],
17
+ })
18
+
19
+ test('the search pane lists, searches on Enter, keeps to the repository and pages', async ($, on) => {
20
+ const settings = { hooks: { UserPromptSubmit: [{ hooks: [{ command: '"/n/node" "/p/peer.mjs" hook prompt # peer-hook' }] }] } }
21
+ const runs: { argv: string[]; cwd?: string }[] = []
22
+ on('env.get', (_, e) => ({ value: (e as { name?: string }).name === 'PWD' ? '/work/shop' : '/home' }) as never)
23
+ on('fs.read', (_, e) => ({ value: String((e as { path?: string }).path ?? '').endsWith('settings.json') ? JSON.stringify(settings) : (() => { throw new Error('none') })() }) as never)
24
+ on('process.run', (_, e) => {
25
+ const { argv, init } = e as { argv: string[]; init?: { cwd?: string } }
26
+ runs.push({ argv, cwd: init?.cwd })
27
+ const page = Number(argv[argv.indexOf('page') + 1])
28
+ const text = argv.slice(argv.indexOf('--json') + 1).filter(w => !w.startsWith('--')).slice(0, -2).join(' ')
29
+ const found = text
30
+ ? { connected: true, url: 'http://127.0.0.1:1933', repo: argv.includes('--here') ? 'github.com-acme-shop' : '', total: 1, page: 1, pages: 1, rows: [row(1)] }
31
+ : { connected: true, url: 'http://127.0.0.1:1933', repo: '', total: 23, page, pages: 2, rows: page === 1 ? [row(23), row(22)] : [row(3)] }
32
+ return { value: { stdout: JSON.stringify(found), stderr: '', exitCode: 0 } } as never
33
+ })
34
+ on('session.id', () => ({ value: 's1' }) as never)
35
+ on('clock.now', () => ({ value: Date.parse('2026-11-10T12:00:00Z') }) as never)
36
+ on('clock.sleep', () => ({ value: undefined }) as never)
37
+ on('ui.open', () => ({ value: { isPlaced: true } }) as never)
38
+ on('command.run', () => ({ text: 'engine' }) as never)
39
+ let copied = ''
40
+ on('ui.copy', (_, e) => { copied = (e as { text: string }).text; return { value: { isCopied: true } } as never })
41
+ on('ui.toast', () => ({ value: undefined }) as never)
42
+
43
+ const opened = await $.command.run({ command: 'peer-all', args: '' } as never)
44
+ expect((opened as { text: string }).text).toContain('in the pane')
45
+ expect(runs[0]!.argv.slice(2)).toEqual(['all', '--json', 'page', '1'])
46
+ expect(runs[0]!.cwd).toBe('/work/shop')
47
+
48
+ const ui = await $.ui.mount({ ...PANE, surface: 'desktop' })
49
+ let texts = (await ui.findAll({ type: 'Text' })).map(t => t.text)
50
+ expect(texts).toContain('Day 23')
51
+ expect(texts).toContain('23 memories')
52
+ expect(texts).toContain('1 / 2')
53
+ expect(texts).not.toContain('Today')
54
+
55
+ // Time: unsorted, then newest first, then oldest first, then unsorted again; each press starts at page 1.
56
+ const sortLabel = async () => (await ui.findAll({ type: 'Button' })).find(b => b.key === 'sort')?.props.label
57
+ expect(await sortLabel()).toBe('Time')
58
+ await ui.press({ key: 'sort' } as never)
59
+ expect(runs.at(-1)!.argv).toContain('--newest')
60
+ expect(await sortLabel()).toBe('Newest ↓')
61
+ // Ordered by time, the page falls into days with their counts.
62
+ expect((await ui.findAll({ type: 'Text' })).map(t => t.text)).toContain('3 Nov')
63
+ await ui.press({ key: 'sort' } as never)
64
+ expect(runs.at(-1)!.argv).toContain('--oldest')
65
+ expect(await sortLabel()).toBe('Oldest ↑')
66
+ await ui.press({ key: 'sort' } as never)
67
+ expect(runs.at(-1)!.argv.some(w => w === '--newest' || w === '--oldest')).toBe(false)
68
+ expect(await sortLabel()).toBe('Time')
69
+
70
+ await ui.press({ key: 'next' } as never)
71
+ expect(runs.at(-1)!.argv.slice(-2)).toEqual(['page', '2'])
72
+ texts = (await ui.findAll({ type: 'Text' })).map(t => t.text)
73
+ expect(texts).toContain('Day 3')
74
+
75
+ // Typing searches by itself once it pauses: only the last text typed is searched.
76
+ const before = runs.length
77
+ await $.ui.input({ plugin: 'peer-thread-mark', key: 'q', text: 'batch li', kind: 'change' })
78
+ expect(runs.length).toBe(before + 1)
79
+ expect(runs.at(-1)!.argv.slice(2)).toEqual(['all', '--json', 'batch', 'li', 'page', '1'])
80
+ await $.ui.input({ plugin: 'peer-thread-mark', key: 'q', text: 'batch limit' })
81
+ expect(runs.at(-1)!.argv.slice(2)).toEqual(['all', '--json', 'batch', 'limit', 'page', '1'])
82
+ texts = (await ui.findAll({ type: 'Text' })).map(t => t.text)
83
+ // The matched line sits under its memory, the searched words drawn bold.
84
+ expect(texts.some(t => t.includes('limit is 100'))).toBe(true)
85
+ const bold = (await ui.findAll({ type: 'Text' })).filter(t => t.props.bold === true).map(t => t.text)
86
+ expect(bold).toContain('batch')
87
+ expect(bold).toContain('limit')
88
+
89
+ await ui.press({ key: 'here' } as never)
90
+ expect(runs.at(-1)!.argv).toContain('--here')
91
+ expect(runs.at(-1)!.argv).toContain('batch')
92
+
93
+ // A press opens the memory in place; Copy takes its address; a second press closes it.
94
+ const uri = 'viking://user/ana/peers/github.com-acme-shop/memories/events/day_1.md'
95
+ await ui.press({ key: uri } as never)
96
+ await ui.press({ key: `${uri}-copy` } as never)
97
+ expect(copied).toBe(uri)
98
+ await ui.press({ key: uri } as never)
99
+ expect((await ui.findAll({ type: 'Button' })).some(b => b.key === `${uri}-copy`)).toBe(false)
100
+ await ui.unmount()
101
+
102
+ // Mobile has no field: the list still shows.
103
+ const phone = await $.ui.mount({ ...PANE, surface: 'mobile' })
104
+ expect(await phone.findAll({ type: 'Input' })).toHaveLength(0)
105
+ expect((await phone.findAll({ type: 'Text' })).map(t => t.text).some(t => t.includes('limit is 100'))).toBe(true)
106
+ await phone.unmount()
107
+ })
@@ -0,0 +1,54 @@
1
+ import { expect, test } from 'claude-code/testing'
2
+
3
+ const PANE = {
4
+ plugin: 'peer-thread-mark',
5
+ component: 'Pane',
6
+ requestId: 'peer-thread-memories',
7
+ props: { title: 'This thread', isFocused: false, bodyColumns: 44, placement: 'dock', scroll: { offset: 0, bodyRows: 40 }, view: {} },
8
+ } as const
9
+
10
+ const SAVES = {
11
+ connected: true,
12
+ loadedAt: 1,
13
+ saves: [
14
+ { at: '2026-11-03T13:42:14Z', items: [
15
+ { change: 'added', type: 'events', name: 'panel scope clarified', lines: ['The panel shows people who share threads.'], systemText: false },
16
+ { change: 'updated', type: 'entities', name: 'peer', lines: ['Shared threads list people who typed /peer.'], systemText: false },
17
+ ] },
18
+ { at: '2026-11-03T13:29:28Z', items: [
19
+ { change: 'added', type: 'events', name: 'project alpha displayed', lines: ['Studio showed project-alpha.'], systemText: true },
20
+ { change: 'updated', type: 'entities', name: 'peer', lines: ['older'], systemText: false },
21
+ ] },
22
+ ],
23
+ }
24
+
25
+ test('the memory pane groups by meaning and copies on press', async ($, on) => {
26
+ const settings = { hooks: { UserPromptSubmit: [{ hooks: [{ command: '"/n/node" "/p/peer.mjs" hook prompt # peer-hook' }] }] } }
27
+ on('env.get', () => ({ value: '/home' }) as never)
28
+ on('fs.read', (_, e) => ({ value: String((e as { path?: string }).path ?? '').endsWith('settings.json') ? JSON.stringify(settings) : (() => { throw new Error('none') })() }) as never)
29
+ on('process.run', () => ({ value: { stdout: JSON.stringify(SAVES), stderr: '', exitCode: 0 } }) as never)
30
+ on('session.id', () => ({ value: 's1' }) as never)
31
+ on('clock.now', () => ({ value: 1 }) as never)
32
+ on('ui.open', () => ({ value: { isPlaced: true } }) as never)
33
+ on('command.run', () => ({ text: 'engine' }) as never)
34
+ let copied = ''
35
+ on('ui.copy', (_, e) => { copied = (e as { text: string }).text; return { value: { isCopied: true } } as never })
36
+ on('ui.toast', () => ({ value: undefined }) as never)
37
+ await $.command.run({ command: 'peer-thread', args: '' } as never)
38
+ for (const surface of ['terminal', 'desktop'] as const) {
39
+ const ui = await $.ui.mount({ ...PANE, surface })
40
+ const texts = (await ui.findAll({ type: 'Text' })).map(t => t.text)
41
+ expect(texts.slice(0, 2)).toEqual(['This thread', '3 memories'])
42
+ expect(texts.filter(t => t === 'Project alpha displayed')).toHaveLength(1)
43
+ expect(texts).toContain('Updated')
44
+ expect(texts.some(t => t.endsWith(':42, 2×'))).toBe(true)
45
+ // Closed rows show no sentence; a press opens one row with its text and Copy.
46
+ expect(texts).not.toContain('The panel shows people who share threads.')
47
+ await ui.press({ key: 'New-2026-11-03T13:42:14Z-0' } as never)
48
+ expect((await ui.find({ type: 'Text', text: /people who share threads/ }))?.text).toBe('The panel shows people who share threads.')
49
+ await ui.press({ key: 'New-2026-11-03T13:42:14Z-0-copy' } as never)
50
+ expect(copied).toBe('Panel scope clarified\nThe panel shows people who share threads.')
51
+ await ui.press({ key: 'New-2026-11-03T13:42:14Z-0' } as never)
52
+ await ui.unmount()
53
+ }
54
+ })
@@ -2,8 +2,51 @@
2
2
  // (the band then draws nothing).
3
3
  export type Shared = { workspace: string; isShared: boolean } | null
4
4
 
5
+ // One shared agent thread as the hub lists it (`peer list --json`).
6
+ export type Thread = {
7
+ session: string
8
+ repository?: string | null
9
+ repoKey?: string | null
10
+ branch?: string | null
11
+ status: 'active' | 'silent' | 'stopped'
12
+ working: boolean
13
+ seenAt: string
14
+ stoppedAt?: string | null
15
+ }
16
+
17
+ // What the threads pane shows: the workspace's list as it last loaded, or why it did not; null before the first look.
18
+ export type Board =
19
+ | {
20
+ workspace: string
21
+ you: string
22
+ people: { email: string; threads: Thread[] }[]
23
+ silentAfterMins: number
24
+ stoppedAfterMins: number
25
+ stoppedShownHours: number
26
+ loadedAt: number
27
+ }
28
+ | { error: string; loadedAt: number }
29
+ | null
30
+
31
+ // What this thread saved to the person's own OpenViking, as `peer thread --json` says it; null before the first look.
32
+ export type MemoryItem = { change: 'added' | 'updated' | 'removed'; type: string; name: string; lines: string[]; systemText: boolean }
33
+ export type Memories =
34
+ | { connected: true; saves: { at: string; items: MemoryItem[] }[]; loadedAt: number }
35
+ | { connected: false; reason: string; loadedAt: number }
36
+ | { error: string; loadedAt: number }
37
+ | null
38
+
39
+ // The search pane (/peer-all): what was asked, and one page of the person's memories as `peer all --json` gave it.
40
+ // sort: '' is OpenViking's own order; 'new' and 'old' order by the time a memory last changed.
41
+ export type AllQuery = { text: string; here: boolean; page: number; sort: '' | 'new' | 'old' }
42
+ export type MemoryRow = { uri: string; repo: string; type: string; title: string; at: string; lines: string[] }
43
+ export type AllFound =
44
+ | { connected: true; url: string; repo: string; total: number; page: number; pages: number; rows: MemoryRow[] }
45
+ | { connected: false; reason: string }
46
+ export type AllMemories = { ask: AllQuery; loading: boolean; found?: AllFound; error?: string } | null
47
+
5
48
  declare module 'claude-code' {
6
49
  interface PluginState {
7
- 'peer-thread-mark': { shared: Shared }
50
+ 'peer-thread-mark': { shared: Shared; board: Shaped<Board>; memories: Shaped<Memories>; all: Shaped<AllMemories>; openMemory: string | null; openAll: string | null }
8
51
  }
9
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webinson/peer",
3
- "version": "0.4.2",
3
+ "version": "0.4.5",
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",