@nikitadmitrieff/feedback-chat 0.1.2 → 0.2.0

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.
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/deploy-agent.ts
4
+ import prompts from "prompts";
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { join, resolve } from "path";
7
+ import { execSync } from "child_process";
8
+ function checkCommand(cmd) {
9
+ try {
10
+ execSync(`which ${cmd}`, { stdio: "ignore" });
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+ function readEnvFile(cwd) {
17
+ const envPath = join(cwd, ".env.local");
18
+ if (!existsSync(envPath)) return {};
19
+ const content = readFileSync(envPath, "utf-8");
20
+ const vars = {};
21
+ for (const line of content.split("\n")) {
22
+ const match = line.match(/^([A-Z_]+)=(.+)$/);
23
+ if (match) vars[match[1]] = match[2];
24
+ }
25
+ return vars;
26
+ }
27
+ async function main() {
28
+ const cwd = resolve(process.cwd());
29
+ console.error();
30
+ console.error(" feedback-chat agent deployment script generator");
31
+ console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
32
+ console.error();
33
+ const hasRailway = checkCommand("railway");
34
+ const hasGh = checkCommand("gh");
35
+ if (!hasRailway) {
36
+ console.error(" \u2717 Railway CLI not found. Install: npm install -g @railway/cli");
37
+ process.exit(1);
38
+ }
39
+ if (!hasGh) {
40
+ console.error(" \u2717 GitHub CLI not found. Install: https://cli.github.com/");
41
+ process.exit(1);
42
+ }
43
+ const envVars = readEnvFile(cwd);
44
+ const githubToken = envVars["GITHUB_TOKEN"] || "";
45
+ const githubRepo = envVars["GITHUB_REPO"] || "";
46
+ const anthropicKey = envVars["ANTHROPIC_API_KEY"] || "";
47
+ if (!githubToken || !githubRepo) {
48
+ console.error(" \u2717 GITHUB_TOKEN and GITHUB_REPO must be set in .env.local");
49
+ console.error(" Run `npx feedback-chat init` first with the +GitHub or +Pipeline tier.");
50
+ process.exit(1);
51
+ }
52
+ const { authMethod } = await prompts({
53
+ type: "select",
54
+ name: "authMethod",
55
+ message: "Claude authentication for the agent",
56
+ choices: [
57
+ {
58
+ title: "Claude Max subscription ($0/run)",
59
+ description: "Uses OAuth credentials from your local keychain",
60
+ value: "max"
61
+ },
62
+ {
63
+ title: "API key (pay per token)",
64
+ description: "Uses ANTHROPIC_API_KEY from .env.local",
65
+ value: "api"
66
+ }
67
+ ]
68
+ });
69
+ if (!authMethod) {
70
+ console.error(" Cancelled.");
71
+ process.exit(0);
72
+ }
73
+ let credentialsJson = "";
74
+ if (authMethod === "max") {
75
+ console.error();
76
+ console.error(" To get your Claude credentials, run:");
77
+ console.error(' security find-generic-password -s "Claude Code-credentials" -a "$USER" -w');
78
+ console.error();
79
+ console.error(" Copy the JSON output (the claudeAiOauth portion).");
80
+ console.error();
81
+ const { creds } = await prompts({
82
+ type: "text",
83
+ name: "creds",
84
+ message: "Paste your Claude credentials JSON (or press Enter to include a placeholder)"
85
+ });
86
+ credentialsJson = creds || "${CLAUDE_CREDENTIALS_JSON}";
87
+ }
88
+ const webhookSecret = "$(openssl rand -hex 32)";
89
+ const envLocalPath = join(cwd, ".env.local");
90
+ const authLine = authMethod === "max" ? `
91
+ CLAUDE_CREDENTIALS_JSON='${credentialsJson.replace(/'/g, "'\\''")}'` : anthropicKey ? `
92
+ ANTHROPIC_API_KEY="${anthropicKey}"` : "";
93
+ const script = `#!/usr/bin/env bash
94
+ set -euo pipefail
95
+
96
+ # \u2500\u2500 feedback-chat agent deployment script \u2500\u2500
97
+ # Generated by: npx feedback-chat deploy-agent
98
+ # Review this script before running it.
99
+
100
+ echo "==> Cloning feedback-chat agent..."
101
+ TMPDIR=$(mktemp -d)
102
+ trap 'rm -rf "$TMPDIR"' EXIT
103
+ git clone --depth 1 https://github.com/NikitaDmitrieff/feedback-chat "$TMPDIR/feedback-chat"
104
+ cd "$TMPDIR/feedback-chat/packages/agent"
105
+
106
+ echo "==> Creating Railway project..."
107
+ railway init
108
+
109
+ echo "==> First deploy (creates the service)..."
110
+ DEPLOY_OUTPUT=$(railway up --detach 2>&1)
111
+ echo "$DEPLOY_OUTPUT"
112
+
113
+ echo "==> Waiting for service to register..."
114
+ sleep 5
115
+
116
+ echo "==> Finding and linking service..."
117
+ SERVICE_NAME=$(railway service status --all 2>&1 | grep -oE '^[a-z][-a-z0-9]*' | head -1)
118
+ if [ -z "$SERVICE_NAME" ]; then
119
+ echo "\u2717 Could not detect service name. Run 'railway service status --all' manually."
120
+ exit 1
121
+ fi
122
+ echo " Linking service: $SERVICE_NAME"
123
+ railway service link "$SERVICE_NAME"
124
+
125
+ echo "==> Setting environment variables..."
126
+ WEBHOOK_SECRET=${webhookSecret}
127
+ railway variables set \\
128
+ GITHUB_TOKEN="${githubToken}" \\
129
+ GITHUB_REPO="${githubRepo}" \\
130
+ WEBHOOK_SECRET="$WEBHOOK_SECRET"${authLine ? ` \\${authLine}` : ""}
131
+
132
+ echo "==> Getting public domain..."
133
+ DOMAIN_OUTPUT=$(railway domain 2>&1)
134
+ AGENT_URL=$(echo "$DOMAIN_OUTPUT" | grep -oE 'https://[^ ]+')
135
+ if [ -z "$AGENT_URL" ]; then
136
+ echo "\u2717 Could not extract domain URL. Output was:"
137
+ echo "$DOMAIN_OUTPUT"
138
+ exit 1
139
+ fi
140
+ echo " Agent URL: $AGENT_URL"
141
+
142
+ echo "==> Setting up GitHub webhook..."
143
+ REPO_OWNER=$(echo "${githubRepo}" | cut -d/ -f1)
144
+ REPO_NAME=$(echo "${githubRepo}" | cut -d/ -f2)
145
+ gh api "repos/$REPO_OWNER/$REPO_NAME/hooks" \\
146
+ -f name=web -f active=true \\
147
+ -f "config[url]=$AGENT_URL/webhook/github" \\
148
+ -f "config[content_type]=json" \\
149
+ -f "config[secret]=$WEBHOOK_SECRET" \\
150
+ -f 'events[]=issues'
151
+
152
+ echo "==> Updating consumer .env.local..."
153
+ echo "AGENT_URL=$AGENT_URL" >> "${envLocalPath}"
154
+
155
+ echo ""
156
+ echo "==> Done! Summary:"
157
+ echo " Agent URL: $AGENT_URL"
158
+ echo " Webhook: $AGENT_URL/webhook/github"
159
+ echo " Webhook secret: $WEBHOOK_SECRET"
160
+ echo ""
161
+ echo " Next steps:"
162
+ echo " 1. Wait for Railway deploy to finish"
163
+ echo " 2. Verify: curl $AGENT_URL/health"
164
+ echo " 3. Test by submitting feedback through the widget"
165
+ `;
166
+ process.stdout.write(script);
167
+ console.error();
168
+ console.error(" Script generated! To run it:");
169
+ console.error(" npx feedback-chat deploy-agent | bash");
170
+ console.error(" Or save and review first:");
171
+ console.error(" npx feedback-chat deploy-agent > deploy.sh && chmod +x deploy.sh && cat deploy.sh");
172
+ console.error();
173
+ }
174
+ main().catch((err) => {
175
+ console.error(err);
176
+ process.exit(1);
177
+ });
package/dist/cli/init.js CHANGED
@@ -4,27 +4,79 @@
4
4
  import prompts from "prompts";
5
5
  import { existsSync, writeFileSync, appendFileSync, readFileSync, mkdirSync } from "fs";
6
6
  import { join, resolve } from "path";
7
- var CHAT_ROUTE_TEMPLATE = (hasGithub) => `import { createFeedbackHandler } from '@nikitadmitrieff/feedback-chat/server'
7
+ import { execSync } from "child_process";
8
+ var CHAT_ROUTE_TEMPLATE = (tier) => `import { createFeedbackHandler } from '@nikitadmitrieff/feedback-chat/server'
8
9
 
9
10
  const handler = createFeedbackHandler({
10
- password: process.env.FEEDBACK_PASSWORD!,${hasGithub ? `
11
+ password: process.env.FEEDBACK_PASSWORD!,${tier !== "chat" ? `
11
12
  github: {
12
13
  token: process.env.GITHUB_TOKEN!,
13
14
  repo: process.env.GITHUB_REPO!,
14
15
  },` : ""}
16
+ // projectContext: 'Describe your app here so the AI gives better responses',
15
17
  })
16
18
 
17
19
  export const POST = handler.POST
18
20
  `;
19
- var STATUS_ROUTE_TEMPLATE = `import { createStatusHandler } from '@nikitadmitrieff/feedback-chat/server'
21
+ var STATUS_ROUTE_TEMPLATE = (tier) => {
22
+ if (tier === "chat") {
23
+ return `import { createStatusHandler } from '@nikitadmitrieff/feedback-chat/server'
20
24
 
21
25
  const handler = createStatusHandler({
22
26
  password: process.env.FEEDBACK_PASSWORD!,
23
27
  })
24
28
 
25
29
  export const { GET, POST } = handler
30
+ `;
31
+ }
32
+ if (tier === "github") {
33
+ return `import { createStatusHandler } from '@nikitadmitrieff/feedback-chat/server'
34
+
35
+ const handler = createStatusHandler({
36
+ password: process.env.FEEDBACK_PASSWORD!,
37
+ github: {
38
+ token: process.env.GITHUB_TOKEN!,
39
+ repo: process.env.GITHUB_REPO!,
40
+ },
41
+ })
42
+
43
+ export const { GET, POST } = handler
44
+ `;
45
+ }
46
+ return `import { createStatusHandler } from '@nikitadmitrieff/feedback-chat/server'
47
+
48
+ const handler = createStatusHandler({
49
+ password: process.env.FEEDBACK_PASSWORD!,
50
+ github: {
51
+ token: process.env.GITHUB_TOKEN!,
52
+ repo: process.env.GITHUB_REPO!,
53
+ },
54
+ agentUrl: process.env.AGENT_URL,
55
+ })
56
+
57
+ export const { GET, POST } = handler
58
+ `;
59
+ };
60
+ var FEEDBACK_BUTTON_TEMPLATE = `'use client'
61
+
62
+ import { useState } from 'react'
63
+ import { FeedbackPanel } from '@nikitadmitrieff/feedback-chat'
64
+ import '@nikitadmitrieff/feedback-chat/styles.css'
65
+
66
+ export function FeedbackButton() {
67
+ const [open, setOpen] = useState(false)
68
+ return <FeedbackPanel isOpen={open} onToggle={() => setOpen(!open)} />
69
+ }
26
70
  `;
27
71
  var SOURCE_DIRECTIVE = '@source "../node_modules/@nikitadmitrieff/feedback-chat/dist/**/*.js";';
72
+ var GITHUB_LABELS = [
73
+ { name: "feedback-bot", color: "0E8A16", description: "Created by feedback widget" },
74
+ { name: "auto-implement", color: "1D76DB", description: "Agent should implement this" },
75
+ { name: "in-progress", color: "FBCA04", description: "Agent is working on this" },
76
+ { name: "agent-failed", color: "D93F0B", description: "Agent build/lint failed" },
77
+ { name: "preview-pending", color: "C5DEF5", description: "PR ready, preview deploying" },
78
+ { name: "rejected", color: "E4E669", description: "User rejected changes" }
79
+ ];
28
80
  function findAppDir(cwd) {
29
81
  const candidates = [
30
82
  join(cwd, "src", "app"),
@@ -84,12 +136,65 @@ function appendEnvVar(envPath, key, value) {
84
136
  appendFileSync(envPath, `${key}=${value}
85
137
  `);
86
138
  }
139
+ function checkReactVersion(cwd) {
140
+ const reactPkgPath = join(cwd, "node_modules", "react", "package.json");
141
+ if (!existsSync(reactPkgPath)) return;
142
+ try {
143
+ const pkg = JSON.parse(readFileSync(reactPkgPath, "utf-8"));
144
+ const version = pkg.version;
145
+ if (version === "19.1.0" || version === "19.1.1") {
146
+ console.error(` \u2717 react@${version} detected \u2014 @ai-sdk/react excludes this version.`);
147
+ console.error(" Fix: npm install react@latest react-dom@latest");
148
+ console.error();
149
+ process.exit(1);
150
+ }
151
+ } catch {
152
+ }
153
+ }
154
+ function hasGhCli() {
155
+ try {
156
+ execSync("which gh", { stdio: "ignore" });
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ function createGitHubLabels(cwd) {
163
+ if (!hasGhCli()) {
164
+ console.log(" \u26A0 GitHub CLI (gh) not found. Create these labels manually:");
165
+ console.log();
166
+ for (const label of GITHUB_LABELS) {
167
+ console.log(` gh label create ${label.name} --color ${label.color} --description "${label.description}" --force`);
168
+ }
169
+ console.log();
170
+ return;
171
+ }
172
+ console.log(" Creating GitHub labels...");
173
+ for (const label of GITHUB_LABELS) {
174
+ try {
175
+ execSync(
176
+ `gh label create ${label.name} --color ${label.color} --description "${label.description}" --force`,
177
+ { cwd, stdio: "ignore" }
178
+ );
179
+ console.log(` Created label: ${label.name}`);
180
+ } catch {
181
+ console.log(` \u26A0 Could not create label: ${label.name}`);
182
+ }
183
+ }
184
+ }
185
+ function detectComponentsDir(cwd) {
186
+ if (existsSync(join(cwd, "src", "app"))) {
187
+ return join(cwd, "src", "components");
188
+ }
189
+ return join(cwd, "components");
190
+ }
87
191
  async function main() {
88
192
  const cwd = resolve(process.cwd());
89
193
  console.log();
90
194
  console.log(" feedback-chat setup wizard");
91
195
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
92
196
  console.log();
197
+ checkReactVersion(cwd);
93
198
  const appDir = findAppDir(cwd);
94
199
  if (!appDir) {
95
200
  console.error(" Could not find app/ or src/app/ directory.");
@@ -98,7 +203,26 @@ async function main() {
98
203
  }
99
204
  console.log(` Found Next.js app directory: ${appDir}`);
100
205
  console.log();
101
- console.log(" \u2500\u2500 Widget Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
206
+ console.log(" \u2500\u2500 Tier Selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
207
+ console.log();
208
+ const tierAnswer = await prompts({
209
+ type: "select",
210
+ name: "tier",
211
+ message: "Choose your tier",
212
+ choices: [
213
+ { title: "Chat only (AI conversations, localStorage persistence)", value: "chat" },
214
+ { title: "+ GitHub (Chat + auto-creates GitHub issues)", value: "github" },
215
+ { title: "+ Pipeline (Chat + GitHub + agent \u2192 PR \u2192 preview \u2192 approve)", value: "pipeline" }
216
+ ],
217
+ initial: 0
218
+ });
219
+ if (tierAnswer.tier === void 0) {
220
+ console.log(" Cancelled.");
221
+ process.exit(0);
222
+ }
223
+ const tier = tierAnswer.tier;
224
+ console.log();
225
+ console.log(" \u2500\u2500 Widget Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
102
226
  console.log();
103
227
  const widgetAnswers = await prompts([
104
228
  {
@@ -116,23 +240,17 @@ async function main() {
116
240
  console.log(" Cancelled.");
117
241
  process.exit(0);
118
242
  }
119
- console.log();
120
- console.log(" \u2500\u2500 GitHub Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
121
- console.log();
122
- const githubAnswer = await prompts({
123
- type: "confirm",
124
- name: "enabled",
125
- message: "Enable GitHub issues? (feedback creates issues for tracking)",
126
- initial: true
127
- });
128
243
  let githubToken = "";
129
244
  let githubRepo = "";
130
- if (githubAnswer.enabled) {
245
+ if (tier !== "chat") {
246
+ console.log();
247
+ console.log(" \u2500\u2500 GitHub Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
248
+ console.log();
131
249
  const ghAnswers = await prompts([
132
250
  {
133
251
  type: "password",
134
252
  name: "token",
135
- message: "GitHub token (needs repo scope)"
253
+ message: "GitHub token (needs repo scope, must start with ghp_)"
136
254
  },
137
255
  {
138
256
  type: "text",
@@ -143,7 +261,18 @@ async function main() {
143
261
  githubToken = ghAnswers.token || "";
144
262
  githubRepo = ghAnswers.repo || "";
145
263
  }
146
- const hasGithub = !!(githubToken && githubRepo);
264
+ let agentUrl = "";
265
+ if (tier === "pipeline") {
266
+ console.log();
267
+ console.log(" \u2500\u2500 Agent Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
268
+ console.log();
269
+ const agentAnswer = await prompts({
270
+ type: "text",
271
+ name: "url",
272
+ message: "Agent URL (e.g., https://your-agent.railway.app)"
273
+ });
274
+ agentUrl = agentAnswer.url || "";
275
+ }
147
276
  let overwrite = false;
148
277
  const chatRoutePath = join(appDir, "api", "feedback", "chat", "route.ts");
149
278
  const statusRoutePath = join(appDir, "api", "feedback", "status", "route.ts");
@@ -157,12 +286,17 @@ async function main() {
157
286
  overwrite = overwriteAnswer.overwrite ?? false;
158
287
  }
159
288
  console.log();
160
- if (safeWriteFile(chatRoutePath, CHAT_ROUTE_TEMPLATE(hasGithub), overwrite)) {
289
+ if (safeWriteFile(chatRoutePath, CHAT_ROUTE_TEMPLATE(tier), overwrite)) {
161
290
  console.log(` Created ${chatRoutePath}`);
162
291
  }
163
- if (safeWriteFile(statusRoutePath, STATUS_ROUTE_TEMPLATE, overwrite)) {
292
+ if (safeWriteFile(statusRoutePath, STATUS_ROUTE_TEMPLATE(tier), overwrite)) {
164
293
  console.log(` Created ${statusRoutePath}`);
165
294
  }
295
+ const componentsDir = detectComponentsDir(cwd);
296
+ const feedbackButtonPath = join(componentsDir, "FeedbackButton.tsx");
297
+ if (safeWriteFile(feedbackButtonPath, FEEDBACK_BUTTON_TEMPLATE, overwrite)) {
298
+ console.log(` Created ${feedbackButtonPath}`);
299
+ }
166
300
  const globalsCss = findGlobalsCss(cwd);
167
301
  if (globalsCss) {
168
302
  if (injectSourceDirective(globalsCss)) {
@@ -178,28 +312,43 @@ async function main() {
178
312
  const envPath = join(cwd, ".env.local");
179
313
  appendEnvVar(envPath, "ANTHROPIC_API_KEY", widgetAnswers.apiKey);
180
314
  appendEnvVar(envPath, "FEEDBACK_PASSWORD", widgetAnswers.password);
181
- if (hasGithub) {
182
- appendEnvVar(envPath, "GITHUB_TOKEN", githubToken);
183
- appendEnvVar(envPath, "GITHUB_REPO", githubRepo);
315
+ if (tier !== "chat") {
316
+ if (githubToken) appendEnvVar(envPath, "GITHUB_TOKEN", githubToken);
317
+ if (githubRepo) appendEnvVar(envPath, "GITHUB_REPO", githubRepo);
318
+ }
319
+ if (tier === "pipeline" && agentUrl) {
320
+ appendEnvVar(envPath, "AGENT_URL", agentUrl);
184
321
  }
185
322
  console.log(` Updated ${envPath}`);
323
+ if (tier !== "chat") {
324
+ console.log();
325
+ createGitHubLabels(cwd);
326
+ }
186
327
  console.log();
187
- console.log(" \u2500\u2500 Add to your layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
328
+ console.log(" \u2500\u2500 Next Steps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
188
329
  console.log();
189
- console.log(" // Create a client component (e.g., components/FeedbackButton.tsx):");
190
- console.log(" 'use client'");
191
- console.log(" import { useState } from 'react'");
192
- console.log(" import { FeedbackPanel } from '@nikitadmitrieff/feedback-chat'");
193
- console.log(" import '@nikitadmitrieff/feedback-chat/styles.css'");
330
+ const usesSrc = existsSync(join(cwd, "src", "app"));
331
+ const importPath = usesSrc ? "@/components/FeedbackButton" : "@/components/FeedbackButton";
332
+ console.log(" 1. Add to your layout.tsx:");
194
333
  console.log();
195
- console.log(" export function FeedbackButton() {");
196
- console.log(" const [open, setOpen] = useState(false)");
197
- console.log(" return <FeedbackPanel isOpen={open} onToggle={() => setOpen(!open)} />");
198
- console.log(" }");
334
+ console.log(` import { FeedbackButton } from '${importPath}'`);
199
335
  console.log();
200
- console.log(" // Then in your layout.tsx:");
201
- console.log(" import { FeedbackButton } from '@/components/FeedbackButton'");
202
- console.log(" // Inside <body>: <FeedbackButton />");
336
+ console.log(" // Inside <body>:");
337
+ console.log(" <FeedbackButton />");
338
+ console.log();
339
+ if (tier === "chat") {
340
+ console.log(" 2. Run npm run dev and open the app.");
341
+ console.log(" Click the feedback bar at the bottom, enter your password, and chat.");
342
+ } else if (tier === "github") {
343
+ console.log(" 2. Run npm run dev and open the app.");
344
+ console.log(" Submit feedback to see issues created on your repo.");
345
+ } else {
346
+ console.log(" 2. Run npm run dev and open the app.");
347
+ console.log(" Submit feedback to see issues created on your repo.");
348
+ console.log();
349
+ console.log(" 3. Deploy the agent service:");
350
+ console.log(" See docs/agent-deployment.md or run: npx feedback-chat deploy-agent");
351
+ }
203
352
  console.log();
204
353
  console.log(" Done.");
205
354
  console.log();
@@ -1,6 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
3
  type Stage = 'created' | 'queued' | 'running' | 'validating' | 'preview_ready' | 'deployed' | 'failed' | 'rejected';
4
+ type ActivityEntry = {
5
+ message: string;
6
+ time: string;
7
+ };
4
8
  type StatusResponse = {
5
9
  stage: Stage;
6
10
  issueNumber: number;
@@ -9,6 +13,7 @@ type StatusResponse = {
9
13
  previewUrl?: string;
10
14
  prNumber?: number;
11
15
  prUrl?: string;
16
+ activity?: ActivityEntry[];
12
17
  };
13
18
  type Conversation = {
14
19
  id: string;
@@ -40,4 +45,4 @@ type PipelineTrackerProps = {
40
45
  };
41
46
  declare function PipelineTracker({ issueUrl, statusEndpoint, }: PipelineTrackerProps): react_jsx_runtime.JSX.Element;
42
47
 
43
- export { type Conversation, FeedbackPanel, type FeedbackPanelProps, PipelineTracker, type Stage, type StatusResponse, useConversations };
48
+ export { type ActivityEntry, type Conversation, FeedbackPanel, type FeedbackPanelProps, PipelineTracker, type Stage, type StatusResponse, useConversations };
@@ -1700,6 +1700,19 @@ var STAGE_INDEX = {
1700
1700
  failed: -1,
1701
1701
  rejected: -1
1702
1702
  };
1703
+ var STAGE_MESSAGE = {
1704
+ created: "Creating issue\u2026",
1705
+ queued: "Waiting for agent\u2026",
1706
+ running: "Running\u2026",
1707
+ validating: "Checking build\u2026",
1708
+ preview_ready: "Deploying preview\u2026"
1709
+ };
1710
+ function formatElapsed(ms) {
1711
+ const totalSeconds = Math.floor(ms / 1e3);
1712
+ const minutes = Math.floor(totalSeconds / 60);
1713
+ const seconds = totalSeconds % 60;
1714
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
1715
+ }
1703
1716
  var TERMINAL_STAGES = ["deployed", "failed", "rejected"];
1704
1717
  var POLL_INTERVAL_MS = 5e3;
1705
1718
  var PREVIEW_POLL_INTERVAL_MS = 15e3;
@@ -1763,6 +1776,8 @@ function PipelineTracker({
1763
1776
  const [changeComment, setChangeComment] = useState6("");
1764
1777
  const [confirmReject, setConfirmReject] = useState6(false);
1765
1778
  const previousStageRef = useRef3(null);
1779
+ const stageStartRef = useRef3(Date.now());
1780
+ const [elapsed, setElapsed] = useState6(0);
1766
1781
  const fetchStatus = useCallback3(async () => {
1767
1782
  try {
1768
1783
  const res = await fetch(`${statusEndpoint}?issue=${issueNumber}`);
@@ -1781,6 +1796,17 @@ function PipelineTracker({
1781
1796
  previousStageRef.current = status.stage;
1782
1797
  }
1783
1798
  }, [status.stage]);
1799
+ useEffect3(() => {
1800
+ stageStartRef.current = Date.now();
1801
+ setElapsed(0);
1802
+ }, [status.stage]);
1803
+ useEffect3(() => {
1804
+ if (TERMINAL_STAGES.includes(status.stage)) return;
1805
+ const interval = setInterval(() => {
1806
+ setElapsed(Date.now() - stageStartRef.current);
1807
+ }, 1e3);
1808
+ return () => clearInterval(interval);
1809
+ }, [status.stage]);
1784
1810
  useEffect3(() => {
1785
1811
  setPipelineActive(issueNumber, "created");
1786
1812
  fetchStatus();
@@ -1828,18 +1854,39 @@ function PipelineTracker({
1828
1854
  const currentIndex = STAGE_INDEX[status.stage];
1829
1855
  const isFailed = status.stage === "failed";
1830
1856
  const failedAtIndex = isFailed ? getFailedStepIndex(previousStageRef.current) : -1;
1857
+ const progressIndex = isFailed ? failedAtIndex : currentIndex;
1858
+ const latestActivity = status.activity?.at(-1)?.message;
1859
+ const activityMessage = latestActivity || STAGE_MESSAGE[status.stage] || "";
1860
+ const isActive = !TERMINAL_STAGES.includes(status.stage);
1831
1861
  return /* @__PURE__ */ jsxs10("div", { className: "rounded-2xl border border-border bg-card p-3 space-y-2", children: [
1832
- /* @__PURE__ */ jsx14("div", { className: "space-y-0", children: STEPS.map((step, i) => {
1833
- const state = deriveStepState(i, currentIndex, failedAtIndex, status.stage);
1834
- return /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 h-6", children: [
1835
- /* @__PURE__ */ jsx14(StepDot, { state }),
1836
- /* @__PURE__ */ jsx14("span", { className: `text-xs ${STEP_LABEL_CLASS[state]}`, children: state === "failed" && status.failReason ? status.failReason : step.label }),
1837
- i === 0 && /* @__PURE__ */ jsxs10("span", { className: "ml-auto text-[11px] text-muted-foreground/60", children: [
1838
- "#",
1839
- issueNumber
1840
- ] })
1841
- ] }, step.stage);
1842
- }) }),
1862
+ /* @__PURE__ */ jsxs10("div", { className: "relative", children: [
1863
+ /* @__PURE__ */ jsx14("div", { className: "absolute left-[7.5px] top-3 bottom-3 w-px bg-muted-foreground/15" }),
1864
+ progressIndex > 0 && /* @__PURE__ */ jsx14(
1865
+ "div",
1866
+ {
1867
+ className: `absolute left-[7.5px] top-3 w-px transition-all duration-700 ease-out ${isFailed ? "bg-destructive/50" : "bg-emerald-500/50"}`,
1868
+ style: { height: `${progressIndex * 24}px` }
1869
+ }
1870
+ ),
1871
+ STEPS.map((step, i) => {
1872
+ const state = deriveStepState(i, currentIndex, failedAtIndex, status.stage);
1873
+ return /* @__PURE__ */ jsxs10("div", { children: [
1874
+ /* @__PURE__ */ jsxs10("div", { className: "relative flex items-center gap-2 h-6", children: [
1875
+ /* @__PURE__ */ jsx14(StepDot, { state }),
1876
+ /* @__PURE__ */ jsx14("span", { className: `text-xs truncate ${STEP_LABEL_CLASS[state]}`, children: step.label }),
1877
+ i === 0 && /* @__PURE__ */ jsxs10("span", { className: "ml-auto text-[11px] text-muted-foreground/60 shrink-0", children: [
1878
+ "#",
1879
+ issueNumber
1880
+ ] })
1881
+ ] }),
1882
+ state === "failed" && status.failReason && /* @__PURE__ */ jsx14("div", { className: "pl-6 pb-0.5", children: /* @__PURE__ */ jsx14("span", { className: "text-[10px] text-destructive/80 line-clamp-2", children: status.failReason }) }),
1883
+ state === "active" && isActive && activityMessage && /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 pl-6 h-5", children: [
1884
+ /* @__PURE__ */ jsx14("span", { className: "text-[10px] text-muted-foreground truncate flex-1", children: activityMessage }),
1885
+ /* @__PURE__ */ jsx14("span", { className: "text-[10px] text-muted-foreground/60 tabular-nums shrink-0", children: formatElapsed(elapsed) })
1886
+ ] })
1887
+ ] }, step.stage);
1888
+ })
1889
+ ] }),
1843
1890
  status.stage === "preview_ready" && status.previewUrl && /* @__PURE__ */ jsxs10("div", { className: "space-y-2.5 pt-2 border-t border-border", children: [
1844
1891
  /* @__PURE__ */ jsxs10(
1845
1892
  "a",
@@ -37,6 +37,10 @@ type StatusHandlerConfig = {
37
37
  agentUrl?: string;
38
38
  };
39
39
  type Stage = 'created' | 'queued' | 'running' | 'validating' | 'preview_ready' | 'deployed' | 'failed' | 'rejected';
40
+ type ActivityEntry = {
41
+ message: string;
42
+ time: string;
43
+ };
40
44
  type StatusResponse = {
41
45
  stage: Stage;
42
46
  issueNumber: number;
@@ -45,6 +49,7 @@ type StatusResponse = {
45
49
  previewUrl?: string;
46
50
  prNumber?: number;
47
51
  prUrl?: string;
52
+ activity?: ActivityEntry[];
48
53
  };
49
54
  /**
50
55
  * Creates Next.js App Router GET and POST handlers for the feedback status endpoint.
@@ -112,4 +117,4 @@ declare function createGitHubIssue({ title, body, labels, }: {
112
117
  labels?: string[];
113
118
  }): Promise<string | null>;
114
119
 
115
- export { type FeedbackHandlerConfig, type GitHubIssueCreator, type Stage, type StatusHandlerConfig, type StatusResponse, buildDefaultPrompt, createFeedbackHandler, createGitHubIssue, createStatusHandler, createTools };
120
+ export { type ActivityEntry, type FeedbackHandlerConfig, type GitHubIssueCreator, type Stage, type StatusHandlerConfig, type StatusResponse, buildDefaultPrompt, createFeedbackHandler, createGitHubIssue, createStatusHandler, createTools };
@@ -284,17 +284,22 @@ async function getPreviewUrl(config, sha) {
284
284
  }
285
285
  return null;
286
286
  }
287
- async function getFailReason(config, issueNumber) {
287
+ async function getActivity(config, issueNumber) {
288
288
  const res = await fetch(
289
289
  `${issueEndpoint(config, issueNumber)}/comments?per_page=5&direction=desc`,
290
290
  { headers: githubHeaders(config.token), cache: "no-store" }
291
291
  );
292
- if (!res.ok) return void 0;
292
+ if (!res.ok) return [];
293
293
  const comments = await res.json();
294
- const failComment = comments.find(
295
- (c) => c.body?.startsWith("Agent failed:")
296
- );
297
- return failComment?.body?.replace("Agent failed:", "").trim();
294
+ if (!Array.isArray(comments)) return [];
295
+ return comments.slice(0, 3).map((c) => {
296
+ const raw = c.body ?? "";
297
+ const clean = raw.replace(/<[^>]*>/g, "").replace(/```[\s\S]*?```/g, "").replace(/`[^`]*`/g, "").replace(/[#*_~>\-|]/g, "").replace(/\s+/g, " ").trim().slice(0, 120);
298
+ return {
299
+ message: clean,
300
+ time: c.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
301
+ };
302
+ }).reverse();
298
303
  }
299
304
  async function isAgentRunning(agentUrl, issueNumber) {
300
305
  if (!agentUrl) return false;
@@ -313,8 +318,13 @@ async function deriveStage(config, issueNumber, agentUrl) {
313
318
  const labels = (issue.labels ?? []).map((l) => l.name);
314
319
  const issueUrl = issue.html_url;
315
320
  if (labels.includes("agent-failed")) {
316
- const failReason = await getFailReason(config, issueNumber);
317
- return { stage: "failed", issueUrl, failReason };
321
+ const activity = await getActivity(config, issueNumber);
322
+ const failComment = activity.find((a) => a.message.startsWith("Agent failed:"));
323
+ let failReason = failComment?.message.replace("Agent failed:", "").trim();
324
+ if (failReason) {
325
+ failReason = failReason.replace(/<[^>]*>/g, "").replace(/```[\s\S]*?```/g, "").replace(/`[^`]*`/g, "").replace(/\s+/g, " ").trim().slice(0, 80);
326
+ }
327
+ return { stage: "failed", issueUrl, failReason, activity };
318
328
  }
319
329
  if (labels.includes("rejected")) {
320
330
  return { stage: "rejected", issueUrl };
@@ -454,7 +464,8 @@ async function handleRequestChanges(config, issueNumber, comment) {
454
464
  await fetch(`${url}/comments`, {
455
465
  method: "POST",
456
466
  headers,
457
- body: JSON.stringify({ body: `**Changes requested:**
467
+ // Must match the string the agent's retry detection looks for
468
+ body: JSON.stringify({ body: `**Modifications demand\xE9es :**
458
469
 
459
470
  ${comment}` })
460
471
  });
@@ -501,6 +512,10 @@ function createStatusHandler(config) {
501
512
  if (!result) {
502
513
  return Response.json({ error: "Issue not found" }, { status: 404 });
503
514
  }
515
+ const terminal = ["deployed", "rejected"];
516
+ if (!terminal.includes(result.stage) && !result.activity) {
517
+ result.activity = await getActivity(ghConfig, issueNumber);
518
+ }
504
519
  const response = { issueNumber, ...result };
505
520
  return Response.json(response);
506
521
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikitadmitrieff/feedback-chat",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/client/index.js",
6
6
  "types": "./dist/client/index.d.ts",
@@ -21,9 +21,10 @@
21
21
  }
22
22
  },
23
23
  "bin": {
24
- "feedback-chat": "./dist/cli/init.js"
24
+ "feedback-chat": "./dist/cli/init.js",
25
+ "feedback-chat-deploy-agent": "./dist/cli/deploy-agent.js"
25
26
  },
26
- "files": ["dist"],
27
+ "files": ["dist", "README.md"],
27
28
  "scripts": {
28
29
  "build": "tsup && cp src/client/styles.css dist/styles.css",
29
30
  "dev": "tsup --watch",