+ {/* ββ Header / Tab bar ββ */}
+
+
+ π€
+ AI Agent
+
+
+
+ {(['chat', 'connectors'] as View[]).map((v) => (
+
+ ))}
+
+
+ {view === 'chat' && connectedAIs.length > 0 && (
+
+ {connectedAIs.map((ai) => (
+
+ ))}
+
+ )}
+
+
+
+
+ {/* ββ Chat view ββ */}
+ {view === 'chat' && (
+
+
+ {messages.length === 0 && (
+
+
π€
+
Vespertide AI Agent
+ {connectedAIs.length === 0 ? (
+
+ AIκ° μ°κ²°λμ§ μμμ΅λλ€.
+
+
+
+ ) : (
+
+ "μ΄ μ€ν€λ§λ₯Ό Notionμ μ 리ν΄μ€"
+ "λ§μ΄κ·Έλ μ΄μ
λ³κ²½μ¬νμ SlackμΌλ‘ 보λ΄μ€"
+ "Post ν
μ΄λΈμ μΈλ±μ€λ₯Ό μΆκ°νλ©΄ μ’μκΉ?"
+
+ )}
+
+ )}
+
+ {messages.map((m, i) => {
+ const isUser = m.role === 'user';
+ return (
+
+ {!isUser && (
+
π€
+ )}
+
+ {m.content}
+
+
+ );
+ })}
+
+ {loading && (
+
+ π€
+ μλ΅ μμ± μ€...
+
+ )}
+
+
+
+
+
+
+ )}
+
+ {/* ββ Connectors view ββ */}
+ {view === 'connectors' && (
+
+
+ {CONNECTORS.filter((c) => c.isAI).map((meta) => (
+ { setActiveAI(meta.service); setView('chat'); }}
+ onConfigure={() => setConfiguring(configuring === meta.service ? null : meta.service)}
+ onKeyChange={(v) => setKeyInputs((p) => ({ ...p, [meta.service]: v }))}
+ onToggleShow={() => setShowKey((p) => ({ ...p, [meta.service]: !p[meta.service] }))}
+ onSave={() => saveKey(meta.service)}
+ onDisconnect={() => disconnect(meta.service)}
+ onOpenBrowser={() => openBrowser(meta.getKeyUrl)}
+ />
+ ))}
+
+
+ {CONNECTORS.filter((c) => !c.isAI).map((meta) => (
+ {}}
+ onConfigure={() => setConfiguring(configuring === meta.service ? null : meta.service)}
+ onKeyChange={(v) => setKeyInputs((p) => ({ ...p, [meta.service]: v }))}
+ onToggleShow={() => setShowKey((p) => ({ ...p, [meta.service]: !p[meta.service] }))}
+ onSave={() => saveKey(meta.service)}
+ onDisconnect={() => disconnect(meta.service)}
+ onOpenBrowser={() => openBrowser(meta.getKeyUrl)}
+ />
+ ))}
+
+ )}
+
+ );
+}
+
+// ββ Sub-components ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function SectionLabel({ label, style }: { label: string; style?: React.CSSProperties }) {
+ return (
+
+ {/* Row */}
+
+
{meta.icon}
+
+
+
+ {meta.label}
+ {isActive && (
+ μ¬μ© μ€
+ )}
+
+
+ {connected
+ ? β μ°κ²°λ¨
+ : {meta.subtitle}
+ }
+
+
+
+
+ {connected && meta.isAI && !isActive && (
+
+ )}
+ {connected ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Config panel */}
+ {configuring && (
+
+ {/* Step-by-step guide */}
+
+
+ ν€ λ°κΈ λ°©λ²
+
+ {meta.steps.map((step, i) => (
+
+ {i + 1}.
+ {step}
+
+ ))}
+
+
+
+
+
+
+ onKeyChange(e.target.value)}
+ placeholder={meta.keyPlaceholder}
+ onKeyDown={(e) => e.key === 'Enter' && onSave()}
+ style={{
+ flex: 1, padding: '7px 10px', borderRadius: 4, fontSize: 12,
+ background: theme === 'light' ? '#ffffff' : 'var(--vscode-input-background, rgba(255,255,255,0.07))',
+ border: `1px solid ${theme === 'light' ? 'rgba(0,0,0,0.2)' : 'var(--vscode-input-border, rgba(255,255,255,0.15))'}`,
+ color: 'var(--node-text)', outline: 'none',
+ }}
+ />
+
+
+
+
+
+ {connected && (
+
+ )}
+
+
+ )}
+
+ );
+}
+
+function btn(variant: 'primary' | 'ghost' | 'active' | 'danger'): React.CSSProperties {
+ const base: React.CSSProperties = {
+ padding: '4px 12px', borderRadius: 4, fontSize: 11,
+ cursor: 'pointer', fontFamily: 'inherit',
+ };
+ if (variant === 'primary') return { ...base, background: 'var(--vscode-button-background, #0e639c)', color: 'var(--vscode-button-foreground, #fff)', border: 'none' };
+ if (variant === 'ghost') return { ...base, background: 'transparent', color: 'var(--node-text)', border: '1px solid var(--node-border)' };
+ if (variant === 'active') return { ...base, background: 'rgba(99,102,241,0.2)', color: '#818cf8', border: '1px solid rgba(99,102,241,0.4)' };
+ if (variant === 'danger') return { ...base, background: 'rgba(239,68,68,0.12)', color: 'var(--diff-rm-sign)', border: '1px solid rgba(239,68,68,0.2)' };
+ return base;
+}
diff --git a/apps/extension-webview/src/App.tsx b/apps/extension-webview/src/App.tsx
new file mode 100644
index 0000000..7c0df03
--- /dev/null
+++ b/apps/extension-webview/src/App.tsx
@@ -0,0 +1,269 @@
+import React, { useState, useEffect } from 'react';
+import { Box, Flex } from '@devup-ui/react';
+import { onMessage } from './vscode';
+import type { HostMessage, OrmType, Schema } from './vscode';
+import OrmEditor from './tabs/OrmEditor';
+import MigrationDiff from './tabs/MigrationDiff';
+import Export from './tabs/Export';
+
+type Tab = 'editor' | 'migration' | 'export';
+
+export const DEFAULT_SCHEMAS: Record