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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
|
-
import { GitBranchIcon, ChevronDownIcon, SpinnerIcon, XIcon } from './icons.js';
|
|
5
|
+
import { GitBranchIcon, ChevronDownIcon, SpinnerIcon, XIcon, PlusIcon } from './icons.js';
|
|
6
6
|
import { Combobox } from './ui/combobox.js';
|
|
7
7
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from './ui/dropdown-menu.js';
|
|
8
8
|
import { cn } from '../utils.js';
|
|
@@ -30,12 +30,14 @@ export function RepoBranchPicker({
|
|
|
30
30
|
onBranchChange,
|
|
31
31
|
getRepositories,
|
|
32
32
|
getBranches,
|
|
33
|
+
createRepository,
|
|
33
34
|
}) {
|
|
34
35
|
const [repos, setRepos] = useState([]);
|
|
35
36
|
const [branches, setBranches] = useState([]);
|
|
36
37
|
const [loadingRepos, setLoadingRepos] = useState(false);
|
|
37
38
|
const [loadingBranches, setLoadingBranches] = useState(false);
|
|
38
39
|
const [reposLoaded, setReposLoaded] = useState(false);
|
|
40
|
+
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
39
41
|
|
|
40
42
|
// Load repos eagerly on mount
|
|
41
43
|
useEffect(() => {
|
|
@@ -67,6 +69,11 @@ export function RepoBranchPicker({
|
|
|
67
69
|
}).catch(() => setLoadingBranches(false));
|
|
68
70
|
}, [repo]);
|
|
69
71
|
|
|
72
|
+
const handleRepoCreated = useCallback((fullName) => {
|
|
73
|
+
setRepos((prev) => [...prev, { full_name: fullName, default_branch: 'main' }]);
|
|
74
|
+
onRepoChange(fullName);
|
|
75
|
+
}, [onRepoChange]);
|
|
76
|
+
|
|
70
77
|
const repoOptions = repos.map((r) => ({ value: r.full_name, label: r.full_name }));
|
|
71
78
|
const branchOptions = branches.map((b) => ({ value: b.name, label: b.name }));
|
|
72
79
|
|
|
@@ -80,6 +87,11 @@ export function RepoBranchPicker({
|
|
|
80
87
|
placeholder="Select repository..."
|
|
81
88
|
loading={loadingRepos}
|
|
82
89
|
highlight={!repo && !loadingRepos}
|
|
90
|
+
footerAction={createRepository ? {
|
|
91
|
+
icon: <PlusIcon size={14} />,
|
|
92
|
+
label: 'Create new repository...',
|
|
93
|
+
onClick: () => setShowCreateDialog(true),
|
|
94
|
+
} : undefined}
|
|
83
95
|
/>
|
|
84
96
|
</div>
|
|
85
97
|
<div className={cn("w-full sm:w-auto sm:min-w-[200px] sm:max-w-[200px]", !repo && "opacity-50 pointer-events-none")}>
|
|
@@ -92,10 +104,99 @@ export function RepoBranchPicker({
|
|
|
92
104
|
highlight={!!repo && !branch && !loadingBranches}
|
|
93
105
|
/>
|
|
94
106
|
</div>
|
|
107
|
+
{showCreateDialog && (
|
|
108
|
+
<CreateRepoDialog
|
|
109
|
+
onClose={() => setShowCreateDialog(false)}
|
|
110
|
+
onCreate={handleRepoCreated}
|
|
111
|
+
createRepository={createRepository}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
95
114
|
</div>
|
|
96
115
|
);
|
|
97
116
|
}
|
|
98
117
|
|
|
118
|
+
function CreateRepoDialog({ onClose, onCreate, createRepository }) {
|
|
119
|
+
const [name, setName] = useState('');
|
|
120
|
+
const [creating, setCreating] = useState(false);
|
|
121
|
+
const [error, setError] = useState(null);
|
|
122
|
+
const inputRef = useRef(null);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const handleEsc = (e) => {
|
|
130
|
+
if (e.key === 'Escape') onClose();
|
|
131
|
+
};
|
|
132
|
+
document.addEventListener('keydown', handleEsc);
|
|
133
|
+
return () => document.removeEventListener('keydown', handleEsc);
|
|
134
|
+
}, [onClose]);
|
|
135
|
+
|
|
136
|
+
const handleSubmit = async (e) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
const trimmed = name.trim();
|
|
139
|
+
if (!trimmed || creating) return;
|
|
140
|
+
setCreating(true);
|
|
141
|
+
setError(null);
|
|
142
|
+
try {
|
|
143
|
+
const repo = await createRepository(trimmed);
|
|
144
|
+
onCreate(repo.full_name);
|
|
145
|
+
onClose();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
setError(err.message || 'Failed to create repository');
|
|
148
|
+
setCreating(false);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return createPortal(
|
|
153
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
154
|
+
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
|
155
|
+
<div
|
|
156
|
+
className="relative z-50 w-full max-w-md mx-4 rounded-lg border border-border bg-background p-6 shadow-lg"
|
|
157
|
+
onClick={(e) => e.stopPropagation()}
|
|
158
|
+
>
|
|
159
|
+
<div className="flex items-center justify-between mb-4">
|
|
160
|
+
<h3 className="text-base font-semibold">Create Repository</h3>
|
|
161
|
+
<button onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors">
|
|
162
|
+
<XIcon size={16} />
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<form onSubmit={handleSubmit}>
|
|
167
|
+
<label className="block text-sm text-muted-foreground mb-1.5">Repository name</label>
|
|
168
|
+
<input
|
|
169
|
+
ref={inputRef}
|
|
170
|
+
type="text"
|
|
171
|
+
value={name}
|
|
172
|
+
onChange={(e) => setName(e.target.value)}
|
|
173
|
+
placeholder="my-project"
|
|
174
|
+
className="w-full rounded-md border border-border bg-muted px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary"
|
|
175
|
+
/>
|
|
176
|
+
{error && <p className="text-xs text-destructive mt-2">{error}</p>}
|
|
177
|
+
<div className="mt-5 flex justify-end gap-2">
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={onClose}
|
|
181
|
+
className="px-3 py-1.5 text-sm border border-border text-muted-foreground hover:text-foreground rounded-md transition-colors"
|
|
182
|
+
>
|
|
183
|
+
Cancel
|
|
184
|
+
</button>
|
|
185
|
+
<button
|
|
186
|
+
type="submit"
|
|
187
|
+
disabled={!name.trim() || creating}
|
|
188
|
+
className="px-3 py-1.5 text-sm font-medium bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 rounded-md transition-colors"
|
|
189
|
+
>
|
|
190
|
+
{creating ? 'Creating...' : 'Create'}
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</form>
|
|
194
|
+
</div>
|
|
195
|
+
</div>,
|
|
196
|
+
document.body
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
99
200
|
/**
|
|
100
201
|
* Workspace toolbar bar with branch info, diff stats, and command buttons.
|
|
101
202
|
* Only rendered when a workspace exists (after first message creates one).
|
|
@@ -109,6 +210,7 @@ export function WorkspaceBar({
|
|
|
109
210
|
diffStats,
|
|
110
211
|
onDiffStatsRefresh,
|
|
111
212
|
onShowDiff,
|
|
213
|
+
chatMode = 'agent',
|
|
112
214
|
}) {
|
|
113
215
|
const [branches, setBranches] = useState([]);
|
|
114
216
|
const [loadingBranches, setLoadingBranches] = useState(false);
|
|
@@ -124,7 +226,7 @@ export function WorkspaceBar({
|
|
|
124
226
|
{branch && (
|
|
125
227
|
<>
|
|
126
228
|
<span className="shrink-0 text-muted-foreground/30 hidden md:inline">/</span>
|
|
127
|
-
<div className="
|
|
229
|
+
<div className="min-w-0">
|
|
128
230
|
<Combobox
|
|
129
231
|
options={branches.map((b) => ({ value: b.name, label: b.name }))}
|
|
130
232
|
value={branch}
|
|
@@ -141,20 +243,20 @@ export function WorkspaceBar({
|
|
|
141
243
|
}).finally(() => setLoadingBranches(false));
|
|
142
244
|
}
|
|
143
245
|
}}
|
|
144
|
-
triggerClassName="font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1"
|
|
145
|
-
triggerLabel={<span
|
|
246
|
+
triggerClassName="inline-block max-w-[70px] md:max-w-[160px] text-left font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1 align-middle"
|
|
247
|
+
triggerLabel={<span title={branch}>{branch}</span>}
|
|
146
248
|
/>
|
|
147
249
|
</div>
|
|
148
250
|
</>
|
|
149
251
|
)}
|
|
150
|
-
{featureBranch && (
|
|
252
|
+
{featureBranch && featureBranch !== branch && (
|
|
151
253
|
<>
|
|
152
254
|
<span className="shrink-0 text-muted-foreground/50">←</span>
|
|
153
255
|
<span className="text-primary truncate min-w-0 cursor-default" title={featureBranch}>{featureBranch}</span>
|
|
154
256
|
</>
|
|
155
257
|
)}
|
|
156
258
|
</div>
|
|
157
|
-
{workspace?.id && <WorkspaceCommandButton workspaceId={workspace.id} diffStats={diffStats} onDiffStatsRefresh={onDiffStatsRefresh} onShowDiff={onShowDiff} />}
|
|
259
|
+
{workspace?.id && <WorkspaceCommandButton workspaceId={workspace.id} diffStats={diffStats} onDiffStatsRefresh={onDiffStatsRefresh} onShowDiff={onShowDiff} chatMode={chatMode} />}
|
|
158
260
|
</div>
|
|
159
261
|
);
|
|
160
262
|
}
|
|
@@ -243,16 +345,33 @@ export function CommandOutputDialog({ title, logs, exitCode, running, onClose })
|
|
|
243
345
|
}
|
|
244
346
|
|
|
245
347
|
const STORAGE_KEY = 'thepopebot-workspace-command';
|
|
348
|
+
const FALLBACK_BY_MODE = { agent: 'push', code: 'create-pr' };
|
|
246
349
|
|
|
247
|
-
function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, onShowDiff }) {
|
|
350
|
+
function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, onShowDiff, chatMode = 'agent' }) {
|
|
351
|
+
const storageKey = `${STORAGE_KEY}:${chatMode}`;
|
|
248
352
|
const [selectedCommand, setSelectedCommandState] = useState(() => {
|
|
249
|
-
try { return localStorage.getItem(
|
|
250
|
-
|
|
353
|
+
try { return localStorage.getItem(storageKey) || FALLBACK_BY_MODE[chatMode] || 'create-pr'; }
|
|
354
|
+
catch { return FALLBACK_BY_MODE[chatMode] || 'create-pr'; }
|
|
251
355
|
});
|
|
252
356
|
const setSelectedCommand = (cmd) => {
|
|
253
357
|
setSelectedCommandState(cmd);
|
|
254
|
-
try { localStorage.setItem(
|
|
358
|
+
try { localStorage.setItem(storageKey, cmd); } catch {}
|
|
255
359
|
};
|
|
360
|
+
|
|
361
|
+
// If user hasn't picked anything for this mode yet, seed from admin default.
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
let stored = null;
|
|
364
|
+
try { stored = localStorage.getItem(storageKey); } catch {}
|
|
365
|
+
if (stored) return;
|
|
366
|
+
let cancelled = false;
|
|
367
|
+
import('../actions.js').then(({ getModeGitActionDefault }) => {
|
|
368
|
+
getModeGitActionDefault(chatMode).then((val) => {
|
|
369
|
+
if (cancelled || !val) return;
|
|
370
|
+
setSelectedCommandState(val);
|
|
371
|
+
}).catch(() => {});
|
|
372
|
+
}).catch(() => {});
|
|
373
|
+
return () => { cancelled = true; };
|
|
374
|
+
}, [chatMode, storageKey]);
|
|
256
375
|
const [commandRunning, setCommandRunning] = useState(false);
|
|
257
376
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
258
377
|
const [commandLogs, setCommandLogs] = useState([]);
|
|
@@ -269,16 +388,6 @@ function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, on
|
|
|
269
388
|
const handleRun = useCallback(async () => {
|
|
270
389
|
if (commandRunning) return;
|
|
271
390
|
|
|
272
|
-
// Refresh diff stats and check for changes before running
|
|
273
|
-
const fresh = await onDiffStatsRefresh?.();
|
|
274
|
-
const stats = fresh || diffStats;
|
|
275
|
-
if (!(stats?.insertions || 0) && !(stats?.deletions || 0)) {
|
|
276
|
-
setDialogOpen(true);
|
|
277
|
-
setCommandLogs([{ stream: 'stderr', raw: 'You have no changes.', parsed: [{ type: 'text', text: 'You have no changes.' }] }]);
|
|
278
|
-
setCommandExitCode(1);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
391
|
setCommandRunning(true);
|
|
283
392
|
setDialogOpen(true);
|
|
284
393
|
setCommandLogs([]);
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { PageLayout } from "./page-layout.js";
|
|
6
|
-
import { SpinnerIcon, RefreshIcon, StopIcon, PlayIcon, TrashIcon, XIcon } from "./icons.js";
|
|
6
|
+
import { SpinnerIcon, RefreshIcon, StopIcon, PlayIcon, TrashIcon, XIcon, FileTextIcon } from "./icons.js";
|
|
7
7
|
import { ConfirmDialog } from "./ui/confirm-dialog.js";
|
|
8
8
|
import { CodeLogView } from "./code-log-view.js";
|
|
9
9
|
import {
|
|
@@ -195,81 +195,99 @@ function ContainerRow({ container, onRequestStop, onShowLogs, isStopping, isStar
|
|
|
195
195
|
/* @__PURE__ */ jsx("td", { className: "py-2.5 pr-3 text-xs text-right hidden md:table-cell whitespace-nowrap", children: isRunning && container.stats ? /* @__PURE__ */ jsx("span", { children: formatBytes(container.stats.memUsage) }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }),
|
|
196
196
|
/* @__PURE__ */ jsx("td", { className: "py-2.5 pr-3 text-xs text-muted-foreground hidden lg:table-cell whitespace-nowrap", children: container.status }),
|
|
197
197
|
/* @__PURE__ */ jsx("td", { className: "py-2.5 text-right whitespace-nowrap", children: /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-1.5", children: [
|
|
198
|
-
|
|
198
|
+
/* @__PURE__ */ jsx(
|
|
199
|
+
"button",
|
|
200
|
+
{
|
|
201
|
+
onClick: () => onShowLogs(container.name),
|
|
202
|
+
title: "Logs",
|
|
203
|
+
"aria-label": "Logs",
|
|
204
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
205
|
+
children: /* @__PURE__ */ jsx(FileTextIcon, { size: 14 })
|
|
206
|
+
}
|
|
207
|
+
),
|
|
208
|
+
isRunning && (isStopping ? /* @__PURE__ */ jsx(
|
|
199
209
|
"button",
|
|
200
210
|
{
|
|
201
211
|
disabled: true,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
]
|
|
212
|
+
title: "Stopping...",
|
|
213
|
+
"aria-label": "Stopping",
|
|
214
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
|
|
215
|
+
children: /* @__PURE__ */ jsx(SpinnerIcon, { size: 14 })
|
|
207
216
|
}
|
|
208
|
-
) : /* @__PURE__ */
|
|
217
|
+
) : /* @__PURE__ */ jsx(
|
|
209
218
|
"button",
|
|
210
219
|
{
|
|
211
220
|
onClick: () => onRequestStop(container.name),
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
]
|
|
221
|
+
title: "Stop",
|
|
222
|
+
"aria-label": "Stop",
|
|
223
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
224
|
+
children: /* @__PURE__ */ jsx(StopIcon, { size: 14 })
|
|
217
225
|
}
|
|
218
226
|
)),
|
|
219
|
-
isStopped && (isStarting ? /* @__PURE__ */
|
|
227
|
+
isStopped && (isStarting ? /* @__PURE__ */ jsx(
|
|
220
228
|
"button",
|
|
221
229
|
{
|
|
222
230
|
disabled: true,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
]
|
|
231
|
+
title: "Starting...",
|
|
232
|
+
"aria-label": "Starting",
|
|
233
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
|
|
234
|
+
children: /* @__PURE__ */ jsx(SpinnerIcon, { size: 14 })
|
|
228
235
|
}
|
|
229
|
-
) : /* @__PURE__ */
|
|
236
|
+
) : /* @__PURE__ */ jsx(
|
|
230
237
|
"button",
|
|
231
238
|
{
|
|
232
239
|
onClick: () => onRequestStop(container.name, "start"),
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
]
|
|
240
|
+
title: "Start",
|
|
241
|
+
"aria-label": "Start",
|
|
242
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
243
|
+
children: /* @__PURE__ */ jsx(PlayIcon, { size: 14 })
|
|
238
244
|
}
|
|
239
245
|
)),
|
|
240
246
|
/* @__PURE__ */ jsx(
|
|
241
|
-
"button",
|
|
242
|
-
{
|
|
243
|
-
onClick: () => onShowLogs(container.name),
|
|
244
|
-
className: "inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-xs font-medium border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
245
|
-
children: "Logs"
|
|
246
|
-
}
|
|
247
|
-
),
|
|
248
|
-
/* @__PURE__ */ jsxs(
|
|
249
247
|
"button",
|
|
250
248
|
{
|
|
251
249
|
onClick: () => handleAction("remove"),
|
|
252
250
|
disabled: removingContainer,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
]
|
|
251
|
+
title: confirmingRemove ? "Confirm remove" : "Remove",
|
|
252
|
+
"aria-label": confirmingRemove ? "Confirm remove" : "Remove",
|
|
253
|
+
className: `inline-flex items-center justify-center rounded-md p-1.5 border transition-colors disabled:opacity-50 disabled:pointer-events-none ${confirmingRemove ? "border-destructive text-destructive hover:bg-destructive/10" : "border-border text-muted-foreground hover:bg-accent hover:text-foreground"}`,
|
|
254
|
+
children: removingContainer ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 14 }) : /* @__PURE__ */ jsx(TrashIcon, { size: 14 })
|
|
258
255
|
}
|
|
259
256
|
)
|
|
260
257
|
] }) })
|
|
261
258
|
] });
|
|
262
259
|
}
|
|
263
260
|
function DockerContainersSection({ containers, loading, onRequestStop, onShowLogs, pendingStop, pendingStart }) {
|
|
261
|
+
const [activeTab, setActiveTab] = useState("running");
|
|
264
262
|
if (loading) {
|
|
265
263
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
266
264
|
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Docker Containers" }),
|
|
267
265
|
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i)) })
|
|
268
266
|
] });
|
|
269
267
|
}
|
|
268
|
+
const runningContainers = containers.filter((c) => c.state === "running");
|
|
269
|
+
const exitedContainers = containers.filter((c) => c.state !== "running");
|
|
270
|
+
const visibleContainers = activeTab === "running" ? runningContainers : exitedContainers;
|
|
271
|
+
const tabs = [
|
|
272
|
+
{ id: "running", label: `Running (${runningContainers.length})` },
|
|
273
|
+
{ id: "exited", label: `Exited (${exitedContainers.length})` }
|
|
274
|
+
];
|
|
270
275
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
271
276
|
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Docker Containers" }),
|
|
272
|
-
|
|
277
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1.5 overflow-x-auto scrollbar-hide max-w-full", children: tabs.map((tab) => {
|
|
278
|
+
const isActive = activeTab === tab.id;
|
|
279
|
+
return /* @__PURE__ */ jsx(
|
|
280
|
+
"button",
|
|
281
|
+
{
|
|
282
|
+
type: "button",
|
|
283
|
+
onClick: () => setActiveTab(tab.id),
|
|
284
|
+
className: `rounded-full px-3 py-1.5 min-h-[36px] inline-flex items-center text-xs font-medium transition-colors shrink-0 whitespace-nowrap ${isActive ? "bg-foreground text-background" : "text-muted-foreground hover:text-foreground hover:bg-accent"}`,
|
|
285
|
+
children: tab.label
|
|
286
|
+
},
|
|
287
|
+
tab.id
|
|
288
|
+
);
|
|
289
|
+
}) }),
|
|
290
|
+
visibleContainers.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-dashed border-border p-8 text-center text-sm text-muted-foreground", children: activeTab === "running" ? "No running containers." : "No exited containers." }) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-left", children: [
|
|
273
291
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-xs text-muted-foreground", children: [
|
|
274
292
|
/* @__PURE__ */ jsx("th", { className: "pb-2 pr-3 font-medium", children: "State" }),
|
|
275
293
|
/* @__PURE__ */ jsx("th", { className: "pb-2 pr-3 font-medium", children: "Container" }),
|
|
@@ -278,7 +296,7 @@ function DockerContainersSection({ containers, loading, onRequestStop, onShowLog
|
|
|
278
296
|
/* @__PURE__ */ jsx("th", { className: "pb-2 pr-3 font-medium hidden lg:table-cell", children: "Status" }),
|
|
279
297
|
/* @__PURE__ */ jsx("th", { className: "pb-2 font-medium text-right", children: "Actions" })
|
|
280
298
|
] }) }),
|
|
281
|
-
/* @__PURE__ */ jsx("tbody", { children:
|
|
299
|
+
/* @__PURE__ */ jsx("tbody", { children: visibleContainers.map((c) => /* @__PURE__ */ jsx(
|
|
282
300
|
ContainerRow,
|
|
283
301
|
{
|
|
284
302
|
container: c,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { PageLayout } from './page-layout.js';
|
|
6
|
-
import { SpinnerIcon, RefreshIcon, StopIcon, PlayIcon, TrashIcon, XIcon } from './icons.js';
|
|
6
|
+
import { SpinnerIcon, RefreshIcon, StopIcon, PlayIcon, TrashIcon, XIcon, FileTextIcon } from './icons.js';
|
|
7
7
|
import { ConfirmDialog } from './ui/confirm-dialog.js';
|
|
8
8
|
import { CodeLogView } from './code-log-view.js';
|
|
9
9
|
import {
|
|
@@ -263,22 +263,32 @@ function ContainerRow({ container, onRequestStop, onShowLogs, isStopping, isStar
|
|
|
263
263
|
</td>
|
|
264
264
|
<td className="py-2.5 text-right whitespace-nowrap">
|
|
265
265
|
<div className="inline-flex items-center gap-1.5">
|
|
266
|
+
<button
|
|
267
|
+
onClick={() => onShowLogs(container.name)}
|
|
268
|
+
title="Logs"
|
|
269
|
+
aria-label="Logs"
|
|
270
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
271
|
+
>
|
|
272
|
+
<FileTextIcon size={14} />
|
|
273
|
+
</button>
|
|
266
274
|
{isRunning && (
|
|
267
275
|
isStopping ? (
|
|
268
276
|
<button
|
|
269
277
|
disabled
|
|
270
|
-
|
|
278
|
+
title="Stopping..."
|
|
279
|
+
aria-label="Stopping"
|
|
280
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors"
|
|
271
281
|
>
|
|
272
|
-
<SpinnerIcon size={
|
|
273
|
-
Stopping...
|
|
282
|
+
<SpinnerIcon size={14} />
|
|
274
283
|
</button>
|
|
275
284
|
) : (
|
|
276
285
|
<button
|
|
277
286
|
onClick={() => onRequestStop(container.name)}
|
|
278
|
-
|
|
287
|
+
title="Stop"
|
|
288
|
+
aria-label="Stop"
|
|
289
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
279
290
|
>
|
|
280
|
-
<StopIcon size={
|
|
281
|
-
Stop
|
|
291
|
+
<StopIcon size={14} />
|
|
282
292
|
</button>
|
|
283
293
|
)
|
|
284
294
|
)}
|
|
@@ -286,38 +296,35 @@ function ContainerRow({ container, onRequestStop, onShowLogs, isStopping, isStar
|
|
|
286
296
|
isStarting ? (
|
|
287
297
|
<button
|
|
288
298
|
disabled
|
|
289
|
-
|
|
299
|
+
title="Starting..."
|
|
300
|
+
aria-label="Starting"
|
|
301
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors"
|
|
290
302
|
>
|
|
291
|
-
<SpinnerIcon size={
|
|
292
|
-
Starting...
|
|
303
|
+
<SpinnerIcon size={14} />
|
|
293
304
|
</button>
|
|
294
305
|
) : (
|
|
295
306
|
<button
|
|
296
307
|
onClick={() => onRequestStop(container.name, 'start')}
|
|
297
|
-
|
|
308
|
+
title="Start"
|
|
309
|
+
aria-label="Start"
|
|
310
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
298
311
|
>
|
|
299
|
-
<PlayIcon size={
|
|
300
|
-
Start
|
|
312
|
+
<PlayIcon size={14} />
|
|
301
313
|
</button>
|
|
302
314
|
)
|
|
303
315
|
)}
|
|
304
|
-
<button
|
|
305
|
-
onClick={() => onShowLogs(container.name)}
|
|
306
|
-
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-xs font-medium border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
307
|
-
>
|
|
308
|
-
Logs
|
|
309
|
-
</button>
|
|
310
316
|
<button
|
|
311
317
|
onClick={() => handleAction('remove')}
|
|
312
318
|
disabled={removingContainer}
|
|
313
|
-
|
|
319
|
+
title={confirmingRemove ? 'Confirm remove' : 'Remove'}
|
|
320
|
+
aria-label={confirmingRemove ? 'Confirm remove' : 'Remove'}
|
|
321
|
+
className={`inline-flex items-center justify-center rounded-md p-1.5 border transition-colors disabled:opacity-50 disabled:pointer-events-none ${
|
|
314
322
|
confirmingRemove
|
|
315
323
|
? 'border-destructive text-destructive hover:bg-destructive/10'
|
|
316
324
|
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
317
325
|
}`}
|
|
318
326
|
>
|
|
319
|
-
{removingContainer ? <SpinnerIcon size={
|
|
320
|
-
{confirmingRemove ? 'Confirm' : 'Remove'}
|
|
327
|
+
{removingContainer ? <SpinnerIcon size={14} /> : <TrashIcon size={14} />}
|
|
321
328
|
</button>
|
|
322
329
|
</div>
|
|
323
330
|
</td>
|
|
@@ -330,6 +337,8 @@ function ContainerRow({ container, onRequestStop, onShowLogs, isStopping, isStar
|
|
|
330
337
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
331
338
|
|
|
332
339
|
function DockerContainersSection({ containers, loading, onRequestStop, onShowLogs, pendingStop, pendingStart }) {
|
|
340
|
+
const [activeTab, setActiveTab] = useState('running');
|
|
341
|
+
|
|
333
342
|
if (loading) {
|
|
334
343
|
return (
|
|
335
344
|
<div className="space-y-4">
|
|
@@ -343,12 +352,42 @@ function DockerContainersSection({ containers, loading, onRequestStop, onShowLog
|
|
|
343
352
|
);
|
|
344
353
|
}
|
|
345
354
|
|
|
355
|
+
const runningContainers = containers.filter((c) => c.state === 'running');
|
|
356
|
+
const exitedContainers = containers.filter((c) => c.state !== 'running');
|
|
357
|
+
const visibleContainers = activeTab === 'running' ? runningContainers : exitedContainers;
|
|
358
|
+
|
|
359
|
+
const tabs = [
|
|
360
|
+
{ id: 'running', label: `Running (${runningContainers.length})` },
|
|
361
|
+
{ id: 'exited', label: `Exited (${exitedContainers.length})` },
|
|
362
|
+
];
|
|
363
|
+
|
|
346
364
|
return (
|
|
347
365
|
<div className="space-y-4">
|
|
348
366
|
<h2 className="text-base font-medium">Docker Containers</h2>
|
|
349
|
-
|
|
367
|
+
|
|
368
|
+
<div className="flex gap-1.5 overflow-x-auto scrollbar-hide max-w-full">
|
|
369
|
+
{tabs.map((tab) => {
|
|
370
|
+
const isActive = activeTab === tab.id;
|
|
371
|
+
return (
|
|
372
|
+
<button
|
|
373
|
+
key={tab.id}
|
|
374
|
+
type="button"
|
|
375
|
+
onClick={() => setActiveTab(tab.id)}
|
|
376
|
+
className={`rounded-full px-3 py-1.5 min-h-[36px] inline-flex items-center text-xs font-medium transition-colors shrink-0 whitespace-nowrap ${
|
|
377
|
+
isActive
|
|
378
|
+
? 'bg-foreground text-background'
|
|
379
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
380
|
+
}`}
|
|
381
|
+
>
|
|
382
|
+
{tab.label}
|
|
383
|
+
</button>
|
|
384
|
+
);
|
|
385
|
+
})}
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{visibleContainers.length === 0 ? (
|
|
350
389
|
<div className="rounded-lg border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
|
|
351
|
-
No containers
|
|
390
|
+
{activeTab === 'running' ? 'No running containers.' : 'No exited containers.'}
|
|
352
391
|
</div>
|
|
353
392
|
) : (
|
|
354
393
|
<div className="overflow-x-auto">
|
|
@@ -364,7 +403,7 @@ function DockerContainersSection({ containers, loading, onRequestStop, onShowLog
|
|
|
364
403
|
</tr>
|
|
365
404
|
</thead>
|
|
366
405
|
<tbody>
|
|
367
|
-
{
|
|
406
|
+
{visibleContainers.map((c) => (
|
|
368
407
|
<ContainerRow
|
|
369
408
|
key={c.id}
|
|
370
409
|
container={c}
|
|
@@ -100,9 +100,19 @@ function CronCard({ cron }) {
|
|
|
100
100
|
type === "agent" && cron.job && /* @__PURE__ */ jsxs("div", { children: [
|
|
101
101
|
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "Job prompt" }),
|
|
102
102
|
/* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: cron.job }),
|
|
103
|
-
(cron.
|
|
104
|
-
/* @__PURE__ */
|
|
105
|
-
|
|
103
|
+
(cron.agent_backend || cron.llm_model || cron.scope) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2 flex-wrap", children: [
|
|
104
|
+
cron.agent_backend && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
105
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Agent:" }),
|
|
106
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium", children: cron.agent_backend })
|
|
107
|
+
] }),
|
|
108
|
+
cron.llm_model && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
109
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Model:" }),
|
|
110
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium", children: cron.llm_model })
|
|
111
|
+
] }),
|
|
112
|
+
cron.scope && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
113
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Scope:" }),
|
|
114
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-mono", children: cron.scope })
|
|
115
|
+
] })
|
|
106
116
|
] })
|
|
107
117
|
] }),
|
|
108
118
|
type === "command" && cron.command && /* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -120,6 +130,10 @@ function CronCard({ cron }) {
|
|
|
120
130
|
cron.vars && Object.keys(cron.vars).length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
121
131
|
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "Variables" }),
|
|
122
132
|
/* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: JSON.stringify(cron.vars, null, 2) })
|
|
133
|
+
] }),
|
|
134
|
+
cron.headers && Object.keys(cron.headers).length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
135
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "Headers" }),
|
|
136
|
+
/* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: JSON.stringify(cron.headers, null, 2) })
|
|
123
137
|
] })
|
|
124
138
|
] })
|
|
125
139
|
] })
|