tui: salvage glm-5.1 improvements; drop broken/unused code

ober

6f50852367557fa38569e828a3ec86241a40ed4d

diff --git a/build-binary.ss b/build-binary.ss
index 96a7f2f..f136327 100644
--- a/build-binary.ss
+++ b/build-binary.ss
@@ -130,6 +130,8 @@
     "lib/jcode/ui/tui-input"
     "lib/jcode/ui/tui-sidebar"
     "lib/jcode/ui/tui-dialog"
+    "lib/jcode/ui/tui-toast"
+    "lib/jcode/ui/tui-syntax"
     "lib/jcode/ui/tui"
     "lib/jcode/ui/cli"))
 
diff --git a/src/jcode/ui/tui-markdown.ss b/src/jcode/ui/tui-markdown.ss
index 5dd37d0..ceb688b 100644
--- a/src/jcode/ui/tui-markdown.ss
+++ b/src/jcode/ui/tui-markdown.ss
@@ -9,7 +9,8 @@
         :jerboa/core
         :jerboa/runtime
         :jcode/ui/tui-ffi
-        :jcode/ui/tui-theme)
+        :jcode/ui/tui-theme
+        :jcode/ui/tui-syntax)
 
 ;; ---- Markdown state (tracks code block across lines) ----
 
@@ -46,7 +47,7 @@
 
       ;; Inside code block
       ((md-state-in-code-block? state)
-       (values (list (seg line 'code-block)) state))
+        (values (highlight-line line (md-state-code-lang state)) state))
 
       ;; Headings
       ((string-prefix? "### " line)
diff --git a/src/jcode/ui/tui-message.ss b/src/jcode/ui/tui-message.ss
index d113d62..ccb6f9f 100644
--- a/src/jcode/ui/tui-message.ss
+++ b/src/jcode/ui/tui-message.ss
@@ -217,20 +217,31 @@
 
 ;; ---- Render a message block to the terminal ----
 
+(def (role->border-face role)
+  (case role
+    ((user)      'msg-border-user)
+    ((assistant) 'msg-border-assistant)
+    ((tool)      'msg-border-tool)
+    ((error)     'msg-border-error)
+    (else        'msg-border-tool)))
+
 (def (render-msg-block! msg x y width max-lines)
   "Render msg-block at position (x, y) within width, up to max-lines rows."
-  (let ((lines (msg-block-lines msg))
-        (bg-face (face-ref 'default)))
+  (let* ((lines (msg-block-lines msg))
+         (bg-face (face-ref 'default))
+         (bf (role->border-face (msg-block-role msg)))
+         (bfg (face-fg-attr bf))
+         (bbg (face-bg-attr bf)))
     (let loop ((lines lines) (row y) (count 0))
       (when (and (pair? lines) (< count max-lines))
         (let ((segs (car lines)))
-          ;; Clear the row first
+          (when (> x 0)
+            (tb-change-cell! (- x 1) row #x258e bfg bbg))
           (let clear-loop ((col x))
             (when (< col (+ x width))
               (tb-change-cell! col row (char->integer #\space)
                                (face-bg bg-face) (face-bg bg-face))
               (clear-loop (+ col 1))))
-          ;; Render segments
           (let seg-loop ((segs segs) (col x))
             (when (and (pair? segs) (< col (+ x width)))
               (let* ((s (car segs))
diff --git a/src/jcode/ui/tui-syntax.ss b/src/jcode/ui/tui-syntax.ss
new file mode 100644
index 0000000..f5768df
--- /dev/null
+++ b/src/jcode/ui/tui-syntax.ss
@@ -0,0 +1,241 @@
+;;; jcode TUI syntax highlighting
+;;; Lightweight keyword-based tokenizers for code blocks.
+
+(export
+  highlight-line
+  highlight-code-block)
+
+(import :std/misc/string
+        :jerboa/core
+        :jerboa/runtime)
+
+(def *scheme-keywords*
+  '("def" "def*" "define" "defstruct" "defclass" "defmethod" "defrule"
+    "defrecord" "lambda" "let" "let*" "letrec" "let-values"
+    "if" "cond" "case" "when" "unless" "and" "or" "not"
+    "begin" "do" "for" "for/collect" "for/fold"
+    "match" "try" "catch" "guard" "unwind-protect" "with-resource"
+    "import" "export" "parameterize" "set!" "quote" "quasiquote"
+    "syntax-rules"))
+
+(def *scheme-builtins*
+  '("car" "cdr" "cons" "list" "append" "map" "filter" "for-each"
+    "apply" "length" "reverse" "null?" "pair?" "list?"
+    "eq?" "eqv?" "equal?" "number?" "string?" "symbol?"
+    "boolean?" "procedure?" "hash-put!" "hash-ref" "hash-get"
+    "hash-key?" "hash-remove!" "hash-for-each" "hash->list"
+    "string-append" "string-length" "string-split" "string-join"
+    "string-trim" "string-prefix?" "string-suffix?" "string-contains"
+    "string-upcase" "string-downcase" "format" "printf"
+    "display" "displayln" "newline" "read" "write"
+    "open-file-input-port" "open-file-output-port" "close-port"
+    "file-exists?" "make-hash-table" "make-vector" "vector-ref"
+    "vector-set!" "vector-length" "char->integer" "integer->char"
+    "string->number" "number->string" "symbol->string" "string->symbol"))
+
+(def *bash-keywords*
+  '("if" "then" "else" "elif" "fi" "for" "while" "do" "done"
+    "case" "esac" "in" "function" "return" "exit" "export" "source"
+    "alias" "unset" "echo" "printf" "read" "cd" "pwd" "ls" "cat"
+    "mkdir" "rm" "cp" "mv" "grep" "sed" "awk" "find" "sort"
+    "uniq" "wc" "head" "tail" "cut" "tr" "chmod" "chown" "sudo"
+    "apt" "brew" "pip" "npm" "cargo" "git" "docker" "make"
+    "curl" "wget" "jq" "ssh" "scp" "rsync" "tar" "gzip"
+    "xargs" "tee" "test"))
+
+(def *python-keywords*
+  '("def" "class" "import" "from" "return" "if" "elif" "else"
+    "for" "while" "with" "as" "try" "except" "finally" "raise"
+    "yield" "lambda" "pass" "break" "continue" "and" "or" "not"
+    "in" "is" "True" "False" "None" "async" "await" "global"
+    "nonlocal" "assert" "del"))
+
+(def *js-keywords*
+  '("function" "const" "let" "var" "if" "else" "for" "while" "do"
+    "switch" "case" "break" "continue" "return" "try" "catch"
+    "finally" "throw" "new" "this" "class" "extends" "import"
+    "export" "from" "default" "async" "await" "yield" "of" "in"
+    "typeof" "instanceof" "void" "delete" "null" "undefined"
+    "true" "false"))
+
+(def *json-keywords* '("true" "false" "null"))
+
+(def *rust-keywords*
+  '("fn" "let" "mut" "const" "if" "else" "for" "while" "loop"
+    "match" "return" "struct" "enum" "impl" "trait" "pub" "use"
+    "mod" "where" "as" "in" "ref" "Self" "self" "super" "crate"
+    "async" "await" "unsafe" "extern" "type" "static" "dyn" "move"
+    "true" "false"))
+
+(def *go-keywords*
+  '("func" "var" "const" "if" "else" "for" "range" "switch" "case"
+    "default" "return" "package" "import" "type" "struct" "interface"
+    "map" "chan" "go" "defer" "select" "break" "continue"
+    "fallthrough" "goto" "nil" "true" "false"))
+
+(def *c-keywords*
+  '("int" "long" "short" "char" "void" "float" "double" "unsigned"
+    "signed" "const" "static" "extern" "volatile" "register" "auto"
+    "if" "else" "for" "while" "do" "switch" "case" "default"
+    "break" "continue" "return" "struct" "union" "enum" "typedef"
+    "sizeof" "NULL" "true" "false" "bool"))
+
+(def *language-configs*
+  `(("scheme"   . ,*scheme-keywords*)
+    ("racket"   . ,*scheme-keywords*)
+    ("r7rs"     . ,*scheme-keywords*)
+    ("lisp"     . ,*scheme-keywords*)
+    ("clojure"  . ,*scheme-keywords*)
+    ("bash"     . ,*bash-keywords*)
+    ("sh"       . ,*bash-keywords*)
+    ("shell"    . ,*bash-keywords*)
+    ("zsh"      . ,*bash-keywords*)
+    ("python"   . ,*python-keywords*)
+    ("py"       . ,*python-keywords*)
+    ("javascript" . ,*js-keywords*)
+    ("js"       . ,*js-keywords*)
+    ("typescript" . ,*js-keywords*)
+    ("ts"       . ,*js-keywords*)
+    ("json"     . ,*json-keywords*)
+    ("rust"     . ,*rust-keywords*)
+    ("go"       . ,*go-keywords*)
+    ("golang"   . ,*go-keywords*)
+    ("c"        . ,*c-keywords*)
+    ("c++"      . ,*c-keywords*)
+    ("cpp"      . ,*c-keywords*)))
+
+(def *scheme-builtins-set* #f)
+
+(def (scheme-builtins-set)
+  (or *scheme-builtins-set*
+      (begin
+        (set! *scheme-builtins-set*
+          (list->hash-table
+            (map (lambda (s) (cons s #t)) *scheme-builtins*)))
+        *scheme-builtins-set*)))
+
+(def (highlight-line line lang)
+  (let ((keywords (assoc lang *language-configs*)))
+    (if keywords
+      (highlight-with-keywords line (cdr keywords) lang)
+      (list (cons line 'code-block)))))
+
+(def (highlight-code-block lines lang)
+  (map (lambda (line) (highlight-line line lang)) lines))
+
+(def (highlight-with-keywords line keywords lang)
+  (if (string-empty? line)
+    '()
+    (let ((comment-pos (find-comment line lang)))
+      (if comment-pos
+        (let ((before (substring line 0 comment-pos))
+              (comment (substring line comment-pos (string-length line))))
+          (append (if (string-empty? before)
+                    '()
+                    (tokenize-words before keywords lang))
+                  (list (cons comment 'syntax-comment))))
+        (tokenize-words line keywords lang)))))
+
+(def (find-comment line lang)
+  (cond
+    ((member lang '("python" "py" "bash" "sh" "shell" "zsh" "ruby" "perl"))
+     (find-unquoted line #\# 0))
+    ((member lang '("scheme" "racket" "r7rs" "lisp" "clojure" "javascript" "js" "typescript" "ts" "rust" "go" "golang" "c" "c++" "cpp" "java"))
+     (find-unquoted line #\; 0))
+    (else #f)))
+
+(def (find-unquoted line ch start)
+  (let ((len (string-length line))
+        (in-string? #f)
+        (string-char #f))
+    (let loop ((i start))
+      (cond
+        ((>= i len) #f)
+        ((and (not in-string?) (char=? (string-ref line i) ch)) i)
+        ((and (not in-string?)
+              (or (char=? (string-ref line i) #\") (char=? (string-ref line i) #\')))
+         (set! in-string? #t)
+         (set! string-char (string-ref line i))
+         (loop (+ i 1)))
+        ((and in-string? (char=? (string-ref line i) string-char))
+         (set! in-string? #f)
+         (loop (+ i 1)))
+        (else (loop (+ i 1)))))))
+
+(def (tokenize-words line keywords lang)
+  (let ((len (string-length line))
+        (kw-set (list->hash-table (map (lambda (s) (cons s #t)) keywords))))
+    (let loop ((i 0) (word-start #f) (acc '()))
+      (cond
+        ((>= i len)
+         (if word-start
+           (reverse (emit-token line word-start i kw-set lang acc))
+           (reverse acc)))
+        ((in-string-at? line i)
+         (let ((str-end (find-string-end line i)))
+           (let ((new-acc (if word-start
+                            (emit-token line word-start i kw-set lang acc)
+                            acc)))
+             (loop (or str-end (string-length line)) #f
+                   (cons (cons (substring line i (or str-end (string-length line)))
+                               'syntax-string)
+                         new-acc)))))
+        ((word-char? (string-ref line i))
+         (if word-start
+           (loop (+ i 1) word-start acc)
+           (loop (+ i 1) i acc)))
+        (else
+         (let ((new-acc (if word-start
+                          (emit-token line word-start i kw-set lang acc)
+                          acc)))
+           (if (char=? (string-ref line i) #\()
+             (loop (+ i 1) #f (cons (cons "(" 'syntax-punctuation) new-acc))
+             (if (char=? (string-ref line i) #\))
+               (loop (+ i 1) #f (cons (cons ")" 'syntax-punctuation) new-acc))
+               (if (char=? (string-ref line i) #\[)
+                 (loop (+ i 1) #f (cons (cons "[" 'syntax-punctuation) new-acc))
+                 (if (char=? (string-ref line i) #\])
+                   (loop (+ i 1) #f (cons (cons "]" 'syntax-punctuation) new-acc))
+                   (loop (+ i 1) #f new-acc)))))))))))
+
+(def (emit-token line start end kw-set lang acc)
+  (let* ((word (substring line start end))
+         (face (cond
+                 ((hash-get kw-set word) 'syntax-keyword)
+                 ((and (equal? lang "scheme") (hash-get (scheme-builtins-set) word))
+                  'syntax-function)
+                 ((looks-like-number? word) 'syntax-number)
+                 (else 'code-block))))
+    (cons (cons word face) acc)))
+
+(def (word-char? ch)
+  (or (char-alphabetic? ch)
+      (char-numeric? ch)
+      (char=? ch #\_)
+      (char=? ch #\!)
+      (char=? ch #\*)
+      (char=? ch #\?)
+      (char=? ch #\+)
+      (char=? ch #\-)
+      (char=? ch #\/)))
+
+(def (in-string-at? line i)
+  (let ((ch (string-ref line i)))
+    (or (char=? ch #\") (char=? ch #\'))))
+
+(def (find-string-end line start)
+  (let ((q (string-ref line start))
+        (len (string-length line)))
+    (let loop ((i (+ start 1)))
+      (cond
+        ((>= i len) len)
+        ((char=? (string-ref line i) #\\) (loop (+ i 2)))
+        ((char=? (string-ref line i) q) (+ i 1))
+        (else (loop (+ i 1)))))))
+
+(def (looks-like-number? s)
+  (let ((len (string-length s)))
+    (and (> len 0)
+         (or (char-numeric? (string-ref s 0))
+             (and (char=? (string-ref s 0) #\#)
+                  (> len 1))))))
diff --git a/src/jcode/ui/tui-theme.ss b/src/jcode/ui/tui-theme.ss
index 1c29809..4cb0493 100644
--- a/src/jcode/ui/tui-theme.ss
+++ b/src/jcode/ui/tui-theme.ss
@@ -6,10 +6,18 @@
   current-theme set-theme!
   face-ref face-fg-attr face-bg-attr
   theme-dark theme-light theme-gruvbox
+  ;; Theme registry
+  register-theme! get-registered-themes cycle-theme-by-name!
+  ;; JSON theme loader
+  load-theme-json! load-themes-from-dir!
   ;; Helpers
   rgb truecolor)
 
-(import :jcode/ui/tui-ffi
+(import :std/text/json
+        :std/misc/string
+        :std/os/path
+        :jcode/ui/tui-ffi
+        :jcode/core/config
         :jerboa/core
         :jerboa/runtime)
 
@@ -114,6 +122,46 @@
       (dim               . ,(make-face (rgb #x80 #x80 #x80) (rgb #x1e #x1e #x1e) #f #f #f))
       (spinner           . ,(make-face (rgb #x4e #xc9 #xb0) (rgb #x1e #x1e #x1e) #f #f #f))
       (divider           . ,(make-face (rgb #x3a #x3a #x3a) (rgb #x1e #x1e #x1e) #f #f #f))
+      ;; Toast faces
+      (toast-error-border  . ,(make-face (rgb #xf4 #x47 #x47) (rgb #x1e #x1e #x1e) #f #f #f))
+      (toast-warning-border . ,(make-face (rgb #xff #xcc #x00) (rgb #x1e #x1e #x1e) #f #f #f))
+      (toast-success-border . ,(make-face (rgb #x4e #xc9 #xb0) (rgb #x1e #x1e #x1e) #f #f #f))
+      (toast-info-border   . ,(make-face (rgb #x56 #x9c #xd6) (rgb #x1e #x1e #x1e) #f #f #f))
+      (toast-bg            . ,(make-face (rgb #xd4 #xd4 #xd4) (rgb #x2d #x2d #x2d) #f #f #f))
+      (toast-title         . ,(make-face (rgb #xff #xff #xff) (rgb #x2d #x2d #x2d) #t #f #f))
+      (toast-message       . ,(make-face (rgb #xd4 #xd4 #xd4) (rgb #x2d #x2d #x2d) #f #f #f))
+      ;; Thinking faces
+      (thinking-text     . ,(make-face (rgb #x80 #x80 #x80) (rgb #x1e #x1e #x1e) #f #t #f))
+      (thinking-border   . ,(make-face (rgb #x3a #x3a #x3a) (rgb #x1e #x1e #x1e) #f #f #f))
+      (thinking-header   . ,(make-face (rgb #xa0 #xa0 #xa0) (rgb #x1e #x1e #x1e) #f #t #f))
+      ;; Syntax faces
+      (syntax-comment    . ,(make-face (rgb #x6a #x99 #x55) (rgb #x1e #x1e #x1e) #f #f #f))
+      (syntax-keyword    . ,(make-face (rgb #x56 #x9c #xd6) (rgb #x1e #x1e #x1e) #t #f #f))
+      (syntax-function   . ,(make-face (rgb #xdc #xdc #xaa) (rgb #x1e #x1e #x1e) #f #f #f))
+      (syntax-variable   . ,(make-face (rgb #x9c #xdc #xfe) (rgb #x1e #x1e #x1e) #f #f #f))
+      (syntax-string     . ,(make-face (rgb #xce #x91 #x78) (rgb #x1e #x1e #x1e) #f #f #f))
+      (syntax-number     . ,(make-face (rgb #xb5 #xce #xa8) (rgb #x1e #x1e #x1e) #f #f #f))
+      (syntax-type       . ,(make-face (rgb #x4e #xc9 #xb0) (rgb #x1e #x1e #x1e) #f #t #f))
+      (syntax-operator   . ,(make-face (rgb #xd4 #xd4 #xd4) (rgb #x1e #x1e #x1e) #f #f #f))
+      (syntax-punctuation . ,(make-face (rgb #x80 #x80 #x80) (rgb #x1e #x1e #x1e) #f #f #f))
+      ;; Selection
+      (selection-bg      . ,(make-face (rgb #xff #xff #xff) (rgb #x26 #x4f #x78) #f #f #f))
+      ;; Leader / palette
+      (leader-indicator  . ,(make-face (rgb #xff #xcc #x00) (rgb #x1e #x1e #x1e) #t #f #f))
+      (leader-dim        . ,(make-face (rgb #x80 #x80 #x80) (rgb #x1e #x1e #x1e) #f #f #f))
+      (palette-match     . ,(make-face (rgb #xff #xcc #x00) (rgb #x2d #x2d #x2d) #t #f #f))
+      (palette-keybind   . ,(make-face (rgb #x80 #x80 #x80) (rgb #x2d #x2d #x2d) #f #f #f))
+      (palette-category  . ,(make-face (rgb #x4e #xc9 #xb0) (rgb #x2d #x2d #x2d) #t #f #f))
+      ;; Home screen
+      (home-logo-fg      . ,(make-face (rgb #x56 #x9c #xd6) (rgb #x1e #x1e #x1e) #t #f #f))
+      (home-logo-shadow  . ,(make-face (rgb #x2d #x2d #x2d) (rgb #x1e #x1e #x1e) #f #f #f))
+      (home-tip-text     . ,(make-face (rgb #x80 #x80 #x80) (rgb #x1e #x1e #x1e) #f #f #f))
+      (home-tip-icon     . ,(make-face (rgb #xff #xcc #x00) (rgb #x1e #x1e #x1e) #f #f #f))
+      ;; Message borders
+      (msg-border-user     . ,(make-face (rgb #x56 #x9c #xd6) (rgb #x1e #x1e #x1e) #f #f #f))
+      (msg-border-assistant . ,(make-face (rgb #x4e #xc9 #xb0) (rgb #x1e #x1e #x1e) #f #f #f))
+      (msg-border-tool     . ,(make-face (rgb #x80 #x80 #x80) (rgb #x1e #x1e #x1e) #f #f #f))
+      (msg-border-error    . ,(make-face (rgb #xf4 #x47 #x47) (rgb #x1e #x1e #x1e) #f #f #f))
       )))
 
 (def theme-light
@@ -166,6 +214,46 @@
       (dim               . ,(make-face (rgb #xa0 #xa0 #xa0) (rgb #xff #xff #xff) #f #f #f))
       (spinner           . ,(make-face (rgb #x09 #x7c #x5a) (rgb #xff #xff #xff) #f #f #f))
       (divider           . ,(make-face (rgb #xc0 #xc0 #xc0) (rgb #xff #xff #xff) #f #f #f))
+      ;; Toast faces
+      (toast-error-border  . ,(make-face (rgb #xc0 #x22 #x22) (rgb #xff #xff #xff) #f #f #f))
+      (toast-warning-border . ,(make-face (rgb #xc0 #x7b #x00) (rgb #xff #xff #xff) #f #f #f))
+      (toast-success-border . ,(make-face (rgb #x09 #x7c #x5a) (rgb #xff #xff #xff) #f #f #f))
+      (toast-info-border   . ,(make-face (rgb #x00 #x51 #xa5) (rgb #xff #xff #xff) #f #f #f))
+      (toast-bg            . ,(make-face (rgb #x1e #x1e #x1e) (rgb #xf0 #xf0 #xf0) #f #f #f))
+      (toast-title         . ,(make-face (rgb #x1e #x1e #x1e) (rgb #xf0 #xf0 #xf0) #t #f #f))
+      (toast-message       . ,(make-face (rgb #x40 #x40 #x40) (rgb #xf0 #xf0 #xf0) #f #f #f))
+      ;; Thinking faces
+      (thinking-text     . ,(make-face (rgb #xa0 #xa0 #xa0) (rgb #xff #xff #xff) #f #t #f))
+      (thinking-border   . ,(make-face (rgb #xc0 #xc0 #xc0) (rgb #xff #xff #xff) #f #f #f))
+      (thinking-header   . ,(make-face (rgb #x80 #x80 #x80) (rgb #xff #xff #xff) #f #t #f))
+      ;; Syntax faces
+      (syntax-comment    . ,(make-face (rgb #x09 #x7c #x5a) (rgb #xff #xff #xff) #f #f #f))
+      (syntax-keyword    . ,(make-face (rgb #x00 #x51 #xa5) (rgb #xff #xff #xff) #t #f #f))
+      (syntax-function   . ,(make-face (rgb #x79 #x5e #x26) (rgb #xff #xff #xff) #f #f #f))
+      (syntax-variable   . ,(make-face (rgb #x00 #x51 #xa5) (rgb #xff #xff #xff) #f #f #f))
+      (syntax-string     . ,(make-face (rgb #xa3 #x15 #x2a) (rgb #xff #xff #xff) #f #f #f))
+      (syntax-number     . ,(make-face (rgb #x09 #x7c #x5a) (rgb #xff #xff #xff) #f #f #f))
+      (syntax-type       . ,(make-face (rgb #x09 #x7c #x5a) (rgb #xff #xff #xff) #f #t #f))
+      (syntax-operator   . ,(make-face (rgb #x1e #x1e #x1e) (rgb #xff #xff #xff) #f #f #f))
+      (syntax-punctuation . ,(make-face (rgb #x80 #x80 #x80) (rgb #xff #xff #xff) #f #f #f))
+      ;; Selection
+      (selection-bg      . ,(make-face (rgb #xff #xff #xff) (rgb #xc0 #xd0 #xf0) #f #f #f))
+      ;; Leader / palette
+      (leader-indicator  . ,(make-face (rgb #xc0 #x7b #x00) (rgb #xff #xff #xff) #t #f #f))
+      (leader-dim        . ,(make-face (rgb #xa0 #xa0 #xa0) (rgb #xff #xff #xff) #f #f #f))
+      (palette-match     . ,(make-face (rgb #xc0 #x7b #x00) (rgb #xf0 #xf0 #xf0) #t #f #f))
+      (palette-keybind   . ,(make-face (rgb #xa0 #xa0 #xa0) (rgb #xf0 #xf0 #xf0) #f #f #f))
+      (palette-category  . ,(make-face (rgb #x09 #x7c #x5a) (rgb #xf0 #xf0 #xf0) #t #f #f))
+      ;; Home screen
+      (home-logo-fg      . ,(make-face (rgb #x00 #x51 #xa5) (rgb #xff #xff #xff) #t #f #f))
+      (home-logo-shadow  . ,(make-face (rgb #xe0 #xe0 #xe0) (rgb #xff #xff #xff) #f #f #f))
+      (home-tip-text     . ,(make-face (rgb #xa0 #xa0 #xa0) (rgb #xff #xff #xff) #f #f #f))
+      (home-tip-icon     . ,(make-face (rgb #xc0 #x7b #x00) (rgb #xff #xff #xff) #f #f #f))
+      ;; Message borders
+      (msg-border-user     . ,(make-face (rgb #x00 #x51 #xa5) (rgb #xff #xff #xff) #f #f #f))
+      (msg-border-assistant . ,(make-face (rgb #x09 #x7c #x5a) (rgb #xff #xff #xff) #f #f #f))
+      (msg-border-tool     . ,(make-face (rgb #xc0 #xc0 #xc0) (rgb #xff #xff #xff) #f #f #f))
+      (msg-border-error    . ,(make-face (rgb #xc0 #x22 #x22) (rgb #xff #xff #xff) #f #f #f))
       )))
 
 (def theme-gruvbox
@@ -218,4 +306,188 @@
       (dim               . ,(make-face (rgb #xa8 #x99 #x84) (rgb #x28 #x28 #x28) #f #f #f))
       (spinner           . ,(make-face (rgb #xb8 #xbb #x26) (rgb #x28 #x28 #x28) #f #f #f))
       (divider           . ,(make-face (rgb #x50 #x49 #x45) (rgb #x28 #x28 #x28) #f #f #f))
+      ;; Toast faces
+      (toast-error-border  . ,(make-face (rgb #xfb #x49 #x34) (rgb #x28 #x28 #x28) #f #f #f))
+      (toast-warning-border . ,(make-face (rgb #xfa #xbd #x2f) (rgb #x28 #x28 #x28) #f #f #f))
+      (toast-success-border . ,(make-face (rgb #xb8 #xbb #x26) (rgb #x28 #x28 #x28) #f #f #f))
+      (toast-info-border   . ,(make-face (rgb #x83 #xa5 #x98) (rgb #x28 #x28 #x28) #f #f #f))
+      (toast-bg            . ,(make-face (rgb #xeb #xdb #xb2) (rgb #x3c #x38 #x36) #f #f #f))
+      (toast-title         . ,(make-face (rgb #xfb #xf1 #xc7) (rgb #x3c #x38 #x36) #t #f #f))
+      (toast-message       . ,(make-face (rgb #xeb #xdb #xb2) (rgb #x3c #x38 #x36) #f #f #f))
+      ;; Thinking faces
+      (thinking-text     . ,(make-face (rgb #xa8 #x99 #x84) (rgb #x28 #x28 #x28) #f #t #f))
+      (thinking-border   . ,(make-face (rgb #x50 #x49 #x45) (rgb #x28 #x28 #x28) #f #f #f))
+      (thinking-header   . ,(make-face (rgb #xa8 #x99 #x84) (rgb #x28 #x28 #x28) #f #t #f))
+      ;; Syntax faces
+      (syntax-comment    . ,(make-face (rgb #xb8 #xbb #x26) (rgb #x28 #x28 #x28) #f #f #f))
+      (syntax-keyword    . ,(make-face (rgb #xfa #xbd #x2f) (rgb #x28 #x28 #x28) #t #f #f))
+      (syntax-function   . ,(make-face (rgb #xfa #xbd #x2f) (rgb #x28 #x28 #x28) #f #f #f))
+      (syntax-variable   . ,(make-face (rgb #x83 #xa5 #x98) (rgb #x28 #x28 #x28) #f #f #f))
+      (syntax-string     . ,(make-face (rgb #xb8 #xbb #x26) (rgb #x28 #x28 #x28) #f #f #f))
+      (syntax-number     . ,(make-face (rgb #xd3 #x86 #x9b) (rgb #x28 #x28 #x28) #f #f #f))
+      (syntax-type       . ,(make-face (rgb #x83 #xa5 #x98) (rgb #x28 #x28 #x28) #f #t #f))
+      (syntax-operator   . ,(make-face (rgb #xeb #xdb #xb2) (rgb #x28 #x28 #x28) #f #f #f))
+      (syntax-punctuation . ,(make-face (rgb #xa8 #x99 #x84) (rgb #x28 #x28 #x28) #f #f #f))
+      ;; Selection
+      (selection-bg      . ,(make-face (rgb #x28 #x28 #x28) (rgb #x50 #x49 #x45) #f #f #f))
+      ;; Leader / palette
+      (leader-indicator  . ,(make-face (rgb #xfa #xbd #x2f) (rgb #x28 #x28 #x28) #t #f #f))
+      (leader-dim        . ,(make-face (rgb #xa8 #x99 #x84) (rgb #x28 #x28 #x28) #f #f #f))
+      (palette-match     . ,(make-face (rgb #xfa #xbd #x2f) (rgb #x3c #x38 #x36) #t #f #f))
+      (palette-keybind   . ,(make-face (rgb #xa8 #x99 #x84) (rgb #x3c #x38 #x36) #f #f #f))
+      (palette-category  . ,(make-face (rgb #xb8 #xbb #x26) (rgb #x3c #x38 #x36) #t #f #f))
+      ;; Home screen
+      (home-logo-fg      . ,(make-face (rgb #xfa #xbd #x2f) (rgb #x28 #x28 #x28) #t #f #f))
+      (home-logo-shadow  . ,(make-face (rgb #x50 #x49 #x45) (rgb #x28 #x28 #x28) #f #f #f))
+      (home-tip-text     . ,(make-face (rgb #xa8 #x99 #x84) (rgb #x28 #x28 #x28) #f #f #f))
+      (home-tip-icon     . ,(make-face (rgb #xfa #xbd #x2f) (rgb #x28 #x28 #x28) #f #f #f))
+      ;; Message borders
+      (msg-border-user     . ,(make-face (rgb #x83 #xa5 #x98) (rgb #x28 #x28 #x28) #f #f #f))
+      (msg-border-assistant . ,(make-face (rgb #xb8 #xbb #x26) (rgb #x28 #x28 #x28) #f #f #f))
+      (msg-border-tool     . ,(make-face (rgb #xa8 #x99 #x84) (rgb #x28 #x28 #x28) #f #f #f))
+       (msg-border-error    . ,(make-face (rgb #xfb #x49 #x34) (rgb #x28 #x28 #x28) #f #f #f))
       )))
+
+;; ---- Theme registry ----
+
+(def *theme-registry* (make-hash-table))
+
+(def (register-theme! name theme-table)
+  (hash-put! *theme-registry* name theme-table))
+
+(def (get-registered-themes)
+  (hash-keys *theme-registry*))
+
+(register-theme! "dark" theme-dark)
+(register-theme! "light" theme-light)
+(register-theme! "gruvbox" theme-gruvbox)
+
+(def (cycle-theme-by-name!)
+  (let* ((names (sort (get-registered-themes) string<?))
+         (current (current-theme))
+         (cur-name (let loop ((ns names))
+                     (cond ((null? ns) (car names))
+                           ((eq? (hash-get *theme-registry* (car ns)) current) (car ns))
+                           (else (loop (cdr ns))))))
+         (idx (let loop ((ns names) (i 0))
+                (cond ((null? ns) 0)
+                      ((equal? (car ns) cur-name) i)
+                      (else (loop (cdr ns) (+ i 1))))))
+         (next-idx (modulo (+ idx 1) (length names)))
+         (next-name (list-ref names next-idx)))
+    (set-theme! (hash-get *theme-registry* next-name))))
+
+;; ---- opencode JSON theme loader ----
+
+(def (parse-hex-color s)
+  (cond
+    ((and (string? s) (>= (string-length s) 7) (char=? (string-ref s 0) #\#))
+     (rgb (string->number (substring s 1 3) 16)
+          (string->number (substring s 3 5) 16)
+          (string->number (substring s 5 7) 16)))
+    ((and (string? s) (>= (string-length s) 5) (char=? (string-ref s 0) #\#))
+     (let ((r (string->number (substring s 1 2) 16))
+           (g (string->number (substring s 2 3) 16))
+           (b (string->number (substring s 3 4) 16)))
+       (rgb (+ (* r 17) r) (+ (* g 17) g) (+ (* b 17) b))))
+    (else TB_DEFAULT)))
+
+(def *opencode-slot-map*
+  '((primary      . (user-label input-prompt))
+    (secondary    . (status-mode-plan))
+    (accent       . (spinner tool-name heading status-mode-build))
+    (error        . (error))
+    (warning      . (toast-warning-border))
+    (success      . (status-mode-build toast-success-border))
+    (info         . (tool-name toast-info-border))
+    (text         . (default assistant-text user-text))
+    (textMuted    . (dim tool-result status-dim))
+    (background   . ())
+    (backgroundPanel . (sidebar-bg completion-item toast-bg))
+    (backgroundElement . (completion-selected code-inline))
+    (border       . (tool-border divider sidebar-divider msg-border-user msg-border-assistant msg-border-tool msg-border-error))
+    (borderActive . (dialog-border sidebar-selected))
+    (borderSubtle . (horizontal-rule blockquote-border thinking-border))
+    (diffAdded    . (diff-added))
+    (diffRemoved  . (diff-removed))
+    (diffContext  . (diff-context))
+    (diffHunkHeader . (diff-hunk))
+    (markdownHeading . (heading))
+    (markdownLink . (link))
+    (markdownCode . (code-block code-inline))
+    (markdownBlockQuote . (blockquote))
+    (markdownEmph . (italic))
+    (markdownStrong . (bold))
+    (markdownHorizontalRule . (horizontal-rule))
+    (markdownListItem . (list-bullet))
+    (syntaxComment . (syntax-comment))
+    (syntaxKeyword . (syntax-keyword))
+    (syntaxString  . (syntax-string))
+    (syntaxNumber  . (syntax-number))
+    (syntaxFunction . (syntax-function))
+    (syntaxVariable . (syntax-variable))
+    (syntaxType    . (syntax-type))
+    (syntaxOperator . (syntax-operator))
+    (syntaxPunctuation . (syntax-punctuation))))
+
+(def (resolve-defs defs val)
+  (if (and (string? val) (string-prefix? "{" val))
+    (let ((obj (try (string->json-object val) (catch (e) #f))))
+      (and (hash-table? obj) (let ((ref (hash-get obj "$ref")))
+                               (and ref (hash-get defs ref)))))
+    val))
+
+(def (json-color->face val defs variant)
+  (cond
+    ((hash-table? val)
+     (let ((c (or (hash-get val variant) (hash-get val "dark") (hash-get val "light"))))
+       (if c
+         (json-color->face c defs variant)
+         (make-face TB_DEFAULT TB_DEFAULT #f #f #f))))
+    ((string? val)
+     (let ((resolved (resolve-defs defs val)))
+       (if (string? resolved)
+         (let ((fg (parse-hex-color resolved)))
+           (make-face fg TB_DEFAULT #f #f #f))
+         (if (hash-table? resolved)
+           (json-color->face resolved defs variant)
+           (make-face TB_DEFAULT TB_DEFAULT #f #f #f)))))
+    (else (make-face TB_DEFAULT TB_DEFAULT #f #f #f))))
+
+(def (load-theme-json! path)
+  (try
+    (let* ((obj (call-with-input-file path read-json))
+           (defs (or (hash-get obj "defs") (make-hash-table)))
+           (theme-obj (or (hash-get obj "theme") obj))
+           (name (or (hash-get obj "name") "custom")))
+      (for-each
+        (lambda (variant)
+          (let ((theme-ht (make-hash-table))
+                (theme-name (string-append name "-" variant)))
+            (hash-for-each
+              (lambda (slot-key val)
+                (let ((mapped (assoc slot-key *opencode-slot-map*)))
+                  (when mapped
+                    (let ((face (json-color->face val defs variant)))
+                      (for-each
+                        (lambda (jcode-slot)
+                          (hash-put! theme-ht jcode-slot face))
+                        (cdr mapped))))))
+              theme-obj)
+            (unless (hash-get theme-ht 'default)
+              (hash-put! theme-ht 'default
+                (make-face (rgb #xd4 #xd4 #xd4)
+                  (if (equal? variant "light") (rgb #xff #xff #xff) (rgb #x1e #x1e #x1e))
+                  #f #f #f)))
+            (register-theme! theme-name theme-ht)))
+        '("dark" "light")))
+    (catch (e) #f)))
+
+(def (load-themes-from-dir! dir)
+  (when (file-exists? dir)
+    (let ((files (try (directory-list dir) (catch (e) '()))))
+      (for-each
+        (lambda (f)
+          (when (string-suffix? ".json" f)
+            (load-theme-json! (path-join dir f))))
+        files))))
diff --git a/src/jcode/ui/tui-toast.ss b/src/jcode/ui/tui-toast.ss
new file mode 100644
index 0000000..3f67f12
--- /dev/null
+++ b/src/jcode/ui/tui-toast.ss
@@ -0,0 +1,95 @@
+;;; jcode TUI toast notifications
+;;; Ephemeral feedback for state changes, errors, and confirmations.
+
+(export
+  make-toast toast? toast-variant toast-title toast-message toast-dismiss-at
+  toast-add! toast-dismiss-oldest! toast-render! toast-tick!
+  *active-toasts*)
+
+(import :jcode/ui/tui-ffi
+        :jcode/ui/tui-theme
+        :jerboa/core
+        :jerboa/runtime)
+
+(defstruct toast (variant title message dismiss-at) transparent: #t)
+
+(def *active-toasts* '())
+
+(def *toast-width* 40)
+(def *toast-display-ms* 3000)
+
+(def (toast-add! variant title message)
+  (let ((t (make-toast variant title message
+              (+ (current-time-ms) *toast-display-ms*))))
+    (set! *active-toasts* (append *active-toasts* (list t)))))
+
+(def (toast-dismiss-oldest!)
+  (when (pair? *active-toasts*)
+    (set! *active-toasts* (cdr *active-toasts*))))
+
+(def (toast-tick!)
+  (let ((now (current-time-ms)))
+    (set! *active-toasts*
+      (filter (lambda (t) (< now (toast-dismiss-at t))) *active-toasts*))))
+
+(def (current-time-ms)
+  (let ((t (current-time)))
+    (+ (* (time-second t) 1000)
+       (quotient (time-nanosecond t) 1000000))))
+
+(def (variant->border-face variant)
+  (case variant
+    ((error)   'toast-error-border)
+    ((warning) 'toast-warning-border)
+    ((success) 'toast-success-border)
+    ((info)    'toast-info-border)
+    (else      'toast-info-border)))
+
+(def (toast-render! screen-w screen-h)
+  (let loop ((toasts *active-toasts*) (offset 0))
+    (when (pair? toasts)
+      (let* ((t (car toasts))
+             (tw *toast-width*)
+             (th 3)
+             (tx (- screen-w tw 2))
+             (ty (+ 2 offset))
+             (bf (variant->border-face (toast-variant t)))
+             (bfg (face-fg-attr bf))
+             (bbg (face-bg-attr 'toast-bg))
+             (tfg (face-fg-attr 'toast-title))
+             (mfg (face-fg-attr 'toast-message)))
+        (draw-toast-box! tx ty tw th bfg bbg tfg mfg
+          (toast-title t) (toast-message t))
+        (loop (cdr toasts) (+ offset th 1))))))
+
+(def (draw-toast-box! x y w h bfg bbg tfg mfg title message)
+  (let ((bg (face-bg-attr 'toast-bg)))
+    (draw-toast-hline! x y w bfg bbg #\─)
+    (clear-toast-row! x (+ y 1) w bg)
+    (tb-change-cell! x (+ y 1) (char->integer #\│) bfg bbg)
+    (tb-change-cell! (+ x w -1) (+ y 1) (char->integer #\│) bfg bbg)
+    (tb-print! (+ x 2) (+ y 1) tfg bg
+      (truncate-str title (- w 4)))
+    (clear-toast-row! x (+ y 2) w bg)
+    (tb-change-cell! x (+ y 2) (char->integer #\│) bfg bbg)
+    (tb-change-cell! (+ x w -1) (+ y 2) (char->integer #\│) bfg bbg)
+    (tb-print! (+ x 2) (+ y 2) mfg bg
+      (truncate-str message (- w 4)))
+    (draw-toast-hline! x (+ y 3) w bfg bbg #\─)))
+
+(def (draw-toast-hline! x y w fg bg ch)
+  (let loop ((col x))
+    (when (< col (+ x w))
+      (tb-change-cell! col y (char->integer ch) fg bg)
+      (loop (+ col 1)))))
+
+(def (clear-toast-row! x y w bg)
+  (let loop ((col x))
+    (when (< col (+ x w))
+      (tb-change-cell! col y (char->integer #\space) bg bg)
+      (loop (+ col 1)))))
+
+(def (truncate-str s max-len)
+  (if (> (string-length s) max-len)
+    (string-append (substring s 0 (- max-len 3)) "...")
+    s))
diff --git a/src/jcode/ui/tui.ss b/src/jcode/ui/tui.ss
index feb85f7..9142839 100644
--- a/src/jcode/ui/tui.ss
+++ b/src/jcode/ui/tui.ss
@@ -16,6 +16,7 @@
         :jcode/ui/tui-input
         :jcode/ui/tui-sidebar
         :jcode/ui/tui-dialog
+        :jcode/ui/tui-toast
         :jcode/core/config
         :jcode/core/models
         :jcode/core/agent
@@ -149,13 +150,14 @@
 
 ;; ---- Layout calculations ----
 
-(def (msg-area-x state) 0)
+(def (msg-area-x state) 1)
 (def (msg-area-y state) 0)
 (def (msg-area-width state)
   (- (app-state-width state)
      (if (app-state-sidebar-visible? state)
        (+ (app-state-sidebar-width state) 1)
-       0)))
+       0)
+     1))
 (def (msg-area-height state)
   (- (app-state-height state)
      (app-state-input-height state)
@@ -192,6 +194,8 @@
       (tb-set-input-mode! in-mode)
       (tb-set-output-mode! TB_OUTPUT_TRUECOLOR))
     (set-theme! theme-dark)
+    (load-themes-from-dir! (path-join (jcode-home) "themes"))
+    (load-themes-from-dir! (path-join (current-directory) ".jcode" "themes"))
     (let* ((w (tb-width))
            (h (tb-height))
            (state (make-fresh-state w h))
@@ -340,6 +344,7 @@
       ((= key TB_KEY_CTRL_T)
        (tui-log "  -> cycle-theme")
        (cycle-theme!)
+       (toast-add! 'success "Theme" "Theme cycled")
        (app-state-dirty?-set! state #t))
 
       ;; Global: Shift-Tab (BACK_TAB) toggle PLAN/BUILD mode (opencode-style)
@@ -392,7 +397,6 @@
            (app-state-dirty?-set! state #t)))))))
 
 (def (handle-mouse! state ev)
-  ;; Mouse wheel scrolling
   (let ((key (tui-event-key ev)))
     (cond
       ((= key TB_KEY_MOUSE_WHEEL_UP)
@@ -419,7 +423,6 @@
         ;; Normal message → agent
         (#t
          (tui-log "submit: sending to agent")
-         ;; Add user message
          (add-message! state (msg-block-user text))
          ;; Auto-scroll to bottom
          (app-state-scroll-offset-set! state 0)
@@ -580,8 +583,8 @@
               (add-message! state (msg-block-user (format "/~a" cmd)))
               (app-state-scroll-offset-set! state 0)
               (run-agent! state prompt)))
-           (else
-            (add-message! state (msg-block-system (format "Unknown command: /~a" cmd))))))))))
+            (else
+             (add-message! state (msg-block-system (format "Unknown command: /~a" cmd))))))))))
 
 ;; ---- Provider / model switching ----
 
@@ -989,12 +992,8 @@
 
 ;; ---- Theme cycling ----
 
-(def *themes* (list theme-dark theme-light theme-gruvbox))
-(def *theme-idx* 0)
-
 (def (cycle-theme!)
-  (set! *theme-idx* (modulo (+ *theme-idx* 1) (length *themes*)))
-  (set-theme! (list-ref *themes* *theme-idx*)))
+  (cycle-theme-by-name!))
 
 ;; ---- Drawing ----
 
@@ -1042,7 +1041,12 @@
     ;; Dialog overlay (on top of everything)
     (when (app-state-dialog state)
       (render-dialog! (app-state-dialog state)
-        (app-state-width state) (app-state-height state)))))
+        (app-state-width state) (app-state-height state)))
+
+    ;; Toast notifications (top-right, on top of everything)
+    (toast-tick!)
+    (when (pair? *active-toasts*)
+      (toast-render! (app-state-width state) (app-state-height state)))))
 
 (def (draw-messages! state)
   (let* ((x (msg-area-x state))
diff --git a/themes/catppuccin.json b/themes/catppuccin.json
new file mode 100644
index 0000000..48e8252
--- /dev/null
+++ b/themes/catppuccin.json
@@ -0,0 +1,112 @@
+{
+  "$schema": "https://opencode.ai/theme.json",
+  "defs": {
+    "lightRosewater": "#dc8a78",
+    "lightFlamingo": "#dd7878",
+    "lightPink": "#ea76cb",
+    "lightMauve": "#8839ef",
+    "lightRed": "#d20f39",
+    "lightMaroon": "#e64553",
+    "lightPeach": "#fe640b",
+    "lightYellow": "#df8e1d",
+    "lightGreen": "#40a02b",
+    "lightTeal": "#179299",
+    "lightSky": "#04a5e5",
+    "lightSapphire": "#209fb5",
+    "lightBlue": "#1e66f5",
+    "lightLavender": "#7287fd",
+    "lightText": "#4c4f69",
+    "lightSubtext1": "#5c5f77",
+    "lightSubtext0": "#6c6f85",
+    "lightOverlay2": "#7c7f93",
+    "lightOverlay1": "#8c8fa1",
+    "lightOverlay0": "#9ca0b0",
+    "lightSurface2": "#acb0be",
+    "lightSurface1": "#bcc0cc",
+    "lightSurface0": "#ccd0da",
+    "lightBase": "#eff1f5",
+    "lightMantle": "#e6e9ef",
+    "lightCrust": "#dce0e8",
+    "darkRosewater": "#f5e0dc",
+    "darkFlamingo": "#f2cdcd",
+    "darkPink": "#f5c2e7",
+    "darkMauve": "#cba6f7",
+    "darkRed": "#f38ba8",
+    "darkMaroon": "#eba0ac",
+    "darkPeach": "#fab387",
+    "darkYellow": "#f9e2af",
+    "darkGreen": "#a6e3a1",
+    "darkTeal": "#94e2d5",
+    "darkSky": "#89dceb",
+    "darkSapphire": "#74c7ec",
+    "darkBlue": "#89b4fa",
+    "darkLavender": "#b4befe",
+    "darkText": "#cdd6f4",
+    "darkSubtext1": "#bac2de",
+    "darkSubtext0": "#a6adc8",
+    "darkOverlay2": "#9399b2",
+    "darkOverlay1": "#7f849c",
+    "darkOverlay0": "#6c7086",
+    "darkSurface2": "#585b70",
+    "darkSurface1": "#45475a",
+    "darkSurface0": "#313244",
+    "darkBase": "#1e1e2e",
+    "darkMantle": "#181825",
+    "darkCrust": "#11111b"
+  },
+  "theme": {
+    "primary": { "dark": "darkBlue", "light": "lightBlue" },
+    "secondary": { "dark": "darkMauve", "light": "lightMauve" },
+    "accent": { "dark": "darkPink", "light": "lightPink" },
+    "error": { "dark": "darkRed", "light": "lightRed" },
+    "warning": { "dark": "darkYellow", "light": "lightYellow" },
+    "success": { "dark": "darkGreen", "light": "lightGreen" },
+    "info": { "dark": "darkTeal", "light": "lightTeal" },
+    "text": { "dark": "darkText", "light": "lightText" },
+    "textMuted": { "dark": "darkOverlay2", "light": "lightOverlay2" },
+    "background": { "dark": "darkBase", "light": "lightBase" },
+    "backgroundPanel": { "dark": "darkMantle", "light": "lightMantle" },
+    "backgroundElement": { "dark": "darkCrust", "light": "lightCrust" },
+    "border": { "dark": "darkSurface0", "light": "lightSurface0" },
+    "borderActive": { "dark": "darkSurface1", "light": "lightSurface1" },
+    "borderSubtle": { "dark": "darkSurface2", "light": "lightSurface2" },
+    "diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
+    "diffRemoved": { "dark": "darkRed", "light": "lightRed" },
+    "diffContext": { "dark": "darkOverlay2", "light": "lightOverlay2" },
+    "diffHunkHeader": { "dark": "darkPeach", "light": "lightPeach" },
+    "diffHighlightAdded": { "dark": "darkGreen", "light": "lightGreen" },
+    "diffHighlightRemoved": { "dark": "darkRed", "light": "lightRed" },
+    "diffAddedBg": { "dark": "#24312b", "light": "#d6f0d9" },
+    "diffRemovedBg": { "dark": "#3c2a32", "light": "#f6dfe2" },
+    "diffContextBg": { "dark": "darkMantle", "light": "lightMantle" },
+    "diffLineNumber": { "dark": "darkSurface1", "light": "lightSurface1" },
+    "diffAddedLineNumberBg": { "dark": "#1e2a25", "light": "#c9e3cb" },
+    "diffRemovedLineNumberBg": { "dark": "#32232a", "light": "#e9d3d6" },
+    "markdownText": { "dark": "darkText", "light": "lightText" },
+    "markdownHeading": { "dark": "darkMauve", "light": "lightMauve" },
+    "markdownLink": { "dark": "darkBlue", "light": "lightBlue" },
+    "markdownLinkText": { "dark": "darkSky", "light": "lightSky" },
+    "markdownCode": { "dark": "darkGreen", "light": "lightGreen" },
+    "markdownBlockQuote": { "dark": "darkYellow", "light": "lightYellow" },
+    "markdownEmph": { "dark": "darkYellow", "light": "lightYellow" },
+    "markdownStrong": { "dark": "darkPeach", "light": "lightPeach" },
+    "markdownHorizontalRule": {
+      "dark": "darkSubtext0",
+      "light": "lightSubtext0"
+    },
+    "markdownListItem": { "dark": "darkBlue", "light": "lightBlue" },
+    "markdownListEnumeration": { "dark": "darkSky", "light": "lightSky" },
+    "markdownImage": { "dark": "darkBlue", "light": "lightBlue" },
+    "markdownImageText": { "dark": "darkSky", "light": "lightSky" },
+    "markdownCodeBlock": { "dark": "darkText", "light": "lightText" },
+    "syntaxComment": { "dark": "darkOverlay2", "light": "lightOverlay2" },
+    "syntaxKeyword": { "dark": "darkMauve", "light": "lightMauve" },
+    "syntaxFunction": { "dark": "darkBlue", "light": "lightBlue" },
+    "syntaxVariable": { "dark": "darkRed", "light": "lightRed" },
+    "syntaxString": { "dark": "darkGreen", "light": "lightGreen" },
+    "syntaxNumber": { "dark": "darkPeach", "light": "lightPeach" },
+    "syntaxType": { "dark": "darkYellow", "light": "lightYellow" },
+    "syntaxOperator": { "dark": "darkSky", "light": "lightSky" },
+    "syntaxPunctuation": { "dark": "darkText", "light": "lightText" }
+  }
+}
diff --git a/themes/dracula.json b/themes/dracula.json
new file mode 100644
index 0000000..c837a0b
--- /dev/null
+++ b/themes/dracula.json
@@ -0,0 +1,219 @@
+{
+  "$schema": "https://opencode.ai/theme.json",
+  "defs": {
+    "background": "#282a36",
+    "currentLine": "#44475a",
+    "selection": "#44475a",
+    "foreground": "#f8f8f2",
+    "comment": "#6272a4",
+    "cyan": "#8be9fd",
+    "green": "#50fa7b",
+    "orange": "#ffb86c",
+    "pink": "#ff79c6",
+    "purple": "#bd93f9",
+    "red": "#ff5555",
+    "yellow": "#f1fa8c"
+  },
+  "theme": {
+    "primary": {
+      "dark": "purple",
+      "light": "purple"
+    },
+    "secondary": {
+      "dark": "pink",
+      "light": "pink"
+    },
+    "accent": {
+      "dark": "cyan",
+      "light": "cyan"
+    },
+    "error": {
+      "dark": "red",
+      "light": "red"
+    },
+    "warning": {
+      "dark": "yellow",
+      "light": "yellow"
+    },
+    "success": {
+      "dark": "green",
+      "light": "green"
+    },
+    "info": {
+      "dark": "orange",
+      "light": "orange"
+    },
+    "text": {
+      "dark": "foreground",
+      "light": "#282a36"
+    },
+    "textMuted": {
+      "dark": "comment",
+      "light": "#6272a4"
+    },
+    "background": {
+      "dark": "#282a36",
+      "light": "#f8f8f2"
+    },
+    "backgroundPanel": {
+      "dark": "#21222c",
+      "light": "#e8e8e2"
+    },
+    "backgroundElement": {
+      "dark": "currentLine",
+      "light": "#d8d8d2"
+    },
+    "border": {
+      "dark": "currentLine",
+      "light": "#c8c8c2"
+    },
+    "borderActive": {
+      "dark": "purple",
+      "light": "purple"
+    },
+    "borderSubtle": {