thepopebot 1.2.76-beta.6 → 1.2.76-beta.8

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/bin/cli.js CHANGED
@@ -329,13 +329,6 @@ async function init() {
329
329
  console.log(' Created .kimi/skills → ../skills/active');
330
330
  }
331
331
 
332
- // Create .agents/skills → ../skills/active symlink
333
- const agentsSkillsLink = path.join(cwd, '.agents', 'skills');
334
- if (!fs.existsSync(agentsSkillsLink)) {
335
- fs.mkdirSync(path.dirname(agentsSkillsLink), { recursive: true });
336
- createDirLink('../skills/active', agentsSkillsLink);
337
- console.log(' Created .agents/skills → ../skills/active');
338
- }
339
332
 
340
333
  // Report backed-up files
341
334
  if (backedUp.length > 0) {
package/lib/ai/index.js CHANGED
@@ -182,10 +182,10 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
182
182
  }
183
183
 
184
184
  try {
185
- await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
185
+ const setupOutput = await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
186
186
  ensureSkills(repoDir, isCodeMode ? 'code' : 'agent');
187
187
  if (needsSetup) {
188
- const result = `Workspace ready on ${featureBranch || branch}`;
188
+ const result = setupOutput || `Workspace ready on ${featureBranch || branch}`;
189
189
  yield { type: 'tool-result', toolCallId: setupToolCallId, result };
190
190
  persistMessage(threadId, 'assistant', JSON.stringify({
191
191
  type: 'tool-invocation',
@@ -221,6 +221,8 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
221
221
  sessionId,
222
222
  permissionMode: codeModeType,
223
223
  attachments,
224
+ workspaceId,
225
+ chatMode: isCodeMode ? 'code' : 'agent',
224
226
  })) {
225
227
  // Write session ID on first meta chunk
226
228
  if (chunk.type === 'meta' && chunk.sessionId) {
@@ -3,6 +3,8 @@ import fs from 'fs';
3
3
  import { query } from '@anthropic-ai/claude-agent-sdk';
4
4
  import { getConfig } from '../../config.js';
5
5
  import { buildAgentAuthEnv } from '../../tools/docker.js';
6
+ import { createAgentJobApiKey } from '../../db/api-keys.js';
7
+ import { getAllAgentJobSecrets } from '../../db/config.js';
6
8
 
7
9
  /**
8
10
  * Claude Agent SDK adapter. Wraps the SDK's query() and yields
@@ -62,7 +64,7 @@ function ensureSessionSymlink(wsBaseDir, workspaceDir) {
62
64
  } catch {}
63
65
  }
64
66
 
65
- export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, sessionId, permissionMode, attachments }) {
67
+ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, sessionId, permissionMode, attachments, workspaceId, chatMode }) {
66
68
  // Point HOME at the workspace volume so the SDK stores session data on the
67
69
  // shared volume (not the EH container's ephemeral filesystem).
68
70
  const wsBaseDir = path.dirname(workspaceDir);
@@ -94,6 +96,23 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
94
96
  // Fall through — env may already have the right vars from process.env
95
97
  }
96
98
 
99
+ // Inject agent job secrets when in agent chat mode
100
+ if (chatMode === 'agent') {
101
+ const shortId = (workspaceId || '').replace(/-/g, '').slice(0, 8);
102
+ const { key: agentJobToken } = createAgentJobApiKey(`claude-code-sdk-${shortId}`);
103
+ env.AGENT_JOB_TOKEN = agentJobToken;
104
+ const appUrl = getConfig('APP_URL');
105
+ if (appUrl) env.APP_URL = appUrl;
106
+
107
+ // Inject plain secrets as env vars (oauth types are null — agent fetches via skill)
108
+ const jobSecrets = getAllAgentJobSecrets();
109
+ for (const { key, value } of jobSecrets) {
110
+ if (value !== null && !env[key]) {
111
+ env[key] = value;
112
+ }
113
+ }
114
+ }
115
+
97
116
  const options = {
98
117
  cwd: workspaceDir,
99
118
  env,
@@ -134,6 +153,7 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
134
153
 
135
154
  // Track tool call state for mapping stream events
136
155
  const activeToolCalls = new Map(); // index → { id, name, argsJson }
156
+ const toolNamesById = new Map(); // toolCallId → toolName (persists for tool-result lookup)
137
157
  const activeThinkingBlocks = new Set(); // indices of active thinking blocks
138
158
 
139
159
  try {
@@ -157,6 +177,7 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
157
177
  const block = event.content_block;
158
178
  if (block.type === 'tool_use') {
159
179
  activeToolCalls.set(event.index, { id: block.id, name: block.name, argsJson: '' });
180
+ toolNamesById.set(block.id, block.name);
160
181
  yield { type: 'tool-call', toolCallId: block.id, toolName: block.name, args: {} };
161
182
  } else if (block.type === 'thinking') {
162
183
  activeThinkingBlocks.add(event.index);
@@ -208,14 +229,23 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
208
229
  : Array.isArray(block.content)
209
230
  ? block.content.map(b => b.type === 'text' ? b.text : JSON.stringify(b)).join('\n')
210
231
  : JSON.stringify(block.content);
211
- yield { type: 'tool-result', toolCallId: block.tool_use_id, result: content };
232
+ yield { type: 'tool-result', toolCallId: block.tool_use_id, toolName: toolNamesById.get(block.tool_use_id), result: content };
212
233
  }
213
234
  }
214
235
  continue;
215
236
  }
216
237
 
217
238
  // ── assistant messages — redundant with streaming, skip ──
218
- if (message.type === 'assistant') continue;
239
+ // But extract tool names so resumed tool-results can carry them.
240
+ if (message.type === 'assistant') {
241
+ const blocks = message.message?.content || [];
242
+ for (const block of blocks) {
243
+ if (block.type === 'tool_use' && block.id && block.name) {
244
+ toolNamesById.set(block.id, block.name);
245
+ }
246
+ }
247
+ continue;
248
+ }
219
249
 
220
250
  // ── result ──
221
251
  if (message.type === 'result') {
@@ -32,20 +32,24 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
32
32
  if (ghToken) env.GH_TOKEN = ghToken;
33
33
 
34
34
  const execOpts = { cwd: workspaceDir, env };
35
+ const log = [];
35
36
 
36
37
  // 1. Create workspace directory
37
38
  mkdirSync(workspaceDir, { recursive: true });
38
39
 
39
40
  // 2. Configure git to use GH_TOKEN for GitHub HTTPS URLs (mirrors setup-git.sh)
40
41
  if (ghToken) {
41
- await run('gh', ['auth', 'setup-git'], execOpts);
42
+ const out = await run('gh', ['auth', 'setup-git'], execOpts);
43
+ if (out) log.push(out);
42
44
  }
43
45
 
44
46
  // 3. Clone if not already a git repo
45
47
  const hasGit = existsSync(path.join(workspaceDir, '.git'));
46
48
  if (!hasGit) {
47
49
  if (!repo) throw new Error('ensureWorkspaceRepo: repo is required for initial clone');
48
- await run('git', ['clone', '--branch', branch || 'main', `https://github.com/${repo}`, '.'], execOpts);
50
+ const out = await run('git', ['clone', '--branch', branch || 'main', `https://github.com/${repo}`, '.'], execOpts);
51
+ log.push(`Cloned ${repo} (branch: ${branch || 'main'})`);
52
+ if (out) log.push(out);
49
53
  }
50
54
 
51
55
  // 3. Git identity (only if not already configured)
@@ -61,6 +65,7 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
61
65
  const email = user.email || `${user.id}+${user.login}@users.noreply.github.com`;
62
66
  await run('git', ['config', 'user.name', name], execOpts);
63
67
  await run('git', ['config', 'user.email', email], execOpts);
68
+ log.push(`Git identity: ${name} <${email}>`);
64
69
  } catch (err) {
65
70
  console.error('[workspace-setup] Failed to set git identity:', err.message);
66
71
  }
@@ -68,7 +73,7 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
68
73
  }
69
74
 
70
75
  // 4. Feature branch checkout
71
- if (!featureBranch) return;
76
+ if (!featureBranch) return log.join('\n');
72
77
 
73
78
  // Already on the right branch locally?
74
79
  try {
@@ -77,8 +82,11 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
77
82
  const current = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], execOpts);
78
83
  if (current !== featureBranch) {
79
84
  await run('git', ['checkout', featureBranch], execOpts);
85
+ log.push(`Checked out ${featureBranch}`);
86
+ } else {
87
+ log.push(`Already on ${featureBranch}`);
80
88
  }
81
- return;
89
+ return log.join('\n');
82
90
  } catch {
83
91
  // Branch doesn't exist locally — check remote
84
92
  }
@@ -88,15 +96,20 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
88
96
  if (remoteCheck) {
89
97
  // Remote branch exists — checkout tracking it
90
98
  await run('git', ['checkout', '-B', featureBranch, `origin/${featureBranch}`], execOpts);
99
+ log.push(`Checked out ${featureBranch} (tracking origin)`);
91
100
  } else {
92
101
  // Create new branch and push
93
102
  await run('git', ['checkout', '-b', featureBranch], execOpts);
94
- await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
103
+ const pushOut = await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
104
+ log.push(`Created and pushed ${featureBranch}`);
105
+ if (pushOut) log.push(pushOut);
95
106
  }
96
107
  } catch (err) {
97
108
  console.error('[workspace-setup] Feature branch error:', err.message);
98
109
  throw err;
99
110
  }
111
+
112
+ return log.join('\n');
100
113
  }
101
114
 
102
115
  /**
@@ -146,6 +146,7 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
146
146
  if (e) e.preventDefault();
147
147
  if (disabled || !input.trim() && !partialText.trim() && files.length === 0 || isStreaming) return;
148
148
  if (canSendOverride !== void 0 && !canSendOverride) return;
149
+ if (isRecording) stopRecording();
149
150
  if (partialText) {
150
151
  const needsSpace = input && !input.endsWith(" ");
151
152
  setInput(input + (needsSpace ? " " : "") + partialText);
@@ -339,12 +340,12 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
339
340
  onClick: () => codeModeSettings.onInteractiveToggle(),
340
341
  onContextMenu: (e) => {
341
342
  e.preventDefault();
342
- if (!codeModeSettings.togglingMode && codeModeSettings.availableAgents?.length > 1) {
343
+ if (!codeModeSettings.togglingMode && codeModeSettings.availableAgents?.length > 1 && codeModeSettings.hasMessages) {
343
344
  setAgentPickerOpen((prev) => !prev);
344
345
  }
345
346
  },
346
347
  disabled: codeModeSettings.togglingMode || codeModeSettings.isInteractiveActive,
347
- title: codeModeSettings.availableAgents?.length > 1 ? "Left-click to launch \xB7 Right-click to pick agent" : void 0,
348
+ title: codeModeSettings.availableAgents?.length > 1 && codeModeSettings.hasMessages ? "Left-click to launch \xB7 Right-click to pick agent" : void 0,
348
349
  className: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors",
349
350
  children: [
350
351
  codeModeSettings.togglingMode && /* @__PURE__ */ jsxs("svg", { className: "animate-spin h-3 w-3", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [
@@ -140,6 +140,7 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
140
140
  if (e) e.preventDefault();
141
141
  if (disabled || (!input.trim() && !partialText.trim() && files.length === 0) || isStreaming) return;
142
142
  if (canSendOverride !== undefined && !canSendOverride) return;
143
+ if (isRecording) stopRecording();
143
144
  if (partialText) {
144
145
  const needsSpace = input && !input.endsWith(' ');
145
146
  setInput(input + (needsSpace ? ' ' : '') + partialText);
@@ -352,12 +353,12 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
352
353
  onClick={() => codeModeSettings.onInteractiveToggle()}
353
354
  onContextMenu={(e) => {
354
355
  e.preventDefault();
355
- if (!codeModeSettings.togglingMode && codeModeSettings.availableAgents?.length > 1) {
356
+ if (!codeModeSettings.togglingMode && codeModeSettings.availableAgents?.length > 1 && codeModeSettings.hasMessages) {
356
357
  setAgentPickerOpen(prev => !prev);
357
358
  }
358
359
  }}
359
360
  disabled={codeModeSettings.togglingMode || codeModeSettings.isInteractiveActive}
360
- title={codeModeSettings.availableAgents?.length > 1 ? 'Left-click to launch · Right-click to pick agent' : undefined}
361
+ title={codeModeSettings.availableAgents?.length > 1 && codeModeSettings.hasMessages ? 'Left-click to launch · Right-click to pick agent' : undefined}
361
362
  className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
362
363
  >
363
364
  {codeModeSettings.togglingMode && (
@@ -204,7 +204,8 @@ function Chat({ chatId, initialMessages = [], workspace = null, chatMode = null
204
204
  isInteractiveActive,
205
205
  onInteractiveToggle: handleInteractiveToggle,
206
206
  togglingMode,
207
- availableAgents
207
+ availableAgents,
208
+ hasMessages: messages.length > 0
208
209
  };
209
210
  const handleBranchChange = useCallback((newBranch) => {
210
211
  setBranch(newBranch);
@@ -255,6 +255,7 @@ export function Chat({ chatId, initialMessages = [], workspace = null, chatMode
255
255
  onInteractiveToggle: handleInteractiveToggle,
256
256
  togglingMode,
257
257
  availableAgents,
258
+ hasMessages: messages.length > 0,
258
259
  };
259
260
 
260
261
  const handleBranchChange = useCallback((newBranch) => {
@@ -200,7 +200,7 @@ function WorkspaceBar({
200
200
  repoName && /* @__PURE__ */ jsx("span", { className: "shrink-0 cursor-default hidden md:inline", title: repo, children: repoName }),
201
201
  branch && /* @__PURE__ */ jsxs(Fragment, { children: [
202
202
  /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/30 hidden md:inline", children: "/" }),
203
- /* @__PURE__ */ jsx("div", { className: "shrink-0 max-w-[120px]", children: /* @__PURE__ */ jsx(
203
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 max-w-[120px] md:max-w-[160px]", children: /* @__PURE__ */ jsx(
204
204
  Combobox,
205
205
  {
206
206
  options: branches.map((b) => ({ value: b.name, label: b.name })),
@@ -327,14 +327,6 @@ function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, on
327
327
  }, []);
328
328
  const handleRun = useCallback(async () => {
329
329
  if (commandRunning) return;
330
- const fresh = await onDiffStatsRefresh?.();
331
- const stats = fresh || diffStats;
332
- if (!(stats?.insertions || 0) && !(stats?.deletions || 0)) {
333
- setDialogOpen(true);
334
- setCommandLogs([{ stream: "stderr", raw: "You have no changes.", parsed: [{ type: "text", text: "You have no changes." }] }]);
335
- setCommandExitCode(1);
336
- return;
337
- }
338
330
  setCommandRunning(true);
339
331
  setDialogOpen(true);
340
332
  setCommandLogs([]);
@@ -225,7 +225,7 @@ export function WorkspaceBar({
225
225
  {branch && (
226
226
  <>
227
227
  <span className="shrink-0 text-muted-foreground/30 hidden md:inline">/</span>
228
- <div className="shrink-0 max-w-[120px]">
228
+ <div className="min-w-0 max-w-[120px] md:max-w-[160px]">
229
229
  <Combobox
230
230
  options={branches.map((b) => ({ value: b.name, label: b.name }))}
231
231
  value={branch}
@@ -370,16 +370,6 @@ function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, on
370
370
  const handleRun = useCallback(async () => {
371
371
  if (commandRunning) return;
372
372
 
373
- // Refresh diff stats and check for changes before running
374
- const fresh = await onDiffStatsRefresh?.();
375
- const stats = fresh || diffStats;
376
- if (!(stats?.insertions || 0) && !(stats?.deletions || 0)) {
377
- setDialogOpen(true);
378
- setCommandLogs([{ stream: 'stderr', raw: 'You have no changes.', parsed: [{ type: 'text', text: 'You have no changes.' }] }]);
379
- setCommandExitCode(1);
380
- return;
381
- }
382
-
383
373
  setCommandRunning(true);
384
374
  setDialogOpen(true);
385
375
  setCommandLogs([]);
@@ -83,32 +83,6 @@ function formatContent(content) {
83
83
  }
84
84
  return JSON.stringify(content, null, 2);
85
85
  }
86
- function ThinkingBlock({ part }) {
87
- const [expanded, setExpanded] = useState(false);
88
- const isStreaming = part.state === "input-streaming" || part.state === "input-available";
89
- const content = typeof part.input === "string" ? part.input : "";
90
- return /* @__PURE__ */ jsxs("div", { className: "my-1", children: [
91
- /* @__PURE__ */ jsxs(
92
- "button",
93
- {
94
- onClick: () => setExpanded(!expanded),
95
- className: "flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors py-0.5",
96
- children: [
97
- /* @__PURE__ */ jsx(
98
- ChevronDownIcon,
99
- {
100
- size: 12,
101
- className: cn("transition-transform shrink-0", !expanded && "-rotate-90")
102
- }
103
- ),
104
- isStreaming && /* @__PURE__ */ jsx(SpinnerIcon, { size: 12, className: "shrink-0" }),
105
- isStreaming ? /* @__PURE__ */ jsx("span", { className: "thinking-shimmer", children: "Thinking..." }) : /* @__PURE__ */ jsx("span", { children: "Thoughts" })
106
- ]
107
- }
108
- ),
109
- expanded && /* @__PURE__ */ jsx("div", { className: "mt-1.5 ml-4 pl-3 border-l border-border/50 text-xs text-muted-foreground/80 whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto", children: content || /* @__PURE__ */ jsx("span", { className: "italic opacity-50", children: "..." }) })
110
- ] });
111
- }
112
86
  function ToolCall({ part, className }) {
113
87
  const [expanded, setExpanded] = useState(false);
114
88
  const toolName = part.toolName || (part.type?.startsWith("tool-") ? part.type.slice(5) : "tool");
@@ -260,7 +234,7 @@ function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
260
234
  return;
261
235
  }
262
236
  setShowWorking(false);
263
- const timer = setTimeout(() => setShowWorking(true), 250);
237
+ const timer = setTimeout(() => setShowWorking(true), 500);
264
238
  return () => clearTimeout(timer);
265
239
  }, [isLoading, partsLength, textLength, hasRunningTool]);
266
240
  const fileParts = message.parts?.filter((p) => p.type === "file") || [];
@@ -399,9 +373,6 @@ function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
399
373
  ] }, i);
400
374
  }
401
375
  if (part.type?.startsWith("tool-")) {
402
- if (part.toolName === "__thinking__") {
403
- return /* @__PURE__ */ jsx(ThinkingBlock, { part }, part.toolCallId || i);
404
- }
405
376
  const prevPart = message.parts[i - 1];
406
377
  const afterText = prevPart?.type === "text";
407
378
  return /* @__PURE__ */ jsx(ToolCall, { part, className: afterText ? "mt-3" : void 0 }, part.toolCallId || i);
@@ -77,37 +77,6 @@ function formatContent(content) {
77
77
  return JSON.stringify(content, null, 2);
78
78
  }
79
79
 
80
- function ThinkingBlock({ part }) {
81
- const [expanded, setExpanded] = useState(false);
82
- const isStreaming = part.state === 'input-streaming' || part.state === 'input-available';
83
- const content = typeof part.input === 'string' ? part.input : '';
84
-
85
- return (
86
- <div className="my-1">
87
- <button
88
- onClick={() => setExpanded(!expanded)}
89
- className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors py-0.5"
90
- >
91
- <ChevronDownIcon
92
- size={12}
93
- className={cn('transition-transform shrink-0', !expanded && '-rotate-90')}
94
- />
95
- {isStreaming && <SpinnerIcon size={12} className="shrink-0" />}
96
- {isStreaming ? (
97
- <span className="thinking-shimmer">Thinking...</span>
98
- ) : (
99
- <span>Thoughts</span>
100
- )}
101
- </button>
102
- {expanded && (
103
- <div className="mt-1.5 ml-4 pl-3 border-l border-border/50 text-xs text-muted-foreground/80 whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto">
104
- {content || <span className="italic opacity-50">...</span>}
105
- </div>
106
- )}
107
- </div>
108
- );
109
- }
110
-
111
80
  function ToolCall({ part, className }) {
112
81
  const [expanded, setExpanded] = useState(false);
113
82
 
@@ -310,7 +279,7 @@ export function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
310
279
  return;
311
280
  }
312
281
  setShowWorking(false);
313
- const timer = setTimeout(() => setShowWorking(true), 250);
282
+ const timer = setTimeout(() => setShowWorking(true), 500);
314
283
  return () => clearTimeout(timer);
315
284
  }, [isLoading, partsLength, textLength, hasRunningTool]);
316
285
 
@@ -482,9 +451,6 @@ export function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
482
451
  );
483
452
  }
484
453
  if (part.type?.startsWith('tool-')) {
485
- if (part.toolName === '__thinking__') {
486
- return <ThinkingBlock key={part.toolCallId || i} part={part} />;
487
- }
488
454
  const prevPart = message.parts[i - 1];
489
455
  const afterText = prevPart?.type === 'text';
490
456
  return <ToolCall key={part.toolCallId || i} part={part} className={afterText ? 'mt-3' : undefined} />;
@@ -812,14 +812,6 @@ function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh,
812
812
  }, [dropupOpen]);
813
813
  const handleRun = useCallback(async () => {
814
814
  if (commandRunning) return;
815
- const fresh = await onDiffStatsRefresh?.();
816
- const stats = fresh || diffStats;
817
- if (!(stats?.insertions || 0) && !(stats?.deletions || 0)) {
818
- setDialogOpen(true);
819
- setCommandOutput("You have no changes.");
820
- setCommandExitCode(1);
821
- return;
822
- }
823
815
  setCommandRunning(true);
824
816
  setDialogOpen(true);
825
817
  setCommandOutput("");
@@ -872,15 +872,6 @@ function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh,
872
872
  const handleRun = useCallback(async () => {
873
873
  if (commandRunning) return;
874
874
 
875
- const fresh = await onDiffStatsRefresh?.();
876
- const stats = fresh || diffStats;
877
- if (!(stats?.insertions || 0) && !(stats?.deletions || 0)) {
878
- setDialogOpen(true);
879
- setCommandOutput('You have no changes.');
880
- setCommandExitCode(1);
881
- return;
882
- }
883
-
884
875
  setCommandRunning(true);
885
876
  setDialogOpen(true);
886
877
  setCommandOutput('');
@@ -4,44 +4,54 @@ import { getDb } from './db/index.js';
4
4
  import { settings } from './db/schema.js';
5
5
 
6
6
  const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
7
+ const ONE_HOUR = 60 * 60 * 1000;
7
8
 
8
9
  async function cleanExpiredAgentJobKeys() {
9
10
  try {
10
11
  const db = getDb();
11
- const cutoff = Date.now() - TWENTY_FOUR_HOURS;
12
12
  const rows = db
13
13
  .select({ id: settings.id, key: settings.key, lastUsedAt: settings.lastUsedAt, createdAt: settings.createdAt })
14
14
  .from(settings)
15
15
  .where(eq(settings.type, 'agent_job_api_key'))
16
16
  .all();
17
17
 
18
- // Filter to candidates not used in the last 24 hours
19
- const candidates = rows.filter(r =>
20
- (r.lastUsedAt !== null ? r.lastUsedAt : r.createdAt) < cutoff
21
- );
18
+ // Split into SDK keys (no container) vs container-backed keys
19
+ const sdkRows = rows.filter(r => r.key.includes('sdk'));
20
+ const containerRows = rows.filter(r => !r.key.includes('sdk'));
22
21
 
23
- if (candidates.length === 0) {
24
- console.log(`[maintenance] No expired agent job keys (${rows.length} active)`);
25
- return;
26
- }
22
+ let deleted = 0;
27
23
 
28
- // Check if the container still exists for each candidate
29
- const { inspectContainer } = await import('./tools/docker.js');
30
- const expiredIds = [];
31
- for (const r of candidates) {
32
- const info = await inspectContainer(r.key);
33
- if (!info) {
34
- expiredIds.push(r.id);
24
+ // SDK keys: delete any older than 1 hour (no container to inspect)
25
+ const sdkCutoff = Date.now() - ONE_HOUR;
26
+ for (const r of sdkRows) {
27
+ const age = r.lastUsedAt !== null ? r.lastUsedAt : r.createdAt;
28
+ if (age < sdkCutoff) {
29
+ db.delete(settings).where(eq(settings.id, r.id)).run();
30
+ deleted++;
35
31
  }
36
32
  }
37
33
 
38
- if (expiredIds.length > 0) {
39
- for (const id of expiredIds) {
40
- db.delete(settings).where(eq(settings.id, id)).run();
34
+ // Container-backed keys: existing logic — 24h expiry + container inspect
35
+ const containerCutoff = Date.now() - TWENTY_FOUR_HOURS;
36
+ const candidates = containerRows.filter(r =>
37
+ (r.lastUsedAt !== null ? r.lastUsedAt : r.createdAt) < containerCutoff
38
+ );
39
+
40
+ if (candidates.length > 0) {
41
+ const { inspectContainer } = await import('./tools/docker.js');
42
+ for (const r of candidates) {
43
+ const info = await inspectContainer(r.key);
44
+ if (!info) {
45
+ db.delete(settings).where(eq(settings.id, r.id)).run();
46
+ deleted++;
47
+ }
41
48
  }
42
- console.log(`[maintenance] Deleted ${expiredIds.length} orphaned agent job key(s)`);
49
+ }
50
+
51
+ if (deleted > 0) {
52
+ console.log(`[maintenance] Deleted ${deleted} expired agent job key(s)`);
43
53
  } else {
44
- console.log(`[maintenance] ${candidates.length} candidate(s) checked, all containers still running`);
54
+ console.log(`[maintenance] No expired agent job keys (${rows.length} active)`);
45
55
  }
46
56
  } catch (err) {
47
57
  console.error('[maintenance] cleanExpiredAgentJobKeys failed:', err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.76-beta.6",
3
+ "version": "1.2.76-beta.8",
4
4
  "type": "module",
5
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
6
6
  "bin": {