thepopebot 1.2.76-beta.2 → 1.2.76-beta.21
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 +3 -3
- package/api/CLAUDE.md +11 -4
- package/api/index.js +56 -18
- package/bin/CLAUDE.md +7 -4
- package/bin/cli.js +25 -45
- package/config/CLAUDE.md +23 -4
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/0022_organic_apocalypse.sql +16 -0
- package/drizzle/0023_needy_ender_wiggin.sql +1 -0
- package/drizzle/meta/0021_snapshot.json +639 -0
- package/drizzle/meta/0022_snapshot.json +743 -0
- package/drizzle/meta/0023_snapshot.json +750 -0
- package/drizzle/meta/_journal.json +21 -0
- package/lib/CLAUDE.md +2 -2
- package/lib/actions.js +9 -1
- package/lib/ai/CLAUDE.md +72 -57
- package/lib/ai/helper-llm.js +108 -0
- package/lib/ai/index.js +308 -438
- package/lib/ai/line-mappers.js +42 -24
- package/lib/ai/scope.js +26 -0
- package/lib/ai/sdk-adapters/CLAUDE.md +114 -0
- package/lib/ai/sdk-adapters/claude-code.js +120 -8
- package/lib/ai/system-prompt.js +34 -0
- package/lib/ai/workspace-setup.js +19 -35
- package/lib/channels/CLAUDE.md +14 -4
- package/lib/channels/base.js +6 -2
- package/lib/channels/commands/index.js +42 -0
- package/lib/channels/commands/session.js +53 -0
- package/lib/channels/commands/verify.js +18 -0
- package/lib/channels/telegram.js +79 -28
- package/lib/chat/CLAUDE.md +4 -4
- package/lib/chat/actions.js +270 -49
- package/lib/chat/api.js +185 -31
- package/lib/chat/components/CLAUDE.md +6 -2
- package/lib/chat/components/chat-input.js +77 -47
- package/lib/chat/components/chat-input.jsx +77 -40
- package/lib/chat/components/chat-page.js +2 -0
- package/lib/chat/components/chat-page.jsx +3 -0
- package/lib/chat/components/chat.js +62 -14
- package/lib/chat/components/chat.jsx +68 -10
- package/lib/chat/components/code-mode-toggle.js +141 -22
- package/lib/chat/components/code-mode-toggle.jsx +129 -20
- package/lib/chat/components/containers-page.js +58 -40
- package/lib/chat/components/containers-page.jsx +64 -25
- package/lib/chat/components/crons-page.js +17 -3
- package/lib/chat/components/crons-page.jsx +34 -6
- package/lib/chat/components/index.js +2 -2
- package/lib/chat/components/message.js +18 -3
- package/lib/chat/components/message.jsx +18 -3
- package/lib/chat/components/profile-page.js +182 -4
- package/lib/chat/components/profile-page.jsx +196 -1
- package/lib/chat/components/scope-picker.js +21 -0
- package/lib/chat/components/scope-picker.jsx +27 -0
- package/lib/chat/components/settings-chat-page.js +11 -11
- package/lib/chat/components/settings-chat-page.jsx +14 -18
- package/lib/chat/components/settings-coding-agents-page.js +110 -16
- package/lib/chat/components/settings-coding-agents-page.jsx +87 -3
- package/lib/chat/components/settings-github-page.js +5 -0
- package/lib/chat/components/settings-github-page.jsx +5 -0
- package/lib/chat/components/settings-layout.js +3 -3
- package/lib/chat/components/settings-layout.jsx +3 -3
- package/lib/chat/components/settings-secrets-layout.js +1 -2
- package/lib/chat/components/settings-secrets-layout.jsx +1 -2
- package/lib/chat/components/settings-secrets-page.js +180 -75
- package/lib/chat/components/settings-secrets-page.jsx +212 -66
- package/lib/chat/components/triggers-page.js +17 -3
- package/lib/chat/components/triggers-page.jsx +34 -6
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/chat/components/ui/dropdown-menu.js +23 -2
- package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
- package/lib/chat/telegram-profile.js +33 -0
- package/lib/cluster/CLAUDE.md +9 -3
- package/lib/code/CLAUDE.md +11 -3
- package/lib/code/actions.js +47 -8
- package/lib/code/terminal-view.js +31 -21
- package/lib/code/terminal-view.jsx +32 -23
- package/lib/config.js +15 -4
- package/lib/containers/CLAUDE.md +16 -6
- package/lib/db/CLAUDE.md +5 -2
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +8 -3
- package/lib/db/config.js +0 -1
- package/lib/db/index.js +12 -0
- package/lib/db/schema.js +24 -1
- package/lib/db/user-channels.js +129 -0
- package/lib/llm-providers.js +8 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/CLAUDE.md +12 -3
- package/lib/tools/assemblyai.js +17 -0
- package/lib/tools/create-agent-job.js +12 -8
- package/lib/tools/docker.js +34 -10
- package/lib/tools/github.js +34 -0
- package/lib/tools/telegram.js +106 -0
- package/lib/utils/render-md.js +44 -18
- package/package.json +8 -8
- package/setup/CLAUDE.md +11 -5
- package/setup/lib/providers.mjs +2 -1
- package/setup/lib/targets.mjs +13 -16
- package/setup/lib/telegram.mjs +8 -69
- package/templates/.env.example +0 -7
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
- package/templates/.gitignore.template +1 -3
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.template +29 -7
- package/templates/agent-job/CLAUDE.md.template +5 -3
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/agent-job/SYSTEM.md +16 -11
- package/templates/agents/CLAUDE.md.template +17 -17
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/data/CLAUDE.md.template +1 -1
- package/templates/docker-compose.custom.yml +1 -0
- package/templates/docker-compose.yml +1 -0
- package/templates/event-handler/CLAUDE.md.template +79 -0
- package/templates/event-handler/TRIGGERS.json +18 -2
- package/templates/skills/CLAUDE.md.template +20 -22
- package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/SKILL.md +2 -2
- package/lib/ai/agent.js +0 -65
- package/lib/ai/async-channel.js +0 -51
- package/lib/ai/model.js +0 -130
- package/lib/ai/tools.js +0 -164
- package/lib/tools/openai.js +0 -37
- package/setup/lib/telegram-verify.mjs +0 -63
- package/setup/setup-telegram.mjs +0 -260
- package/templates/agent-job/SOUL.md +0 -17
- /package/templates/{skills/active/.gitkeep → coding-workspace/SYSTEM.md} +0 -0
- /package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/agent-job-secrets.js +0 -0
- /package/templates/skills/{library/playwright-cli → playwright-cli}/SKILL.md +0 -0
|
@@ -55,12 +55,32 @@ function ActionCard({ action, index }) {
|
|
|
55
55
|
<pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48">
|
|
56
56
|
{action.job}
|
|
57
57
|
</pre>
|
|
58
|
-
{(action.
|
|
59
|
-
<div className="flex items-center gap-2 mt-2">
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
{(action.agent_backend || action.llm_model || action.scope) && (
|
|
59
|
+
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
|
60
|
+
{action.agent_backend && (
|
|
61
|
+
<>
|
|
62
|
+
<span className="text-xs font-medium text-muted-foreground">Agent:</span>
|
|
63
|
+
<span className="inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium">
|
|
64
|
+
{action.agent_backend}
|
|
65
|
+
</span>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
{action.llm_model && (
|
|
69
|
+
<>
|
|
70
|
+
<span className="text-xs font-medium text-muted-foreground">Model:</span>
|
|
71
|
+
<span className="inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium">
|
|
72
|
+
{action.llm_model}
|
|
73
|
+
</span>
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
{action.scope && (
|
|
77
|
+
<>
|
|
78
|
+
<span className="text-xs font-medium text-muted-foreground">Scope:</span>
|
|
79
|
+
<span className="inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-mono">
|
|
80
|
+
{action.scope}
|
|
81
|
+
</span>
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
64
84
|
</div>
|
|
65
85
|
)}
|
|
66
86
|
</div>
|
|
@@ -83,6 +103,14 @@ function ActionCard({ action, index }) {
|
|
|
83
103
|
</pre>
|
|
84
104
|
</div>
|
|
85
105
|
)}
|
|
106
|
+
{action.headers && Object.keys(action.headers).length > 0 && (
|
|
107
|
+
<div>
|
|
108
|
+
<p className="text-xs font-medium text-muted-foreground mb-1">Headers</p>
|
|
109
|
+
<pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48">
|
|
110
|
+
{JSON.stringify(action.headers, null, 2)}
|
|
111
|
+
</pre>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
86
114
|
</div>
|
|
87
115
|
)}
|
|
88
116
|
</div>
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
4
|
import { ChevronDownIcon, CheckIcon, SearchIcon } from "../icons.js";
|
|
5
5
|
import { cn } from "../../utils.js";
|
|
6
|
-
function Combobox({ options = [], value, onChange, placeholder = "Select...", loading = false, disabled = false, highlight = false, side = "bottom", onOpen, triggerClassName, triggerLabel }) {
|
|
6
|
+
function Combobox({ options = [], value, onChange, placeholder = "Select...", loading = false, disabled = false, highlight = false, side = "bottom", onOpen, triggerClassName, triggerLabel, footerAction }) {
|
|
7
7
|
const [open, setOpen] = useState(false);
|
|
8
8
|
const [filter, setFilter] = useState("");
|
|
9
9
|
const ref = useRef(null);
|
|
@@ -93,7 +93,23 @@ function Combobox({ options = [], value, onChange, placeholder = "Select...", lo
|
|
|
93
93
|
]
|
|
94
94
|
},
|
|
95
95
|
opt.value
|
|
96
|
-
)) })
|
|
96
|
+
)) }),
|
|
97
|
+
footerAction && /* @__PURE__ */ jsx("div", { className: "border-t border-border p-1", children: /* @__PURE__ */ jsxs(
|
|
98
|
+
"button",
|
|
99
|
+
{
|
|
100
|
+
type: "button",
|
|
101
|
+
onClick: () => {
|
|
102
|
+
setOpen(false);
|
|
103
|
+
setFilter("");
|
|
104
|
+
footerAction.onClick();
|
|
105
|
+
},
|
|
106
|
+
className: "flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-sm text-left transition-colors hover:bg-accent text-muted-foreground",
|
|
107
|
+
children: [
|
|
108
|
+
footerAction.icon,
|
|
109
|
+
/* @__PURE__ */ jsx("span", { children: footerAction.label })
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
) })
|
|
97
113
|
] })
|
|
98
114
|
] });
|
|
99
115
|
}
|
|
@@ -4,7 +4,7 @@ import { useState, useRef, useEffect, useCallback } from 'react';
|
|
|
4
4
|
import { ChevronDownIcon, CheckIcon, SearchIcon } from '../icons.js';
|
|
5
5
|
import { cn } from '../../utils.js';
|
|
6
6
|
|
|
7
|
-
export function Combobox({ options = [], value, onChange, placeholder = 'Select...', loading = false, disabled = false, highlight = false, side = 'bottom', onOpen, triggerClassName, triggerLabel }) {
|
|
7
|
+
export function Combobox({ options = [], value, onChange, placeholder = 'Select...', loading = false, disabled = false, highlight = false, side = 'bottom', onOpen, triggerClassName, triggerLabel, footerAction }) {
|
|
8
8
|
const [open, setOpen] = useState(false);
|
|
9
9
|
const [filter, setFilter] = useState('');
|
|
10
10
|
const ref = useRef(null);
|
|
@@ -117,6 +117,22 @@ export function Combobox({ options = [], value, onChange, placeholder = 'Select.
|
|
|
117
117
|
))
|
|
118
118
|
)}
|
|
119
119
|
</div>
|
|
120
|
+
{footerAction && (
|
|
121
|
+
<div className="border-t border-border p-1">
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={() => {
|
|
125
|
+
setOpen(false);
|
|
126
|
+
setFilter('');
|
|
127
|
+
footerAction.onClick();
|
|
128
|
+
}}
|
|
129
|
+
className="flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-sm text-left transition-colors hover:bg-accent text-muted-foreground"
|
|
130
|
+
>
|
|
131
|
+
{footerAction.icon}
|
|
132
|
+
<span>{footerAction.label}</span>
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
120
136
|
</div>
|
|
121
137
|
)}
|
|
122
138
|
</div>
|
|
@@ -30,11 +30,30 @@ function DropdownMenuContent({ children, className, align = "start", side = "bot
|
|
|
30
30
|
const updatePosition = useCallback(() => {
|
|
31
31
|
if (!triggerRef.current) return;
|
|
32
32
|
const rect = triggerRef.current.getBoundingClientRect();
|
|
33
|
+
const margin = 8;
|
|
34
|
+
let left = align === "start" ? rect.left : void 0;
|
|
35
|
+
let right = align === "end" ? window.innerWidth - rect.right : void 0;
|
|
36
|
+
const menuWidth = ref.current?.getBoundingClientRect().width || 0;
|
|
37
|
+
if (menuWidth > 0) {
|
|
38
|
+
if (align === "end") {
|
|
39
|
+
const computedLeft = rect.right - menuWidth;
|
|
40
|
+
if (computedLeft < margin) {
|
|
41
|
+
right = void 0;
|
|
42
|
+
left = margin;
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
const computedRight = rect.left + menuWidth;
|
|
46
|
+
if (computedRight > window.innerWidth - margin) {
|
|
47
|
+
left = void 0;
|
|
48
|
+
right = margin;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
setPos({
|
|
34
53
|
top: side === "bottom" ? rect.bottom + sideOffset : void 0,
|
|
35
54
|
bottom: side === "top" ? window.innerHeight - rect.top + sideOffset : void 0,
|
|
36
|
-
left
|
|
37
|
-
right
|
|
55
|
+
left,
|
|
56
|
+
right
|
|
38
57
|
});
|
|
39
58
|
}, [triggerRef, side, align, sideOffset]);
|
|
40
59
|
useEffect(() => {
|
|
@@ -43,6 +62,7 @@ function DropdownMenuContent({ children, className, align = "start", side = "bot
|
|
|
43
62
|
return;
|
|
44
63
|
}
|
|
45
64
|
updatePosition();
|
|
65
|
+
const raf = requestAnimationFrame(updatePosition);
|
|
46
66
|
const handleClickOutside = (e) => {
|
|
47
67
|
if (ref.current && !ref.current.contains(e.target) && triggerRef.current && !triggerRef.current.contains(e.target)) {
|
|
48
68
|
onOpenChange(false);
|
|
@@ -56,6 +76,7 @@ function DropdownMenuContent({ children, className, align = "start", side = "bot
|
|
|
56
76
|
document.addEventListener("keydown", handleEsc);
|
|
57
77
|
window.addEventListener("scroll", handleScroll, true);
|
|
58
78
|
return () => {
|
|
79
|
+
cancelAnimationFrame(raf);
|
|
59
80
|
document.removeEventListener("click", handleClickOutside);
|
|
60
81
|
document.removeEventListener("keydown", handleEsc);
|
|
61
82
|
window.removeEventListener("scroll", handleScroll, true);
|
|
@@ -47,17 +47,41 @@ export function DropdownMenuContent({ children, className, align = 'start', side
|
|
|
47
47
|
const updatePosition = useCallback(() => {
|
|
48
48
|
if (!triggerRef.current) return;
|
|
49
49
|
const rect = triggerRef.current.getBoundingClientRect();
|
|
50
|
+
const margin = 8;
|
|
51
|
+
let left = align === 'start' ? rect.left : undefined;
|
|
52
|
+
let right = align === 'end' ? window.innerWidth - rect.right : undefined;
|
|
53
|
+
|
|
54
|
+
// Clamp horizontally so the menu never overflows the viewport.
|
|
55
|
+
const menuWidth = ref.current?.getBoundingClientRect().width || 0;
|
|
56
|
+
if (menuWidth > 0) {
|
|
57
|
+
if (align === 'end') {
|
|
58
|
+
const computedLeft = rect.right - menuWidth;
|
|
59
|
+
if (computedLeft < margin) {
|
|
60
|
+
right = undefined;
|
|
61
|
+
left = margin;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
const computedRight = rect.left + menuWidth;
|
|
65
|
+
if (computedRight > window.innerWidth - margin) {
|
|
66
|
+
left = undefined;
|
|
67
|
+
right = margin;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
setPos({
|
|
51
73
|
top: side === 'bottom' ? rect.bottom + sideOffset : undefined,
|
|
52
74
|
bottom: side === 'top' ? window.innerHeight - rect.top + sideOffset : undefined,
|
|
53
|
-
left
|
|
54
|
-
right
|
|
75
|
+
left,
|
|
76
|
+
right,
|
|
55
77
|
});
|
|
56
78
|
}, [triggerRef, side, align, sideOffset]);
|
|
57
79
|
|
|
58
80
|
useEffect(() => {
|
|
59
81
|
if (!open) { setPos(null); return; }
|
|
60
82
|
updatePosition();
|
|
83
|
+
// Re-run after render so menu width is measurable for clamping.
|
|
84
|
+
const raf = requestAnimationFrame(updatePosition);
|
|
61
85
|
const handleClickOutside = (e) => {
|
|
62
86
|
if (ref.current && !ref.current.contains(e.target) && triggerRef.current && !triggerRef.current.contains(e.target)) {
|
|
63
87
|
onOpenChange(false);
|
|
@@ -71,6 +95,7 @@ export function DropdownMenuContent({ children, className, align = 'start', side
|
|
|
71
95
|
document.addEventListener('keydown', handleEsc);
|
|
72
96
|
window.addEventListener('scroll', handleScroll, true);
|
|
73
97
|
return () => {
|
|
98
|
+
cancelAnimationFrame(raf);
|
|
74
99
|
document.removeEventListener('click', handleClickOutside);
|
|
75
100
|
document.removeEventListener('keydown', handleEsc);
|
|
76
101
|
window.removeEventListener('scroll', handleScroll, true);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getUserChannel } from '../db/user-channels.js';
|
|
2
|
+
import { getConfig } from '../config.js';
|
|
3
|
+
import { validateBotToken } from '../tools/telegram.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the initial server-rendered state for the Telegram profile tab.
|
|
7
|
+
* Returns `{ status: 'unlinked' | 'pending' | 'verified', ...fields, botUsername }`.
|
|
8
|
+
*/
|
|
9
|
+
export async function getTelegramProfileInitial(userId) {
|
|
10
|
+
const row = getUserChannel(userId, 'telegram');
|
|
11
|
+
const botToken = getConfig('TELEGRAM_BOT_TOKEN');
|
|
12
|
+
let botUsername = null;
|
|
13
|
+
if (botToken) {
|
|
14
|
+
const info = await validateBotToken(botToken);
|
|
15
|
+
if (info.valid) botUsername = info.botInfo.username;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!row) return { status: 'unlinked', botUsername };
|
|
19
|
+
if (row.verifiedAt) {
|
|
20
|
+
return {
|
|
21
|
+
status: 'verified',
|
|
22
|
+
verifiedAt: row.verifiedAt,
|
|
23
|
+
channelChatId: row.channelChatId,
|
|
24
|
+
botUsername,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
status: 'pending',
|
|
29
|
+
code: row.code,
|
|
30
|
+
expiresAt: row.codeExpiresAt,
|
|
31
|
+
botUsername,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/lib/cluster/CLAUDE.md
CHANGED
|
@@ -41,9 +41,15 @@ Roles support multiple concurrent triggers. All paths use `canRunRole()` as a sh
|
|
|
41
41
|
|
|
42
42
|
## Concurrency & Validation
|
|
43
43
|
|
|
44
|
-
`canRunRole(roleIdOrData)` is the
|
|
44
|
+
`canRunRole(roleIdOrData)` is the synchronous validation function. It checks cluster enabled status and concurrency limits. Returns `{ allowed, reason?, roleData? }`. Reasons: `disabled` (cluster off), `concurrency` (at max), `not_found`.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
`acquireAndRunRole(roleId, payload?)` is the **atomic gate** that all trigger paths actually use. It calls `canRunRole()` and `runClusterRole()` together so two simultaneous triggers can't both pass the concurrency check before either container is observable to `listContainers()`. Manual UI triggers, webhooks, cron, and file-watch all funnel through this.
|
|
47
|
+
|
|
48
|
+
Each role has `maxConcurrency` (default 1). `canRunRole()` counts running instances via `listContainers()`.
|
|
49
|
+
|
|
50
|
+
## Plan Mode (Roles)
|
|
51
|
+
|
|
52
|
+
`cluster_roles.planMode` (default `0`) gates the worker into Claude's plan-mode (read-only). When set, the worker is launched with `PERMISSION=plan` so it cannot execute mutating tools. Useful for review/analysis roles.
|
|
47
53
|
|
|
48
54
|
## Prompt Architecture
|
|
49
55
|
|
|
@@ -75,6 +81,6 @@ Built by `buildTemplateVars()` → `buildWorkerSystemPrompt()` + `resolveCluster
|
|
|
75
81
|
## DB Tables
|
|
76
82
|
|
|
77
83
|
- `clusters` — cluster metadata (name, system_prompt, folders, enabled)
|
|
78
|
-
- `cluster_roles` — role definitions scoped to a cluster (role_name, role, prompt, trigger_config, max_concurrency, cleanup_worker_dir, folders)
|
|
84
|
+
- `cluster_roles` — role definitions scoped to a cluster (role_name, role, prompt, trigger_config, max_concurrency, plan_mode, cleanup_worker_dir, folders)
|
|
79
85
|
|
|
80
86
|
Workers are ephemeral containers, not database entities.
|
package/lib/code/CLAUDE.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Data Flow
|
|
4
4
|
|
|
5
|
-
Chat agent's `coding_agent` tool → `runInteractiveContainer()` in `lib/tools/docker.js` → Docker container runs `coding-agent-
|
|
5
|
+
Chat agent's synthetic `coding_agent` tool wrapper → `runInteractiveContainer()` in `lib/tools/docker.js` → Docker container runs `coding-agent-{agent}` image (interactive runtime) → ttyd on port 7681 fronts a tmux session that runs the agent CLI → browser navigates to `/code/{id}` → `TerminalView` (xterm.js) opens WebSocket → `ws-proxy.js` authenticates and proxies to container.
|
|
6
|
+
|
|
7
|
+
The interactive container's entrypoint launches a tmux session via the per-agent `docker/coding-agent/scripts/agents/<agent>/start-coding-session.sh` script. Multiple terminal tabs can attach to the same tmux session and survive WebSocket reconnects. Tab close + reopen reattaches; container restart re-creates the session with the prior agent session resumed (see "Session Continuity" below).
|
|
6
8
|
|
|
7
9
|
## WebSocket Auth
|
|
8
10
|
|
|
@@ -23,12 +25,18 @@ All actions use `requireAuth()` with ownership checks: `getCodeWorkspaces()`, `c
|
|
|
23
25
|
|
|
24
26
|
## Multi-Agent Backends
|
|
25
27
|
|
|
26
|
-
Code workspaces support multiple coding agent backends.
|
|
28
|
+
Code workspaces support multiple coding agent backends. Selection is **per-workspace** via the `codingAgent` column on `code_workspaces`, falling back to the global `CODING_AGENT` config key, then to `claude-code`. See `lib/code/actions.js:410`: `const agent = workspace.codingAgent || getConfig('CODING_AGENT') || 'claude-code';`. The same fallback chain is used by `lib/ai/index.js` for chat-mode streaming.
|
|
27
29
|
|
|
28
|
-
**Supported agents**: `claude-code`, `pi`, `gemini-cli`, `codex-cli`, `opencode`. Each uses a different Docker image variant (`docker/coding-agent/Dockerfile.*`) and agent-specific setup/auth scripts in `docker/coding-agent/scripts/`.
|
|
30
|
+
**Supported agents**: `claude-code`, `pi-coding-agent`, `gemini-cli`, `codex-cli`, `opencode`, `kimi-cli`. Each uses a different Docker image variant (`docker/coding-agent/Dockerfile.*`) and agent-specific setup/auth scripts in `docker/coding-agent/scripts/`.
|
|
29
31
|
|
|
30
32
|
**Configuration**: Users configure agents via `/admin/event-handler/coding-agents` — enable/disable agents, set per-agent auth mode (OAuth vs API key), provider, and model. `setCodingAgentDefault()` sets the global default. `buildAgentAuthEnv()` in `lib/tools/docker.js` resolves credentials from the settings DB at container launch time.
|
|
31
33
|
|
|
32
34
|
**Container streaming**: `lib/containers/stream.js` provides an SSE endpoint (`/stream/containers`) that polls Docker for container stats every 3 seconds. Used by the Containers admin page for live monitoring.
|
|
33
35
|
|
|
34
36
|
**Backend API in messages**: When an agent produces output, the `backendApi` field in message chunks identifies which agent backend generated the response.
|
|
37
|
+
|
|
38
|
+
## Session Continuity
|
|
39
|
+
|
|
40
|
+
Code workspaces, chat-mode (SDK adapter), and headless agent jobs share session continuity through `lib/ai/session-manager.js`. Session IDs are written to per-port files inside the workspace volume — `~/.{agent}-ttyd-sessions/${PORT}` (or scope-prefixed when `SCOPE` is set). The agent CLI captures its session ID via per-agent hooks (see `docker/coding-agent/CLAUDE.md` § Session Tracking for the 5 patterns). On the next launch the entrypoint reads the saved ID and passes the agent's resume flag (`--continue`, `--resume`, `--session`, depending on agent).
|
|
41
|
+
|
|
42
|
+
Headless `run.sh` always reads from port `7681` — so SDK chat, manual chat tools, and code workspaces all converge on the same conversation when they share a workspace.
|
package/lib/code/actions.js
CHANGED
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
} from '../db/code-workspaces.js';
|
|
14
14
|
import {
|
|
15
15
|
getChatByWorkspaceId,
|
|
16
|
+
touchChatUpdatedAt,
|
|
16
17
|
} from '../db/chats.js';
|
|
18
|
+
import { buildCodingAgentSystemPrompt } from '../ai/system-prompt.js';
|
|
19
|
+
import { resolveAgentScope } from '../ai/scope.js';
|
|
20
|
+
import { workspaceDir as getWorkspaceDir } from '../tools/docker.js';
|
|
17
21
|
import {
|
|
18
22
|
addSession,
|
|
19
23
|
getSession as getTermSession,
|
|
@@ -136,7 +140,15 @@ export async function ensureCodeWorkspaceContainer(id) {
|
|
|
136
140
|
|
|
137
141
|
// Inject agent job secrets when the linked chat is in agent mode
|
|
138
142
|
const chat = getChatByWorkspaceId(id);
|
|
139
|
-
const
|
|
143
|
+
const isAgent = chat?.chatMode === 'agent';
|
|
144
|
+
const injectSecrets = isAgent;
|
|
145
|
+
|
|
146
|
+
// Resolve scope for system prompt skills
|
|
147
|
+
const wsBaseDir = getWorkspaceDir(id);
|
|
148
|
+
const repoDir = (await import('path')).join(wsBaseDir, 'workspace');
|
|
149
|
+
const wsScope = workspace.scope || null;
|
|
150
|
+
const { skillsDir } = resolveAgentScope(repoDir, wsScope);
|
|
151
|
+
const systemPrompt = buildCodingAgentSystemPrompt(isAgent ? 'agent' : 'code', isAgent ? skillsDir : null, isAgent ? wsScope : null);
|
|
140
152
|
|
|
141
153
|
try {
|
|
142
154
|
const { inspectContainer, startContainer, removeContainer, runInteractiveContainer } =
|
|
@@ -145,14 +157,17 @@ export async function ensureCodeWorkspaceContainer(id) {
|
|
|
145
157
|
const info = await inspectContainer(workspace.containerName);
|
|
146
158
|
|
|
147
159
|
if (!info) {
|
|
148
|
-
// Container not found — recreate
|
|
160
|
+
// Container not found — recreate using the agent that was originally chosen at launch time
|
|
149
161
|
await runInteractiveContainer({
|
|
150
162
|
containerName: workspace.containerName,
|
|
163
|
+
codingAgent: workspace.codingAgent || undefined,
|
|
151
164
|
repo: workspace.repo,
|
|
152
165
|
branch: workspace.branch,
|
|
153
166
|
featureBranch: workspace.featureBranch,
|
|
154
167
|
workspaceId: id,
|
|
155
168
|
injectSecrets,
|
|
169
|
+
systemPrompt,
|
|
170
|
+
scope: workspace.scope || undefined,
|
|
156
171
|
});
|
|
157
172
|
return { status: 'created' };
|
|
158
173
|
}
|
|
@@ -172,15 +187,18 @@ export async function ensureCodeWorkspaceContainer(id) {
|
|
|
172
187
|
}
|
|
173
188
|
}
|
|
174
189
|
|
|
175
|
-
// Dead, bad state, or start failed — remove and recreate
|
|
190
|
+
// Dead, bad state, or start failed — remove and recreate using the originally chosen agent
|
|
176
191
|
await removeContainer(workspace.containerName);
|
|
177
192
|
await runInteractiveContainer({
|
|
178
193
|
containerName: workspace.containerName,
|
|
194
|
+
codingAgent: workspace.codingAgent || undefined,
|
|
179
195
|
repo: workspace.repo,
|
|
180
196
|
branch: workspace.branch,
|
|
181
197
|
featureBranch: workspace.featureBranch,
|
|
182
198
|
workspaceId: id,
|
|
183
199
|
injectSecrets,
|
|
200
|
+
systemPrompt,
|
|
201
|
+
scope: workspace.scope || undefined,
|
|
184
202
|
});
|
|
185
203
|
return { status: 'created' };
|
|
186
204
|
} catch (err) {
|
|
@@ -194,7 +212,7 @@ export async function ensureCodeWorkspaceContainer(id) {
|
|
|
194
212
|
* @param {string} id - Workspace ID
|
|
195
213
|
* @returns {Promise<{success: boolean, containerName?: string, message?: string}>}
|
|
196
214
|
*/
|
|
197
|
-
export async function startInteractiveMode(id) {
|
|
215
|
+
export async function startInteractiveMode(id, agentOverride) {
|
|
198
216
|
const user = await requireAuth();
|
|
199
217
|
const workspace = getCodeWorkspaceById(id);
|
|
200
218
|
if (!workspace || workspace.userId !== user.id) {
|
|
@@ -207,25 +225,38 @@ export async function startInteractiveMode(id) {
|
|
|
207
225
|
|
|
208
226
|
// Inject agent job secrets when the linked chat is in agent mode
|
|
209
227
|
const chat = getChatByWorkspaceId(id);
|
|
210
|
-
const
|
|
228
|
+
const isAgent = chat?.chatMode === 'agent';
|
|
229
|
+
const injectSecrets = isAgent;
|
|
230
|
+
|
|
231
|
+
// Resolve scope for system prompt skills
|
|
232
|
+
const wsBase = getWorkspaceDir(id);
|
|
233
|
+
const repoPath = (await import('path')).join(wsBase, 'workspace');
|
|
234
|
+
const wsScope2 = workspace.scope || null;
|
|
235
|
+
const { skillsDir: scopedSkillsDir } = resolveAgentScope(repoPath, wsScope2);
|
|
236
|
+
const systemPrompt = buildCodingAgentSystemPrompt(isAgent ? 'agent' : 'code', isAgent ? scopedSkillsDir : null, isAgent ? wsScope2 : null);
|
|
211
237
|
|
|
212
238
|
try {
|
|
213
239
|
const { getConfig } = await import('../config.js');
|
|
214
|
-
|
|
240
|
+
// Use the explicitly chosen agent, then fall back to the global config default
|
|
241
|
+
const agent = agentOverride || getConfig('CODING_AGENT') || 'claude-code';
|
|
215
242
|
const shortId = id.replace(/-/g, '').slice(0, 8);
|
|
216
243
|
const containerName = `${agent}-interactive-${shortId}`;
|
|
217
244
|
|
|
218
245
|
const { runInteractiveContainer } = await import('../tools/docker.js');
|
|
219
246
|
await runInteractiveContainer({
|
|
220
247
|
containerName,
|
|
248
|
+
codingAgent: agent,
|
|
221
249
|
repo: workspace.repo,
|
|
222
250
|
branch: workspace.branch,
|
|
223
251
|
featureBranch: workspace.featureBranch,
|
|
224
252
|
workspaceId: id,
|
|
225
253
|
injectSecrets,
|
|
254
|
+
systemPrompt,
|
|
255
|
+
scope: workspace.scope || undefined,
|
|
226
256
|
});
|
|
227
257
|
|
|
228
|
-
|
|
258
|
+
// Persist both the container name and the agent so recovery can use the same image
|
|
259
|
+
updateContainerName(id, containerName, agent);
|
|
229
260
|
return { success: true, containerName };
|
|
230
261
|
} catch (err) {
|
|
231
262
|
console.error(`[startInteractiveMode] workspace=${id}`, err);
|
|
@@ -373,8 +404,10 @@ export async function createTerminalSession(id, type = 'shell') {
|
|
|
373
404
|
const { randomUUID } = await import('crypto');
|
|
374
405
|
|
|
375
406
|
// Start ttyd in the background, then find its PID via pgrep
|
|
407
|
+
// Use the agent the workspace was originally launched with — not the current global config,
|
|
408
|
+
// which may have changed since the container was started (causing wrong script path = session error)
|
|
376
409
|
const { getConfig } = await import('../config.js');
|
|
377
|
-
const agent = getConfig('CODING_AGENT') || 'claude-code';
|
|
410
|
+
const agent = workspace.codingAgent || getConfig('CODING_AGENT') || 'claude-code';
|
|
378
411
|
const command = type === 'code'
|
|
379
412
|
? `nohup env PORT=${port} /scripts/common/start-ttyd-session.sh /scripts/agents/${agent}/start-coding-session.sh > /dev/null 2>&1 &`
|
|
380
413
|
: `nohup env PORT=${port} /scripts/common/start-shell-session.sh > /dev/null 2>&1 &`;
|
|
@@ -593,6 +626,12 @@ export async function getWorkspaceDiffStats(id, authenticatedUser) {
|
|
|
593
626
|
|
|
594
627
|
updateHasChanges(id, insertions > 0 || deletions > 0);
|
|
595
628
|
|
|
629
|
+
// Keep the linked chat's updatedAt fresh so it doesn't fall behind in the sidebar
|
|
630
|
+
if (insertions > 0 || deletions > 0) {
|
|
631
|
+
const chat = getChatByWorkspaceId(id);
|
|
632
|
+
if (chat) touchChatUpdatedAt(chat.id);
|
|
633
|
+
}
|
|
634
|
+
|
|
596
635
|
// Sync featureBranch in DB if the actual branch differs
|
|
597
636
|
if (currentBranch && currentBranch !== workspace.featureBranch) {
|
|
598
637
|
const { updateFeatureBranch } = await import('../db/code-workspaces.js');
|
|
@@ -175,7 +175,7 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
175
175
|
fitAddonRef.current = fitAddon;
|
|
176
176
|
term.open(containerRef.current);
|
|
177
177
|
const style = document.createElement("style");
|
|
178
|
-
style.textContent = `.xterm { padding: 5px; background-color: ${theme.background} !important; } .xterm-viewport { background-color: ${theme.background} !important; } .xterm-rows span { pointer-events: none; }`;
|
|
178
|
+
style.textContent = `.xterm { padding: 5px; background-color: ${theme.background} !important; } .xterm-viewport { background-color: ${theme.background} !important; touch-action: none; } .xterm-rows span { pointer-events: none; }`;
|
|
179
179
|
containerRef.current.appendChild(style);
|
|
180
180
|
styleRef.current = style;
|
|
181
181
|
containerRef.current.style.backgroundColor = theme.background;
|
|
@@ -188,7 +188,7 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
188
188
|
toolbarRef.current.style.setProperty("--tb-dropup-bg", theme.background);
|
|
189
189
|
}
|
|
190
190
|
fitAddon.fit();
|
|
191
|
-
const
|
|
191
|
+
const termContainer = containerRef.current;
|
|
192
192
|
let lastTouchY = null;
|
|
193
193
|
let touchScrollAccum = 0;
|
|
194
194
|
const LINE_HEIGHT = 15;
|
|
@@ -197,6 +197,7 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
197
197
|
lastTouchY = ev.touches[0].clientY;
|
|
198
198
|
touchScrollAccum = 0;
|
|
199
199
|
}
|
|
200
|
+
ev.stopPropagation();
|
|
200
201
|
};
|
|
201
202
|
const onTouchMove = (ev) => {
|
|
202
203
|
if (lastTouchY === null || ev.touches.length !== 1) return;
|
|
@@ -210,16 +211,15 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
210
211
|
touchScrollAccum -= lines * LINE_HEIGHT;
|
|
211
212
|
}
|
|
212
213
|
ev.preventDefault();
|
|
214
|
+
ev.stopPropagation();
|
|
213
215
|
};
|
|
214
216
|
const onTouchEnd = () => {
|
|
215
217
|
lastTouchY = null;
|
|
216
218
|
touchScrollAccum = 0;
|
|
217
219
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
screenEl.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
222
|
-
}
|
|
220
|
+
termContainer.addEventListener("touchstart", onTouchStart, { passive: false, capture: true });
|
|
221
|
+
termContainer.addEventListener("touchmove", onTouchMove, { passive: false, capture: true });
|
|
222
|
+
termContainer.addEventListener("touchend", onTouchEnd, { passive: true, capture: true });
|
|
223
223
|
term.onData((data) => {
|
|
224
224
|
const ws = wsRef.current;
|
|
225
225
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -258,11 +258,9 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
258
258
|
clearTimeout(resizeTimeout);
|
|
259
259
|
clearTimeout(retryTimer.current);
|
|
260
260
|
window.removeEventListener("resize", handleResize);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
screenEl.removeEventListener("touchend", onTouchEnd);
|
|
265
|
-
}
|
|
261
|
+
termContainer.removeEventListener("touchstart", onTouchStart, { capture: true });
|
|
262
|
+
termContainer.removeEventListener("touchmove", onTouchMove, { capture: true });
|
|
263
|
+
termContainer.removeEventListener("touchend", onTouchEnd, { capture: true });
|
|
266
264
|
if (wsRef.current) wsRef.current.close();
|
|
267
265
|
term.dispose();
|
|
268
266
|
};
|
|
@@ -782,7 +780,7 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
782
780
|
] }) })
|
|
783
781
|
] });
|
|
784
782
|
}
|
|
785
|
-
const STORAGE_KEY = "thepopebot-workspace-command";
|
|
783
|
+
const STORAGE_KEY = "thepopebot-workspace-command:code";
|
|
786
784
|
function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh, onShowDiff }) {
|
|
787
785
|
const [selectedCommand, setSelectedCommandState] = useState(() => {
|
|
788
786
|
try {
|
|
@@ -798,6 +796,26 @@ function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh,
|
|
|
798
796
|
} catch {
|
|
799
797
|
}
|
|
800
798
|
};
|
|
799
|
+
useEffect(() => {
|
|
800
|
+
let stored = null;
|
|
801
|
+
try {
|
|
802
|
+
stored = localStorage.getItem(STORAGE_KEY);
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
if (stored) return;
|
|
806
|
+
let cancelled = false;
|
|
807
|
+
import("../chat/actions.js").then(({ getModeGitActionDefault }) => {
|
|
808
|
+
getModeGitActionDefault("code").then((val) => {
|
|
809
|
+
if (cancelled || !val) return;
|
|
810
|
+
setSelectedCommandState(val);
|
|
811
|
+
}).catch(() => {
|
|
812
|
+
});
|
|
813
|
+
}).catch(() => {
|
|
814
|
+
});
|
|
815
|
+
return () => {
|
|
816
|
+
cancelled = true;
|
|
817
|
+
};
|
|
818
|
+
}, []);
|
|
801
819
|
const [commandRunning, setCommandRunning] = useState(false);
|
|
802
820
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
803
821
|
const [commandOutput, setCommandOutput] = useState("");
|
|
@@ -814,14 +832,6 @@ function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh,
|
|
|
814
832
|
}, [dropupOpen]);
|
|
815
833
|
const handleRun = useCallback(async () => {
|
|
816
834
|
if (commandRunning) return;
|
|
817
|
-
const fresh = await onDiffStatsRefresh?.();
|
|
818
|
-
const stats = fresh || diffStats;
|
|
819
|
-
if (!(stats?.insertions || 0) && !(stats?.deletions || 0)) {
|
|
820
|
-
setDialogOpen(true);
|
|
821
|
-
setCommandOutput("You have no changes.");
|
|
822
|
-
setCommandExitCode(1);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
835
|
setCommandRunning(true);
|
|
826
836
|
setDialogOpen(true);
|
|
827
837
|
setCommandOutput("");
|