gigaclaw 1.4.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.
- package/LICENSE +26 -0
- package/README.md +237 -0
- package/api/CLAUDE.md +19 -0
- package/api/index.js +265 -0
- package/bin/cli.js +823 -0
- package/bin/local.sh +85 -0
- package/bin/postinstall.js +63 -0
- package/config/index.js +26 -0
- package/config/instrumentation.js +62 -0
- package/drizzle/0000_initial.sql +52 -0
- package/drizzle/0001_nostalgic_sersi.sql +11 -0
- package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
- package/drizzle/0003_rename_code_workspaces.sql +5 -0
- package/drizzle/meta/0000_snapshot.json +321 -0
- package/drizzle/meta/0001_snapshot.json +390 -0
- package/drizzle/meta/0002_snapshot.json +411 -0
- package/drizzle/meta/0003_snapshot.json +419 -0
- package/drizzle/meta/_journal.json +34 -0
- package/lib/actions.js +44 -0
- package/lib/ai/agent.js +86 -0
- package/lib/ai/index.js +342 -0
- package/lib/ai/model.js +180 -0
- package/lib/ai/tools.js +269 -0
- package/lib/ai/web-search.js +42 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +27 -0
- package/lib/auth/edge-config.js +27 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +62 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +148 -0
- package/lib/chat/actions.js +579 -0
- package/lib/chat/api.js +140 -0
- package/lib/chat/components/app-sidebar.js +213 -0
- package/lib/chat/components/app-sidebar.jsx +279 -0
- package/lib/chat/components/chat-header.js +192 -0
- package/lib/chat/components/chat-header.jsx +223 -0
- package/lib/chat/components/chat-input.js +236 -0
- package/lib/chat/components/chat-input.jsx +249 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +99 -0
- package/lib/chat/components/chat-page.jsx +121 -0
- package/lib/chat/components/chat.js +153 -0
- package/lib/chat/components/chat.jsx +199 -0
- package/lib/chat/components/chats-page.js +367 -0
- package/lib/chat/components/chats-page.jsx +394 -0
- package/lib/chat/components/code-mode-toggle.js +132 -0
- package/lib/chat/components/code-mode-toggle.jsx +163 -0
- package/lib/chat/components/crons-page.js +172 -0
- package/lib/chat/components/crons-page.jsx +244 -0
- package/lib/chat/components/greeting.js +11 -0
- package/lib/chat/components/greeting.jsx +16 -0
- package/lib/chat/components/icons.js +805 -0
- package/lib/chat/components/icons.jsx +751 -0
- package/lib/chat/components/index.js +20 -0
- package/lib/chat/components/message.js +363 -0
- package/lib/chat/components/message.jsx +422 -0
- package/lib/chat/components/messages.js +65 -0
- package/lib/chat/components/messages.jsx +74 -0
- package/lib/chat/components/notifications-page.js +56 -0
- package/lib/chat/components/notifications-page.jsx +87 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/pull-requests-page.js +103 -0
- package/lib/chat/components/pull-requests-page.jsx +113 -0
- package/lib/chat/components/settings-layout.js +39 -0
- package/lib/chat/components/settings-layout.jsx +53 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +138 -0
- package/lib/chat/components/sidebar-history-item.jsx +119 -0
- package/lib/chat/components/sidebar-history.js +167 -0
- package/lib/chat/components/sidebar-history.jsx +220 -0
- package/lib/chat/components/sidebar-user-nav.js +61 -0
- package/lib/chat/components/sidebar-user-nav.jsx +77 -0
- package/lib/chat/components/swarm-page.js +157 -0
- package/lib/chat/components/swarm-page.jsx +210 -0
- package/lib/chat/components/tool-call.js +89 -0
- package/lib/chat/components/tool-call.jsx +107 -0
- package/lib/chat/components/triggers-page.js +153 -0
- package/lib/chat/components/triggers-page.jsx +221 -0
- package/lib/chat/components/ui/combobox.js +98 -0
- package/lib/chat/components/ui/combobox.jsx +114 -0
- package/lib/chat/components/ui/confirm-dialog.js +53 -0
- package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
- package/lib/chat/components/ui/dropdown-menu.js +194 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
- package/lib/chat/components/ui/rename-dialog.js +78 -0
- package/lib/chat/components/ui/rename-dialog.jsx +74 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +228 -0
- package/lib/chat/components/ui/sidebar.jsx +246 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/components/upgrade-dialog.js +151 -0
- package/lib/chat/components/upgrade-dialog.jsx +170 -0
- package/lib/chat/utils.js +11 -0
- package/lib/code/actions.js +153 -0
- package/lib/code/code-page.js +22 -0
- package/lib/code/code-page.jsx +25 -0
- package/lib/code/index.js +1 -0
- package/lib/code/terminal-view.js +201 -0
- package/lib/code/terminal-view.jsx +224 -0
- package/lib/code/ws-proxy.js +80 -0
- package/lib/cron.js +246 -0
- package/lib/db/api-keys.js +163 -0
- package/lib/db/chats.js +168 -0
- package/lib/db/code-workspaces.js +110 -0
- package/lib/db/index.js +52 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +66 -0
- package/lib/db/update-check.js +96 -0
- package/lib/db/users.js +89 -0
- package/lib/paths.js +42 -0
- package/lib/tools/create-job.js +97 -0
- package/lib/tools/docker.js +146 -0
- package/lib/tools/github.js +271 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +292 -0
- package/lib/triggers.js +104 -0
- package/lib/utils/render-md.js +111 -0
- package/package.json +118 -0
- package/setup/lib/auth.mjs +81 -0
- package/setup/lib/env.mjs +21 -0
- package/setup/lib/fs-utils.mjs +20 -0
- package/setup/lib/github.mjs +149 -0
- package/setup/lib/prerequisites.mjs +155 -0
- package/setup/lib/prompts.mjs +267 -0
- package/setup/lib/providers.mjs +105 -0
- package/setup/lib/sync.mjs +125 -0
- package/setup/lib/targets.mjs +45 -0
- package/setup/lib/telegram-verify.mjs +63 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/setup-cloud.mjs +833 -0
- package/setup/setup-local.mjs +377 -0
- package/setup/setup-telegram.mjs +265 -0
- package/setup/setup.mjs +87 -0
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +104 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/notify-job-failed.yml +64 -0
- package/templates/.github/workflows/notify-pr-complete.yml +119 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
- package/templates/.github/workflows/run-job.yml +89 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
- package/templates/.gitignore.template +45 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
- package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
- package/templates/CLAUDE.md +29 -0
- package/templates/CLAUDE.md.template +308 -0
- package/templates/app/api/[...gigaclaw]/route.js +1 -0
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +9 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
- package/templates/app/components/ascii-logo.jsx +12 -0
- package/templates/app/components/login-form.jsx +92 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +5 -0
- package/templates/app/globals.css +90 -0
- package/templates/app/layout.js +33 -0
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +7 -0
- package/templates/app/pull-requests/page.js +7 -0
- package/templates/app/settings/crons/page.js +5 -0
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/settings/triggers/page.js +5 -0
- package/templates/app/stream/chat/route.js +1 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/triggers/page.js +5 -0
- package/templates/config/CODE_PLANNING.md +14 -0
- package/templates/config/CRONS.json +56 -0
- package/templates/config/HEARTBEAT.md +3 -0
- package/templates/config/JOB_AGENT.md +30 -0
- package/templates/config/JOB_PLANNING.md +240 -0
- package/templates/config/JOB_SUMMARY.md +130 -0
- package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
- package/templates/config/SOUL.md +48 -0
- package/templates/config/TRIGGERS.json +58 -0
- package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
- package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
- package/templates/docker/claude-code-job/Dockerfile +34 -0
- package/templates/docker/claude-code-job/entrypoint.sh +149 -0
- package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
- package/templates/docker/claude-code-workspace/Dockerfile +61 -0
- package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
- package/templates/docker/event-handler/Dockerfile +20 -0
- package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
- package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
- package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
- package/templates/docker-compose.local.yml +78 -0
- package/templates/docker-compose.yml +64 -0
- package/templates/instrumentation.js +6 -0
- package/templates/middleware.js +23 -0
- package/templates/next.config.mjs +3 -0
- package/templates/postcss.config.mjs +5 -0
- package/templates/public/favicon.ico +0 -0
- package/templates/server.js +25 -0
- package/templates/skills/LICENSE +21 -0
- package/templates/skills/README.md +119 -0
- package/templates/skills/brave-search/SKILL.md +79 -0
- package/templates/skills/brave-search/content.js +86 -0
- package/templates/skills/brave-search/package-lock.json +621 -0
- package/templates/skills/brave-search/package.json +14 -0
- package/templates/skills/brave-search/search.js +199 -0
- package/templates/skills/browser-tools/SKILL.md +196 -0
- package/templates/skills/browser-tools/browser-content.js +103 -0
- package/templates/skills/browser-tools/browser-cookies.js +35 -0
- package/templates/skills/browser-tools/browser-eval.js +53 -0
- package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/templates/skills/browser-tools/browser-nav.js +44 -0
- package/templates/skills/browser-tools/browser-pick.js +162 -0
- package/templates/skills/browser-tools/browser-screenshot.js +34 -0
- package/templates/skills/browser-tools/browser-start.js +87 -0
- package/templates/skills/browser-tools/package-lock.json +2556 -0
- package/templates/skills/browser-tools/package.json +19 -0
- package/templates/skills/google-docs/SKILL.md +23 -0
- package/templates/skills/google-docs/create.sh +69 -0
- package/templates/skills/google-drive/SKILL.md +47 -0
- package/templates/skills/google-drive/delete.sh +47 -0
- package/templates/skills/google-drive/download.sh +50 -0
- package/templates/skills/google-drive/list.sh +41 -0
- package/templates/skills/google-drive/upload.sh +76 -0
- package/templates/skills/kie-ai/SKILL.md +38 -0
- package/templates/skills/kie-ai/generate-image.sh +77 -0
- package/templates/skills/kie-ai/generate-video.sh +69 -0
- package/templates/skills/llm-secrets/SKILL.md +34 -0
- package/templates/skills/llm-secrets/llm-secrets.js +33 -0
- package/templates/skills/modify-self/SKILL.md +12 -0
- package/templates/skills/youtube-transcript/SKILL.md +41 -0
- package/templates/skills/youtube-transcript/package-lock.json +24 -0
- package/templates/skills/youtube-transcript/package.json +8 -0
- package/templates/skills/youtube-transcript/transcript.js +84 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { PageLayout } from './page-layout.js';
|
|
5
|
+
import { MessageIcon, CodeIcon, TrashIcon, SearchIcon, PlusIcon, MoreHorizontalIcon, StarIcon, StarFilledIcon, PencilIcon, ExportIcon } from './icons.js';
|
|
6
|
+
import { getChats, deleteChat, renameChat, starChat, exportAllChats } from '../actions.js';
|
|
7
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from './ui/dropdown-menu.js';
|
|
8
|
+
import { ConfirmDialog } from './ui/confirm-dialog.js';
|
|
9
|
+
import { cn } from '../utils.js';
|
|
10
|
+
|
|
11
|
+
function groupChatsByDate(chats) {
|
|
12
|
+
const now = new Date();
|
|
13
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
14
|
+
const yesterday = new Date(today.getTime() - 86400000);
|
|
15
|
+
const last7Days = new Date(today.getTime() - 7 * 86400000);
|
|
16
|
+
const last30Days = new Date(today.getTime() - 30 * 86400000);
|
|
17
|
+
|
|
18
|
+
const groups = {
|
|
19
|
+
Starred: [],
|
|
20
|
+
Today: [],
|
|
21
|
+
Yesterday: [],
|
|
22
|
+
'Last 7 Days': [],
|
|
23
|
+
'Last 30 Days': [],
|
|
24
|
+
Older: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (const chat of chats) {
|
|
28
|
+
if (chat.starred) {
|
|
29
|
+
groups.Starred.push(chat);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const date = new Date(chat.updatedAt);
|
|
33
|
+
if (date >= today) {
|
|
34
|
+
groups.Today.push(chat);
|
|
35
|
+
} else if (date >= yesterday) {
|
|
36
|
+
groups.Yesterday.push(chat);
|
|
37
|
+
} else if (date >= last7Days) {
|
|
38
|
+
groups['Last 7 Days'].push(chat);
|
|
39
|
+
} else if (date >= last30Days) {
|
|
40
|
+
groups['Last 30 Days'].push(chat);
|
|
41
|
+
} else {
|
|
42
|
+
groups.Older.push(chat);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return groups;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function timeAgo(date) {
|
|
50
|
+
const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
|
|
51
|
+
if (seconds < 60) return 'just now';
|
|
52
|
+
const minutes = Math.floor(seconds / 60);
|
|
53
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
54
|
+
const hours = Math.floor(minutes / 60);
|
|
55
|
+
if (hours < 24) return `${hours}h ago`;
|
|
56
|
+
const days = Math.floor(hours / 24);
|
|
57
|
+
if (days < 30) return `${days}d ago`;
|
|
58
|
+
const months = Math.floor(days / 30);
|
|
59
|
+
return `${months}mo ago`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ChatsPage({ session }) {
|
|
63
|
+
const [chats, setChats] = useState([]);
|
|
64
|
+
const [loading, setLoading] = useState(true);
|
|
65
|
+
const [query, setQuery] = useState('');
|
|
66
|
+
const [isExportingAll, setIsExportingAll] = useState(false);
|
|
67
|
+
|
|
68
|
+
const navigateToChat = (id) => {
|
|
69
|
+
window.location.href = id ? `/chat/${id}` : '/';
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const loadChats = async () => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await getChats();
|
|
75
|
+
setChats(result);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Failed to load chats:', err);
|
|
78
|
+
} finally {
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
loadChats();
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const handler = () => loadChats();
|
|
89
|
+
window.addEventListener('chatsupdated', handler);
|
|
90
|
+
return () => window.removeEventListener('chatsupdated', handler);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const handleDelete = async (chatId) => {
|
|
94
|
+
setChats((prev) => prev.filter((c) => c.id !== chatId));
|
|
95
|
+
const { success } = await deleteChat(chatId);
|
|
96
|
+
if (!success) loadChats();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleStar = async (chatId) => {
|
|
100
|
+
setChats((prev) =>
|
|
101
|
+
prev.map((c) => (c.id === chatId ? { ...c, starred: c.starred ? 0 : 1 } : c))
|
|
102
|
+
);
|
|
103
|
+
const { success } = await starChat(chatId);
|
|
104
|
+
if (!success) loadChats();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleExportAll = async (format) => {
|
|
108
|
+
if (isExportingAll) return;
|
|
109
|
+
setIsExportingAll(true);
|
|
110
|
+
try {
|
|
111
|
+
const exports = await exportAllChats(format);
|
|
112
|
+
if (!exports || exports.length === 0) return;
|
|
113
|
+
for (let i = 0; i < exports.length; i++) {
|
|
114
|
+
const { filename, content, mimeType } = exports[i];
|
|
115
|
+
const blob = new Blob([content], { type: mimeType });
|
|
116
|
+
const url = URL.createObjectURL(blob);
|
|
117
|
+
const a = document.createElement('a');
|
|
118
|
+
a.href = url;
|
|
119
|
+
a.download = filename;
|
|
120
|
+
document.body.appendChild(a);
|
|
121
|
+
a.click();
|
|
122
|
+
document.body.removeChild(a);
|
|
123
|
+
URL.revokeObjectURL(url);
|
|
124
|
+
if (i < exports.length - 1) await new Promise((r) => setTimeout(r, 120));
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error('Export all failed:', err);
|
|
128
|
+
} finally {
|
|
129
|
+
setIsExportingAll(false);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleRename = async (chatId, title) => {
|
|
134
|
+
setChats((prev) =>
|
|
135
|
+
prev.map((c) => (c.id === chatId ? { ...c, title } : c))
|
|
136
|
+
);
|
|
137
|
+
const { success } = await renameChat(chatId, title);
|
|
138
|
+
if (!success) loadChats();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const filtered = query
|
|
142
|
+
? chats.filter((c) => c.title?.toLowerCase().includes(query.toLowerCase()))
|
|
143
|
+
: chats;
|
|
144
|
+
|
|
145
|
+
const grouped = groupChatsByDate(filtered);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<PageLayout session={session}>
|
|
149
|
+
{/* Header */}
|
|
150
|
+
<div className="flex items-center justify-between mb-6">
|
|
151
|
+
<h1 className="text-2xl font-semibold">Chats</h1>
|
|
152
|
+
<div className="flex items-center gap-2">
|
|
153
|
+
<DropdownMenu>
|
|
154
|
+
<DropdownMenuTrigger asChild>
|
|
155
|
+
<button
|
|
156
|
+
className="inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium border border-input bg-background hover:bg-muted disabled:opacity-50"
|
|
157
|
+
disabled={isExportingAll || chats.length === 0}
|
|
158
|
+
aria-label="Export all chats"
|
|
159
|
+
>
|
|
160
|
+
<ExportIcon size={14} />
|
|
161
|
+
{isExportingAll ? 'Exporting…' : 'Export All'}
|
|
162
|
+
</button>
|
|
163
|
+
</DropdownMenuTrigger>
|
|
164
|
+
<DropdownMenuContent align="end">
|
|
165
|
+
<DropdownMenuItem onClick={() => handleExportAll('md')}>
|
|
166
|
+
<span>Markdown (.md)</span>
|
|
167
|
+
</DropdownMenuItem>
|
|
168
|
+
<DropdownMenuItem onClick={() => handleExportAll('txt')}>
|
|
169
|
+
<span>Plain Text (.txt)</span>
|
|
170
|
+
</DropdownMenuItem>
|
|
171
|
+
<DropdownMenuItem onClick={() => handleExportAll('json')}>
|
|
172
|
+
<span>JSON (.json)</span>
|
|
173
|
+
</DropdownMenuItem>
|
|
174
|
+
</DropdownMenuContent>
|
|
175
|
+
</DropdownMenu>
|
|
176
|
+
<a
|
|
177
|
+
href="/"
|
|
178
|
+
onClick={(e) => { e.preventDefault(); navigateToChat(null); }}
|
|
179
|
+
className="inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium bg-foreground text-background hover:bg-foreground/90"
|
|
180
|
+
style={{ textDecoration: 'inherit' }}
|
|
181
|
+
>
|
|
182
|
+
<PlusIcon size={14} />
|
|
183
|
+
New chat
|
|
184
|
+
</a>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Search */}
|
|
189
|
+
<div className="relative mb-4">
|
|
190
|
+
<input
|
|
191
|
+
type="text"
|
|
192
|
+
placeholder="Search your chats..."
|
|
193
|
+
value={query}
|
|
194
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
195
|
+
className="w-full rounded-md border border-input bg-background px-9 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
196
|
+
/>
|
|
197
|
+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none">
|
|
198
|
+
<SearchIcon size={16} />
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Count */}
|
|
203
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
204
|
+
{filtered.length} {filtered.length === 1 ? 'chat' : 'chats'}
|
|
205
|
+
</p>
|
|
206
|
+
|
|
207
|
+
{/* Chat list */}
|
|
208
|
+
{loading ? (
|
|
209
|
+
<div className="flex flex-col gap-3">
|
|
210
|
+
{[...Array(5)].map((_, i) => (
|
|
211
|
+
<div key={i} className="h-14 animate-pulse rounded-md bg-border/50" />
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
) : filtered.length === 0 ? (
|
|
215
|
+
<p className="text-sm text-muted-foreground py-8 text-center">
|
|
216
|
+
{query ? 'No chats match your search.' : 'No chats yet. Start a conversation!'}
|
|
217
|
+
</p>
|
|
218
|
+
) : (
|
|
219
|
+
<div className="flex flex-col">
|
|
220
|
+
{Object.entries(grouped).map(([label, groupChats]) =>
|
|
221
|
+
groupChats.length > 0 ? (
|
|
222
|
+
<div key={label} className="mb-4">
|
|
223
|
+
<h2 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
|
224
|
+
{label}
|
|
225
|
+
</h2>
|
|
226
|
+
<div className="flex flex-col divide-y divide-border">
|
|
227
|
+
{groupChats.map((chat) => (
|
|
228
|
+
<ChatRow
|
|
229
|
+
key={chat.id}
|
|
230
|
+
chat={chat}
|
|
231
|
+
onNavigate={navigateToChat}
|
|
232
|
+
onDelete={handleDelete}
|
|
233
|
+
onStar={handleStar}
|
|
234
|
+
onRename={handleRename}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
) : null
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</PageLayout>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
248
|
+
const [hovered, setHovered] = useState(false);
|
|
249
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
250
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
251
|
+
const [editing, setEditing] = useState(false);
|
|
252
|
+
const [editTitle, setEditTitle] = useState(chat.title || '');
|
|
253
|
+
const inputRef = useRef(null);
|
|
254
|
+
|
|
255
|
+
const showMenu = hovered || dropdownOpen;
|
|
256
|
+
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (editing && inputRef.current) {
|
|
259
|
+
inputRef.current.focus();
|
|
260
|
+
inputRef.current.select();
|
|
261
|
+
}
|
|
262
|
+
}, [editing]);
|
|
263
|
+
|
|
264
|
+
const startRename = () => {
|
|
265
|
+
setEditTitle(chat.title || '');
|
|
266
|
+
setEditing(true);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const saveRename = () => {
|
|
270
|
+
const trimmed = editTitle.trim();
|
|
271
|
+
if (trimmed && trimmed !== chat.title) {
|
|
272
|
+
onRename(chat.id, trimmed);
|
|
273
|
+
}
|
|
274
|
+
setEditing(false);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const cancelRename = () => {
|
|
278
|
+
setEditing(false);
|
|
279
|
+
setEditTitle(chat.title || '');
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<a
|
|
284
|
+
href={chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`}
|
|
285
|
+
className="relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md"
|
|
286
|
+
style={{ textDecoration: 'inherit', color: 'inherit' }}
|
|
287
|
+
onMouseEnter={() => setHovered(true)}
|
|
288
|
+
onMouseLeave={() => setHovered(false)}
|
|
289
|
+
onClick={(e) => {
|
|
290
|
+
if (editing) { e.preventDefault(); return; }
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
if (chat.codeWorkspaceId && chat.containerName) {
|
|
293
|
+
window.location.href = `/code/${chat.codeWorkspaceId}`;
|
|
294
|
+
} else {
|
|
295
|
+
onNavigate(chat.id);
|
|
296
|
+
}
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
{chat.codeWorkspaceId && chat.containerName ? <CodeIcon size={16} /> : <MessageIcon size={16} />}
|
|
300
|
+
<div className="flex-1 min-w-0">
|
|
301
|
+
{editing ? (
|
|
302
|
+
<input
|
|
303
|
+
ref={inputRef}
|
|
304
|
+
type="text"
|
|
305
|
+
value={editTitle}
|
|
306
|
+
onChange={(e) => setEditTitle(e.target.value)}
|
|
307
|
+
onKeyDown={(e) => {
|
|
308
|
+
if (e.key === 'Enter') saveRename();
|
|
309
|
+
if (e.key === 'Escape') cancelRename();
|
|
310
|
+
}}
|
|
311
|
+
onBlur={saveRename}
|
|
312
|
+
onClick={(e) => e.stopPropagation()}
|
|
313
|
+
className="w-full text-sm bg-background border border-input rounded px-1.5 py-0.5 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
314
|
+
/>
|
|
315
|
+
) : (
|
|
316
|
+
<span
|
|
317
|
+
className="text-sm truncate block"
|
|
318
|
+
onDoubleClick={(e) => {
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
startRename();
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
{chat.title || 'New Chat'}
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
326
|
+
<span className="text-xs text-muted-foreground">
|
|
327
|
+
Last message {timeAgo(chat.updatedAt)}
|
|
328
|
+
</span>
|
|
329
|
+
</div>
|
|
330
|
+
{!editing && (
|
|
331
|
+
<div className={cn(
|
|
332
|
+
'shrink-0',
|
|
333
|
+
showMenu ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
334
|
+
)}>
|
|
335
|
+
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
|
336
|
+
<DropdownMenuTrigger asChild>
|
|
337
|
+
<button
|
|
338
|
+
className={cn(
|
|
339
|
+
'rounded-md p-1.5',
|
|
340
|
+
'text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
341
|
+
)}
|
|
342
|
+
aria-label="Chat options"
|
|
343
|
+
>
|
|
344
|
+
<MoreHorizontalIcon size={14} />
|
|
345
|
+
</button>
|
|
346
|
+
</DropdownMenuTrigger>
|
|
347
|
+
<DropdownMenuContent align="end" side="bottom">
|
|
348
|
+
<DropdownMenuItem
|
|
349
|
+
onClick={(e) => {
|
|
350
|
+
e.stopPropagation();
|
|
351
|
+
onStar(chat.id);
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
{chat.starred ? <StarFilledIcon size={14} /> : <StarIcon size={14} />}
|
|
355
|
+
{chat.starred ? 'Unstar' : 'Star'}
|
|
356
|
+
</DropdownMenuItem>
|
|
357
|
+
<DropdownMenuItem
|
|
358
|
+
onClick={(e) => {
|
|
359
|
+
e.stopPropagation();
|
|
360
|
+
startRename();
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
<PencilIcon size={14} />
|
|
364
|
+
Rename
|
|
365
|
+
</DropdownMenuItem>
|
|
366
|
+
<DropdownMenuSeparator />
|
|
367
|
+
<DropdownMenuItem
|
|
368
|
+
className="text-destructive hover:text-destructive"
|
|
369
|
+
onClick={(e) => {
|
|
370
|
+
e.stopPropagation();
|
|
371
|
+
setConfirmDelete(true);
|
|
372
|
+
}}
|
|
373
|
+
>
|
|
374
|
+
<TrashIcon size={14} />
|
|
375
|
+
Delete
|
|
376
|
+
</DropdownMenuItem>
|
|
377
|
+
</DropdownMenuContent>
|
|
378
|
+
</DropdownMenu>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
<ConfirmDialog
|
|
382
|
+
open={confirmDelete}
|
|
383
|
+
title="Delete chat?"
|
|
384
|
+
description="This will permanently delete this chat and all its messages."
|
|
385
|
+
confirmLabel="Delete"
|
|
386
|
+
onConfirm={() => {
|
|
387
|
+
setConfirmDelete(false);
|
|
388
|
+
onDelete(chat.id);
|
|
389
|
+
}}
|
|
390
|
+
onCancel={() => setConfirmDelete(false)}
|
|
391
|
+
/>
|
|
392
|
+
</a>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { GitBranchIcon } from "./icons.js";
|
|
5
|
+
import { Combobox } from "./ui/combobox.js";
|
|
6
|
+
import { cn } from "../utils.js";
|
|
7
|
+
function CodeModeToggle({
|
|
8
|
+
enabled,
|
|
9
|
+
onToggle,
|
|
10
|
+
repo,
|
|
11
|
+
onRepoChange,
|
|
12
|
+
branch,
|
|
13
|
+
onBranchChange,
|
|
14
|
+
locked,
|
|
15
|
+
getRepositories,
|
|
16
|
+
getBranches
|
|
17
|
+
}) {
|
|
18
|
+
const [repos, setRepos] = useState([]);
|
|
19
|
+
const [branches, setBranches] = useState([]);
|
|
20
|
+
const [loadingRepos, setLoadingRepos] = useState(false);
|
|
21
|
+
const [loadingBranches, setLoadingBranches] = useState(false);
|
|
22
|
+
const [reposLoaded, setReposLoaded] = useState(false);
|
|
23
|
+
const handleToggle = useCallback(() => {
|
|
24
|
+
if (locked) return;
|
|
25
|
+
const next = !enabled;
|
|
26
|
+
onToggle(next);
|
|
27
|
+
if (next && !reposLoaded) {
|
|
28
|
+
setLoadingRepos(true);
|
|
29
|
+
getRepositories().then((data) => {
|
|
30
|
+
setRepos(data || []);
|
|
31
|
+
setReposLoaded(true);
|
|
32
|
+
setLoadingRepos(false);
|
|
33
|
+
}).catch(() => setLoadingRepos(false));
|
|
34
|
+
}
|
|
35
|
+
if (!next) {
|
|
36
|
+
onRepoChange("");
|
|
37
|
+
onBranchChange("");
|
|
38
|
+
setBranches([]);
|
|
39
|
+
}
|
|
40
|
+
}, [locked, enabled, reposLoaded, onToggle, onRepoChange, onBranchChange, getRepositories]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!repo || locked) return;
|
|
43
|
+
setLoadingBranches(true);
|
|
44
|
+
setBranches([]);
|
|
45
|
+
getBranches(repo).then((data) => {
|
|
46
|
+
const branchList = data || [];
|
|
47
|
+
setBranches(branchList);
|
|
48
|
+
const defaultBranch = branchList.find((b) => b.isDefault);
|
|
49
|
+
if (defaultBranch) {
|
|
50
|
+
onBranchChange(defaultBranch.name);
|
|
51
|
+
}
|
|
52
|
+
setLoadingBranches(false);
|
|
53
|
+
}).catch(() => setLoadingBranches(false));
|
|
54
|
+
}, [repo]);
|
|
55
|
+
if (!process.env.NEXT_PUBLIC_CODE_WORKSPACE) return null;
|
|
56
|
+
if (locked && enabled) {
|
|
57
|
+
return /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2.5 text-sm text-muted-foreground", children: [
|
|
58
|
+
repo && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
59
|
+
/* @__PURE__ */ jsx(GitBranchIcon, { size: 14 }),
|
|
60
|
+
/* @__PURE__ */ jsx("span", { children: repo })
|
|
61
|
+
] }),
|
|
62
|
+
branch && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
63
|
+
/* @__PURE__ */ jsx("span", { className: "opacity-40", children: "\xB7" }),
|
|
64
|
+
/* @__PURE__ */ jsx("span", { children: branch })
|
|
65
|
+
] })
|
|
66
|
+
] }) });
|
|
67
|
+
}
|
|
68
|
+
const repoOptions = repos.map((r) => ({ value: r.full_name, label: r.full_name }));
|
|
69
|
+
const branchOptions = branches.map((b) => ({ value: b.name, label: b.name }));
|
|
70
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-3", children: [
|
|
71
|
+
/* @__PURE__ */ jsxs(
|
|
72
|
+
"button",
|
|
73
|
+
{
|
|
74
|
+
type: "button",
|
|
75
|
+
onClick: handleToggle,
|
|
76
|
+
className: "inline-flex items-center gap-2 group",
|
|
77
|
+
role: "switch",
|
|
78
|
+
"aria-checked": enabled,
|
|
79
|
+
"aria-label": "Toggle Code mode",
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
"span",
|
|
83
|
+
{
|
|
84
|
+
className: cn(
|
|
85
|
+
"relative inline-flex h-5 w-9 shrink-0 rounded-full transition-colors duration-200",
|
|
86
|
+
enabled ? "bg-primary" : "bg-muted-foreground/30"
|
|
87
|
+
),
|
|
88
|
+
children: /* @__PURE__ */ jsx(
|
|
89
|
+
"span",
|
|
90
|
+
{
|
|
91
|
+
className: cn(
|
|
92
|
+
"absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform duration-200",
|
|
93
|
+
enabled && "translate-x-4"
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: cn(
|
|
100
|
+
"text-xs font-medium transition-colors",
|
|
101
|
+
enabled ? "text-foreground" : "text-muted-foreground group-hover:text-foreground"
|
|
102
|
+
), children: "Code" })
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
107
|
+
/* @__PURE__ */ jsx("div", { className: "w-full sm:w-auto sm:min-w-[220px]", children: /* @__PURE__ */ jsx(
|
|
108
|
+
Combobox,
|
|
109
|
+
{
|
|
110
|
+
options: repoOptions,
|
|
111
|
+
value: repo,
|
|
112
|
+
onChange: onRepoChange,
|
|
113
|
+
placeholder: "Select repository...",
|
|
114
|
+
loading: loadingRepos
|
|
115
|
+
}
|
|
116
|
+
) }),
|
|
117
|
+
/* @__PURE__ */ jsx("div", { className: cn("w-full sm:w-auto sm:min-w-[180px]", !repo && "opacity-50 pointer-events-none"), children: /* @__PURE__ */ jsx(
|
|
118
|
+
Combobox,
|
|
119
|
+
{
|
|
120
|
+
options: branchOptions,
|
|
121
|
+
value: branch,
|
|
122
|
+
onChange: onBranchChange,
|
|
123
|
+
placeholder: "Select branch...",
|
|
124
|
+
loading: loadingBranches
|
|
125
|
+
}
|
|
126
|
+
) })
|
|
127
|
+
] })
|
|
128
|
+
] });
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
CodeModeToggle
|
|
132
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { GitBranchIcon } from './icons.js';
|
|
5
|
+
import { Combobox } from './ui/combobox.js';
|
|
6
|
+
import { cn } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Code mode toggle with repo/branch pickers.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} props
|
|
12
|
+
* @param {boolean} props.enabled - Whether code mode is on
|
|
13
|
+
* @param {Function} props.onToggle - Toggle callback
|
|
14
|
+
* @param {string} props.repo - Selected repo
|
|
15
|
+
* @param {Function} props.onRepoChange - Repo change callback
|
|
16
|
+
* @param {string} props.branch - Selected branch
|
|
17
|
+
* @param {Function} props.onBranchChange - Branch change callback
|
|
18
|
+
* @param {boolean} props.locked - Whether the controls are locked (after first message)
|
|
19
|
+
* @param {Function} props.getRepositories - Server action to fetch repos
|
|
20
|
+
* @param {Function} props.getBranches - Server action to fetch branches
|
|
21
|
+
*/
|
|
22
|
+
export function CodeModeToggle({
|
|
23
|
+
enabled,
|
|
24
|
+
onToggle,
|
|
25
|
+
repo,
|
|
26
|
+
onRepoChange,
|
|
27
|
+
branch,
|
|
28
|
+
onBranchChange,
|
|
29
|
+
locked,
|
|
30
|
+
getRepositories,
|
|
31
|
+
getBranches,
|
|
32
|
+
}) {
|
|
33
|
+
const [repos, setRepos] = useState([]);
|
|
34
|
+
const [branches, setBranches] = useState([]);
|
|
35
|
+
const [loadingRepos, setLoadingRepos] = useState(false);
|
|
36
|
+
const [loadingBranches, setLoadingBranches] = useState(false);
|
|
37
|
+
const [reposLoaded, setReposLoaded] = useState(false);
|
|
38
|
+
|
|
39
|
+
// Load repos on first toggle-on
|
|
40
|
+
const handleToggle = useCallback(() => {
|
|
41
|
+
if (locked) return;
|
|
42
|
+
const next = !enabled;
|
|
43
|
+
onToggle(next);
|
|
44
|
+
if (next && !reposLoaded) {
|
|
45
|
+
setLoadingRepos(true);
|
|
46
|
+
getRepositories().then((data) => {
|
|
47
|
+
setRepos(data || []);
|
|
48
|
+
setReposLoaded(true);
|
|
49
|
+
setLoadingRepos(false);
|
|
50
|
+
}).catch(() => setLoadingRepos(false));
|
|
51
|
+
}
|
|
52
|
+
if (!next) {
|
|
53
|
+
onRepoChange('');
|
|
54
|
+
onBranchChange('');
|
|
55
|
+
setBranches([]);
|
|
56
|
+
}
|
|
57
|
+
}, [locked, enabled, reposLoaded, onToggle, onRepoChange, onBranchChange, getRepositories]);
|
|
58
|
+
|
|
59
|
+
// Load branches when repo changes
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!repo || locked) return;
|
|
62
|
+
setLoadingBranches(true);
|
|
63
|
+
setBranches([]);
|
|
64
|
+
getBranches(repo).then((data) => {
|
|
65
|
+
const branchList = data || [];
|
|
66
|
+
setBranches(branchList);
|
|
67
|
+
// Auto-select default branch
|
|
68
|
+
const defaultBranch = branchList.find((b) => b.isDefault);
|
|
69
|
+
if (defaultBranch) {
|
|
70
|
+
onBranchChange(defaultBranch.name);
|
|
71
|
+
}
|
|
72
|
+
setLoadingBranches(false);
|
|
73
|
+
}).catch(() => setLoadingBranches(false));
|
|
74
|
+
}, [repo]);
|
|
75
|
+
|
|
76
|
+
if (!process.env.NEXT_PUBLIC_CODE_WORKSPACE) return null;
|
|
77
|
+
|
|
78
|
+
// Locked mode: show as centered inline label
|
|
79
|
+
if (locked && enabled) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex justify-center">
|
|
82
|
+
<div className="inline-flex items-center gap-2.5 text-sm text-muted-foreground">
|
|
83
|
+
{repo && (
|
|
84
|
+
<>
|
|
85
|
+
<GitBranchIcon size={14} />
|
|
86
|
+
<span>{repo}</span>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
{branch && (
|
|
90
|
+
<>
|
|
91
|
+
<span className="opacity-40">·</span>
|
|
92
|
+
<span>{branch}</span>
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const repoOptions = repos.map((r) => ({ value: r.full_name, label: r.full_name }));
|
|
101
|
+
const branchOptions = branches.map((b) => ({ value: b.name, label: b.name }));
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
105
|
+
{/* Slide toggle + label */}
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={handleToggle}
|
|
109
|
+
className="inline-flex items-center gap-2 group"
|
|
110
|
+
role="switch"
|
|
111
|
+
aria-checked={enabled}
|
|
112
|
+
aria-label="Toggle Code mode"
|
|
113
|
+
>
|
|
114
|
+
{/* Track */}
|
|
115
|
+
<span
|
|
116
|
+
className={cn(
|
|
117
|
+
'relative inline-flex h-5 w-9 shrink-0 rounded-full transition-colors duration-200',
|
|
118
|
+
enabled ? 'bg-primary' : 'bg-muted-foreground/30'
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
{/* Knob */}
|
|
122
|
+
<span
|
|
123
|
+
className={cn(
|
|
124
|
+
'absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform duration-200',
|
|
125
|
+
enabled && 'translate-x-4'
|
|
126
|
+
)}
|
|
127
|
+
/>
|
|
128
|
+
</span>
|
|
129
|
+
{/* Label */}
|
|
130
|
+
<span className={cn(
|
|
131
|
+
'text-xs font-medium transition-colors',
|
|
132
|
+
enabled ? 'text-foreground' : 'text-muted-foreground group-hover:text-foreground'
|
|
133
|
+
)}>
|
|
134
|
+
Code
|
|
135
|
+
</span>
|
|
136
|
+
</button>
|
|
137
|
+
|
|
138
|
+
{/* Repo/branch pickers — inline, both always visible */}
|
|
139
|
+
{enabled && (
|
|
140
|
+
<>
|
|
141
|
+
<div className="w-full sm:w-auto sm:min-w-[220px]">
|
|
142
|
+
<Combobox
|
|
143
|
+
options={repoOptions}
|
|
144
|
+
value={repo}
|
|
145
|
+
onChange={onRepoChange}
|
|
146
|
+
placeholder="Select repository..."
|
|
147
|
+
loading={loadingRepos}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<div className={cn("w-full sm:w-auto sm:min-w-[180px]", !repo && "opacity-50 pointer-events-none")}>
|
|
151
|
+
<Combobox
|
|
152
|
+
options={branchOptions}
|
|
153
|
+
value={branch}
|
|
154
|
+
onChange={onBranchChange}
|
|
155
|
+
placeholder="Select branch..."
|
|
156
|
+
loading={loadingBranches}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|