diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt
index 315f79b50..7e5633dee 100644
--- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt
@@ -1,17 +1,26 @@
 package net.mamoe.mirai.console.graphical
 
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
 import net.mamoe.mirai.console.graphical.view.PrimaryView
 import tornadofx.App
+import tornadofx.find
 import tornadofx.launch
 
 fun main(args: Array<String>) {
-    launch<MainApp>(args)
+    launch<MiraiGraphicalUI>(args)
 }
 
-class MainApp: App(PrimaryView::class) {
+class MiraiGraphicalUI: App(PrimaryView::class) {
 
     override fun init() {
         super.init()
 
+        MiraiConsole.start(find<MiraiGraphicalUIController>())
+    }
+
+    override fun stop() {
+        super.stop()
+        MiraiConsole.stop()
     }
 }
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiController.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiController.kt
deleted file mode 100644
index e8220e847..000000000
--- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiController.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package net.mamoe.mirai.console.graphical.controller
-
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.console.MiraiConsoleUI
-import net.mamoe.mirai.utils.LoginSolver
-import tornadofx.Controller
-
-class MiraiController : Controller(), MiraiConsoleUI {
-    override fun pushLog(identity: Long, message: String) {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-    }
-
-    override fun prePushBot(identity: Long) {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-    }
-
-    override fun pushBot(bot: Bot) {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-    }
-
-    override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-    }
-
-    override suspend fun requestInput(question: String): String {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-    }
-
-    override fun pushBotAdminStatus(identity: Long, admins: List<Long>) {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-    }
-
-    override fun createLoginSolver(): LoginSolver {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-    }
-}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt
new file mode 100644
index 000000000..d35d65ca6
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt
@@ -0,0 +1,85 @@
+package net.mamoe.mirai.console.graphical.controller
+
+import javafx.application.Platform
+import javafx.stage.Modality
+import kotlinx.io.core.IoBuffer
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.MiraiConsoleUI
+import net.mamoe.mirai.console.graphical.model.BotModel
+import net.mamoe.mirai.console.graphical.model.ConsoleInfo
+import net.mamoe.mirai.console.graphical.model.VerificationCodeModel
+import net.mamoe.mirai.console.graphical.view.VerificationCodeFragment
+import net.mamoe.mirai.utils.LoginSolver
+import tornadofx.Controller
+import tornadofx.Scope
+import tornadofx.find
+import tornadofx.observableListOf
+
+class MiraiGraphicalUIController : Controller(), MiraiConsoleUI {
+
+    private val loginSolver = GraphicalLoginSolver()
+    private val cache = mutableMapOf<Long, BotModel>()
+    val mainLog = observableListOf<String>()
+    val botList = observableListOf<BotModel>()
+    val consoleInfo = ConsoleInfo()
+
+    fun login(qq: String, psd: String) {
+        MiraiConsole.CommandListener.commandChannel.offer("/login $qq $psd")
+    }
+
+    override fun pushLog(identity: Long, message: String) = Platform.runLater {
+        when (identity) {
+            0L -> mainLog.add(message)
+            else -> cache[identity]?.logHistory?.add(message)
+        }
+    }
+
+    override fun prePushBot(identity: Long) = Platform.runLater {
+        BotModel(identity).also {
+            cache[identity] = it
+            botList.add(it)
+        }
+    }
+
+    override fun pushBot(bot: Bot) = Platform.runLater {
+        cache[bot.uin]?.bot = bot
+    }
+
+    override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) {
+        Platform.runLater {
+            consoleInfo.consoleVersion = consoleVersion
+            consoleInfo.consoleBuild = consoleBuild
+            consoleInfo.coreVersion = coreVersion
+        }
+    }
+
+    override suspend fun requestInput(question: String): String {
+        val model = VerificationCodeModel()
+        find<VerificationCodeFragment>(Scope(model)).openModal(
+            modality = Modality.APPLICATION_MODAL,
+            resizable = false
+        )
+        return model.code.value
+    }
+
+    override fun pushBotAdminStatus(identity: Long, admins: List<Long>) = Platform.runLater {
+        cache[identity]?.admins?.setAll(admins)
+    }
+
+    override fun createLoginSolver(): LoginSolver = loginSolver
+}
+
+class GraphicalLoginSolver : LoginSolver() {
+    override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
+        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+    }
+
+    override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
+        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+    }
+
+    override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
+        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+    }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt
new file mode 100644
index 000000000..8d15d6998
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt
@@ -0,0 +1,13 @@
+package net.mamoe.mirai.console.graphical.model
+
+import javafx.beans.property.SimpleObjectProperty
+import net.mamoe.mirai.Bot
+import tornadofx.*
+
+class BotModel(val uin: Long) {
+    val botProperty = SimpleObjectProperty<Bot>(null)
+    var bot: Bot by botProperty
+
+    val logHistory = observableListOf<String>()
+    val admins = observableListOf<Long>()
+}
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt
new file mode 100644
index 000000000..9c43ecc1e
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt
@@ -0,0 +1,17 @@
+package net.mamoe.mirai.console.graphical.model
+
+import javafx.beans.property.SimpleStringProperty
+import tornadofx.setValue
+import tornadofx.getValue
+
+class ConsoleInfo {
+
+    val consoleVersionProperty = SimpleStringProperty()
+    var consoleVersion by consoleVersionProperty
+
+    val consoleBuildProperty = SimpleStringProperty()
+    var consoleBuild by consoleBuildProperty
+
+    val coreVersionProperty = SimpleStringProperty()
+    var coreVersion by coreVersionProperty
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt
new file mode 100644
index 000000000..5582cfa8c
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt
@@ -0,0 +1,17 @@
+package net.mamoe.mirai.console.graphical.model
+
+import javafx.beans.property.SimpleStringProperty
+import tornadofx.ItemViewModel
+import tornadofx.getValue
+import tornadofx.setValue
+
+class VerificationCode {
+    val codeProperty = SimpleStringProperty("")
+    var code: String by codeProperty
+}
+
+class VerificationCodeModel(code: VerificationCode) : ItemViewModel<VerificationCode>(code) {
+    constructor(): this(VerificationCode())
+
+    val code = bind(VerificationCode::codeProperty)
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginFragment.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginFragment.kt
new file mode 100644
index 000000000..1290b7808
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginFragment.kt
@@ -0,0 +1,27 @@
+package net.mamoe.mirai.console.graphical.view
+
+import javafx.beans.property.SimpleStringProperty
+import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
+import tornadofx.*
+
+class LoginFragment : Fragment() {
+
+    private val controller = find<MiraiGraphicalUIController>(FX.defaultScope)
+    private val qq = SimpleStringProperty()
+    private val psd = SimpleStringProperty()
+
+    override val root = form {
+        fieldset("登录") {
+            field("QQ") {
+                textfield(qq)
+            }
+            field("密码") {
+                passwordfield(psd)
+            }
+            button("登录").action {
+                controller.login(qq.value, psd.value)
+                close()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt
index 5cded1380..1900e25cc 100644
--- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt
@@ -1,11 +1,56 @@
 package net.mamoe.mirai.console.graphical.view
 
-import tornadofx.View
-import tornadofx.borderpane
+import javafx.scene.control.TabPane
+import javafx.stage.Modality
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
+import tornadofx.*
 
 class PrimaryView : View() {
 
+    private val controller = find<MiraiGraphicalUIController>()
+
     override val root = borderpane {
 
+        top = menubar {
+            menu("机器人") {
+                item("登录").action {
+                    find<LoginFragment>().openModal(
+                        modality = Modality.APPLICATION_MODAL,
+                        resizable = false
+                    )
+                }
+            }
+        }
+
+        left = listview(controller.botList) {
+            fitToParentHeight()
+
+            cellFormat {
+
+                graphic = vbox {
+                    label(it.uin.toString())
+//                    label(stringBinding(it.botProperty) { if (value != null) value.nick else "登陆中" })
+                }
+
+                onDoubleClick {
+                    (center as TabPane).tab(it.uin.toString()) {
+                        listview(it.logHistory)
+
+                        isClosable = true
+                        select()
+                    }
+                }
+            }
+        }
+
+        center = tabpane {
+            tab("Main") {
+                listview(controller.mainLog)
+
+                isClosable = false
+            }
+        }
     }
+
 }
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/VerificationCodeFragment.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/VerificationCodeFragment.kt
new file mode 100644
index 000000000..7e386ba8a
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/VerificationCodeFragment.kt
@@ -0,0 +1,19 @@
+package net.mamoe.mirai.console.graphical.view
+
+import javafx.scene.Parent
+import tornadofx.*
+
+class VerificationCodeFragment : Fragment() {
+
+    override val root = vbox {
+        //TODO: 显示验证码
+
+        form {
+            fieldset {
+                field("验证码") {
+                    textfield()
+                }
+            }
+        }
+    }
+}
\ No newline at end of file