thepopebot 1.2.75-beta.2 → 1.2.75-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.
Files changed (120) hide show
  1. package/README.md +1 -1
  2. package/api/CLAUDE.md +1 -1
  3. package/api/index.js +5 -12
  4. package/bin/CLAUDE.md +1 -1
  5. package/bin/cli.js +329 -14
  6. package/bin/docker-build.js +5 -0
  7. package/bin/managed-paths.js +0 -7
  8. package/bin/sync.js +84 -0
  9. package/config/CLAUDE.md +1 -29
  10. package/config/instrumentation.js +1 -1
  11. package/lib/CLAUDE.md +3 -3
  12. package/lib/ai/CLAUDE.md +24 -3
  13. package/lib/ai/agent.js +8 -5
  14. package/lib/ai/async-channel.js +51 -0
  15. package/lib/ai/headless-stream.js +3 -0
  16. package/lib/ai/index.js +149 -173
  17. package/lib/ai/line-mappers.js +72 -9
  18. package/lib/ai/tools.js +40 -28
  19. package/lib/chat/actions.js +34 -6
  20. package/lib/chat/api.js +17 -1
  21. package/lib/chat/components/chat-header.js +4 -0
  22. package/lib/chat/components/chat-header.jsx +4 -0
  23. package/lib/chat/components/chat-input.js +1 -0
  24. package/lib/chat/components/chat-input.jsx +1 -0
  25. package/lib/chat/components/chat.js +9 -1
  26. package/lib/chat/components/chat.jsx +15 -2
  27. package/lib/chat/components/chats-page.js +3 -3
  28. package/lib/chat/components/chats-page.jsx +4 -6
  29. package/lib/chat/components/crons-page.js +1 -1
  30. package/lib/chat/components/crons-page.jsx +1 -1
  31. package/lib/chat/components/message.js +12 -4
  32. package/lib/chat/components/message.jsx +17 -4
  33. package/lib/chat/components/settings-chat-page.js +2 -1
  34. package/lib/chat/components/settings-chat-page.jsx +4 -1
  35. package/lib/chat/components/settings-coding-agents-page.js +139 -1
  36. package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
  37. package/lib/chat/components/settings-jobs-page.js +13 -2
  38. package/lib/chat/components/settings-jobs-page.jsx +15 -1
  39. package/lib/chat/components/settings-secrets-layout.js +1 -1
  40. package/lib/chat/components/settings-secrets-layout.jsx +1 -1
  41. package/lib/chat/components/sidebar-history-item.js +3 -3
  42. package/lib/chat/components/sidebar-history-item.jsx +4 -6
  43. package/lib/chat/components/triggers-page.js +1 -1
  44. package/lib/chat/components/triggers-page.jsx +1 -1
  45. package/lib/cluster/actions.js +4 -4
  46. package/lib/cluster/execute.js +3 -1
  47. package/lib/code/actions.js +34 -11
  48. package/lib/code/code-page.js +40 -40
  49. package/lib/code/code-page.jsx +36 -36
  50. package/lib/code/port-forwards.js +17 -3
  51. package/lib/code/terminal-view.js +16 -0
  52. package/lib/code/terminal-view.jsx +18 -0
  53. package/lib/config.js +4 -0
  54. package/lib/cron.js +3 -3
  55. package/lib/db/api-keys.js +22 -61
  56. package/lib/db/config.js +23 -0
  57. package/lib/db/index.js +3 -1
  58. package/lib/maintenance.js +34 -11
  59. package/lib/paths.js +1 -38
  60. package/lib/tools/create-agent-job.js +0 -4
  61. package/lib/tools/docker.js +23 -16
  62. package/lib/triggers.js +4 -3
  63. package/lib/utils/render-md.js +3 -1
  64. package/package.json +2 -1
  65. package/setup/setup-ssl.mjs +414 -0
  66. package/templates/.github/workflows/rebuild-event-handler.yml +3 -0
  67. package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
  68. package/templates/.gitignore.template +7 -3
  69. package/templates/.tmp/CLAUDE.md.template +5 -0
  70. package/templates/CLAUDE.md +3 -2
  71. package/templates/CLAUDE.md.template +24 -357
  72. package/templates/agent-job/CLAUDE.md.template +57 -0
  73. package/templates/agent-job/CRONS.json +16 -0
  74. package/templates/{config/agent-job → agent-job}/SOUL.md +3 -3
  75. package/templates/agent-job/SYSTEM.md +60 -0
  76. package/templates/agents/CLAUDE.md.template +54 -0
  77. package/templates/data/CLAUDE.md.template +5 -0
  78. package/templates/docker-compose.custom.yml +41 -62
  79. package/templates/docker-compose.yml +14 -21
  80. package/templates/event-handler/CLAUDE.md.template +0 -0
  81. package/templates/logs/CLAUDE.md.template +5 -0
  82. package/templates/skills/CLAUDE.md.template +57 -32
  83. package/templates/skills/active/.gitkeep +0 -0
  84. package/templates/skills/library/agent-job-secrets/SKILL.md +23 -0
  85. package/templates/skills/library/agent-job-secrets/agent-job-secrets.js +62 -0
  86. package/templates/.pi/extensions/env-sanitizer/index.ts +0 -48
  87. package/templates/.pi/extensions/env-sanitizer/package.json +0 -5
  88. package/templates/README.md +0 -75
  89. package/templates/config/CLAUDE.md.template +0 -40
  90. package/templates/config/CRONS.json +0 -56
  91. package/templates/config/agent-job/AGENT_JOB.md +0 -30
  92. package/templates/cron/CLAUDE.md.template +0 -24
  93. package/templates/docker-compose.litellm.yml +0 -82
  94. package/templates/docs/CLAUDE.md.template +0 -12
  95. package/templates/docs/CLI.md +0 -59
  96. package/templates/docs/CLUSTERS.md +0 -151
  97. package/templates/docs/CONFIGURATION.md +0 -181
  98. package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
  99. package/templates/docs/GETTING_STARTED.md +0 -64
  100. package/templates/docs/SECURITY.md +0 -61
  101. package/templates/docs/SKILLS.md +0 -113
  102. package/templates/docs/UPGRADING.md +0 -92
  103. package/templates/skills/LICENSE +0 -21
  104. package/templates/skills/README.md +0 -117
  105. package/templates/skills/agent-job-secrets/SKILL.md +0 -25
  106. package/templates/skills/agent-job-secrets/agent-job-secrets.js +0 -66
  107. package/templates/traefik-dynamic.yml.example +0 -7
  108. package/templates/triggers/CLAUDE.md.template +0 -41
  109. /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
  110. /package/templates/{cron → data}/.gitkeep +0 -0
  111. /package/templates/{logs → data/clusters}/.gitkeep +0 -0
  112. /package/templates/{triggers → data/db}/.gitkeep +0 -0
  113. /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
  114. /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
  115. /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
  116. /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
  117. /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
  118. /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
  119. /package/templates/{config → event-handler}/litellm/main.yaml +0 -0
  120. /package/templates/skills/{playwright-cli → library/playwright-cli}/SKILL.md +0 -0
@@ -107,7 +107,7 @@ export function mapClaudeCodeLine(parsed) {
107
107
  // User messages without tool_result (e.g. subagent prompts) — skip
108
108
  if (events.length === 0) return [{ type: 'skip' }];
109
109
  } else if (type === 'result' && result) {
110
- events.push({ type: 'text', text: result, _resultSummary: result });
110
+ events.push({ type: 'text', text: result });
111
111
  }
112
112
 
113
113
  return events;
@@ -176,7 +176,7 @@ export function mapPiLine(parsed) {
176
176
  .map(b => b.text)
177
177
  .join('');
178
178
  if (text) {
179
- events.push({ type: 'text', text, _resultSummary: text });
179
+ events.push({ type: 'text', text });
180
180
  }
181
181
  }
182
182
  }
@@ -233,7 +233,7 @@ export function mapGeminiLine(parsed) {
233
233
  const stats = parsed.stats;
234
234
  if (stats) {
235
235
  const summary = `Completed (${stats.total_tokens || 0} tokens, ${stats.tool_calls || 0} tool calls, ${((stats.duration_ms || 0) / 1000).toFixed(1)}s)`;
236
- events.push({ type: 'text', text: summary, _resultSummary: summary });
236
+ events.push({ type: 'text', text: summary });
237
237
  }
238
238
  return events.length ? events : [{ type: 'skip' }];
239
239
  } else if (type === 'error') {
@@ -245,6 +245,74 @@ export function mapGeminiLine(parsed) {
245
245
  return events;
246
246
  }
247
247
 
248
+ // ─────────────────────────────────────────────────────────────────────────────
249
+ // Kimi CLI: OpenAI-style function calling JSON
250
+ //
251
+ // Event shapes (from real output):
252
+ // role: "assistant", content: [], tool_calls: [{ type: "function", id, function: { name, arguments } }]
253
+ // role: "assistant", content: "text..." (with or without tool_calls)
254
+ // role: "tool", content: "...", tool_call_id: "..."
255
+ // ─────────────────────────────────────────────────────────────────────────────
256
+
257
+ /**
258
+ * Map a Kimi CLI line to chat events.
259
+ * @param {object} parsed - Parsed JSON object
260
+ * @returns {Array<object>}
261
+ */
262
+ export function mapKimiLine(parsed) {
263
+ const events = [];
264
+ const { role, content, tool_calls, tool_call_id } = parsed;
265
+
266
+ if (role === 'assistant') {
267
+ // Text content — can be a string or an array of blocks
268
+ if (typeof content === 'string' && content) {
269
+ events.push({ type: 'text', text: content });
270
+ } else if (Array.isArray(content)) {
271
+ for (const block of content) {
272
+ if (typeof block === 'string' && block) {
273
+ events.push({ type: 'text', text: block });
274
+ } else if (block?.type === 'text' && block.text) {
275
+ events.push({ type: 'text', text: block.text });
276
+ }
277
+ }
278
+ }
279
+
280
+ // Tool calls — OpenAI function calling format
281
+ if (Array.isArray(tool_calls)) {
282
+ for (const tc of tool_calls) {
283
+ if (tc.type === 'function' && tc.function) {
284
+ let args = {};
285
+ try {
286
+ args = typeof tc.function.arguments === 'string'
287
+ ? JSON.parse(tc.function.arguments)
288
+ : tc.function.arguments || {};
289
+ } catch { /* leave as empty object */ }
290
+ events.push({
291
+ type: 'tool-call',
292
+ toolCallId: tc.id || '',
293
+ toolName: tc.function.name || '',
294
+ args,
295
+ });
296
+ }
297
+ }
298
+ }
299
+
300
+ // Assistant message with only empty content and no tool calls — skip
301
+ if (events.length === 0) return [{ type: 'skip' }];
302
+ } else if (role === 'tool') {
303
+ const resultText = typeof content === 'string' ? content :
304
+ Array.isArray(content) ? content.map(b => (typeof b === 'string' ? b : b?.text || '')).join('') :
305
+ JSON.stringify(content);
306
+ events.push({
307
+ type: 'tool-result',
308
+ toolCallId: tool_call_id || '',
309
+ result: resultText,
310
+ });
311
+ }
312
+
313
+ return events;
314
+ }
315
+
248
316
  // ─────────────────────────────────────────────────────────────────────────────
249
317
  // Codex CLI: --json
250
318
  //
@@ -312,7 +380,7 @@ export function mapCodexLine(parsed) {
312
380
  const usage = parsed.usage;
313
381
  if (usage) {
314
382
  const summary = `Completed (${usage.input_tokens || 0} input, ${usage.output_tokens || 0} output tokens)`;
315
- events.push({ type: 'text', text: summary, _resultSummary: summary });
383
+ events.push({ type: 'text', text: summary });
316
384
  }
317
385
  return events.length ? events : [{ type: 'skip' }];
318
386
  } else if (type === 'turn.failed') {
@@ -350,11 +418,6 @@ export function mapOpenCodeLine(parsed) {
350
418
  // Text output — part.text contains the assistant's response
351
419
  if (type === 'text' && part?.text) {
352
420
  events.push({ type: 'text', text: part.text });
353
- // If step_finish follows with reason "stop", this is the final answer
354
- // We mark it as result summary so it gets captured in LangGraph memory
355
- if (part.text.length > 50) {
356
- events[events.length - 1]._resultSummary = part.text;
357
- }
358
421
  }
359
422
 
360
423
  // Tool use — OpenCode emits a single event with completed state (input + output)
package/lib/ai/tools.js CHANGED
@@ -54,7 +54,7 @@ const agentChatCodingTool = tool(
54
54
  const codingAgent = getConfig('CODING_AGENT') || 'claude-code';
55
55
  const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
56
56
 
57
- const { runHeadlessContainer } = await import('../tools/docker.js');
57
+ const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
58
58
  const { backendApi } = await runHeadlessContainer({
59
59
  containerName,
60
60
  repo,
@@ -66,20 +66,27 @@ const agentChatCodingTool = tool(
66
66
  injectSecrets: true,
67
67
  });
68
68
 
69
- return JSON.stringify({
70
- success: true,
71
- status: 'started',
72
- containerName,
73
- featureBranch,
74
- codingAgent,
75
- backendApi,
76
- });
69
+ const chunks = [{ type: 'meta', codingAgent, backendApi }];
70
+ const streamCallback = runtime.configurable.streamCallback;
71
+ const { parseHeadlessStream } = await import('./headless-stream.js');
72
+
73
+ const logStream = await tailContainerLogs(containerName);
74
+
75
+ for await (const chunk of parseHeadlessStream(logStream, codingAgent)) {
76
+ chunks.push(chunk);
77
+ streamCallback?.(chunk);
78
+ }
79
+
80
+ const exitCode = await waitForContainer(containerName);
81
+ await removeContainer(containerName);
82
+ streamCallback?.(null);
83
+
84
+ chunks.push({ type: 'exit', exitCode });
85
+ return JSON.stringify(chunks);
77
86
  } catch (err) {
78
87
  console.error('[coding_agent] Failed:', err);
79
- return JSON.stringify({
80
- success: false,
81
- error: err.message || 'Failed to launch investigation container',
82
- });
88
+ runtime.configurable.streamCallback?.(null);
89
+ return JSON.stringify([{ type: 'error', message: err.message }]);
83
90
  }
84
91
  },
85
92
  {
@@ -91,7 +98,6 @@ const agentChatCodingTool = tool(
91
98
  'A direct copy of the coding task including all relevant context from the conversation.'
92
99
  ),
93
100
  }),
94
- returnDirect: true,
95
101
  }
96
102
  );
97
103
 
@@ -110,7 +116,7 @@ const codeChatCodingTool = tool(
110
116
  const featureBranch = workspace?.featureBranch;
111
117
  const mode = codeModeType === 'code' ? 'dangerous' : 'plan';
112
118
 
113
- const { runHeadlessContainer } = await import('../tools/docker.js');
119
+ const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
114
120
  const codingAgent = getConfig('CODING_AGENT') || 'claude-code';
115
121
  const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
116
122
 
@@ -120,20 +126,27 @@ const codeChatCodingTool = tool(
120
126
  mode,
121
127
  });
122
128
 
123
- return JSON.stringify({
124
- success: true,
125
- status: 'started',
126
- containerName,
127
- featureBranch,
128
- codingAgent,
129
- backendApi,
130
- });
129
+ const chunks = [{ type: 'meta', codingAgent, backendApi }];
130
+ const streamCallback = runtime.configurable.streamCallback;
131
+ const { parseHeadlessStream } = await import('./headless-stream.js');
132
+
133
+ const logStream = await tailContainerLogs(containerName);
134
+
135
+ for await (const chunk of parseHeadlessStream(logStream, codingAgent)) {
136
+ chunks.push(chunk);
137
+ streamCallback?.(chunk);
138
+ }
139
+
140
+ const exitCode = await waitForContainer(containerName);
141
+ await removeContainer(containerName);
142
+ streamCallback?.(null);
143
+
144
+ chunks.push({ type: 'exit', exitCode });
145
+ return JSON.stringify(chunks);
131
146
  } catch (err) {
132
147
  console.error('[coding_agent] Failed:', err);
133
- return JSON.stringify({
134
- success: false,
135
- error: err.message || 'Failed to launch headless coding task',
136
- });
148
+ runtime.configurable.streamCallback?.(null);
149
+ return JSON.stringify([{ type: 'error', message: err.message }]);
137
150
  }
138
151
  },
139
152
  {
@@ -145,7 +158,6 @@ const codeChatCodingTool = tool(
145
158
  'A direct copy of the coding task including all relevant context from the conversation.'
146
159
  ),
147
160
  }),
148
- returnDirect: true,
149
161
  }
150
162
  );
151
163
 
@@ -556,12 +556,13 @@ export async function getRunnersStatus(page = 1) {
556
556
  */
557
557
  export async function getRunnersConfig() {
558
558
  await requireAuth();
559
- const { cronsFile, triggersFile } = await import('../paths.js');
559
+ const { PROJECT_ROOT } = await import('../paths.js');
560
560
  const fs = await import('fs');
561
+ const path = await import('path');
561
562
  let crons = [];
562
563
  let triggers = [];
563
- try { crons = JSON.parse(fs.readFileSync(cronsFile, 'utf8')); } catch {}
564
- try { triggers = JSON.parse(fs.readFileSync(triggersFile, 'utf8')); } catch {}
564
+ try { crons = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'agent-job/CRONS.json'), 'utf8')); } catch {}
565
+ try { triggers = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'event-handler/TRIGGERS.json'), 'utf8')); } catch {}
565
566
  return { crons, triggers };
566
567
  }
567
568
 
@@ -653,6 +654,9 @@ export async function getCodingAgentSettings() {
653
654
  const openCodeEnabled = getConfig('CODING_AGENT_OPENCODE_ENABLED');
654
655
  const openCodeProvider = getConfig('CODING_AGENT_OPENCODE_PROVIDER') || '';
655
656
  const openCodeModel = getConfig('CODING_AGENT_OPENCODE_MODEL') || '';
657
+ const kimiCliEnabled = getConfig('CODING_AGENT_KIMI_CLI_ENABLED');
658
+ const kimiCliProvider = getConfig('CODING_AGENT_KIMI_CLI_PROVIDER') || '';
659
+ const kimiCliModel = getConfig('CODING_AGENT_KIMI_CLI_MODEL') || '';
656
660
 
657
661
  // Credential readiness
658
662
  const oauthTokenCount = getOAuthTokenCount('claudeCode');
@@ -694,6 +698,11 @@ export async function getCodingAgentSettings() {
694
698
  provider: openCodeProvider,
695
699
  model: openCodeModel,
696
700
  },
701
+ kimiCli: {
702
+ enabled: kimiCliEnabled === 'true',
703
+ provider: kimiCliProvider,
704
+ model: kimiCliModel,
705
+ },
697
706
  builtinProviders: BUILTIN_PROVIDERS,
698
707
  credentialStatuses,
699
708
  customProviders,
@@ -735,6 +744,10 @@ export async function updateCodingAgentConfig(agent, config) {
735
744
  if (config.enabled !== undefined) setConfigValue('CODING_AGENT_OPENCODE_ENABLED', String(config.enabled));
736
745
  if (config.provider !== undefined) setConfigValue('CODING_AGENT_OPENCODE_PROVIDER', config.provider);
737
746
  if (config.model !== undefined) setConfigValue('CODING_AGENT_OPENCODE_MODEL', config.model);
747
+ } else if (agent === 'kimi-cli') {
748
+ if (config.enabled !== undefined) setConfigValue('CODING_AGENT_KIMI_CLI_ENABLED', String(config.enabled));
749
+ if (config.provider !== undefined) setConfigValue('CODING_AGENT_KIMI_CLI_PROVIDER', config.provider);
750
+ if (config.model !== undefined) setConfigValue('CODING_AGENT_KIMI_CLI_MODEL', config.model);
738
751
  } else {
739
752
  return { error: 'Invalid agent' };
740
753
  }
@@ -973,8 +986,8 @@ async function syncLitellmConfig() {
973
986
  try {
974
987
  const fs = await import('fs');
975
988
  const path = await import('path');
976
- const { configDir } = await import('../paths.js');
977
- const litellmDir = path.default.join(configDir, 'litellm');
989
+ const { PROJECT_ROOT } = await import('../paths.js');
990
+ const litellmDir = path.default.join(PROJECT_ROOT, 'event-handler/litellm');
978
991
  if (!fs.default.existsSync(litellmDir)) return;
979
992
 
980
993
  const { getConfig } = await import('../config.js');
@@ -1258,7 +1271,7 @@ export async function initiateOAuthFlow({ secretName, clientId, clientSecret, to
1258
1271
  try {
1259
1272
  const { createOAuthState } = await import('../oauth/helper.js');
1260
1273
  const redirectUri = `${process.env.AUTH_URL}/api/oauth/callback`;
1261
- const state = createOAuthState({ secretName, clientId, clientSecret, tokenUrl, secretType: secretType || 'agent_job_secret', returnPath: returnPath || '/admin/event-handler/agent-jobs' });
1274
+ const state = createOAuthState({ secretName, clientId, clientSecret, tokenUrl, secretType: secretType || 'agent_job_secret', returnPath: returnPath || '/admin/event-handler/agent-secrets' });
1262
1275
  return { state, redirectUri };
1263
1276
  } catch (err) {
1264
1277
  console.error('Failed to initiate OAuth flow:', err);
@@ -1266,6 +1279,21 @@ export async function initiateOAuthFlow({ secretName, clientId, clientSecret, to
1266
1279
  }
1267
1280
  }
1268
1281
 
1282
+ /**
1283
+ * Get stored OAuth credentials for an agent job secret (for re-authorization pre-fill).
1284
+ * @param {string} name
1285
+ */
1286
+ export async function getOAuthSecretCredentials(name) {
1287
+ await requireAuth();
1288
+ try {
1289
+ const { getAgentJobSecretOAuthCredentials } = await import('../db/config.js');
1290
+ return getAgentJobSecretOAuthCredentials(name) || { error: 'Not an OAuth secret' };
1291
+ } catch (err) {
1292
+ console.error('Failed to get OAuth credentials:', err);
1293
+ return { error: 'Failed to get credentials' };
1294
+ }
1295
+ }
1296
+
1269
1297
  /**
1270
1298
  * Delete an agent job secret.
1271
1299
  * @param {string} name
package/lib/chat/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { auth } from '../auth/index.js';
2
2
  import { chatStream } from '../ai/index.js';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
+ import { getConfig } from '../config.js';
4
5
 
5
6
  /**
6
7
  * POST handler for /stream/chat — streaming chat with session auth.
@@ -114,11 +115,26 @@ export async function POST(request) {
114
115
  toolCallId: chunk.toolCallId,
115
116
  toolName: chunk.toolName,
116
117
  });
118
+ // Enrich coding_agent input with active agent identity from config
119
+ let input = chunk.args;
120
+ if (chunk.toolName === 'coding_agent') {
121
+ const agent = getConfig('CODING_AGENT') || 'claude-code';
122
+ const providerKeys = {
123
+ 'claude-code': 'CODING_AGENT_CLAUDE_CODE_BACKEND',
124
+ 'pi-coding-agent': 'CODING_AGENT_PI_PROVIDER',
125
+ 'gemini-cli': 'CODING_AGENT_GEMINI_CLI_PROVIDER',
126
+ 'codex-cli': 'CODING_AGENT_CODEX_CLI_PROVIDER',
127
+ 'opencode': 'CODING_AGENT_OPENCODE_PROVIDER',
128
+ 'kimi-cli': 'CODING_AGENT_KIMI_CLI_PROVIDER',
129
+ };
130
+ const backendApi = getConfig(providerKeys[agent]) || 'anthropic';
131
+ input = { ...chunk.args, codingAgent: agent, backendApi };
132
+ }
117
133
  writer.write({
118
134
  type: 'tool-input-available',
119
135
  toolCallId: chunk.toolCallId,
120
136
  toolName: chunk.toolName,
121
- input: chunk.args,
137
+ input,
122
138
  });
123
139
 
124
140
  } else if (chunk.type === 'tool-result') {
@@ -30,6 +30,7 @@ function ChatHeader({ chatId: chatIdProp, workspaceId }) {
30
30
  setStarred(data.starred || 0);
31
31
  setResolvedChatId(data.chatId);
32
32
  if (data.chatMode) setChatMode(data.chatMode);
33
+ document.title = data.title;
33
34
  }
34
35
  }).catch(() => {
35
36
  });
@@ -41,6 +42,7 @@ function ChatHeader({ chatId: chatIdProp, workspaceId }) {
41
42
  setTitle(data.title);
42
43
  setStarred(data.starred || 0);
43
44
  if (data.chatMode) setChatMode(data.chatMode);
45
+ document.title = data.title;
44
46
  }
45
47
  }).catch(() => {
46
48
  });
@@ -51,6 +53,7 @@ function ChatHeader({ chatId: chatIdProp, workspaceId }) {
51
53
  if (e.detail.chatId === chatId) {
52
54
  setTitle(e.detail.title);
53
55
  if (e.detail.chatMode) setChatMode(e.detail.chatMode);
56
+ document.title = e.detail.title;
54
57
  }
55
58
  };
56
59
  const starHandler = (e) => {
@@ -63,6 +66,7 @@ function ChatHeader({ chatId: chatIdProp, workspaceId }) {
63
66
  return () => {
64
67
  window.removeEventListener("chatTitleUpdated", titleHandler);
65
68
  window.removeEventListener("chatStarUpdated", starHandler);
69
+ document.title = "ThePopeBot";
66
70
  };
67
71
  }, [fetchMeta, chatId]);
68
72
  useEffect(() => {
@@ -38,6 +38,7 @@ export function ChatHeader({ chatId: chatIdProp, workspaceId }) {
38
38
  setStarred(data.starred || 0);
39
39
  setResolvedChatId(data.chatId);
40
40
  if (data.chatMode) setChatMode(data.chatMode);
41
+ document.title = data.title;
41
42
  }
42
43
  })
43
44
  .catch(() => {});
@@ -51,6 +52,7 @@ export function ChatHeader({ chatId: chatIdProp, workspaceId }) {
51
52
  setTitle(data.title);
52
53
  setStarred(data.starred || 0);
53
54
  if (data.chatMode) setChatMode(data.chatMode);
55
+ document.title = data.title;
54
56
  }
55
57
  })
56
58
  .catch(() => {});
@@ -62,6 +64,7 @@ export function ChatHeader({ chatId: chatIdProp, workspaceId }) {
62
64
  if (e.detail.chatId === chatId) {
63
65
  setTitle(e.detail.title);
64
66
  if (e.detail.chatMode) setChatMode(e.detail.chatMode);
67
+ document.title = e.detail.title;
65
68
  }
66
69
  };
67
70
  const starHandler = (e) => {
@@ -74,6 +77,7 @@ export function ChatHeader({ chatId: chatIdProp, workspaceId }) {
74
77
  return () => {
75
78
  window.removeEventListener('chatTitleUpdated', titleHandler);
76
79
  window.removeEventListener('chatStarUpdated', starHandler);
80
+ document.title = 'ThePopeBot';
77
81
  };
78
82
  }, [fetchMeta, chatId]);
79
83
 
@@ -88,6 +88,7 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
88
88
  if (!textarea) return;
89
89
  textarea.style.height = "auto";
90
90
  textarea.style.height = `${textarea.scrollHeight}px`;
91
+ textarea.scrollTop = textarea.scrollHeight;
91
92
  }, []);
92
93
  useEffect(() => {
93
94
  adjustHeight();
@@ -70,6 +70,7 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
70
70
  if (!textarea) return;
71
71
  textarea.style.height = 'auto';
72
72
  textarea.style.height = `${textarea.scrollHeight}px`;
73
+ textarea.scrollTop = textarea.scrollHeight;
73
74
  }, []);
74
75
 
75
76
  useEffect(() => {
@@ -94,7 +94,12 @@ function Chat({ chatId, initialMessages = [], workspace = null, chatMode = null
94
94
  const isFinished = prevStatus.current !== "ready" && status === "ready";
95
95
  if (isMount || isFinished) {
96
96
  fetch(`/code/workspace-diff/${workspaceState.id}`).then((r) => r.json()).then((r) => {
97
- if (r.success) setDiffStats(r);
97
+ if (r.success) {
98
+ setDiffStats(r);
99
+ if (r.currentBranch) {
100
+ setWorkspaceState((prev) => prev && r.currentBranch !== prev.featureBranch ? { ...prev, featureBranch: r.currentBranch } : prev);
101
+ }
102
+ }
98
103
  }).catch(() => {
99
104
  });
100
105
  }
@@ -199,6 +204,9 @@ function Chat({ chatId, initialMessages = [], workspace = null, chatMode = null
199
204
  const data = await r.json();
200
205
  if (data.success) {
201
206
  setDiffStats(data);
207
+ if (data.currentBranch) {
208
+ setWorkspaceState((prev) => prev && data.currentBranch !== prev.featureBranch ? { ...prev, featureBranch: data.currentBranch } : prev);
209
+ }
202
210
  return data;
203
211
  }
204
212
  } catch {
@@ -118,7 +118,14 @@ export function Chat({ chatId, initialMessages = [], workspace = null, chatMode
118
118
  if (isMount || isFinished) {
119
119
  fetch(`/code/workspace-diff/${workspaceState.id}`)
120
120
  .then(r => r.json())
121
- .then(r => { if (r.success) setDiffStats(r); })
121
+ .then(r => {
122
+ if (r.success) {
123
+ setDiffStats(r);
124
+ if (r.currentBranch) {
125
+ setWorkspaceState(prev => prev && r.currentBranch !== prev.featureBranch ? { ...prev, featureBranch: r.currentBranch } : prev);
126
+ }
127
+ }
128
+ })
122
129
  .catch(() => {});
123
130
  }
124
131
  prevStatus.current = status;
@@ -244,7 +251,13 @@ export function Chat({ chatId, initialMessages = [], workspace = null, chatMode
244
251
  try {
245
252
  const r = await fetch(`/code/workspace-diff/${workspaceState.id}`);
246
253
  const data = await r.json();
247
- if (data.success) { setDiffStats(data); return data; }
254
+ if (data.success) {
255
+ setDiffStats(data);
256
+ if (data.currentBranch) {
257
+ setWorkspaceState(prev => prev && data.currentBranch !== prev.featureBranch ? { ...prev, featureBranch: data.currentBranch } : prev);
258
+ }
259
+ return data;
260
+ }
248
261
  } catch {}
249
262
  return null;
250
263
  }, [workspaceState?.id]);
@@ -263,10 +263,10 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
263
263
  }
264
264
  },
265
265
  children: [
266
- chat.chatMode === "code" ? /* @__PURE__ */ jsxs("span", { className: "relative", children: [
267
- /* @__PURE__ */ jsx(CodeIcon, { size: 16 }),
266
+ /* @__PURE__ */ jsxs("span", { className: "relative", children: [
267
+ chat.chatMode === "code" ? /* @__PURE__ */ jsx(CodeIcon, { size: 16 }) : /* @__PURE__ */ jsx(AgentIcon, { size: 16 }),
268
268
  chat.hasChanges ? /* @__PURE__ */ jsx("span", { className: "absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" }) : null
269
- ] }) : /* @__PURE__ */ jsx(AgentIcon, { size: 16 }),
269
+ ] }),
270
270
  /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
271
271
  editing ? /* @__PURE__ */ jsx(
272
272
  "input",
@@ -312,12 +312,10 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
312
312
  }
313
313
  }}
314
314
  >
315
- {chat.chatMode === 'code' ? (
316
- <span className="relative">
317
- <CodeIcon size={16} />
318
- {chat.hasChanges ? <span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" /> : null}
319
- </span>
320
- ) : <AgentIcon size={16} />}
315
+ <span className="relative">
316
+ {chat.chatMode === 'code' ? <CodeIcon size={16} /> : <AgentIcon size={16} />}
317
+ {chat.hasChanges ? <span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" /> : null}
318
+ </span>
321
319
  <div className="flex-1 min-w-0">
322
320
  {editing ? (
323
321
  <input
@@ -152,7 +152,7 @@ function CronsPage() {
152
152
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1", children: "No cron jobs configured" }),
153
153
  /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground max-w-sm", children: [
154
154
  "Add scheduled jobs by editing ",
155
- /* @__PURE__ */ jsx("span", { className: "font-mono", children: "config/CRONS.json" }),
155
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: "agent-job/CRONS.json" }),
156
156
  " in your project."
157
157
  ] })
158
158
  ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
@@ -216,7 +216,7 @@ export function CronsPage() {
216
216
  </div>
217
217
  <p className="text-sm font-medium mb-1">No cron jobs configured</p>
218
218
  <p className="text-xs text-muted-foreground max-w-sm">
219
- Add scheduled jobs by editing <span className="font-mono">config/CRONS.json</span> in your project.
219
+ Add scheduled jobs by editing <span className="font-mono">agent-job/CRONS.json</span> in your project.
220
220
  </p>
221
221
  </div>
222
222
  ) : (
@@ -150,11 +150,19 @@ function ToolCall({ part, className }) {
150
150
  /* @__PURE__ */ jsx(WrenchIcon, { size: 14, className: "text-muted-foreground shrink-0 mt-0.5" }),
151
151
  /* @__PURE__ */ jsx("span", { className: "flex flex-col min-w-0 flex-1", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
152
152
  /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: displayName }),
153
- isDone && (() => {
153
+ (() => {
154
154
  try {
155
- const o = typeof part.output === "string" ? JSON.parse(part.output) : part.output;
156
- if (o?.codingAgent || o?.backendApi) {
157
- return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: [o.codingAgent, o.backendApi].filter(Boolean).join(" \xB7 ") });
155
+ const agent = part.input?.codingAgent;
156
+ const backend = part.input?.backendApi;
157
+ if (agent || backend) {
158
+ return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: [agent, backend].filter(Boolean).join(" \xB7 ") });
159
+ }
160
+ if (isDone) {
161
+ const o = typeof part.output === "string" ? JSON.parse(part.output) : part.output;
162
+ const meta = Array.isArray(o) ? o.find((e) => e.type === "meta") : o;
163
+ if (meta?.codingAgent || meta?.backendApi) {
164
+ return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: [meta.codingAgent, meta.backendApi].filter(Boolean).join(" \xB7 ") });
165
+ }
158
166
  }
159
167
  } catch {
160
168
  }
@@ -154,16 +154,29 @@ function ToolCall({ part, className }) {
154
154
  <span className="flex flex-col min-w-0 flex-1">
155
155
  <span className="flex items-center gap-2">
156
156
  <span className="font-medium text-foreground">{displayName}</span>
157
- {isDone && (() => {
157
+ {(() => {
158
158
  try {
159
- const o = typeof part.output === 'string' ? JSON.parse(part.output) : part.output;
160
- if (o?.codingAgent || o?.backendApi) {
159
+ // Read from input (available immediately) or output meta (historical chats)
160
+ const agent = part.input?.codingAgent;
161
+ const backend = part.input?.backendApi;
162
+ if (agent || backend) {
161
163
  return (
162
164
  <span className="text-xs text-muted-foreground">
163
- {[o.codingAgent, o.backendApi].filter(Boolean).join(' · ')}
165
+ {[agent, backend].filter(Boolean).join(' · ')}
164
166
  </span>
165
167
  );
166
168
  }
169
+ if (isDone) {
170
+ const o = typeof part.output === 'string' ? JSON.parse(part.output) : part.output;
171
+ const meta = Array.isArray(o) ? o.find(e => e.type === 'meta') : o;
172
+ if (meta?.codingAgent || meta?.backendApi) {
173
+ return (
174
+ <span className="text-xs text-muted-foreground">
175
+ {[meta.codingAgent, meta.backendApi].filter(Boolean).join(' · ')}
176
+ </span>
177
+ );
178
+ }
179
+ }
167
180
  } catch {}
168
181
  return null;
169
182
  })()}
@@ -74,7 +74,7 @@ function ActiveConfig({ settings, onSave }) {
74
74
  }
75
75
  useEffect(() => {
76
76
  if (settings?.active) {
77
- const prov = settings.active.provider || availableProviders[0]?.slug || "";
77
+ const prov = settings.active.provider || "";
78
78
  const resolved = availableProviders.find((p) => p.slug === prov);
79
79
  const models = resolved?.models || [];
80
80
  const def = models.find((m) => m.default);
@@ -149,6 +149,7 @@ function ActiveConfig({ settings, onSave }) {
149
149
  onChange: (e) => handleProviderChange(e.target.value),
150
150
  className: "w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground",
151
151
  children: [
152
+ !provider && availableProviders.length > 0 && /* @__PURE__ */ jsx("option", { value: "", children: "Select Provider" }),
152
153
  availableProviders.map((p) => /* @__PURE__ */ jsx("option", { value: p.slug, children: p.name }, p.slug)),
153
154
  availableProviders.length === 0 && /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "No providers configured" })
154
155
  ]
@@ -91,7 +91,7 @@ function ActiveConfig({ settings, onSave }) {
91
91
 
92
92
  useEffect(() => {
93
93
  if (settings?.active) {
94
- const prov = settings.active.provider || availableProviders[0]?.slug || '';
94
+ const prov = settings.active.provider || '';
95
95
  const resolved = availableProviders.find((p) => p.slug === prov);
96
96
  const models = resolved?.models || [];
97
97
  const def = models.find((m) => m.default);
@@ -171,6 +171,9 @@ function ActiveConfig({ settings, onSave }) {
171
171
  onChange={(e) => handleProviderChange(e.target.value)}
172
172
  className="w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground"
173
173
  >
174
+ {!provider && availableProviders.length > 0 && (
175
+ <option value="">Select Provider</option>
176
+ )}
174
177
  {availableProviders.map((p) => (
175
178
  <option key={p.slug} value={p.slug}>{p.name}</option>
176
179
  ))}