coaia-visualizer 1.6.0 → 1.6.2

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 (58) hide show
  1. package/.dockerignore +9 -0
  2. package/COMPLETE_IMPLEMENTATION_REPORT.md +215 -0
  3. package/QUICK_START_MCP_TESTING.md +236 -0
  4. package/README.md +1 -1
  5. package/README_TELESCOPED_NAVIGATION.md +247 -0
  6. package/STC.md +24 -0
  7. package/STCGOAL.md +0 -0
  8. package/STCISSUE.md +48 -0
  9. package/STCKIN.md +0 -0
  10. package/STCMASTERY.md +0 -0
  11. package/STUDY_REPORT.md +510 -0
  12. package/app/page.tsx +4 -23
  13. package/cli.ts +8 -0
  14. package/components/chart-detail-editable.tsx +3 -2
  15. package/components/chart-detail.tsx +1 -6
  16. package/components/edit-action-step.tsx +0 -2
  17. package/components/metadata-projections.tsx +208 -0
  18. package/components/relation-graph.tsx +27 -91
  19. package/components.json +21 -0
  20. package/direct-test.sh +180 -0
  21. package/dist/cli.js +7 -0
  22. package/dist/skill.js +342 -0
  23. package/docker-compose.test.yml +69 -0
  24. package/index.tsx +0 -26
  25. package/jgwill.coaia-visualizer-8--496dca71-d476-4ac9-ba9f-376add118dd8--260208.txt +2612 -0
  26. package/lib/chart-editor.ts +13 -13
  27. package/lib/jsonl-parser.ts +55 -8
  28. package/lib/jsonl-preservation.ts +405 -0
  29. package/lib/types.ts +32 -15
  30. package/mcp/dist/api-client.d.ts +138 -0
  31. package/mcp/dist/api-client.d.ts.map +1 -0
  32. package/mcp/dist/api-client.js +115 -0
  33. package/mcp/dist/api-client.js.map +1 -0
  34. package/mcp/dist/index.d.ts +2 -0
  35. package/mcp/dist/index.d.ts.map +1 -0
  36. package/mcp/dist/index.js +286 -0
  37. package/mcp/dist/index.js.map +1 -0
  38. package/mcp/dist/tools/index.d.ts +18 -0
  39. package/mcp/dist/tools/index.d.ts.map +1 -0
  40. package/mcp/dist/tools/index.js +322 -0
  41. package/mcp/dist/tools/index.js.map +1 -0
  42. package/mcp/package-lock.json +210 -0
  43. package/package.json +2 -27
  44. package/rispecs/github-project-runtime-memory-integration.spec.md +381 -0
  45. package/skill.ts +385 -0
  46. package/test-data/test-master.jsonl +11 -0
  47. package/test-results/.last-run.json +4 -0
  48. package/test-scripts/README.md +325 -0
  49. package/test-scripts/rich-jsonl-preservation.spec.ts +118 -0
  50. package/test-scripts/run-all-tests.sh +38 -0
  51. package/test-scripts/test-01-basic-operations.sh +87 -0
  52. package/test-scripts/test-02-telescope-creation.sh +91 -0
  53. package/test-scripts/test-03-navigation.sh +87 -0
  54. package/test-scripts/test-global-cli.spec.ts +220 -0
  55. package/test-scripts/verify-global-cli.sh +87 -0
  56. package/tsconfig.json +41 -0
  57. package/components/github-provenance.tsx +0 -226
  58. package/lib/github-provenance.ts +0 -316
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Test 03: Navigation & Chart Hierarchy"
5
+ echo "======================================"
6
+
7
+ TEST_RESULTS_DIR="/test-results/test-03"
8
+ mkdir -p "$TEST_RESULTS_DIR"
9
+
10
+ # Load chart IDs from previous tests
11
+ PARENT_CHART_ID=$(cat /test-results/test-01/chart-id.txt 2>/dev/null || echo "")
12
+ TELESCOPED_CHART_ID=$(cat /test-results/test-02/telescoped-chart-id.txt 2>/dev/null || echo "")
13
+
14
+ echo "Parent chart: $PARENT_CHART_ID"
15
+ echo "Telescoped chart: $TELESCOPED_CHART_ID"
16
+
17
+ if [ -z "$PARENT_CHART_ID" ]; then
18
+ echo "✗ No parent chart ID available"
19
+ exit 1
20
+ fi
21
+
22
+ echo "1. Testing parent chart retrieval..."
23
+ curl -X GET "http://visualizer-app:4321/api/charts/$PARENT_CHART_ID" \
24
+ -H "X-API-Key: test-api-key" \
25
+ -o "$TEST_RESULTS_DIR/parent-chart.json" || true
26
+
27
+ # Verify parent chart has metadata about children
28
+ HAS_SUBCHARTS=$(cat "$TEST_RESULTS_DIR/parent-chart.json" | jq -r '.chart.subCharts != null' 2>/dev/null || echo "false")
29
+
30
+ echo " Parent has subCharts array: $HAS_SUBCHARTS"
31
+
32
+ if [ -n "$TELESCOPED_CHART_ID" ] && [ "$TELESCOPED_CHART_ID" != "null" ]; then
33
+ echo "2. Testing telescoped chart retrieval..."
34
+ curl -X GET "http://visualizer-app:4321/api/charts/$TELESCOPED_CHART_ID" \
35
+ -H "X-API-Key: test-api-key" \
36
+ -o "$TEST_RESULTS_DIR/telescoped-chart.json" || true
37
+
38
+ # Verify telescoped chart has parent metadata
39
+ PARENT_REF=$(cat "$TEST_RESULTS_DIR/telescoped-chart.json" | jq -r '.chart.metadata.parentChart' 2>/dev/null || echo "")
40
+
41
+ echo " Telescoped chart parent reference: $PARENT_REF"
42
+
43
+ if [ -n "$PARENT_REF" ] && [ "$PARENT_REF" != "null" ]; then
44
+ echo "✓ Navigation hierarchy is properly established"
45
+ else
46
+ echo "⚠ Parent reference not found in telescoped chart"
47
+ fi
48
+ else
49
+ echo "⚠ No telescoped chart ID available to test navigation"
50
+ fi
51
+
52
+ echo "3. Testing search functionality..."
53
+ curl -X GET "http://visualizer-app:4321/api/charts/search?query=MCP" \
54
+ -H "X-API-Key: test-api-key" \
55
+ -o "$TEST_RESULTS_DIR/search-results.json" || true
56
+
57
+ SEARCH_COUNT=$(cat "$TEST_RESULTS_DIR/search-results.json" | jq -r '.results | length' 2>/dev/null || echo "0")
58
+ echo " Search returned $SEARCH_COUNT results"
59
+
60
+ echo "4. Listing all charts to verify hierarchy..."
61
+ curl -X GET "http://visualizer-app:4321/api/charts" \
62
+ -H "X-API-Key: test-api-key" \
63
+ -o "$TEST_RESULTS_DIR/all-charts.json" || true
64
+
65
+ TOTAL_CHARTS=$(cat "$TEST_RESULTS_DIR/all-charts.json" | jq -r '.charts | length' 2>/dev/null || echo "0")
66
+ echo " Total charts in system: $TOTAL_CHARTS"
67
+
68
+ echo ""
69
+ echo "Test 03: COMPLETED"
70
+ echo ""
71
+
72
+ # Generate final summary
73
+ cat > /test-results/navigation-summary.txt << EOF
74
+ Navigation Test Summary
75
+ ========================
76
+ Parent Chart ID: $PARENT_CHART_ID
77
+ Telescoped Chart ID: $TELESCOPED_CHART_ID
78
+ Total Charts: $TOTAL_CHARTS
79
+ Search Results: $SEARCH_COUNT
80
+
81
+ Parent has subCharts: $HAS_SUBCHARTS
82
+ Telescoped has parent ref: $([ -n "$PARENT_REF" ] && echo "yes" || echo "no")
83
+
84
+ Test Status: PASSED
85
+ EOF
86
+
87
+ cat /test-results/navigation-summary.txt
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Test globally installed CLI with Playwright
3
+ * Tests the fix for Turbopack file watching issue
4
+ */
5
+
6
+ import { test, expect } from '@playwright/test';
7
+ import { spawn, ChildProcess } from 'child_process';
8
+ import { promises as fs } from 'fs';
9
+ import path from 'path';
10
+
11
+ let cliProcess: ChildProcess | null = null;
12
+ const CLI_PORT = 3099;
13
+ const TEST_TIMEOUT = 60000; // 60 seconds for server startup
14
+
15
+ test.describe('Global CLI Installation Tests', () => {
16
+ test.beforeEach(async () => {
17
+ // Kill any existing process on the port
18
+ try {
19
+ await new Promise<void>((resolve) => {
20
+ const kill = spawn('fuser', ['-k', `${CLI_PORT}/tcp`]);
21
+ kill.on('close', () => resolve());
22
+ setTimeout(resolve, 1000);
23
+ });
24
+ } catch (e) {
25
+ // Port might not be in use
26
+ }
27
+ });
28
+
29
+ test.afterEach(async () => {
30
+ // Clean up CLI process
31
+ if (cliProcess) {
32
+ cliProcess.kill('SIGTERM');
33
+ await new Promise(resolve => setTimeout(resolve, 2000));
34
+ if (!cliProcess.killed) {
35
+ cliProcess.kill('SIGKILL');
36
+ }
37
+ cliProcess = null;
38
+ }
39
+ });
40
+
41
+ test('should launch CLI without file watch errors', async ({ page }) => {
42
+ test.setTimeout(TEST_TIMEOUT);
43
+
44
+ const sampleFile = path.join(__dirname, '../samples/tradingchart.jsonl');
45
+
46
+ // Verify sample file exists
47
+ await fs.access(sampleFile);
48
+
49
+ // Launch CLI with sample file
50
+ let cliOutput = '';
51
+ let cliError = '';
52
+ let hasFileWatchError = false;
53
+ let serverReady = false;
54
+
55
+ cliProcess = spawn(
56
+ 'coaia-visualizer',
57
+ ['-M', sampleFile, '--port', CLI_PORT.toString(), '--no-open'],
58
+ {
59
+ stdio: ['ignore', 'pipe', 'pipe'],
60
+ env: { ...process.env },
61
+ }
62
+ );
63
+
64
+ // Monitor output
65
+ cliProcess.stdout?.on('data', (data) => {
66
+ const output = data.toString();
67
+ cliOutput += output;
68
+ console.log('[CLI stdout]', output);
69
+
70
+ if (output.includes('Local:') || output.includes('localhost')) {
71
+ serverReady = true;
72
+ }
73
+ });
74
+
75
+ cliProcess.stderr?.on('data', (data) => {
76
+ const error = data.toString();
77
+ cliError += error;
78
+ console.log('[CLI stderr]', error);
79
+
80
+ // Check for file watch errors
81
+ if (
82
+ error.includes('OS file watch limit reached') ||
83
+ error.includes('TurbopackInternalError') ||
84
+ error.includes('about ["/home/') ||
85
+ error.includes('ENOSPC')
86
+ ) {
87
+ hasFileWatchError = true;
88
+ }
89
+ });
90
+
91
+ // Wait for server to be ready (max 45 seconds)
92
+ const startTime = Date.now();
93
+ while (!serverReady && Date.now() - startTime < 45000) {
94
+ await new Promise(resolve => setTimeout(resolve, 500));
95
+
96
+ // Fail fast if file watch error detected
97
+ if (hasFileWatchError) {
98
+ break;
99
+ }
100
+ }
101
+
102
+ // Verify no file watch errors
103
+ expect(hasFileWatchError, 'Should not have file watch errors').toBe(false);
104
+ expect(serverReady, 'Server should be ready').toBe(true);
105
+
106
+ // Navigate to the app
107
+ await page.goto(`http://localhost:${CLI_PORT}`, {
108
+ waitUntil: 'networkidle',
109
+ timeout: 15000
110
+ });
111
+
112
+ // Verify page loaded successfully
113
+ await expect(page.locator('body')).toBeVisible();
114
+
115
+ // Check that it's the visualizer app (should have charts or similar content)
116
+ const title = await page.title();
117
+ console.log('Page title:', title);
118
+
119
+ // Take a screenshot for verification
120
+ await page.screenshot({
121
+ path: path.join(__dirname, '../test-data/global-cli-test.png'),
122
+ fullPage: true
123
+ });
124
+
125
+ console.log('✅ CLI launched successfully without file watch errors');
126
+ });
127
+
128
+ test('should load sample chart data correctly', async ({ page }) => {
129
+ test.setTimeout(TEST_TIMEOUT);
130
+
131
+ const sampleFile = path.join(__dirname, '../samples/tradingchart.jsonl');
132
+
133
+ // Launch CLI
134
+ let serverReady = false;
135
+ cliProcess = spawn(
136
+ 'coaia-visualizer',
137
+ ['-M', sampleFile, '--port', CLI_PORT.toString(), '--no-open'],
138
+ {
139
+ stdio: ['ignore', 'pipe', 'pipe'],
140
+ env: { ...process.env },
141
+ }
142
+ );
143
+
144
+ cliProcess.stdout?.on('data', (data) => {
145
+ const output = data.toString();
146
+ if (output.includes('Local:') || output.includes('localhost')) {
147
+ serverReady = true;
148
+ }
149
+ });
150
+
151
+ // Wait for server
152
+ const startTime = Date.now();
153
+ while (!serverReady && Date.now() - startTime < 45000) {
154
+ await new Promise(resolve => setTimeout(resolve, 500));
155
+ }
156
+
157
+ expect(serverReady).toBe(true);
158
+
159
+ // Navigate and test the UI
160
+ await page.goto(`http://localhost:${CLI_PORT}`, {
161
+ waitUntil: 'networkidle',
162
+ timeout: 15000
163
+ });
164
+
165
+ // Wait for content to load
166
+ await page.waitForTimeout(2000);
167
+
168
+ // Check for chart elements (adjust selectors based on your UI)
169
+ const snapshot = await page.locator('body').textContent();
170
+ console.log('Page content includes:', snapshot?.substring(0, 500));
171
+
172
+ // Verify the app is functional
173
+ await expect(page.locator('body')).toBeVisible();
174
+
175
+ console.log('✅ Sample chart data loaded successfully');
176
+ });
177
+
178
+ test('should handle different ports correctly', async ({ page }) => {
179
+ test.setTimeout(TEST_TIMEOUT);
180
+
181
+ const alternatePort = 3098;
182
+ const sampleFile = path.join(__dirname, '../samples/tradingchart.jsonl');
183
+
184
+ // Launch on alternate port
185
+ let serverReady = false;
186
+ cliProcess = spawn(
187
+ 'coaia-visualizer',
188
+ ['-M', sampleFile, '-p', alternatePort.toString(), '--no-open'],
189
+ {
190
+ stdio: ['ignore', 'pipe', 'pipe'],
191
+ env: { ...process.env },
192
+ }
193
+ );
194
+
195
+ cliProcess.stdout?.on('data', (data) => {
196
+ const output = data.toString();
197
+ if (output.includes(`${alternatePort}`)) {
198
+ serverReady = true;
199
+ }
200
+ });
201
+
202
+ // Wait for server
203
+ const startTime = Date.now();
204
+ while (!serverReady && Date.now() - startTime < 45000) {
205
+ await new Promise(resolve => setTimeout(resolve, 500));
206
+ }
207
+
208
+ expect(serverReady).toBe(true);
209
+
210
+ // Verify server is on alternate port
211
+ await page.goto(`http://localhost:${alternatePort}`, {
212
+ waitUntil: 'networkidle',
213
+ timeout: 15000
214
+ });
215
+
216
+ await expect(page.locator('body')).toBeVisible();
217
+
218
+ console.log('✅ Alternate port configuration works');
219
+ });
220
+ });
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+ # Verify global CLI installation works without file watch errors
3
+
4
+ set -e
5
+
6
+ PORT=3099
7
+ SAMPLE_FILE="/a/src/coaia-visualizer/samples/tradingchart.jsonl"
8
+ TIMEOUT=45
9
+ ERROR_LOG="/tmp/cli-error-check.log"
10
+
11
+ echo "🧪 Testing globally installed coaia-visualizer CLI"
12
+ echo "📁 Sample file: $SAMPLE_FILE"
13
+ echo "🌐 Port: $PORT"
14
+ echo ""
15
+
16
+ # Clean up any existing process on the port
17
+ fuser -k ${PORT}/tcp 2>/dev/null || true
18
+ sleep 2
19
+
20
+ # Start CLI in background
21
+ rm -f "$ERROR_LOG"
22
+ cd /tmp
23
+ coaia-visualizer -M "$SAMPLE_FILE" --port $PORT --no-open > "$ERROR_LOG" 2>&1 &
24
+ CLI_PID=$!
25
+
26
+ echo "⏳ Waiting for server to start (PID: $CLI_PID)..."
27
+
28
+ # Wait for server to be ready
29
+ for i in $(seq 1 $TIMEOUT); do
30
+ if grep -q "Ready in" "$ERROR_LOG" 2>/dev/null; then
31
+ echo "✅ Server started successfully"
32
+ break
33
+ fi
34
+
35
+ if grep -E "(OS file watch limit|TurbopackInternalError|FATAL)" "$ERROR_LOG" 2>/dev/null; then
36
+ echo "❌ File watch error detected:"
37
+ grep -E "(OS file watch limit|TurbopackInternalError|FATAL)" "$ERROR_LOG"
38
+ kill $CLI_PID 2>/dev/null
39
+ exit 1
40
+ fi
41
+
42
+ if ! kill -0 $CLI_PID 2>/dev/null; then
43
+ echo "❌ CLI process died unexpectedly"
44
+ tail -20 "$ERROR_LOG"
45
+ exit 1
46
+ fi
47
+
48
+ sleep 1
49
+ done
50
+
51
+ # Test server response
52
+ echo "🔍 Testing HTTP response..."
53
+ if curl -s -f http://localhost:$PORT > /dev/null; then
54
+ echo "✅ Server responding correctly"
55
+ else
56
+ echo "❌ Server not responding"
57
+ kill $CLI_PID 2>/dev/null
58
+ exit 1
59
+ fi
60
+
61
+ # Check for file watch errors in output
62
+ echo "🔍 Checking for file watch errors..."
63
+ if grep -qE "(OS file watch limit|TurbopackInternalError|about \[\"/home/)" "$ERROR_LOG"; then
64
+ echo "❌ File watch errors detected:"
65
+ grep -E "(OS file watch limit|TurbopackInternalError|about \[\"/home/)" "$ERROR_LOG"
66
+ kill $CLI_PID 2>/dev/null
67
+ exit 1
68
+ else
69
+ echo "✅ No file watch errors detected"
70
+ fi
71
+
72
+ # Verify project directory
73
+ echo "🔍 Verifying Turbopack project directory..."
74
+ if grep -q "dir: '/a/src/coaia-visualizer'" "$ERROR_LOG"; then
75
+ echo "✅ Turbopack using correct project directory"
76
+ else
77
+ echo "⚠️ Warning: Could not verify Turbopack directory"
78
+ grep "Creating turbopack project" "$ERROR_LOG" || true
79
+ fi
80
+
81
+ # Clean up
82
+ kill $CLI_PID 2>/dev/null
83
+ wait $CLI_PID 2>/dev/null || true
84
+
85
+ echo ""
86
+ echo "✅ All tests passed! CLI works correctly without file watch errors."
87
+ echo "📋 Full output saved to: $ERROR_LOG"
package/tsconfig.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "dom",
5
+ "dom.iterable",
6
+ "esnext"
7
+ ],
8
+ "allowJs": true,
9
+ "target": "ES6",
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }
@@ -1,226 +0,0 @@
1
- "use client"
2
-
3
- import { useState } from "react"
4
- import type { LucideIcon } from "lucide-react"
5
- import { AlertTriangle, CheckCircle2, Cloud, ExternalLink, FolderOpen, Github, GitBranch, XCircle } from "lucide-react"
6
-
7
- import { Badge } from "@/components/ui/badge"
8
- import type { EntityRecord } from "@/lib/types"
9
- import {
10
- formatGithubIssueRef,
11
- formatProjectItemRef,
12
- getGithubEntityProvenance,
13
- getGithubIssueRef,
14
- getGithubSyncState,
15
- getProjectFieldEntries,
16
- type GithubSyncState,
17
- } from "@/lib/github-provenance"
18
- import { cn } from "@/lib/utils"
19
-
20
- interface SyncStatePillProps {
21
- syncState?: GithubSyncState
22
- className?: string
23
- }
24
-
25
- const syncStateConfig: Record<GithubSyncState, { label: string; className: string; Icon: LucideIcon }> = {
26
- synced: {
27
- label: "synced",
28
- className: "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
29
- Icon: CheckCircle2,
30
- },
31
- diverged: {
32
- label: "diverged",
33
- className: "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
34
- Icon: AlertTriangle,
35
- },
36
- conflict: {
37
- label: "conflict",
38
- className: "border-destructive/40 bg-destructive/10 text-destructive",
39
- Icon: XCircle,
40
- },
41
- "project-only": {
42
- label: "project-only",
43
- className: "border-slate-500/40 bg-slate-500/10 text-slate-700 dark:text-slate-300",
44
- Icon: Cloud,
45
- },
46
- "chart-only": {
47
- label: "chart-only",
48
- className: "border-slate-500/40 bg-slate-500/10 text-slate-700 dark:text-slate-300",
49
- Icon: FolderOpen,
50
- },
51
- }
52
-
53
- export function SyncStatePill({ syncState, className }: SyncStatePillProps) {
54
- if (!syncState) return null
55
-
56
- const { label, className: stateClassName, Icon } = syncStateConfig[syncState]
57
-
58
- return (
59
- <Badge variant="outline" className={cn("gap-1 text-xs", stateClassName, className)}>
60
- <Icon className="w-3 h-3" />
61
- {label}
62
- </Badge>
63
- )
64
- }
65
-
66
- interface ProjectSourceBadgeProps {
67
- entity: EntityRecord
68
- className?: string
69
- compact?: boolean
70
- }
71
-
72
- export function ProjectSourceBadge({ entity, className, compact = false }: ProjectSourceBadgeProps) {
73
- const [selectedLensIndex, setSelectedLensIndex] = useState(0)
74
- const provenance = getGithubEntityProvenance(entity)
75
-
76
- if (!provenance.hasGithubMetadata) return null
77
-
78
- const projectItems = provenance.projectItems
79
- const activeLensIndex = projectItems.length > 0 ? Math.min(selectedLensIndex, projectItems.length - 1) : 0
80
- const activeProjectItem = projectItems[activeLensIndex]
81
- const projectRef = formatProjectItemRef(activeProjectItem)
82
- const issueRef = formatGithubIssueRef(provenance.issue)
83
- const projectTitle = activeProjectItem?.projectTitle
84
- const fieldEntries = getProjectFieldEntries(entity, activeProjectItem)
85
- const sourceLabel = getSourceLabel(provenance.source?.system, provenance.source?.toolName, provenance.lastSyncedAt)
86
- const projectTooltip = [
87
- projectTitle,
88
- activeProjectItem?.itemId && `item ${activeProjectItem.itemId}`,
89
- activeProjectItem?.projectId && `project ${activeProjectItem.projectId}`,
90
- activeProjectItem?.url,
91
- ]
92
- .filter(Boolean)
93
- .join(" · ")
94
-
95
- return (
96
- <div
97
- className={cn(
98
- "rounded-md border border-indigo-500/20 bg-indigo-500/5 p-3 text-sm",
99
- compact && "p-2",
100
- className,
101
- )}
102
- >
103
- <div className="flex flex-wrap items-center gap-2">
104
- {activeProjectItem?.url ? (
105
- <a href={activeProjectItem.url} target="_blank" rel="noreferrer" title={projectTooltip}>
106
- <ProjectBadge label={projectRef ?? "Project bridge"} />
107
- </a>
108
- ) : (
109
- <ProjectBadge label={projectRef ?? "Project bridge"} title={projectTooltip} />
110
- )}
111
-
112
- {provenance.issue &&
113
- (provenance.issue.url ? (
114
- <a href={provenance.issue.url} target="_blank" rel="noreferrer" title={issueRef}>
115
- <Badge variant="outline" className="gap-1 text-xs">
116
- <Github className="w-3 h-3" />#{provenance.issue.number}
117
- <ExternalLink className="w-3 h-3" />
118
- </Badge>
119
- </a>
120
- ) : (
121
- <Badge variant="outline" className="gap-1 text-xs" title={issueRef}>
122
- <Github className="w-3 h-3" />#{provenance.issue.number}
123
- </Badge>
124
- ))}
125
-
126
- <SyncStatePill syncState={provenance.syncState} />
127
-
128
- {projectItems.length > 1 && (
129
- <select
130
- aria-label="Active GitHub project lens"
131
- value={activeLensIndex}
132
- onChange={(event) => setSelectedLensIndex(Number(event.target.value))}
133
- className="h-7 rounded-md border border-border bg-background px-2 text-xs text-foreground"
134
- >
135
- {projectItems.map((item, index) => (
136
- <option key={`${formatProjectItemRef(item) ?? item.itemId ?? "project"}-${index}`} value={index}>
137
- {item.projectTitle ?? formatProjectItemRef(item) ?? `Project ${index + 1}`}
138
- </option>
139
- ))}
140
- </select>
141
- )}
142
- </div>
143
-
144
- {!compact && (projectTitle || sourceLabel || fieldEntries.length > 0) && (
145
- <div className="mt-2 space-y-2 text-xs text-muted-foreground">
146
- {(projectTitle || sourceLabel) && (
147
- <div className="flex flex-wrap items-center gap-2">
148
- {projectTitle && <span className="font-medium text-foreground">{projectTitle}</span>}
149
- {sourceLabel && <span>{sourceLabel}</span>}
150
- </div>
151
- )}
152
-
153
- {fieldEntries.length > 0 && (
154
- <div className="flex flex-wrap gap-1.5">
155
- {fieldEntries.map((field) => (
156
- <span
157
- key={field.key}
158
- className="inline-flex max-w-full items-center gap-1 rounded-md border border-border bg-background px-2 py-1"
159
- title={`${field.label}: ${field.value}`}
160
- >
161
- <span className="font-medium text-foreground">{field.label}</span>
162
- <span className="max-w-[18rem] truncate">{field.value}</span>
163
- </span>
164
- ))}
165
- </div>
166
- )}
167
- </div>
168
- )}
169
- </div>
170
- )
171
- }
172
-
173
- function ProjectBadge({ label, title }: { label: string; title?: string }) {
174
- return (
175
- <Badge className="gap-1 bg-indigo-600 text-white hover:bg-indigo-600/90" title={title}>
176
- <GitBranch className="w-3 h-3" />
177
- Project: {label}
178
- </Badge>
179
- )
180
- }
181
-
182
- interface ActionStepIssueBadgeProps {
183
- action: EntityRecord
184
- className?: string
185
- }
186
-
187
- export function ActionStepIssueBadge({ action, className }: ActionStepIssueBadgeProps) {
188
- const issue = getGithubIssueRef(action)
189
- const syncState = getGithubSyncState(action)
190
- const issueRef = formatGithubIssueRef(issue)
191
-
192
- if (!issue && !syncState) return null
193
-
194
- return (
195
- <span className={cn("inline-flex flex-wrap items-center gap-1", className)}>
196
- {issue &&
197
- (issue.url ? (
198
- <a href={issue.url} target="_blank" rel="noreferrer" title={issueRef}>
199
- <Badge variant="outline" className="gap-1 text-xs">
200
- <Github className="w-3 h-3" />#{issue.number}
201
- <ExternalLink className="w-3 h-3" />
202
- </Badge>
203
- </a>
204
- ) : (
205
- <Badge variant="outline" className="gap-1 text-xs" title={issueRef}>
206
- <Github className="w-3 h-3" />#{issue.number}
207
- </Badge>
208
- ))}
209
- <SyncStatePill syncState={syncState} />
210
- </span>
211
- )
212
- }
213
-
214
- function getSourceLabel(system?: string, toolName?: string, lastSyncedAt?: string): string | undefined {
215
- const parts = [system, toolName, formatDateTime(lastSyncedAt)].filter(Boolean)
216
- return parts.length > 0 ? parts.join(" · ") : undefined
217
- }
218
-
219
- function formatDateTime(value?: string): string | undefined {
220
- if (!value) return undefined
221
-
222
- const date = new Date(value)
223
- if (Number.isNaN(date.getTime())) return value
224
-
225
- return date.toLocaleString()
226
- }