SLOPSHOPPER

pdx-relay-probe

Probe (M1): consume a pdx control message in session.receive, then submit and clear.

newtimer
A shopper browsing a rack in a slop shop
README

Purdex

人與 agent 協作的工作站。把 tmux session、Claude Code 串流、對話式 agent 統一在一個介面,跨 workspace、跨 host 並行作業。

仍在 alpha (1.0.0-alpha.233),僅支援 macOS。原名 tmux-box / tmux-ai-term。

能做什麼

  • 多 workspace 並行,每個 workspace 內可開多 tab、tab 可分割多 pane
  • Agent 三種模式:terminal / stream(wrap) / 對話
  • 跨機(Tailnet)多 host 操作
  • 整合 Claude Code、Codex、OpenCode 的 hook 與狀態指示

詞彙與設計定錨見 PRODUCT.md。

技術棧

Go daemon + React 19 SPA + Electron 41 殼。

開發

cd spa && pnpm install
pnpm dev         # SPA dev server
pnpm test        # vitest
pnpm build       # SPA build
pnpm electron:build   # 從 root;產出 dist/mac/ + dist/mac-arm64/

環境與跨機開發流程見 CLAUDE.md。

建置

Daemon(Go)需要存取 private module lab.protype.tw/wake/nexen。建置前, 這台機器需完成兩項一次性設定:

go env -w GOPRIVATE=lab.protype.tw
git config --global url."ssh://git@lab.protype.tw:9079/".insteadOf "https://lab.protype.tw/"

make build(或單獨 make check-goenv)會先驗證這兩項,缺一就印出對應指令 並以非零狀態結束,不會進入 go build。

冷快取驗證(確認上述設定不依賴呼叫端的 shell 環境變數也能運作;spec §6 step 0):

GOWORK=off GOMODCACHE=$(mktemp -d) go build ./cmd/pdx

文件

License

MIT

Source 1 files
hooks/register.js 57 lines
1// Probe (M1): can a pdx peer message be consumed by session.receive, and can the
2// mod then drive prompt.submit and /clear from a timer?
3const MARK = '[pdx-relay:control]'
4let phase = 'idle'
5
6async function log($, msg) {
7  const line = new Date().toISOString() + ' [' + phase + '] ' + msg + '\n'
8  try {
9    const path = (await $.session.cwd()) + '/probe.log'
10    const old = await $.fs.read(path).catch(() => '')
11    await $.fs.write(path, old + line)
12  } catch {}
13}
14
15function later($, fn) {
16  $.clock.after(100, () => fn().catch((err) => log($, 'deferred failed: ' + String(err))))
17}
18
19export function register(on) {
20  on('session.start', async ($, e, next) => {
21    await log($, 'loaded session=' + (await $.session.id()))
22    return next(e)
23  })
24
25  on('session.receive', async ($, e, next) => {
26    await log($, 'receive origin=' + JSON.stringify(e.origin) + ' agentId=' + (e.agentId || '-') + ' text=' + JSON.stringify(String(e.text).slice(0, 160)))
27    if (!String(e.text).includes(MARK)) return next(e)
28    phase = 'submitting'
29    later($, async () => {
30      await $.prompt.submit({ text: '這是探測。請只回覆一行:PROBE-SUBMIT-OK' })
31      await log($, 'prompt.submit sent')
32    })
33    return { consumed: 'pdx-relay control (probe)' }
34  })
35
36  on('turn.complete', async ($, e, next) => {
37    const r = await next(e)
38    if (e.agentId) return r
39    await log($, 'turn.complete')
40    if (phase === 'submitting') {
41      phase = 'clearing'
42      later($, async () => {
43        await $.command.run({ command: 'clear' })
44        await log($, 'command.run(clear) returned')
45      })
46    }
47    return r
48  })
49
50  on('classic.SessionStart', async ($, e, next) => {
51    const r = await next(e)
52    await log($, 'classic.SessionStart source=' + e.source + ' session=' + (await $.session.id()))
53    if (e.source === 'clear' && phase === 'clearing') phase = 'done'
54    return r
55  })
56}
57