bom-pi 0.0.0-stage → 0.0.1
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 +33 -2
- package/apps/README.md +7 -0
- package/apps/web/README.md +21 -0
- package/apps/web/pocket-adapter.mjs +302 -0
- package/apps/web/public/LICENSE.pocket +21 -0
- package/apps/web/public/activity-fold.js +28 -0
- package/apps/web/public/agent-now.js +37 -0
- package/apps/web/public/agent-tile.js +79 -0
- package/apps/web/public/app.js +975 -0
- package/apps/web/public/apple-touch-icon.png +0 -0
- package/apps/web/public/artifact-bridge.js +687 -0
- package/apps/web/public/avatar.js +87 -0
- package/apps/web/public/back.js +554 -0
- package/apps/web/public/box-keys.js +216 -0
- package/apps/web/public/browser-panel.js +1097 -0
- package/apps/web/public/browser.js +68 -0
- package/apps/web/public/calls.js +139 -0
- package/apps/web/public/chat.js +919 -0
- package/apps/web/public/clock.js +34 -0
- package/apps/web/public/commands.js +578 -0
- package/apps/web/public/composer.js +2068 -0
- package/apps/web/public/diff-parse.js +672 -0
- package/apps/web/public/diff.js +1646 -0
- package/apps/web/public/dock.js +299 -0
- package/apps/web/public/extension-ui.js +2734 -0
- package/apps/web/public/files-panel.js +775 -0
- package/apps/web/public/files.js +368 -0
- package/apps/web/public/fonts/FiraCode-VF.woff2 +0 -0
- package/apps/web/public/fonts/Geist.woff2 +0 -0
- package/apps/web/public/fonts/IBMPlexMono-Variable-Italic.woff2 +0 -0
- package/apps/web/public/fonts/IBMPlexMono-Variable.woff2 +0 -0
- package/apps/web/public/fonts/InterVariable-Italic.woff2 +0 -0
- package/apps/web/public/fonts/InterVariable.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-Bold.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-BoldItalic.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-Italic.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-Regular.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-SemiBold.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-SemiBoldItalic.woff2 +0 -0
- package/apps/web/public/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/apps/web/public/fonts/JetBrainsMono.woff2 +0 -0
- package/apps/web/public/fonts/NerdFontsSymbolsMono.woff2 +0 -0
- package/apps/web/public/fonts/SOURCES.md +69 -0
- package/apps/web/public/gestures.js +610 -0
- package/apps/web/public/highlight.js +480 -0
- package/apps/web/public/history.js +70 -0
- package/apps/web/public/home.js +169 -0
- package/apps/web/public/icon-192.png +0 -0
- package/apps/web/public/icon-512.png +0 -0
- package/apps/web/public/icon-maskable-512.png +0 -0
- package/apps/web/public/icon.svg +16 -0
- package/apps/web/public/index.html +71 -0
- package/apps/web/public/launcher.js +1293 -0
- package/apps/web/public/linked-sessions.js +55 -0
- package/apps/web/public/live.js +39 -0
- package/apps/web/public/manifest.webmanifest +32 -0
- package/apps/web/public/model-refresh.js +73 -0
- package/apps/web/public/notify.js +251 -0
- package/apps/web/public/peeks.js +920 -0
- package/apps/web/public/pip-attachments.js +182 -0
- package/apps/web/public/pip-window.js +141 -0
- package/apps/web/public/pip.js +1217 -0
- package/apps/web/public/quote-reply.js +109 -0
- package/apps/web/public/quote-text.js +31 -0
- package/apps/web/public/rich.js +403 -0
- package/apps/web/public/run-frame.js +106 -0
- package/apps/web/public/safe-mode.js +71 -0
- package/apps/web/public/seen.js +169 -0
- package/apps/web/public/session-tree.js +117 -0
- package/apps/web/public/sessions.js +2695 -0
- package/apps/web/public/share.js +126 -0
- package/apps/web/public/sheets/appearance.js +820 -0
- package/apps/web/public/sheets/branch.js +353 -0
- package/apps/web/public/sheets/extensions.js +818 -0
- package/apps/web/public/sheets/file.js +318 -0
- package/apps/web/public/sheets/find.js +250 -0
- package/apps/web/public/sheets/folder.js +680 -0
- package/apps/web/public/sheets/gallery.js +2340 -0
- package/apps/web/public/sheets/hooks.js +350 -0
- package/apps/web/public/sheets/memory.js +531 -0
- package/apps/web/public/sheets/menu.js +580 -0
- package/apps/web/public/sheets/message.js +220 -0
- package/apps/web/public/sheets/model.js +412 -0
- package/apps/web/public/sheets/people.js +414 -0
- package/apps/web/public/sheets/pi-sessions.js +264 -0
- package/apps/web/public/sheets/providers.js +525 -0
- package/apps/web/public/sheets/running.js +130 -0
- package/apps/web/public/sheets/schedules.js +71 -0
- package/apps/web/public/sheets/session.js +249 -0
- package/apps/web/public/sheets/shortcuts.js +101 -0
- package/apps/web/public/sheets/spend.js +150 -0
- package/apps/web/public/sheets/text.js +53 -0
- package/apps/web/public/sheets/viewers.js +363 -0
- package/apps/web/public/sheets/worktree.js +50 -0
- package/apps/web/public/sheets.js +410 -0
- package/apps/web/public/signin.js +57 -0
- package/apps/web/public/slots.js +208 -0
- package/apps/web/public/store.js +1760 -0
- package/apps/web/public/style.css +18625 -0
- package/apps/web/public/subagents-board.js +879 -0
- package/apps/web/public/subagents.js +273 -0
- package/apps/web/public/sw.js +169 -0
- package/apps/web/public/taps.js +26 -0
- package/apps/web/public/theme-surface.d.ts +4 -0
- package/apps/web/public/theme-surface.js +266 -0
- package/apps/web/public/theme.js +845 -0
- package/apps/web/public/themes/lantern/colors.toml +25 -0
- package/apps/web/public/themes/lantern/direction.css +60 -0
- package/apps/web/public/themes/lantern/frame.css +254 -0
- package/apps/web/public/themes/lantern/theme.css +10 -0
- package/apps/web/public/themes/lantern/theme.json +7 -0
- package/apps/web/public/themes/pocket/theme.css +189 -0
- package/apps/web/public/themes/pocket/theme.json +6 -0
- package/apps/web/public/themes.js +556 -0
- package/apps/web/public/transcript.js +3348 -0
- package/apps/web/public/trust.js +295 -0
- package/apps/web/public/turns.js +507 -0
- package/apps/web/public/ui.js +1910 -0
- package/apps/web/public/voice.js +345 -0
- package/apps/web/server.mjs +774 -0
- package/package.json +43 -4
- package/packages/cli/src/bom-pi.mjs +452 -0
- package/packages/cli/templates/blank.factory.json +7 -0
- package/packages/cli/templates/dev.factory.json +11 -0
- package/packages/cli/templates/docs.factory.json +12 -0
- package/packages/cli/templates/ops.factory.json +14 -0
- package/packages/cli/templates/triage.factory.json +10 -0
- package/packages/core/src/daemon.mjs +1962 -0
- package/packages/core/src/limit-gate.mjs +192 -0
- package/packages/core/src/rpc-client.mjs +66 -0
- package/systemd/README.md +11 -0
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bom-pi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
bom × pi — hệ sinh thái durable cho pi trên nền `@earendil-works/pi-durable`, lý niệm herdr:
|
|
4
|
+
**core daemon headless sở hữu state, clients là process riêng mỏng, thay tự do**.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
packages/core bom-core daemon — pi-durable Harness + JSON-RPC 2.0 NDJSON unix socket
|
|
8
|
+
packages/cli bom-pi — cli mỏng: hello / status / shutdown
|
|
9
|
+
apps/ client apps (M2 herdr-plugin, M3 web) — chưa có
|
|
10
|
+
docs/ BLUEPRINT.md (kiến trúc chốt) · DECISIONS.md (nhật ký quyết định)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Chạy
|
|
14
|
+
|
|
15
|
+
Daemon đang chạy như **systemd user service** (`bom-core.service` — enabled, tự restart khi crash):
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
systemctl --user status bom-core # trạng thái
|
|
19
|
+
journalctl --user -u bom-core -f # log
|
|
20
|
+
node packages/cli/src/bom-pi.mjs status
|
|
21
|
+
node packages/cli/src/bom-pi.mjs models # model khả dụng (account pi thật)
|
|
22
|
+
node packages/cli/src/bom-pi.mjs submit "..." --model zai/glm-5.3-flash
|
|
23
|
+
node packages/cli/src/bom-pi.mjs watch # transcript live (Ctrl+C thoát)
|
|
24
|
+
node packages/cli/src/bom-pi.mjs transcript
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Socket `$XDG_RUNTIME_DIR/bom-pi.sock` · data `~/.local/state/bom-pi/` · credentials
|
|
28
|
+
`~/.pi/agent/auth.json` (ModelRuntime của pi — cùng account với pi CLI).
|
|
29
|
+
|
|
30
|
+
Real-test mỗi vòng: `npm run smoke` (19 assert — boot/hello/status/models/transcript/lỗi chuẩn/
|
|
31
|
+
notification/null-crash-regression/double-start/shutdown/reopen/SIGTERM, dùng thư mục tạm riêng).
|
|
32
|
+
|
|
33
|
+
Milestone (M1 xong: submit/resume + events + systemd + kill-test) + bảng tra "mượn gì từ đâu":
|
|
34
|
+
`docs/BLUEPRINT.md`.
|
package/apps/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# apps/ — client apps (process riêng, không chứa state bền)
|
|
2
|
+
|
|
3
|
+
- `herdr-plugin/` (M2): manifest + scripts plugin herdr `pi.bom-bridge` — status/send/approve +
|
|
4
|
+
tokens roles + agent.view.set.
|
|
5
|
+
- `web/` (M3): bom-web minimal — transcript + approve + submit từ phone (SSE + token, tailscale).
|
|
6
|
+
|
|
7
|
+
Chưa có gì ở đây cho tới khi M1 xong (xem `../docs/BLUEPRINT.md` §4 milestone).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# bom-web — client web minimal (M3)
|
|
2
|
+
|
|
3
|
+
Transcript + submit + live events từ phone, không dependency (`node apps/web/server.mjs`).
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
# local (không token, chỉ 127.0.0.1)
|
|
7
|
+
npm run web
|
|
8
|
+
|
|
9
|
+
# từ phone qua tailscale: đặt token
|
|
10
|
+
BOM_PI_TOKEN=$(head -c16 /dev/urandom | base64) npm run web
|
|
11
|
+
# rồi mở http://<tailscale-ip>:8791/?token=<token>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
| Endpoint | Làm gì |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `GET /` | trang chính (model picker + input + transcript live) |
|
|
17
|
+
| `GET /api/hello` `/api/status` `/api/models` `/api/transcript` | proxy bom-core |
|
|
18
|
+
| `GET /api/events` | SSE — forward `events.update` / `events.heartbeat` |
|
|
19
|
+
| `POST /api/submit` `{text, model?}` | nộp turn (requestId `web-*` idempotent-ish) |
|
|
20
|
+
|
|
21
|
+
Kiến trúc: web là CLIENT mỏng — mọi state nằm ở bom-core daemon, web chết tự do.
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// pocket-adapter — đưa wire contract của pi-pocket (TannerMidd, MIT) vào bom-core.
|
|
2
|
+
// UI nguyên bản nằm ở public/ (không sửa); file này dịch /api/* pocket → RPC bom-pi.
|
|
3
|
+
// Phần bom-pi chưa có tính năng: trả giá trị rỗng hợp lệ + ghi chú PORT(bom-pi) ngay tại nhánh.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
|
|
9
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
export const COOKIE = 'pocket_auth';
|
|
11
|
+
|
|
12
|
+
// ---- projectEntry: port từ pi-pocket src/server/projection.ts (MIT) ----
|
|
13
|
+
// Pi-durable EntryRecord → ClientEntry mà UI pocket render. Cùng pi-durable 1.4.0 ⇒ shape 1-1.
|
|
14
|
+
const clip = (text, limit = 4000) => (text.length > limit ? { text: text.slice(0, limit), clipped: true } : { text });
|
|
15
|
+
const textOfContent = (content) =>
|
|
16
|
+
typeof content === 'string' ? content
|
|
17
|
+
: Array.isArray(content) ? content.filter((p) => p?.type === 'text').map((p) => p.text).join('\n')
|
|
18
|
+
: '';
|
|
19
|
+
const countImages = (content) => (Array.isArray(content) ? content.filter((p) => p?.type === 'image').length : 0);
|
|
20
|
+
const madeAt = (m) => (typeof m?.timestamp === 'number' && m.timestamp > 0 ? { at: m.timestamp } : {});
|
|
21
|
+
|
|
22
|
+
function projectBlocks(content, full = false) {
|
|
23
|
+
if (!Array.isArray(content)) return [];
|
|
24
|
+
return content
|
|
25
|
+
.filter((p) => p?.type === 'text' || p?.type === 'thinking')
|
|
26
|
+
.map((p) => (p.type === 'thinking' ? { type: 'thinking', text: String(p.text ?? '') } : { type: 'text', text: String(p.text ?? '') }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function projectEntry(entry, full = false) {
|
|
30
|
+
const m = entry?.model?.[0];
|
|
31
|
+
const id = entry.id;
|
|
32
|
+
switch (entry?.kind) {
|
|
33
|
+
case 'pi.user':
|
|
34
|
+
return { id, kind: 'user', text: textOfContent(m?.content), images: countImages(m?.content), ...madeAt(m) };
|
|
35
|
+
case 'pi.assistant': {
|
|
36
|
+
const blocks = [];
|
|
37
|
+
if (Array.isArray(m?.content)) {
|
|
38
|
+
for (const p of m.content) {
|
|
39
|
+
if (p?.type === 'text' || p?.type === 'thinking') blocks.push(p.type === 'thinking' ? { type: 'thinking', text: String(p.text ?? '') } : { type: 'text', text: String(p.text ?? '') });
|
|
40
|
+
else if (p?.type === 'toolCall') blocks.push({ type: 'toolCall', id: String(p.id ?? ''), name: String(p.name ?? '?'), args: p.arguments ?? p.args ?? {} });
|
|
41
|
+
}
|
|
42
|
+
} else if (typeof m?.text === 'string' && m.text) {
|
|
43
|
+
blocks.push({ type: 'text', text: m.text }); // bom lightEntries: "[tool write]" fallback
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
id, kind: 'assistant', blocks,
|
|
47
|
+
...(typeof m?.stopReason === 'string' ? { stopReason: m.stopReason } : {}),
|
|
48
|
+
...(m?.errorMessage ? { error: String(m.errorMessage).slice(0, 200) } : {}),
|
|
49
|
+
...(typeof m?.model === 'string' ? { model: m.model } : {}),
|
|
50
|
+
...(typeof m?.provider === 'string' ? { provider: m.provider } : {}),
|
|
51
|
+
...madeAt(m),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
case 'pi.tool-result': {
|
|
55
|
+
const { text, clipped } = full ? { text: textOfContent(m?.content), clipped: undefined } : clip(textOfContent(m?.content) || String(m?.text ?? ''));
|
|
56
|
+
return {
|
|
57
|
+
id, kind: 'toolResult', callId: String(m?.toolCallId ?? ''), name: String(m?.toolName ?? '?'), text,
|
|
58
|
+
isError: m?.isError === true, ...(clipped ? { clipped } : {}), ...madeAt(m),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
case 'pi.compaction':
|
|
62
|
+
return { id, kind: 'compaction', summary: textOfContent(m?.content) };
|
|
63
|
+
case 'pi.reset': {
|
|
64
|
+
const text = textOfContent(m?.content);
|
|
65
|
+
return text === '' ? { id, kind: 'reset' } : { id, kind: 'reset', text };
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
return undefined; // pi.system + bookkeeping: UI không vẽ
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- map model bom-pi → model pocket ----
|
|
73
|
+
function mapModel(mm) {
|
|
74
|
+
return {
|
|
75
|
+
provider: String(mm.provider ?? '?'),
|
|
76
|
+
id: String(mm.modelId ?? mm.id ?? '?'),
|
|
77
|
+
name: String(mm.name ?? mm.modelId ?? '?'),
|
|
78
|
+
contextWindow: Number(mm.contextWindow ?? 200000),
|
|
79
|
+
reasoning: mm.reasoning !== false,
|
|
80
|
+
images: mm.images === true,
|
|
81
|
+
levels: Array.isArray(mm.levels) ? mm.levels : ['off', 'low', 'medium', 'high'],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- approvals bom → pocket shape {id, conversationId, tool, subject, reason, requestedBy} ----
|
|
86
|
+
function mapApproval(a) {
|
|
87
|
+
return {
|
|
88
|
+
id: String(a.id),
|
|
89
|
+
conversationId: Number(a.conversationId ?? 0),
|
|
90
|
+
tool: String(a.tool ?? 'call'),
|
|
91
|
+
subject: String(a.command ?? a.summary ?? ''), // command đầy đủ — card Review hiện nguyên lệnh
|
|
92
|
+
reason: String(a.reason ?? a.type ?? 'bom-pi approval gate'),
|
|
93
|
+
requestedBy: String(a.requestedBy ?? 'bom'),
|
|
94
|
+
at: Number(a.at ?? 0),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const OWNER = { id: 'bom-owner', name: 'Owner', role: 'owner' };
|
|
99
|
+
|
|
100
|
+
export function makePocketAdapter({ rpc, checkToken }) {
|
|
101
|
+
// me-shape: mọi thứ UI cần biết về server (chụp từ pocket 0.27.0 thật)
|
|
102
|
+
async function mePayload() {
|
|
103
|
+
const hello = await rpc('hello');
|
|
104
|
+
let models = [];
|
|
105
|
+
try { models = (await rpc('models.list')).models ?? []; } catch { /* PORT(bom-pi): models.list lỗi → danh sách rỗng */ }
|
|
106
|
+
let themes = [{ id: 'pocket', name: 'Pocket', description: "The app's own look: soft tiles over the colors you choose." },
|
|
107
|
+
{ id: 'lantern', name: 'Lantern', description: 'Calm and dark for long reading.' }];
|
|
108
|
+
return {
|
|
109
|
+
user: OWNER, users: [OWNER],
|
|
110
|
+
models: models.map(mapModel),
|
|
111
|
+
guard: { available: false, enabled: false, detail: 'bom-pi: mọi tool đều qua approval gate của daemon' },
|
|
112
|
+
server: { pocketVersion: '0.27.0-bom', supervised: false, startedAt: 0, home: process.env.HOME ?? '/home', defaultCwd: process.env.HOME ?? '/home' },
|
|
113
|
+
folderFavorites: [], themes,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function sessionsPayload() {
|
|
118
|
+
const list = await rpc('session.list');
|
|
119
|
+
const convs = list.conversations ?? [];
|
|
120
|
+
const appr = (await rpc('approvals.list')).pending ?? [];
|
|
121
|
+
// bom-pi session.list không có title/thời gian — nạp từ transcript.raw:
|
|
122
|
+
// title = message user đầu (đúng cách pocket), updatedAt = at entry cuối
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const c of convs) {
|
|
125
|
+
let title = null; let updatedAt = 0;
|
|
126
|
+
try {
|
|
127
|
+
const raw = await rpc('transcript.raw', { conversationId: c.id });
|
|
128
|
+
const first = (raw.entries ?? []).find((e) => e.kind === 'pi.user');
|
|
129
|
+
const m0 = first?.model?.[0];
|
|
130
|
+
const text = typeof m0?.content === 'string' ? m0.content : Array.isArray(m0?.content) ? m0.content.filter((p) => p?.type === 'text').map((p) => p.text).join(' ') : '';
|
|
131
|
+
if (text) title = text.split('\n')[0].slice(0, 60);
|
|
132
|
+
for (const e of raw.entries ?? []) { const at = e.model?.[0]?.timestamp; if (typeof at === 'number' && at > updatedAt) updatedAt = at; }
|
|
133
|
+
} catch { /* conversation đã đổi trạng thái — bỏ qua metadata */ }
|
|
134
|
+
out.push({
|
|
135
|
+
id: c.id,
|
|
136
|
+
cwd: c.cwd ?? '',
|
|
137
|
+
createdAt: updatedAt,
|
|
138
|
+
updatedAt,
|
|
139
|
+
createdBy: OWNER.id,
|
|
140
|
+
busy: false, // PORT(bom-pi): session.list chưa báo busy — làm mới qua events.update
|
|
141
|
+
waiting: appr.some((a) => Number(a.conversationId) === Number(c.id)),
|
|
142
|
+
model: c.model?.modelId ?? 'glm-5.3',
|
|
143
|
+
title,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// view: transcript + trạng thái một conversation (shape chụp từ pocket)
|
|
150
|
+
async function viewPayload(convId) {
|
|
151
|
+
const raw = await rpc('transcript.raw', { conversationId: convId });
|
|
152
|
+
const entries = (raw.entries ?? []).map((e) => projectEntry(e)).filter(Boolean);
|
|
153
|
+
const appr = ((await rpc('approvals.list')).pending ?? []).filter((a) => Number(a.conversationId) === Number(convId));
|
|
154
|
+
let conv = null;
|
|
155
|
+
try { conv = (await rpc('session.list')).conversations.find((c) => Number(c.id) === Number(convId)) ?? null; } catch { conv = null; }
|
|
156
|
+
return {
|
|
157
|
+
full: true,
|
|
158
|
+
conversation: { id: Number(convId), kind: 'session', title: conv?.title ?? `session ${convId}`, cwd: conv?.cwd ?? '', archived: false },
|
|
159
|
+
entries,
|
|
160
|
+
order: entries.map((e) => e.id),
|
|
161
|
+
live: { busy: conv?.busy === true || conv?.running === true },
|
|
162
|
+
agent: { model: { provider: conv?.model?.provider ?? 'zai', modelId: conv?.model?.modelId ?? 'glm-5.3' }, thinkingLevel: 'high', cwd: conv?.cwd ?? '', available: true, modelName: conv?.model?.modelId ?? 'GLM-5.3', contextWindow: 1000000, reasoning: true, images: false, levels: ['low', 'high', 'max'] },
|
|
163
|
+
stats: { cost: 0 },
|
|
164
|
+
clients: 1, viewers: ['Owner'],
|
|
165
|
+
approvals: appr.map(mapApproval),
|
|
166
|
+
inbox: [], artifacts: [], subagents: [], subagentsHeld: null,
|
|
167
|
+
authors: {}, reactions: {}, pins: [], turns: { on: false, asks: [] }, decisions: {},
|
|
168
|
+
plan: { on: false }, schedules: [], branch: null, goal: null, watch: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json', 'access-control-allow-origin': '*' }); res.end(JSON.stringify(obj)); };
|
|
173
|
+
const readBody = (req) => new Promise((done) => { let b = ''; req.on('data', (c) => (b += c)); req.on('end', () => { try { done(JSON.parse(b || '{}')); } catch { done({}); } }); });
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Mount pocket wire. Trả true nếu đã xử lý request.
|
|
177
|
+
* Auth: cookie pocket_auth (sign-in qua /login?token=) hoặc Authorization Bearer (device/master bom).
|
|
178
|
+
*/
|
|
179
|
+
return async function pocketRoute(req, res, url, authed) {
|
|
180
|
+
const path = url.pathname.replace(/^\/api\//, '');
|
|
181
|
+
const parts = path.split('/').filter(Boolean);
|
|
182
|
+
const [first, second, third] = parts;
|
|
183
|
+
const method = req.method;
|
|
184
|
+
const isPost = method !== 'GET' && method !== 'HEAD';
|
|
185
|
+
|
|
186
|
+
if (isPost && req.headers['x-pocket'] !== '1') return false; // POST pocket luôn có X-Pocket (api.ts l365)
|
|
187
|
+
if (!['me', 'login', 'logout', 'sessions', 'c', 'providers', 'themes', 'theme', 'events', 'poll',
|
|
188
|
+
'users', 'hooks', 'subagents', 'pip', 'extensions', 'extension-settings', 'fs', 'visibility', 'peeks',
|
|
189
|
+
'folder-favorites', 'spend', 'pi-sessions', 'x', 'repo-watches', 'approvals'].includes(first)) return false;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// ---- sign-in/lifecycle ----
|
|
193
|
+
if (first === 'me' && method === 'GET') return json(res, 200, await mePayload()), true;
|
|
194
|
+
if (first === 'logout' && isPost) {
|
|
195
|
+
res.writeHead(200, { 'set-cookie': `${COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`, 'content-type': 'application/json' });
|
|
196
|
+
return res.end('{}'), true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---- sessions ----
|
|
200
|
+
if (first === 'sessions' && second === undefined && method === 'GET') return json(res, 200, await sessionsPayload()), true;
|
|
201
|
+
if (first === 'sessions' && second === undefined && isPost) {
|
|
202
|
+
const body = await readBody(req);
|
|
203
|
+
const r = await rpc('session.new', { cwd: body.cwd });
|
|
204
|
+
return json(res, 200, { id: r.conversationId ?? r.id }), true;
|
|
205
|
+
}
|
|
206
|
+
if (first === 'sessions' && second !== undefined && isPost) {
|
|
207
|
+
await readBody(req);
|
|
208
|
+
return json(res, 200, { ok: true }), true; // PORT(bom-pi): rename/archive session — daemon chưa lưu, chấp nhận im lặng
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- conversation ----
|
|
212
|
+
if (first === 'c' && second !== undefined) {
|
|
213
|
+
const id = Number(second);
|
|
214
|
+
if (third === 'submit' && isPost) {
|
|
215
|
+
const body = await readBody(req);
|
|
216
|
+
const r = await rpc('session.submit', { conversationId: id, text: body.text, ...(body.requestId ? { requestId: String(body.requestId) } : {}) });
|
|
217
|
+
return json(res, 200, { submissionId: r.submissionId ?? 0, requestId: String(body.requestId ?? '') }), true;
|
|
218
|
+
}
|
|
219
|
+
if (third === 'configure' && isPost) {
|
|
220
|
+
const body = await readBody(req);
|
|
221
|
+
// pocket gửi {model: {provider, id, level?}} → bom session.configure {model:{provider,modelId}}
|
|
222
|
+
const model = body.model ? { provider: body.model.provider, modelId: body.model.id ?? body.model.modelId } : undefined;
|
|
223
|
+
return json(res, 200, await rpc('session.configure', { conversationId: id, ...(model ? { model } : {}) })), true;
|
|
224
|
+
}
|
|
225
|
+
if (third === 'abort' && isPost) return json(res, 200, await rpc('session.abort', { conversationId: id })), true;
|
|
226
|
+
if (third === 'reset' && isPost) { await readBody(req); return json(res, 200, await rpc('session.reset', { conversationId: id })), true; }
|
|
227
|
+
if (third === 'history' && method === 'GET') {
|
|
228
|
+
const raw = await rpc('transcript.raw', { conversationId: id });
|
|
229
|
+
// pocket history: OLDEST FIRST (transcripts.ts out.reverse()) — transcript.raw đã oldest-first
|
|
230
|
+
return json(res, 200, (raw.entries ?? []).map((e) => projectEntry(e)).filter(Boolean)), true;
|
|
231
|
+
}
|
|
232
|
+
if ((third === 'context' || third === 'messages') && method === 'GET') {
|
|
233
|
+
const raw = await rpc('transcript.raw', { conversationId: id });
|
|
234
|
+
const entries = (raw.entries ?? []).map((e) => projectEntry(e)).filter(Boolean);
|
|
235
|
+
return json(res, 200, { entries, last: entries.at(-1)?.id ?? 0 }), true;
|
|
236
|
+
}
|
|
237
|
+
if (third === 'entry' && method === 'GET') {
|
|
238
|
+
const raw = await rpc('transcript.raw', { conversationId: id });
|
|
239
|
+
const e = (raw.entries ?? []).find((x) => Number(x.id) === Number(parts[3]));
|
|
240
|
+
if (e) { json(res, 200, projectEntry(e, true)); return true; }
|
|
241
|
+
json(res, 404, { error: 'no entry' });
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
if (third === 'turns' && isPost) return json(res, 200, { ok: true }), true; // PORT(bom-pi): multiplayer turns — chưa có
|
|
245
|
+
if (third === 'typing' && isPost) return json(res, 200, { ok: true }), true; // PORT(bom-pi): presence
|
|
246
|
+
if (isPost) { await readBody(req); return json(res, 200, { ok: true }), true; } // PORT(bom-pi): plan/chat/pin/notes/fork/... — noop
|
|
247
|
+
return json(res, 200, {}), true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- approvals ----
|
|
251
|
+
if (first === 'approvals' && second !== undefined && isPost) {
|
|
252
|
+
const body = await readBody(req);
|
|
253
|
+
await rpc('approvals.answer', { id: parts.slice(1).join('/'), allow: body.allow === true, by: 'web' });
|
|
254
|
+
return json(res, 200, true), true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---- providers/themes ----
|
|
258
|
+
if (first === 'providers' && method === 'GET') {
|
|
259
|
+
const models = ((await rpc('models.list')).models ?? []).map(mapModel);
|
|
260
|
+
return json(res, 200, { providers: [{ name: 'bom-pi', models, signedIn: true }] }), true;
|
|
261
|
+
}
|
|
262
|
+
if (first === 'theme' && method === 'GET') return json(res, 200, { theme: 'pocket', defaultTheme: null }), true;
|
|
263
|
+
if (first === 'themes' && method === 'GET') return json(res, 200, (await mePayload()).themes), true;
|
|
264
|
+
if (first === 'themes' && isPost) return json(res, 200, { ok: true }), true; // PORT(bom-pi): duplicate/edit theme folders
|
|
265
|
+
|
|
266
|
+
// ---- events (SSE) ----
|
|
267
|
+
if (first === 'events' && method === 'GET') {
|
|
268
|
+
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
269
|
+
const send = (name, data) => res.write(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
270
|
+
const cid = Number(url.searchParams.get('c'));
|
|
271
|
+
const push = async (why) => {
|
|
272
|
+
try {
|
|
273
|
+
send('hello', await mePayload());
|
|
274
|
+
send('sessions', await sessionsPayload());
|
|
275
|
+
if (Number.isInteger(cid) && cid > 0) send('view', await viewPayload(cid));
|
|
276
|
+
} catch (e) { send('notice', { level: 'error', message: String(e?.message ?? e).slice(0, 120) }); }
|
|
277
|
+
};
|
|
278
|
+
await push('open');
|
|
279
|
+
// bom-pi events.update (SSE legacy) làm tín hiệu làm mới — pull-on-push
|
|
280
|
+
const conn = await rpc.__subscribeEvents(cid, () => push('update'));
|
|
281
|
+
const ping = setInterval(() => res.write(': ping\n\n'), 20000);
|
|
282
|
+
req.on('close', () => { clearInterval(ping); conn.close(); });
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---- stub rỗng: tính năng pocket chưa có ở bom-pi ----
|
|
287
|
+
if (first === 'users' && method === 'GET') return json(res, 200, [OWNER]), true;
|
|
288
|
+
if (first === 'subagents' && method === 'GET') return json(res, 200, []), true;
|
|
289
|
+
if (first === 'hooks' && method === 'GET') return json(res, 200, []), true;
|
|
290
|
+
if (first === 'folder-favorites' && method === 'GET') return json(res, 200, []), true;
|
|
291
|
+
if (first === 'pi-sessions' && method === 'GET') return json(res, 200, []), true;
|
|
292
|
+
if (first === 'fs') return json(res, 200, { entries: [] }), true; // PORT(bom-pi): file tree
|
|
293
|
+
if (first === 'spend' && method === 'GET') return json(res, 200, { people: [], sessions: [] }), true;
|
|
294
|
+
if (isPost || method === 'PUT') { await readBody(req); return json(res, 200, { ok: true }), true; }
|
|
295
|
+
return json(res, 200, method === 'GET' ? [] : {}), true;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
return json(res, e?.status ?? 500, { error: String(e?.message ?? e).replace(/^\d+: /, '') }), true;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const fourth = (parts) => parts[3];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanner Middleton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Whether an activity group in the conversation shows its timeline. Appearance's Keep activity folded keeps every group
|
|
2
|
+
// on its one line until someone opens it, also while it runs or sits in the newest turn; off, the newest turn's groups
|
|
3
|
+
// open as they come. A jump to one of a group's answers opens it as a click does, and a thought alone in its group keeps
|
|
4
|
+
// its own rule (its words follow Show thinking). No DOM here, so `test/activity-fold.test.ts` can check it.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether a group opens, from what it is and what was chosen for it.
|
|
8
|
+
*
|
|
9
|
+
* `chosen` is a click on its line, or a jump to one of its answers (`pocket:reveal`): true opened it, false closed it,
|
|
10
|
+
* undefined leaves it to the rules below. `lone` is the group's only step when that step is a thought, else null.
|
|
11
|
+
* `fold` is Appearance's Keep activity folded.
|
|
12
|
+
*/
|
|
13
|
+
export function activityOpen({ chosen, lone, live, newest, reading, fold }) {
|
|
14
|
+
// A thought whose words have not come yet (a session opening) stays folded until they have.
|
|
15
|
+
if (lone?.partial === true) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (chosen !== undefined) {
|
|
20
|
+
return chosen;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (lone) {
|
|
24
|
+
return reading === true && lone.streaming !== true && Boolean(lone.text?.trim());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return fold === true ? false : live === true || newest === true;
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// What a working subagent does this moment, from its peek: the call it waits on, the call it runs, or its words. The
|
|
2
|
+
// subagents bar and the peek tiles both say it, for a row each.
|
|
3
|
+
import { describeCall } from "./calls.js";
|
|
4
|
+
import { store } from "./store.js";
|
|
5
|
+
|
|
6
|
+
/** What a working subagent does this moment: its peek's waiting call, running call, or words. */
|
|
7
|
+
export function nowDoing(agent, peek = store.state.peeks?.[agent.conversationId]) {
|
|
8
|
+
// Its request waits for its provider's limit to clear: that is what it does, whatever its peek last showed.
|
|
9
|
+
if (agent.limited !== undefined) {
|
|
10
|
+
return "Waiting for a usage limit to clear";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const waiting = peek?.approvals?.[0];
|
|
14
|
+
|
|
15
|
+
if (waiting) {
|
|
16
|
+
return `Waiting for approval: ${waiting.tool} ${waiting.subject}`.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const last = (peek?.lines ?? []).at(-1);
|
|
20
|
+
|
|
21
|
+
if (last?.kind === "tool" && last.status === "running") {
|
|
22
|
+
const call = describeCall({ name: last.name, args: last.args });
|
|
23
|
+
|
|
24
|
+
return [call.icon, call.label, call.subject].filter(Boolean).join(" ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (last?.kind === "shell") {
|
|
28
|
+
return `$ ${last.command}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (last?.kind === "text") {
|
|
32
|
+
return last.text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Before its peek arrives, nothing is known but that it works.
|
|
36
|
+
return peek === undefined ? "Working…" : "Thinking…";
|
|
37
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// One subagent at work, as a sub-tile: the state square in its session's folder color, its name and state, and what it
|
|
2
|
+
// does this moment, from its own peek (or, until that arrives, what it was asked). Peek tiles nest these in a session's
|
|
3
|
+
// tile; the subagents board shows them under a session's row.
|
|
4
|
+
import { useRef } from "preact/hooks";
|
|
5
|
+
import { nowDoing } from "./agent-now.js";
|
|
6
|
+
import { store } from "./store.js";
|
|
7
|
+
import { html } from "./ui.js";
|
|
8
|
+
|
|
9
|
+
/** Two clicks (or taps) this close together open the subagent, where `onOpen` is given: the board's. */
|
|
10
|
+
const DOUBLE_MS = 350;
|
|
11
|
+
|
|
12
|
+
/** Whether it waits for an approval: the board knows at once; a peek tile, from the subagent's own peek. */
|
|
13
|
+
function waitingOf(agent, peek) {
|
|
14
|
+
return agent.waiting === true || (peek?.approvals?.length ?? 0) > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What it does now; before its own peek arrives, what it was asked says more than "Working…". A call it waits on is
|
|
19
|
+
* the call alone: the tile says it needs approval.
|
|
20
|
+
*/
|
|
21
|
+
function doingOf(agent, peek) {
|
|
22
|
+
const approval = agent.waiting === true ? agent.approval : peek?.approvals?.[0];
|
|
23
|
+
|
|
24
|
+
if (approval !== undefined) {
|
|
25
|
+
return `${approval.tool} ${approval.subject}`.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const doing = nowDoing(agent, peek);
|
|
29
|
+
|
|
30
|
+
return peek === undefined && agent.asked !== undefined ? `Working on: ${agent.asked}` : doing;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A subagent's sub-tile. `folder` is its session's folder color; `selected` marks it as the one the board shows.
|
|
35
|
+
* `onActivate` takes a click (a peek tile opens it; the board picks it), and `onOpen` a double click or tap.
|
|
36
|
+
*/
|
|
37
|
+
export function AgentTile({ agent, folder, selected = false, onActivate, onOpen }) {
|
|
38
|
+
const peek = agent.peek ?? store.state.peeks?.[agent.conversationId];
|
|
39
|
+
const waiting = waitingOf(agent, peek);
|
|
40
|
+
const state = waiting ? "waiting" : "working";
|
|
41
|
+
const doing = doingOf(agent, peek);
|
|
42
|
+
const last = useRef(0);
|
|
43
|
+
|
|
44
|
+
// A double click, or a double tap: the first picks it, the second opens it. No native dblclick, which a double
|
|
45
|
+
// tap does not always make.
|
|
46
|
+
const activate = () => {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
|
|
49
|
+
if (onOpen !== undefined && now - last.current < DOUBLE_MS) {
|
|
50
|
+
last.current = 0;
|
|
51
|
+
onOpen();
|
|
52
|
+
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
last.current = now;
|
|
57
|
+
onActivate();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return html`<button
|
|
61
|
+
class=${`agent-tile ${state} ${selected ? "on" : ""}`}
|
|
62
|
+
data-peek=${agent.conversationId}
|
|
63
|
+
data-peek-kind="agent"
|
|
64
|
+
title=${`${agent.name}: ${doing}`}
|
|
65
|
+
aria-label=${`${agent.name}, ${waiting ? "needs approval" : "working"}: ${doing}`}
|
|
66
|
+
onClick=${activate}
|
|
67
|
+
>
|
|
68
|
+
<span class="agent-tile-head">
|
|
69
|
+
<span
|
|
70
|
+
class=${`state-mark ${state}`}
|
|
71
|
+
style=${`--folder:${waiting ? "var(--warn)" : (folder ?? "var(--muted)")}`}
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
></span>
|
|
74
|
+
<span class="agent-tile-name">${agent.name}</span>
|
|
75
|
+
<span class="agent-tile-state">${waiting ? "needs approval" : "working"}</span>
|
|
76
|
+
</span>
|
|
77
|
+
<span class="agent-tile-now">${doing}</span>
|
|
78
|
+
</button>`;
|
|
79
|
+
}
|