2
0
Эх сурвалжийг харах

wip: css/ui and desktop work

Adam 4 сар өмнө
parent
commit
47d9e01765
52 өөрчлөгдсөн 532 нэмэгдсэн , 1588 устгасан
  1. 2 1
      package.json
  2. 3 2
      packages/css/package.json
  3. BIN
      packages/css/src/assets/fonts/geist-italic.ttf
  4. BIN
      packages/css/src/assets/fonts/geist-italic.woff2
  5. BIN
      packages/css/src/assets/fonts/geist-mono-italic.ttf
  6. BIN
      packages/css/src/assets/fonts/geist-mono-italic.woff2
  7. BIN
      packages/css/src/assets/fonts/geist-mono.ttf
  8. BIN
      packages/css/src/assets/fonts/geist-mono.woff2
  9. BIN
      packages/css/src/assets/fonts/geist.ttf
  10. BIN
      packages/css/src/assets/fonts/geist.woff2
  11. 6 25
      packages/css/src/base.css
  12. 36 35
      packages/css/src/components/button.css
  13. 52 50
      packages/css/src/components/select.css
  14. 25 28
      packages/css/src/components/tabs.css
  15. 53 0
      packages/css/src/components/tooltip.css
  16. 1 0
      packages/css/src/index.css
  17. 3 2
      packages/css/src/tailwind.css
  18. 19 9
      packages/css/src/theme.css
  19. 2 3
      packages/desktop/index.html
  20. 2 1
      packages/desktop/package.json
  21. 0 163
      packages/desktop/scripts/vite-theme-plugin.ts
  22. 115 122
      packages/desktop/src/components/editor-pane.tsx
  23. 2 1
      packages/desktop/src/components/file-tree.tsx
  24. 2 2
      packages/desktop/src/components/prompt-form.tsx
  25. 0 217
      packages/desktop/src/components/resizeable-pane.tsx
  26. 2 1
      packages/desktop/src/components/select-dialog.tsx
  27. 0 108
      packages/desktop/src/components/select.tsx
  28. 1 1
      packages/desktop/src/components/session-list.tsx
  29. 2 1
      packages/desktop/src/components/session-timeline.tsx
  30. 0 48
      packages/desktop/src/components/sidebar-nav.tsx
  31. 0 1
      packages/desktop/src/context/index.ts
  32. 25 25
      packages/desktop/src/context/local.tsx
  33. 0 92
      packages/desktop/src/context/theme.tsx
  34. 1 168
      packages/desktop/src/index.css
  35. 22 31
      packages/desktop/src/index.tsx
  36. 56 54
      packages/desktop/src/pages/index.tsx
  37. 0 5
      packages/desktop/src/pages/layout.tsx
  38. 0 36
      packages/desktop/src/ui/button.tsx
  39. 1 1
      packages/desktop/src/ui/collapsible.tsx
  40. 0 109
      packages/desktop/src/ui/icon.tsx
  41. 0 5
      packages/desktop/src/ui/index.ts
  42. 0 13
      packages/desktop/src/ui/link.tsx
  43. 0 125
      packages/desktop/src/ui/logo.tsx
  44. 0 71
      packages/desktop/src/ui/tabs.tsx
  45. 0 2
      packages/desktop/vite.config.ts
  46. 2 1
      packages/ui/package.json
  47. 32 6
      packages/ui/src/app.tsx
  48. 44 0
      packages/ui/src/components/fonts.tsx
  49. 2 0
      packages/ui/src/components/index.ts
  50. 4 18
      packages/ui/src/components/tooltip.tsx
  51. 6 4
      packages/ui/src/index.css
  52. 9 1
      packages/ui/src/index.tsx

+ 2 - 1
package.json

@@ -25,6 +25,8 @@
       "@tsconfig/bun": "1.0.9",
       "@cloudflare/workers-types": "4.20251008.0",
       "@openauthjs/openauth": "0.0.0-20250322224806",
+      "@solidjs/meta": "0.29.4",
+      "@tailwindcss/vite": "4.1.11",
       "diff": "8.0.2",
       "ai": "5.0.8",
       "hono": "4.7.10",
@@ -36,7 +38,6 @@
       "remeda": "2.26.0",
       "solid-js": "1.9.9",
       "tailwindcss": "4.1.11",
-      "@tailwindcss/vite": "4.1.11",
       "vite": "7.1.4",
       "vite-plugin-solid": "2.11.8"
     }

+ 3 - 2
packages/css/package.json

@@ -1,10 +1,11 @@
 {
   "name": "@opencode-ai/css",
-  "version": "0.15.5",
+  "version": "0.15.4",
   "type": "module",
   "exports": {
     ".": "./src/index.css",
-    "./*": "./src/*"
+    "./*": "./src/*",
+    "./fonts/*": "./src/assets/fonts/*"
   },
   "scripts": {
     "dev": "bun run dev.ts",

BIN
packages/css/src/assets/fonts/geist-italic.ttf


BIN
packages/css/src/assets/fonts/geist-italic.woff2


BIN
packages/css/src/assets/fonts/geist-mono-italic.ttf


BIN
packages/css/src/assets/fonts/geist-mono-italic.woff2


BIN
packages/css/src/assets/fonts/geist-mono.ttf


BIN
packages/css/src/assets/fonts/geist-mono.woff2


BIN
packages/css/src/assets/fonts/geist.ttf


BIN
packages/css/src/assets/fonts/geist.woff2


+ 6 - 25
packages/css/src/base.css

@@ -30,18 +30,9 @@ html,
   line-height: 1.5; /* 1 */
   -webkit-text-size-adjust: 100%; /* 2 */
   tab-size: 4; /* 3 */
-  font-family: --theme(
-    --default-font-family,
-    ui-sans-serif,
-    system-ui,
-    sans-serif,
-    "Apple Color Emoji",
-    "Segoe UI Emoji",
-    "Segoe UI Symbol",
-    "Noto Color Emoji"
-  ); /* 4 */
-  font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */
-  font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */
+  font-family: var(--font-sans); /* 4 */
+  font-feature-settings: var(--font-sans--font-feature-settings, normal); /* 5 */
+  font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */
   -webkit-tap-highlight-color: transparent; /* 7 */
 }
 
@@ -110,19 +101,9 @@ code,
 kbd,
 samp,
 pre {
-  font-family: --theme(
-    --default-mono-font-family,
-    ui-monospace,
-    SFMono-Regular,
-    Menlo,
-    Monaco,
-    Consolas,
-    "Liberation Mono",
-    "Courier New",
-    monospace
-  ); /* 1 */
-  font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */
-  font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */
+  font-family: var(--font-mono); /* 1 */
+  font-feature-settings: var(--font-mono--font-feature-settings, normal); /* 2 */
+  font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 3 */
   font-size: 1em; /* 4 */
 }
 

+ 36 - 35
packages/css/src/components/button.css

@@ -6,76 +6,66 @@
   border-style: solid;
   border-width: 1px;
   border-radius: var(--radius-md);
-  font-family: var(--font-sans);
   font-size: var(--text-base);
   line-height: var(--text-base--line-height);
   font-weight: var(--font-weight-normal);
   text-decoration: none;
   user-select: none;
-  gap: calc(var(--spacing) * 2);
-
-  &:disabled {
-    opacity: 0.5;
-    cursor: not-allowed;
-  }
-
-  &:focus {
-    outline: none;
-  }
+  gap: calc(var(--spacing) * 0.5);
 
   &[data-variant="primary"] {
-    border-color: var(--border-default-border);
-    background-color: var(--surface-brand-surface-brand);
-    color: var(--text-on-brand-text-strong-on-brand);
+    border-color: var(--border-base);
+    background-color: var(--surface-brand-base);
+    color: var(--text-on-brand-strong);
 
     &:hover:not(:disabled) {
-      border-color: var(--border-default-border-hover);
-      background-color: var(--surface-brand-surface-brand-hover);
+      border-color: var(--border-hover);
+      background-color: var(--surface-brand-hover);
     }
     &:active:not(:disabled) {
-      border-color: var(--border-default-border-active);
-      background-color: var(--surface-brand-surface-brand-active);
+      border-color: var(--border-active);
+      background-color: var(--surface-brand-active);
     }
     &:focus:not(:disabled) {
-      border-color: var(--border-default-border-focus);
-      background-color: var(--surface-brand-surface-brand-focus);
+      border-color: var(--border-focus);
+      background-color: var(--surface-brand-focus);
     }
   }
 
   &[data-variant="secondary"] {
-    border-color: var(--border-default-border);
-    background-color: var(--surface-default-surface);
-    color: var(--text-default-text);
+    border-color: var(--border-base);
+    background-color: var(--surface-base);
+    color: var(--text-strong);
 
     &:hover:not(:disabled) {
-      border-color: var(--border-default-border-hover);
-      background-color: var(--surface-default-surface-hover);
+      border-color: var(--border-hover);
+      background-color: var(--surface-hover);
     }
     &:active:not(:disabled) {
-      border-color: var(--border-default-border-active);
-      background-color: var(--surface-default-surface-active);
+      border-color: var(--border-active);
+      background-color: var(--surface-active);
     }
     &:focus:not(:disabled) {
-      border-color: var(--border-default-border-focus);
-      background-color: var(--surface-default-surface-focus);
+      border-color: var(--border-focus);
+      background-color: var(--surface-focus);
     }
   }
 
   &[data-variant="ghost"] {
     border-color: transparent;
     background-color: transparent;
-    color: var(--text-default-text);
+    color: var(--text-strong);
 
     &:hover:not(:disabled) {
-      background-color: var(--surface-default-surface-hover);
+      background-color: var(--surface-hover);
     }
     &:active:not(:disabled) {
-      border-color: var(--border-default-border-active);
-      background-color: var(--surface-default-surface-active);
+      border-color: var(--border-active);
+      background-color: var(--surface-active);
     }
     &:focus:not(:disabled) {
-      border-color: var(--border-default-border-focus);
-      background-color: var(--surface-default-surface-focus);
+      border-color: var(--border-focus);
+      background-color: var(--surface-focus);
     }
   }
 
@@ -90,4 +80,15 @@
     font-size: var(--text-sm);
     line-height: var(--text-sm--line-height);
   }
+
+  &:disabled {
+    border-color: var(--border-disabled);
+    background-color: var(--surface-disabled);
+    color: var(--text-weak);
+    cursor: not-allowed;
+  }
+
+  &:focus {
+    outline: none;
+  }
 }

+ 52 - 50
packages/css/src/components/select.css

@@ -1,12 +1,49 @@
 [data-component="select"] {
-  &:disabled {
-    opacity: 0.5;
-    cursor: not-allowed;
+  [data-slot="trigger"] {
+    [data-slot="value"] {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    [data-slot="icon"] {
+      width: fit-content;
+      height: fit-content;
+      flex-shrink: 0;
+      color: var(--text-weak);
+      transition: transform 0.1s ease-in-out;
+    }
   }
+}
 
-  &:focus {
-    outline: none;
-    box-shadow: 0 0 0 2px var(--color-primary);
+[data-component="select-content"] {
+  min-width: 8rem;
+  overflow: hidden;
+  border-radius: var(--radius-md);
+  border-width: 1px;
+  border-style: solid;
+  border-color: var(--border-weak-base);
+  background-color: var(--surface-raised-base);
+  padding: calc(var(--spacing) * 1);
+  box-shadow: var(--shadow-md);
+  z-index: 50;
+
+  &[data-closed] {
+    animation: select-close 0.15s ease-out;
+  }
+
+  &[data-expanded] {
+    animation: select-open 0.15s ease-out;
+  }
+
+  [data-slot="list"] {
+    overflow-y: auto;
+    max-height: 12rem;
+    white-space: nowrap;
+    overflow-x: hidden;
+
+    &:focus {
+      outline: none;
+    }
   }
 
   [data-slot="section"] {
@@ -14,7 +51,7 @@
     line-height: var(--text-xs--line-height);
     font-weight: var(--font-weight-light);
     text-transform: uppercase;
-    color: var(--text-default-text-weak);
+    color: var(--text-weak);
     opacity: 0.6;
     margin-top: calc(var(--spacing) * 3);
     margin-left: calc(var(--spacing) * 2);
@@ -31,7 +68,7 @@
     border-radius: var(--radius-sm);
     font-size: var(--text-xs);
     line-height: var(--text-xs--line-height);
-    color: var(--text-default-text);
+    color: var(--text-base);
     cursor: pointer;
     transition:
       background-color 0.2s ease-in-out,
@@ -40,60 +77,25 @@
     user-select: none;
 
     &[data-highlighted] {
-      background-color: var(--surface-default-surface);
+      background-color: var(--surface-base);
     }
 
     &[data-disabled] {
+      background-color: var(--surface-disabled);
       pointer-events: none;
-      opacity: 0.5;
     }
 
     [data-slot="item-indicator"] {
       margin-left: auto;
     }
-  }
 
-  [data-slot="trigger"] {
-    [data-slot="value"] {
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
+    &:focus {
+      outline: none;
     }
-    [data-slot="icon"] {
-      width: fit-content;
-      height: fit-content;
-      flex-shrink: 0;
-      color: var(--text-default-text-weak);
-      transition: transform 0.1s ease-in-out;
-    }
-  }
-}
-
-[data-component="select-content"] {
-  min-width: 8rem;
-  overflow: hidden;
-  border-radius: var(--radius-md);
-  border-width: 1px;
-  border-style: solid;
-  border-color: var(--border-default-border-weak);
-  background-color: var(--surface-raised-surface-raised);
-  padding: calc(var(--spacing) * 1);
-  box-shadow: var(--shadow-md);
-  z-index: 50;
 
-  &[data-closed] {
-    animation: select-close 0.15s ease-out;
-  }
-
-  &[data-expanded] {
-    animation: select-open 0.15s ease-out;
-  }
-
-  [data-slot="list"] {
-    overflow-y: auto;
-    max-height: 12rem;
-    white-space: nowrap;
-    overflow-x: hidden;
+    &:hover {
+      background-color: var(--surface-hover);
+    }
   }
 }
 

+ 25 - 28
packages/css/src/components/tabs.css

@@ -1,98 +1,95 @@
 [data-component="tabs"] {
+  width: 100%;
+  height: 100%;
   display: flex;
   flex-direction: column;
-  height: 100%;
+  border-width: 1px;
+  border-style: solid;
+  border-radius: var(--radius-sm);
+  border-color: var(--border-weak-base);
+  background-color: var(--background-weaker);
+  overflow: clip;
 
   & [data-slot="list"] {
+    width: 100%;
     position: relative;
     display: flex;
     align-items: center;
-    background-color: var(--surface-default-surface);
     overflow-x: auto;
 
     /* Hide scrollbar */
     scrollbar-width: none;
     -ms-overflow-style: none;
-
     &::-webkit-scrollbar {
       display: none;
     }
 
-    /* Divider between tabs */
-    & > [data-slot="trigger"]:not(:first-child) {
-      border-left: 1px solid var(--border-default-border-weak);
-    }
-
     /* After element to fill remaining space */
     &::after {
       content: "";
       display: block;
       flex-grow: 1;
-      height: calc(var(--spacing) * 8);
-      border-left: 1px solid var(--border-default-border-weak);
-      border-bottom: 1px solid var(--border-default-border-weak);
+      height: 100%;
+      border-bottom: 1px solid var(--border-weak-base);
+      background-color: var(--background-weak);
+      border-top-right-radius: var(--radius-sm);
     }
 
     &:empty::after {
-      border-left: none;
+      display: none;
     }
   }
 
   & [data-slot="trigger"] {
     position: relative;
-    padding: 0 calc(var(--spacing) * 3);
-    height: calc(var(--spacing) * 8);
+    height: 36px;
+    padding: 8px 12px;
     display: flex;
     align-items: center;
     font-size: var(--text-sm);
     font-weight: var(--font-weight-medium);
-    color: var(--text-default-text-weak);
+    color: var(--text-weak);
     cursor: pointer;
     white-space: nowrap;
     flex-shrink: 0;
-    border-bottom: 1px solid var(--border-default-border-weak);
-    background-color: transparent;
+    border-bottom: 1px solid var(--border-weak-base);
+    border-right: 1px solid var(--border-weak-base);
+    background-color: var(--background-weak);
     transition:
       background-color 0.15s ease,
       color 0.15s ease;
 
     &:disabled {
       pointer-events: none;
-      opacity: 0.5;
+      color: var(--text-weaker);
     }
-
     &:focus-visible {
       outline: none;
-      box-shadow: 0 0 0 2px var(--border-default-border-focus);
+      box-shadow: 0 0 0 2px var(--border-focus);
     }
-
     &[data-selected] {
-      color: var(--text-default-text);
-      background-color: var(--surface-panel-surface);
+      color: var(--text-base);
+      background-color: transparent;
       border-bottom-color: transparent;
     }
-
     &:hover:not(:disabled):not([data-selected]) {
-      color: var(--text-default-text);
+      color: var(--text-strong);
     }
   }
 
   & [data-slot="content"] {
-    background-color: var(--surface-panel-surface);
     overflow-y: auto;
     flex: 1;
 
     /* Hide scrollbar */
     scrollbar-width: none;
     -ms-overflow-style: none;
-
     &::-webkit-scrollbar {
       display: none;
     }
 
     &:focus-visible {
       outline: none;
-      box-shadow: 0 0 0 2px var(--border-default-border-focus);
     }
   }
 }

+ 53 - 0
packages/css/src/components/tooltip.css

@@ -0,0 +1,53 @@
+/* [data-component="tooltip-trigger"] { */
+/*   display: flex; */
+/*   align-items: center; */
+/* } */
+
+[data-component="tooltip"] {
+  z-index: 1000;
+  max-width: 320px;
+  border-radius: var(--radius-md);
+  background-color: var(--surface-base);
+  padding: calc(var(--spacing) * 0.5) calc(var(--spacing) * 1);
+  font-size: var(--text-xs);
+  font-weight: var(--font-weight-medium);
+  color: var(--text-base);
+  box-shadow: var(--shadow-md);
+  pointer-events: none !important;
+  transition: all 150ms ease-out;
+  transform: translate3d(0, 0, 0);
+  transform-origin: var(--kb-tooltip-content-transform-origin);
+
+  &[data-expanded] {
+    opacity: 1;
+    transform: translate3d(0, 0, 0);
+  }
+
+  &[data-closed] {
+    opacity: 0;
+  }
+
+  &[data-placement="top"] {
+    &[data-closed] {
+      transform: translate3d(0, 4px, 0);
+    }
+  }
+
+  &[data-placement="bottom"] {
+    &[data-closed] {
+      transform: translate3d(0, -4px, 0);
+    }
+  }
+
+  &[data-placement="left"] {
+    &[data-closed] {
+      transform: translate3d(4px, 0, 0);
+    }
+  }
+
+  &[data-placement="right"] {
+    &[data-closed] {
+      transform: translate3d(-4px, 0, 0);
+    }
+  }
+}

+ 1 - 0
packages/css/src/index.css

@@ -9,5 +9,6 @@
 @import "./components/icon.css" layer(components);
 @import "./components/select.css" layer(components);
 @import "./components/tabs.css" layer(components);
+@import "./components/tooltip.css" layer(components);
 
 @import "./utilities.css" layer(utilities);

+ 3 - 2
packages/css/src/tailwind.css

@@ -1,8 +1,9 @@
-@import "./index.css";
-
+@layer theme, base, components, utilities;
 @import "tailwindcss/theme.css" layer(theme);
 @import "tailwindcss/utilities.css" layer(utilities);
 
+@import "./index.css";
+
 @theme {
   --shadow-*: initial;
   --shadow-xs-border-selected: var(--shadow-xs-border-selected);

+ 19 - 9
packages/css/src/theme.css

@@ -1,8 +1,13 @@
 :root {
   --font-sans:
-    ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+    geist, geist-fallback, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
+    "Segoe UI Symbol", "Noto Color Emoji";
+  --font-sans--font-feature-settings: "ss02" 1;
   --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
-  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  --font-mono:
+    geist-mono, geist-mono-fallback, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+    "Courier New", monospace;
+  --font-mono--font-feature-settings: "ss02" 1;
 
   --size-12: 12;
   --size-14: 14;
@@ -99,13 +104,18 @@
   --radius-3xl: 1.5rem;
   --radius-4xl: 2rem;
 
-  --shadow-2xs: 0 1px rgb(0 0 0 / 0.05);
-  --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
-  --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
-  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
-  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
-  --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
-  --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
+  --shadow-xs-border-selected:
+    0 0 0 3px var(--border-weak-selected, rgba(1, 103, 255, 0.29)),
+    0 0 0 1px var(--border-selected, rgba(0, 74, 255, 0.99)), 0 1px 2px -1px rgba(19, 16, 16, 0.25),
+    0 1px 2px 0 rgba(19, 16, 16, 0.08), 0 1px 3px 0 rgba(19, 16, 16, 0.12);
+
+  /* --shadow-2xs: 0 1px rgb(0 0 0 / 0.05); */
+  /* --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); */
+  /* --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); */
+  /* --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); */
+  /* --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); */
+  /* --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); */
+  /* --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); */
 
   /* --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05); */
   /* --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05); */

+ 2 - 3
packages/desktop/index.html

@@ -1,12 +1,11 @@
 <!doctype html>
-<html lang="en" class="h-full bg-background">
+<html lang="en" class="h-full bg-background-weak">
   <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="theme-color" content="#000000" />
     <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.svg" />
-    <link rel="stylesheet" href="/src/assets/theme.css" />
-    <title>opencode</title>
+    <title>OpenCode</title>
   </head>
   <body class="h-full overscroll-none select-none">
     <script>

+ 2 - 1
packages/desktop/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/desktop",
-  "version": "0.15.5",
+  "version": "0.15.4",
   "description": "",
   "type": "module",
   "scripts": {
@@ -29,6 +29,7 @@
     "@solid-primitives/event-bus": "1.1.2",
     "@solid-primitives/resize-observer": "2.1.3",
     "@solid-primitives/scroll": "2.1.3",
+    "@solidjs/meta": "catalog:",
     "@solidjs/router": "0.15.3",
     "@thisbeyond/solid-dnd": "0.7.5",
     "diff": "catalog:",

+ 0 - 163
packages/desktop/scripts/vite-theme-plugin.ts

@@ -1,163 +0,0 @@
-import type { Plugin } from "vite"
-import { readdir, readFile, writeFile } from "fs/promises"
-import { join, resolve } from "path"
-
-interface ThemeDefinition {
-  $schema?: string
-  defs?: Record<string, string>
-  theme: Record<string, any>
-}
-
-interface ResolvedThemeColor {
-  dark: string
-  light: string
-}
-
-class ColorResolver {
-  private colors: Map<string, any> = new Map()
-  private visited: Set<string> = new Set()
-
-  constructor(defs: Record<string, string> = {}, theme: Record<string, any> = {}) {
-    Object.entries(defs).forEach(([key, value]) => {
-      this.colors.set(key, value)
-    })
-    Object.entries(theme).forEach(([key, value]) => {
-      this.colors.set(key, value)
-    })
-  }
-
-  resolveColor(key: string, value: any): ResolvedThemeColor {
-    if (this.visited.has(key)) {
-      throw new Error(`Circular reference detected for color ${key}`)
-    }
-
-    this.visited.add(key)
-
-    try {
-      if (typeof value === "string") {
-        if (value === "none") return { dark: value, light: value }
-        if (value.startsWith("#")) {
-          return { dark: value.toLowerCase(), light: value.toLowerCase() }
-        }
-        const resolved = this.resolveReference(value)
-        return { dark: resolved, light: resolved }
-      }
-      if (typeof value === "object" && value !== null) {
-        const dark = this.resolveColorValue(value.dark || value.light || "#000000")
-        const light = this.resolveColorValue(value.light || value.dark || "#FFFFFF")
-        return { dark, light }
-      }
-      return { dark: "#000000", light: "#FFFFFF" }
-    } finally {
-      this.visited.delete(key)
-    }
-  }
-
-  private resolveColorValue(value: any): string {
-    if (typeof value === "string") {
-      if (value === "none") return value
-      if (value.startsWith("#")) {
-        return value.toLowerCase()
-      }
-      return this.resolveReference(value)
-    }
-    return value
-  }
-
-  private resolveReference(ref: string): string {
-    const colorValue = this.colors.get(ref)
-    if (colorValue === undefined) {
-      throw new Error(`Color reference '${ref}' not found`)
-    }
-    if (typeof colorValue === "string") {
-      if (colorValue === "none") return colorValue
-      if (colorValue.startsWith("#")) {
-        return colorValue.toLowerCase()
-      }
-      return this.resolveReference(colorValue)
-    }
-    return colorValue
-  }
-}
-
-function kebabCase(str: string): string {
-  return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
-}
-
-function parseTheme(themeData: ThemeDefinition): Record<string, ResolvedThemeColor> {
-  const resolver = new ColorResolver(themeData.defs, themeData.theme)
-  const colors: Record<string, ResolvedThemeColor> = {}
-  Object.entries(themeData.theme).forEach(([key, value]) => {
-    colors[key] = resolver.resolveColor(key, value)
-  })
-  return colors
-}
-
-async function loadThemes(): Promise<Record<string, Record<string, ResolvedThemeColor>>> {
-  const themesDir = resolve(__dirname, "../../tui/internal/theme/themes")
-  const files = await readdir(themesDir)
-  const themes: Record<string, Record<string, ResolvedThemeColor>> = {}
-
-  for (const file of files) {
-    if (!file.endsWith(".json")) continue
-
-    const themeName = file.replace(".json", "")
-    const themeData: ThemeDefinition = JSON.parse(await readFile(join(themesDir, file), "utf-8"))
-
-    themes[themeName] = parseTheme(themeData)
-  }
-
-  return themes
-}
-
-function generateCSS(themes: Record<string, Record<string, ResolvedThemeColor>>): string {
-  let css = `/* Auto-generated theme CSS - Do not edit manually */\n:root {\n`
-
-  const defaultTheme = themes["opencode"] || Object.values(themes)[0]
-  if (defaultTheme) {
-    Object.entries(defaultTheme).forEach(([key, color]) => {
-      const cssVar = `--theme-${kebabCase(key)}`
-      css += `  ${cssVar}: ${color.light};\n`
-    })
-  }
-  css += `}\n\n`
-
-  Object.entries(themes).forEach(([themeName, colors]) => {
-    css += `[data-theme="${themeName}"][data-dark="false"] {\n`
-    Object.entries(colors).forEach(([key, color]) => {
-      const cssVar = `--theme-${kebabCase(key)}`
-      css += `  ${cssVar}: ${color.light};\n`
-    })
-    css += `}\n\n`
-
-    css += `[data-theme="${themeName}"][data-dark="true"] {\n`
-    Object.entries(colors).forEach(([key, color]) => {
-      const cssVar = `--theme-${kebabCase(key)}`
-      css += `  ${cssVar}: ${color.dark};\n`
-    })
-    css += `}\n\n`
-  })
-
-  return css
-}
-
-export function generateThemeCSS(): Plugin {
-  return {
-    name: "generate-theme-css",
-    async buildStart() {
-      try {
-        console.log("Generating theme CSS...")
-        const themes = await loadThemes()
-        const css = generateCSS(themes)
-
-        const outputPath = resolve(__dirname, "../src/assets/theme.css")
-        await writeFile(outputPath, css)
-
-        console.log(`✅ Generated theme CSS with ${Object.keys(themes).length} themes`)
-        console.log(`   Output: ${outputPath}`)
-      } catch (error) {
-        throw new Error(`Theme CSS generation failed: ${error}`)
-      }
-    },
-  }
-}

+ 115 - 122
packages/desktop/src/components/editor-pane.tsx

@@ -1,6 +1,7 @@
 import { For, Match, Show, Switch, createSignal, splitProps } from "solid-js"
-import { Tabs } from "@/ui/tabs"
-import { FileIcon, Icon, IconButton, Logo, Tooltip } from "@/ui"
+import { Tabs, Tooltip } from "@opencode-ai/ui"
+import { Icon } from "@opencode-ai/ui"
+import { FileIcon, IconButton } from "@/ui"
 import {
   DragDropProvider,
   DragDropSensors,
@@ -64,127 +65,119 @@ export default function EditorPane(props: EditorPaneProps): JSX.Element {
   }
 
   return (
-    <div class="relative flex h-full flex-col">
-      <DragDropProvider
-        onDragStart={handleDragStart}
-        onDragEnd={handleDragEnd}
-        onDragOver={handleDragOver}
-        collisionDetector={closestCenter}
-      >
-        <DragDropSensors />
-        <ConstrainDragYAxis />
-        <Tabs
-          class="relative grow w-full flex flex-col h-full"
-          value={local.file.active()?.path}
-          onChange={handleTabChange}
-        >
-          <div class="sticky top-0 shrink-0 flex">
-            <Tabs.List class="grow">
-              <SortableProvider ids={local.file.opened().map((file) => file.path)}>
-                <For each={local.file.opened()}>
-                  {(file) => (
-                    <SortableTab file={file} onTabClick={localProps.onFileClick} onTabClose={handleTabClose} />
-                  )}
-                </For>
-              </SortableProvider>
-            </Tabs.List>
-            <div class="shrink-0 h-full flex items-center gap-1 px-2 border-b border-border-subtle/40">
-              <Show when={local.file.active() && local.file.active()!.content?.diff}>
-                {(() => {
-                  const activeFile = local.file.active()!
-                  const view = local.file.view(activeFile.path)
-                  return (
-                    <div class="flex items-center gap-1">
-                      <Show when={view !== "raw"}>
-                        <div class="mr-1 flex items-center gap-1">
-                          <Tooltip value="Previous change" placement="bottom">
-                            <IconButton size="xs" variant="ghost" onClick={() => navigateChange(-1)}>
-                              <Icon name="arrow-up" size={14} />
-                            </IconButton>
-                          </Tooltip>
-                          <Tooltip value="Next change" placement="bottom">
-                            <IconButton size="xs" variant="ghost" onClick={() => navigateChange(1)}>
-                              <Icon name="arrow-down" size={14} />
-                            </IconButton>
-                          </Tooltip>
-                        </div>
-                      </Show>
-                      <Tooltip value="Raw" placement="bottom">
-                        <IconButton
-                          size="xs"
-                          variant="ghost"
-                          classList={{
-                            "text-text": view === "raw",
-                            "text-text-muted/70": view !== "raw",
-                            "bg-background-element": view === "raw",
-                          }}
-                          onClick={() => local.file.setView(activeFile.path, "raw")}
-                        >
-                          <Icon name="file-text" size={14} />
-                        </IconButton>
-                      </Tooltip>
-                      <Tooltip value="Unified diff" placement="bottom">
-                        <IconButton
-                          size="xs"
-                          variant="ghost"
-                          classList={{
-                            "text-text": view === "diff-unified",
-                            "text-text-muted/70": view !== "diff-unified",
-                            "bg-background-element": view === "diff-unified",
-                          }}
-                          onClick={() => local.file.setView(activeFile.path, "diff-unified")}
-                        >
-                          <Icon name="checklist" size={14} />
-                        </IconButton>
-                      </Tooltip>
-                      <Tooltip value="Split diff" placement="bottom">
-                        <IconButton
-                          size="xs"
-                          variant="ghost"
-                          classList={{
-                            "text-text": view === "diff-split",
-                            "text-text-muted/70": view !== "diff-split",
-                            "bg-background-element": view === "diff-split",
-                          }}
-                          onClick={() => local.file.setView(activeFile.path, "diff-split")}
-                        >
-                          <Icon name="columns" size={14} />
-                        </IconButton>
-                      </Tooltip>
-                    </div>
-                  )
-                })()}
-              </Show>
-            </div>
+    <DragDropProvider
+      onDragStart={handleDragStart}
+      onDragEnd={handleDragEnd}
+      onDragOver={handleDragOver}
+      collisionDetector={closestCenter}
+    >
+      <DragDropSensors />
+      <ConstrainDragYAxis />
+      <Tabs value={local.file.active()?.path} onChange={handleTabChange}>
+        <div class="sticky top-0 shrink-0 flex">
+          <Tabs.List>
+            <SortableProvider ids={local.file.opened().map((file) => file.path)}>
+              <For each={local.file.opened()}>
+                {(file) => <SortableTab file={file} onTabClick={localProps.onFileClick} onTabClose={handleTabClose} />}
+              </For>
+            </SortableProvider>
+          </Tabs.List>
+          <div class="hidden shrink-0 h-full _flex items-center gap-1 px-2 border-b border-border-subtle/40">
+            <Show when={local.file.active() && local.file.active()!.content?.diff}>
+              {(() => {
+                const activeFile = local.file.active()!
+                const view = local.file.view(activeFile.path)
+                return (
+                  <div class="flex items-center gap-1">
+                    <Show when={view !== "raw"}>
+                      <div class="mr-1 flex items-center gap-1">
+                        <Tooltip value="Previous change" placement="bottom">
+                          <IconButton size="xs" variant="ghost" onClick={() => navigateChange(-1)}>
+                            <Icon name="arrow-up" size={14} />
+                          </IconButton>
+                        </Tooltip>
+                        <Tooltip value="Next change" placement="bottom">
+                          <IconButton size="xs" variant="ghost" onClick={() => navigateChange(1)}>
+                            <Icon name="arrow-down" size={14} />
+                          </IconButton>
+                        </Tooltip>
+                      </div>
+                    </Show>
+                    <Tooltip value="Raw" placement="bottom">
+                      <IconButton
+                        size="xs"
+                        variant="ghost"
+                        classList={{
+                          "text-text": view === "raw",
+                          "text-text-muted/70": view !== "raw",
+                          "bg-background-element": view === "raw",
+                        }}
+                        onClick={() => local.file.setView(activeFile.path, "raw")}
+                      >
+                        <Icon name="file-text" size={14} />
+                      </IconButton>
+                    </Tooltip>
+                    <Tooltip value="Unified diff" placement="bottom">
+                      <IconButton
+                        size="xs"
+                        variant="ghost"
+                        classList={{
+                          "text-text": view === "diff-unified",
+                          "text-text-muted/70": view !== "diff-unified",
+                          "bg-background-element": view === "diff-unified",
+                        }}
+                        onClick={() => local.file.setView(activeFile.path, "diff-unified")}
+                      >
+                        <Icon name="checklist" size={14} />
+                      </IconButton>
+                    </Tooltip>
+                    <Tooltip value="Split diff" placement="bottom">
+                      <IconButton
+                        size="xs"
+                        variant="ghost"
+                        classList={{
+                          "text-text": view === "diff-split",
+                          "text-text-muted/70": view !== "diff-split",
+                          "bg-background-element": view === "diff-split",
+                        }}
+                        onClick={() => local.file.setView(activeFile.path, "diff-split")}
+                      >
+                        <Icon name="columns" size={14} />
+                      </IconButton>
+                    </Tooltip>
+                  </div>
+                )
+              })()}
+            </Show>
           </div>
-          <For each={local.file.opened()}>
-            {(file) => (
-              <Tabs.Content value={file.path} class="grow h-full pt-1 select-text">
-                {(() => {
-                  const view = local.file.view(file.path)
-                  const showRaw = view === "raw" || !file.content?.diff
-                  const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "")
-                  return <Code path={file.path} code={code} class="[&_code]:pb-60" />
-                })()}
-              </Tabs.Content>
-            )}
-          </For>
-        </Tabs>
-        <DragOverlay>
-          {(() => {
-            const id = activeItem()
-            if (!id) return null
-            const draggedFile = local.file.node(id)
-            if (!draggedFile) return null
-            return (
-              <div class="relative px-3 h-8 flex items-center text-sm font-medium text-text whitespace-nowrap shrink-0 bg-background-panel border-x border-border-subtle/40 border-b border-b-transparent">
-                <TabVisual file={draggedFile} />
-              </div>
-            )
-          })()}
-        </DragOverlay>
-      </DragDropProvider>
-    </div>
+        </div>
+        <For each={local.file.opened()}>
+          {(file) => (
+            <Tabs.Content value={file.path} class="select-text">
+              {(() => {
+                const view = local.file.view(file.path)
+                const showRaw = view === "raw" || !file.content?.diff
+                const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "")
+                return <Code path={file.path} code={code} class="[&_code]:pb-60" />
+              })()}
+            </Tabs.Content>
+          )}
+        </For>
+      </Tabs>
+      <DragOverlay>
+        {(() => {
+          const id = activeItem()
+          if (!id) return null
+          const draggedFile = local.file.node(id)
+          if (!draggedFile) return null
+          return (
+            <div class="relative px-3 h-8 flex items-center text-sm font-medium text-text whitespace-nowrap shrink-0 bg-background-panel border-x border-border-subtle/40 border-b border-b-transparent">
+              <TabVisual file={draggedFile} />
+            </div>
+          )
+        })()}
+      </DragOverlay>
+    </DragDropProvider>
   )
 }
 

+ 2 - 1
packages/desktop/src/components/file-tree.tsx

@@ -1,6 +1,7 @@
 import { useLocal } from "@/context"
 import type { LocalFile } from "@/context/local"
-import { Collapsible, FileIcon, Tooltip } from "@/ui"
+import { Tooltip } from "@opencode-ai/ui"
+import { Collapsible, FileIcon } from "@/ui"
 import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
 import { Dynamic } from "solid-js/web"
 

+ 2 - 2
packages/desktop/src/components/prompt-form.tsx

@@ -1,8 +1,8 @@
 import { For, Show, createMemo, onCleanup, type JSX } from "solid-js"
 import { createStore } from "solid-js/store"
 import { Popover } from "@kobalte/core/popover"
-import { Button, FileIcon, Icon, IconButton, Tooltip } from "@/ui"
-import { Select } from "@/components/select"
+import { Tooltip, Button, Icon, Select } from "@opencode-ai/ui"
+import { FileIcon, IconButton } from "@/ui"
 import { useLocal } from "@/context"
 import type { FileContext, LocalFile } from "@/context/local"
 import { getDirectory, getFilename } from "@/utils"

+ 0 - 217
packages/desktop/src/components/resizeable-pane.tsx

@@ -1,217 +0,0 @@
-import { batch, createContext, createMemo, createSignal, onCleanup, Show, useContext } from "solid-js"
-import type { ComponentProps, JSX } from "solid-js"
-import { createStore } from "solid-js/store"
-import { useLocal } from "@/context"
-
-type PaneDefault = number | { size: number; visible?: boolean }
-
-type LayoutContextValue = {
-  id: string
-  register: (pane: string, options: { min?: number | string; max?: number | string }) => void
-  size: (pane: string) => number
-  visible: (pane: string) => boolean
-  percent: (pane: string) => number
-  next: (pane: string) => string | undefined
-  startDrag: (left: string, right: string | undefined, event: MouseEvent) => void
-  dragging: () => string | undefined
-}
-
-const LayoutContext = createContext<LayoutContextValue | undefined>(undefined)
-
-export interface ResizeableLayoutProps {
-  id: string
-  defaults: Record<string, PaneDefault>
-  class?: ComponentProps<"div">["class"]
-  classList?: ComponentProps<"div">["classList"]
-  children: JSX.Element
-}
-
-export interface ResizeablePaneProps {
-  id: string
-  minSize?: number | string
-  maxSize?: number | string
-  class?: ComponentProps<"div">["class"]
-  classList?: ComponentProps<"div">["classList"]
-  children: JSX.Element
-}
-
-export function ResizeableLayout(props: ResizeableLayoutProps) {
-  const local = useLocal()
-  const [meta, setMeta] = createStore<Record<string, { min: number; max: number; minPx?: number; maxPx?: number }>>({})
-  const [dragging, setDragging] = createSignal<string>()
-  let container: HTMLDivElement | undefined
-
-  local.layout.ensure(props.id, props.defaults)
-
-  const order = createMemo(() => local.layout.order(props.id))
-  const visibleOrder = createMemo(() => order().filter((pane) => local.layout.visible(props.id, pane)))
-  const totalVisible = createMemo(() => {
-    const panes = visibleOrder()
-    if (!panes.length) return 0
-    return panes.reduce((total, pane) => total + local.layout.size(props.id, pane), 0)
-  })
-
-  const percent = (pane: string) => {
-    const panes = visibleOrder()
-    if (!panes.length) return 0
-    const total = totalVisible()
-    if (!total) return 100 / panes.length
-    return (local.layout.size(props.id, pane) / total) * 100
-  }
-
-  const nextPane = (pane: string) => {
-    const panes = visibleOrder()
-    const index = panes.indexOf(pane)
-    if (index === -1) return undefined
-    return panes[index + 1]
-  }
-
-  const minMax = (pane: string) => meta[pane] ?? { min: 5, max: 95 }
-
-  const pxToPercent = (px: number, total: number) => (px / total) * 100
-
-  const boundsForPair = (left: string, right: string, total: number) => {
-    const leftMeta = minMax(left)
-    const rightMeta = minMax(right)
-    const containerWidth = container?.getBoundingClientRect().width ?? 0
-
-    let minLeft = leftMeta.min
-    let maxLeft = leftMeta.max
-    let minRight = rightMeta.min
-    let maxRight = rightMeta.max
-
-    if (containerWidth && leftMeta.minPx !== undefined) minLeft = pxToPercent(leftMeta.minPx, containerWidth)
-    if (containerWidth && leftMeta.maxPx !== undefined) maxLeft = pxToPercent(leftMeta.maxPx, containerWidth)
-    if (containerWidth && rightMeta.minPx !== undefined) minRight = pxToPercent(rightMeta.minPx, containerWidth)
-    if (containerWidth && rightMeta.maxPx !== undefined) maxRight = pxToPercent(rightMeta.maxPx, containerWidth)
-
-    const finalMinLeft = Math.max(minLeft, total - maxRight)
-    const finalMaxLeft = Math.min(maxLeft, total - minRight)
-    return {
-      min: Math.min(finalMinLeft, finalMaxLeft),
-      max: Math.max(finalMinLeft, finalMaxLeft),
-    }
-  }
-
-  const setPair = (left: string, right: string, leftSize: number, rightSize: number) => {
-    batch(() => {
-      local.layout.setSize(props.id, left, leftSize)
-      local.layout.setSize(props.id, right, rightSize)
-    })
-  }
-
-  const startDrag = (left: string, right: string | undefined, event: MouseEvent) => {
-    if (!right) return
-    if (!container) return
-    const rect = container.getBoundingClientRect()
-    if (!rect.width) return
-    event.preventDefault()
-    const startX = event.clientX
-    const startLeft = local.layout.size(props.id, left)
-    const startRight = local.layout.size(props.id, right)
-    const total = startLeft + startRight
-    const bounds = boundsForPair(left, right, total)
-    const move = (moveEvent: MouseEvent) => {
-      const delta = ((moveEvent.clientX - startX) / rect.width) * 100
-      const nextLeft = Math.max(bounds.min, Math.min(bounds.max, startLeft + delta))
-      const nextRight = total - nextLeft
-      setPair(left, right, nextLeft, nextRight)
-    }
-    const stop = () => {
-      setDragging()
-      document.removeEventListener("mousemove", move)
-      document.removeEventListener("mouseup", stop)
-    }
-    setDragging(left)
-    document.addEventListener("mousemove", move)
-    document.addEventListener("mouseup", stop)
-    onCleanup(() => stop())
-  }
-
-  const register = (pane: string, options: { min?: number | string; max?: number | string }) => {
-    let min = 5
-    let max = 95
-    let minPx: number | undefined
-    let maxPx: number | undefined
-
-    if (typeof options.min === "string" && options.min.endsWith("px")) {
-      minPx = parseInt(options.min)
-      min = 0
-    } else if (typeof options.min === "number") {
-      min = options.min
-    }
-
-    if (typeof options.max === "string" && options.max.endsWith("px")) {
-      maxPx = parseInt(options.max)
-      max = 100
-    } else if (typeof options.max === "number") {
-      max = options.max
-    }
-
-    setMeta(pane, () => ({ min, max, minPx, maxPx }))
-    const fallback = props.defaults[pane]
-    local.layout.ensurePane(props.id, pane, fallback ?? { size: min, visible: true })
-  }
-
-  const contextValue: LayoutContextValue = {
-    id: props.id,
-    register,
-    size: (pane) => local.layout.size(props.id, pane),
-    visible: (pane) => local.layout.visible(props.id, pane),
-    percent,
-    next: nextPane,
-    startDrag,
-    dragging,
-  }
-
-  return (
-    <LayoutContext.Provider value={contextValue}>
-      <div
-        ref={(node) => {
-          container = node ?? undefined
-        }}
-        class={props.class ? `relative flex h-full w-full ${props.class}` : "relative flex h-full w-full"}
-        classList={props.classList}
-      >
-        {props.children}
-      </div>
-    </LayoutContext.Provider>
-  )
-}
-
-export function ResizeablePane(props: ResizeablePaneProps) {
-  const context = useContext(LayoutContext)!
-  context.register(props.id, { min: props.minSize, max: props.maxSize })
-  const visible = () => context.visible(props.id)
-  const width = () => context.percent(props.id)
-  const next = () => context.next(props.id)
-  const dragging = () => context.dragging() === props.id
-
-  return (
-    <Show when={visible()}>
-      <div
-        class={props.class ? `relative flex h-full flex-col ${props.class}` : "relative flex h-full flex-col"}
-        classList={props.classList}
-        style={{
-          width: `${width()}%`,
-          flex: `0 0 ${width()}%`,
-        }}
-      >
-        {props.children}
-        <Show when={next()}>
-          <div
-            class="absolute top-0 -right-1 h-full w-1.5 cursor-col-resize z-50 group"
-            onMouseDown={(event) => context.startDrag(props.id, next(), event)}
-          >
-            <div
-              classList={{
-                "w-0.5 h-full bg-transparent transition-colors group-hover:bg-border-active": true,
-                "bg-border-active!": dragging(),
-              }}
-            />
-          </div>
-        </Show>
-      </div>
-    </Show>
-  )
-}

+ 2 - 1
packages/desktop/src/components/select-dialog.tsx

@@ -1,6 +1,7 @@
 import { createEffect, Show, For, createMemo, type JSX, createResource } from "solid-js"
 import { Dialog } from "@kobalte/core/dialog"
-import { Icon, IconButton } from "@/ui"
+import { Icon } from "@opencode-ai/ui"
+import { IconButton } from "@/ui"
 import { createStore } from "solid-js/store"
 import { entries, flatMap, groupBy, map, pipe } from "remeda"
 import { createList } from "solid-list"

+ 0 - 108
packages/desktop/src/components/select.tsx

@@ -1,108 +0,0 @@
-import { Select as KobalteSelect } from "@kobalte/core/select"
-import { createMemo } from "solid-js"
-import type { ComponentProps } from "solid-js"
-import { Icon } from "@/ui/icon"
-import { pipe, groupBy, entries, map } from "remeda"
-import { Button, type ButtonProps } from "@/ui"
-
-export interface SelectProps<T> {
-  placeholder?: string
-  options: T[]
-  current?: T
-  value?: (x: T) => string
-  label?: (x: T) => string
-  groupBy?: (x: T) => string
-  onSelect?: (value: T | undefined) => void
-  class?: ComponentProps<"div">["class"]
-  classList?: ComponentProps<"div">["classList"]
-}
-
-export function Select<T>(props: SelectProps<T> & ButtonProps) {
-  const grouped = createMemo(() => {
-    const result = pipe(
-      props.options,
-      groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
-      // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
-      entries(),
-      map(([k, v]) => ({ category: k, options: v })),
-    )
-    return result
-  })
-
-  return (
-    <KobalteSelect<T, { category: string; options: T[] }>
-      value={props.current}
-      options={grouped()}
-      optionValue={(x) => (props.value ? props.value(x) : (x as string))}
-      optionTextValue={(x) => (props.label ? props.label(x) : (x as string))}
-      optionGroupChildren="options"
-      placeholder={props.placeholder}
-      sectionComponent={(props) => (
-        <KobalteSelect.Section class="text-xs uppercase text-text-muted/60 font-light mt-3 first:mt-0 ml-2">
-          {props.section.rawValue.category}
-        </KobalteSelect.Section>
-      )}
-      itemComponent={(itemProps) => (
-        <KobalteSelect.Item
-          classList={{
-            "relative flex cursor-pointer select-none items-center": true,
-            "rounded-sm px-2 py-0.5 text-xs outline-none text-text": true,
-            "transition-colors data-[disabled]:pointer-events-none": true,
-            "data-[highlighted]:bg-background-element data-[disabled]:opacity-50": true,
-            [props.class ?? ""]: !!props.class,
-          }}
-          {...itemProps}
-        >
-          <KobalteSelect.ItemLabel>
-            {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
-          </KobalteSelect.ItemLabel>
-          <KobalteSelect.ItemIndicator class="ml-auto">
-            <Icon name="checkmark" size={16} />
-          </KobalteSelect.ItemIndicator>
-        </KobalteSelect.Item>
-      )}
-      onChange={(v) => {
-        props.onSelect?.(v ?? undefined)
-      }}
-    >
-      <KobalteSelect.Trigger
-        as={Button}
-        size={props.size || "sm"}
-        variant={props.variant || "secondary"}
-        classList={{
-          ...(props.classList ?? {}),
-          [props.class ?? ""]: !!props.class,
-        }}
-      >
-        <KobalteSelect.Value<T> class="truncate">
-          {(state) => {
-            const selected = state.selectedOption() ?? props.current
-            if (!selected) return props.placeholder || ""
-            if (props.label) return props.label(selected)
-            return selected as string
-          }}
-        </KobalteSelect.Value>
-        <KobalteSelect.Icon
-          classList={{
-            "group size-fit shrink-0 text-text-muted transition-transform duration-100": true,
-          }}
-        >
-          <Icon name="chevron-up" size={16} class="-my-2 group-data-[expanded]:rotate-180" />
-          <Icon name="chevron-down" size={16} class="-my-2 group-data-[expanded]:rotate-180" />
-        </KobalteSelect.Icon>
-      </KobalteSelect.Trigger>
-      <KobalteSelect.Portal>
-        <KobalteSelect.Content
-          classList={{
-            "min-w-32 overflow-hidden rounded-md border border-border-subtle/40": true,
-            "bg-background-panel p-1 shadow-md z-50": true,
-            "data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95": true,
-            "data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true,
-          }}
-        >
-          <KobalteSelect.Listbox class="overflow-y-auto max-h-48 whitespace-nowrap overflow-x-hidden" />
-        </KobalteSelect.Content>
-      </KobalteSelect.Portal>
-    </KobalteSelect>
-  )
-}

+ 1 - 1
packages/desktop/src/components/session-list.tsx

@@ -1,5 +1,5 @@
 import { useSync, useLocal } from "@/context"
-import { Tooltip } from "@/ui"
+import { Tooltip } from "@opencode-ai/ui"
 import { DateTime } from "luxon"
 import { VList } from "virtua/solid"
 

+ 2 - 1
packages/desktop/src/components/session-timeline.tsx

@@ -1,5 +1,6 @@
 import { useLocal, useSync } from "@/context"
-import { Collapsible, Icon } from "@/ui"
+import { Icon } from "@opencode-ai/ui"
+import { Collapsible } from "@/ui"
 import type { Part, ToolPart } from "@opencode-ai/sdk"
 import { DateTime } from "luxon"
 import {

+ 0 - 48
packages/desktop/src/components/sidebar-nav.tsx

@@ -1,48 +0,0 @@
-import { For } from "solid-js"
-import { Icon, Link, Logo, Tooltip } from "@/ui"
-import { useLocation } from "@solidjs/router"
-
-const navigation = [
-  { name: "Sessions", href: "/sessions", icon: "dashboard" as const },
-  { name: "Commands", href: "/commands", icon: "slash" as const },
-  { name: "Agents", href: "/agents", icon: "bolt" as const },
-  { name: "Providers", href: "/providers", icon: "cloud" as const },
-  { name: "Tools (MCP)", href: "/tools", icon: "hammer" as const },
-  { name: "LSP", href: "/lsp", icon: "code" as const },
-  { name: "Settings", href: "/settings", icon: "settings" as const },
-]
-
-export default function SidebarNav() {
-  const location = useLocation()
-  return (
-    <div class="hidden md:fixed md:inset-y-0 md:left-0 md:z-50 md:block md:w-16 md:overflow-y-auto md:bg-background-panel md:pb-4">
-      <div class="flex h-16 shrink-0 items-center justify-center">
-        <Logo variant="mark" size={28} />
-      </div>
-      <nav class="mt-5">
-        <ul role="list" class="flex flex-col items-center space-y-1">
-          <For each={navigation}>
-            {(item) => (
-              <li>
-                <Tooltip placement="right" value={item.name}>
-                  <Link
-                    href={item.href}
-                    classList={{
-                      "bg-background-element text-text": location.pathname.startsWith(item.href),
-                      "text-text-muted hover:bg-background-element hover:text-text": location.pathname !== item.href,
-                      "flex gap-x-3 rounded-md p-3 text-sm font-semibold": true,
-                      "focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-border-active": true,
-                    }}
-                  >
-                    <Icon name={item.icon} size={20} />
-                    <span class="sr-only">{item.name}</span>
-                  </Link>
-                </Tooltip>
-              </li>
-            )}
-          </For>
-        </ul>
-      </nav>
-    </div>
-  )
-}

+ 0 - 1
packages/desktop/src/context/index.ts

@@ -4,4 +4,3 @@ export { MarkedProvider, useMarked } from "./marked"
 export { SDKProvider, useSDK } from "./sdk"
 export { ShikiProvider, useShiki } from "./shiki"
 export { SyncProvider, useSync } from "./sync"
-export { ThemeProvider, useTheme } from "./theme"

+ 25 - 25
packages/desktop/src/context/local.tsx

@@ -131,31 +131,31 @@ function init() {
     const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
     const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
 
-    createEffect((prev: FileStatus[]) => {
-      const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
-      for (const p of removed) {
-        setStore(
-          "node",
-          p.path,
-          produce((draft) => {
-            draft.status = undefined
-            draft.view = "raw"
-          }),
-        )
-        load(p.path)
-      }
-      for (const p of sync.data.changes) {
-        if (store.node[p.path] === undefined) {
-          fetch(p.path).then(() => {
-            if (store.node[p.path] === undefined) return
-            setStore("node", p.path, "status", p)
-          })
-        } else {
-          setStore("node", p.path, "status", p)
-        }
-      }
-      return sync.data.changes
-    }, sync.data.changes)
+    // createEffect((prev: FileStatus[]) => {
+    //   const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
+    //   for (const p of removed) {
+    //     setStore(
+    //       "node",
+    //       p.path,
+    //       produce((draft) => {
+    //         draft.status = undefined
+    //         draft.view = "raw"
+    //       }),
+    //     )
+    //     load(p.path)
+    //   }
+    //   for (const p of sync.data.changes) {
+    //     if (store.node[p.path] === undefined) {
+    //       fetch(p.path).then(() => {
+    //         if (store.node[p.path] === undefined) return
+    //         setStore("node", p.path, "status", p)
+    //       })
+    //     } else {
+    //       setStore("node", p.path, "status", p)
+    //     }
+    //   }
+    //   return sync.data.changes
+    // }, sync.data.changes)
 
     const changed = (path: string) => {
       const node = store.node[path]

+ 0 - 92
packages/desktop/src/context/theme.tsx

@@ -1,92 +0,0 @@
-import {
-  createContext,
-  useContext,
-  createSignal,
-  createEffect,
-  onMount,
-  type ParentComponent,
-  onCleanup,
-} from "solid-js"
-
-export interface ThemeContextValue {
-  theme: string | undefined
-  isDark: boolean
-  setTheme: (themeName: string) => void
-  setDarkMode: (isDark: boolean) => void
-}
-
-const ThemeContext = createContext<ThemeContextValue>()
-
-export const useTheme = () => {
-  const context = useContext(ThemeContext)
-  if (!context) {
-    throw new Error("useTheme must be used within a ThemeProvider")
-  }
-  return context
-}
-
-interface ThemeProviderProps {
-  defaultTheme?: string
-  defaultDarkMode?: boolean
-}
-
-const themes = ["opencode", "tokyonight", "ayu", "nord", "catppuccin"]
-
-export const ThemeProvider: ParentComponent<ThemeProviderProps> = (props) => {
-  const [theme, setThemeSignal] = createSignal<string | undefined>()
-  const [isDark, setIsDark] = createSignal(props.defaultDarkMode ?? false)
-
-  const handleKeyDown = (event: KeyboardEvent) => {
-    if (event.key === "t" && event.ctrlKey) {
-      event.preventDefault()
-      const current = theme()
-      if (!current) return
-      const index = themes.indexOf(current)
-      const next = themes[(index + 1) % themes.length]
-      setTheme(next)
-    }
-  }
-
-  onMount(() => {
-    window.addEventListener("keydown", handleKeyDown)
-  })
-
-  onCleanup(() => {
-    window.removeEventListener("keydown", handleKeyDown)
-  })
-
-  onMount(() => {
-    const savedTheme = localStorage.getItem("theme") ?? "opencode"
-    const savedDarkMode = localStorage.getItem("darkMode") ?? "true"
-    setIsDark(savedDarkMode === "true")
-    setTheme(savedTheme)
-  })
-
-  createEffect(() => {
-    const currentTheme = theme()
-    const darkMode = isDark()
-    if (currentTheme) {
-      document.documentElement.setAttribute("data-theme", currentTheme)
-      document.documentElement.setAttribute("data-dark", darkMode.toString())
-    }
-  })
-
-  const setTheme = async (theme: string) => {
-    setThemeSignal(theme)
-    localStorage.setItem("theme", theme)
-  }
-
-  const setDarkMode = (dark: boolean) => {
-    setIsDark(dark)
-    localStorage.setItem("darkMode", dark.toString())
-  }
-
-  const contextValue: ThemeContextValue = {
-    theme: theme(),
-    isDark: isDark(),
-    setTheme,
-    setDarkMode,
-  }
-
-  return <ThemeContext.Provider value={contextValue}>{props.children}</ThemeContext.Provider>
-}

+ 1 - 168
packages/desktop/src/index.css

@@ -1,168 +1 @@
-@import "tailwindcss";
-
-:root {
-  interpolate-size: allow-keywords;
-}
-
-@layer components {
-  [data-popper-positioner] {
-    pointer-events: none;
-  }
-
-  body {
-    line-height: 1;
-  }
-
-  ::selection {
-    background-color: color-mix(in srgb, var(--color-primary) 33%, transparent);
-    /* background-color: var(--color-primary); */
-    /* color: var(--color-background); */
-  }
-
-  ::-webkit-scrollbar-track {
-    background: var(--theme-background-panel);
-  }
-
-  ::-webkit-scrollbar-thumb {
-    background-color: var(--theme-border-subtle);
-    border-radius: 6px;
-  }
-
-  * {
-    scrollbar-color: var(--theme-border-subtle) var(--theme-background-panel);
-  }
-
-  .prose h1 {
-    color: var(--color-text);
-    font-size: var(--text-sm);
-    line-height: var(--text-sm--line-height);
-    margin-bottom: calc(var(--spacing) * 3);
-  }
-  .prose h2 {
-    color: var(--color-text);
-    font-size: var(--text-sm);
-    line-height: var(--text-sm--line-height);
-    margin-bottom: calc(var(--spacing) * 3);
-  }
-  .prose h3 {
-    color: var(--color-text);
-    font-size: var(--text-xs);
-    line-height: var(--text-xs--line-height);
-    margin-bottom: calc(var(--spacing) * 2);
-  }
-  .prose h4 {
-    color: var(--color-text);
-    font-size: var(--text-xs);
-    line-height: var(--text-xs--line-height);
-    margin-bottom: calc(var(--spacing) * 2);
-  }
-  .prose h5 {
-    color: var(--color-text);
-    font-size: var(--text-xs);
-    line-height: var(--text-xs--line-height);
-    margin-bottom: calc(var(--spacing) * 2);
-  }
-  .prose h6 {
-    color: var(--color-text);
-    font-size: var(--text-xs);
-    line-height: var(--text-xs--line-height);
-    margin-bottom: calc(var(--spacing) * 2);
-  }
-  .prose p {
-    font-size: var(--text-xs);
-    line-height: var(--text-xs--line-height);
-    margin-bottom: calc(var(--spacing) * 2);
-  }
-  .prose strong {
-    color: var(--color-text);
-  }
-  .prose ul,
-  ol {
-    list-style-type: disc;
-    list-style-position: inside;
-    margin-bottom: calc(var(--spacing) * 2);
-  }
-  .prose pre {
-    background-color: var(--color-background-panel);
-    padding: calc(var(--spacing) * 2);
-    border-radius: var(--radius-md);
-    border: 1px solid var(--color-border-subtle);
-    overflow-x: auto;
-    white-space: pre;
-    margin-bottom: calc(var(--spacing) * 2);
-    @apply no-scrollbar;
-  }
-  .prose code {
-    font-family: var(--font-mono);
-    font-size: var(--text-xs);
-    line-height: var(--text-xs--line-height);
-  }
-  .prose blockquote {
-    margin-bottom: calc(var(--spacing) * 2);
-  }
-}
-
-@utility no-scrollbar {
-  &::-webkit-scrollbar {
-    display: none;
-  }
-  /* Hide scrollbar for IE, Edge and Firefox */
-  & {
-    -ms-overflow-style: none; /* IE and Edge */
-    scrollbar-width: none; /* Firefox */
-  }
-}
-
-@theme {
-  --color-*: initial;
-  --color-primary: var(--theme-primary);
-  --color-secondary: var(--theme-secondary);
-  --color-accent: var(--theme-accent);
-  --color-error: var(--theme-error);
-  --color-warning: var(--theme-warning);
-  --color-success: var(--theme-success);
-  --color-info: var(--theme-info);
-  --color-text: var(--theme-text);
-  --color-text-muted: var(--theme-text-muted);
-  --color-background: var(--theme-background);
-  --color-background-panel: var(--theme-background-panel);
-  --color-background-element: var(--theme-background-element);
-  --color-border: var(--theme-border);
-  --color-border-active: var(--theme-border-active);
-  --color-border-subtle: var(--theme-border-subtle);
-  --color-diff-added: var(--theme-diff-added);
-  --color-diff-removed: var(--theme-diff-removed);
-  --color-diff-context: var(--theme-diff-context);
-  --color-diff-hunk-header: var(--theme-diff-hunk-header);
-  --color-diff-highlight-added: var(--theme-diff-highlight-added);
-  --color-diff-highlight-removed: var(--theme-diff-highlight-removed);
-  --color-diff-added-bg: var(--theme-diff-added-bg);
-  --color-diff-removed-bg: var(--theme-diff-removed-bg);
-  --color-diff-context-bg: var(--theme-diff-context-bg);
-  --color-diff-line-number: var(--theme-diff-line-number);
-  --color-diff-added-line-number-bg: var(--theme-diff-added-line-number-bg);
-  --color-diff-removed-line-number-bg: var(--theme-diff-removed-line-number-bg);
-  --color-markdown-text: var(--theme-markdown-text);
-  --color-markdown-heading: var(--theme-markdown-heading);
-  --color-markdown-link: var(--theme-markdown-link);
-  --color-markdown-link-text: var(--theme-markdown-link-text);
-  --color-markdown-code: var(--theme-markdown-code);
-  --color-markdown-block-quote: var(--theme-markdown-block-quote);
-  --color-markdown-emph: var(--theme-markdown-emph);
-  --color-markdown-strong: var(--theme-markdown-strong);
-  --color-markdown-horizontal-rule: var(--theme-markdown-horizontal-rule);
-  --color-markdown-list-item: var(--theme-markdown-list-item);
-  --color-markdown-list-enumeration: var(--theme-markdown-list-enumeration);
-  --color-markdown-image: var(--theme-markdown-image);
-  --color-markdown-image-text: var(--theme-markdown-image-text);
-  --color-markdown-code-block: var(--theme-markdown-code-block);
-  --color-syntax-comment: var(--theme-syntax-comment);
-  --color-syntax-keyword: var(--theme-syntax-keyword);
-  --color-syntax-function: var(--theme-syntax-function);
-  --color-syntax-variable: var(--theme-syntax-variable);
-  --color-syntax-string: var(--theme-syntax-string);
-  --color-syntax-number: var(--theme-syntax-number);
-  --color-syntax-type: var(--theme-syntax-type);
-  --color-syntax-operator: var(--theme-syntax-operator);
-  --color-syntax-punctuation: var(--theme-syntax-punctuation);
-}
+@import "@opencode-ai/css/tailwind.css";

+ 22 - 31
packages/desktop/src/index.tsx

@@ -1,21 +1,13 @@
 /* @refresh reload */
+import "@/index.css"
 import { render } from "solid-js/web"
 import { Router, Route } from "@solidjs/router"
-import "@/index.css"
-import Layout from "@/pages/layout"
+import { MetaProvider } from "@solidjs/meta"
+import { EventProvider, SDKProvider, SyncProvider, LocalProvider, ShikiProvider, MarkedProvider } from "@/context"
+import { Fonts } from "@opencode-ai/ui"
 import Home from "@/pages"
-import {
-  EventProvider,
-  SDKProvider,
-  SyncProvider,
-  LocalProvider,
-  ThemeProvider,
-  ShikiProvider,
-  MarkedProvider,
-} from "@/context"
 
 const root = document.getElementById("root")
-
 if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
   throw new Error(
     "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
@@ -24,25 +16,24 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
 
 render(
   () => (
-    <div class="h-full bg-background text-text-muted">
-      <ThemeProvider defaultTheme="opencode" defaultDarkMode={true}>
-        <ShikiProvider>
-          <MarkedProvider>
-            <SDKProvider>
-              <EventProvider>
-                <SyncProvider>
-                  <LocalProvider>
-                    <Router root={Layout}>
-                      <Route path="/" component={Home} />
-                    </Router>
-                  </LocalProvider>
-                </SyncProvider>
-              </EventProvider>
-            </SDKProvider>
-          </MarkedProvider>
-        </ShikiProvider>
-      </ThemeProvider>
-    </div>
+    <ShikiProvider>
+      <MarkedProvider>
+        <SDKProvider>
+          <EventProvider>
+            <SyncProvider>
+              <LocalProvider>
+                <MetaProvider>
+                  <Fonts />
+                  <Router>
+                    <Route path="/" component={Home} />
+                  </Router>
+                </MetaProvider>
+              </LocalProvider>
+            </SyncProvider>
+          </EventProvider>
+        </SDKProvider>
+      </MarkedProvider>
+    </ShikiProvider>
   ),
   root!,
 )

+ 56 - 54
packages/desktop/src/pages/index.tsx

@@ -1,5 +1,5 @@
-import { FileIcon, Icon, IconButton, Tooltip } from "@/ui"
-import * as KobalteTabs from "@kobalte/core/tabs"
+import { Icon, Tooltip } from "@opencode-ai/ui"
+import { FileIcon, IconButton } from "@/ui"
 import FileTree from "@/components/file-tree"
 import EditorPane from "@/components/editor-pane"
 import { For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
@@ -11,9 +11,7 @@ import SessionTimeline from "@/components/session-timeline"
 import PromptForm, { type PromptContentPart, type PromptSubmitValue } from "@/components/prompt-form"
 import { createStore } from "solid-js/store"
 import { getDirectory, getFilename } from "@/utils"
-import { Select } from "@/components/select"
-import { Tabs } from "@/ui/tabs"
-import { Code } from "@/components/code"
+import { PromptInput } from "@/components/prompt-input"
 
 export default function Page() {
   const local = useLocal()
@@ -52,7 +50,7 @@ export default function Page() {
     const focused = document.activeElement === inputRef
     if (focused) {
       if (event.key === "Escape") {
-        inputRef?.blur()
+        // inputRef?.blur()
       }
       return
     }
@@ -79,7 +77,7 @@ export default function Page() {
     }
 
     if (event.key.length === 1 && event.key !== "Unidentified") {
-      inputRef?.focus()
+      // inputRef?.focus()
     }
   }
 
@@ -106,6 +104,8 @@ export default function Page() {
     }
   }
 
+  const handlePromptSubmit2 = () => {}
+
   const handlePromptSubmit = async (prompt: PromptSubmitValue) => {
     const existingSession = local.session.active()
     let session = existingSession
@@ -231,51 +231,54 @@ export default function Page() {
         <div class="shrink-0 w-70">
           <SessionList />
         </div>
-        <div class="grow w-full min-w-0 overflow-y-auto flex justify-center">
-          <Show when={local.session.active()}>
-            {(activeSession) => <SessionTimeline session={activeSession().id} class="max-w-xl" />}
-          </Show>
-        </div>
-        <div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
-          <FileTree path="" onFileClick={handleFileClick} />
-        </div>
-        <div class="hidden shrink-0 w-56 p-2">
-          <Show
-            when={local.file.changes().length}
-            fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
-          >
-            <ul class="">
-              <For each={local.file.changes()}>
-                {(path) => (
-                  <li>
-                    <button
-                      onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
-                      class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 cursor-pointer hover:bg-background-element"
-                    >
-                      <FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
-                      <span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
-                      <span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
-                        {getDirectory(path)}
-                      </span>
-                    </button>
-                  </li>
-                )}
-              </For>
-            </ul>
-          </Show>
-        </div>
-        <div class="hidden grow min-w-0">
-          <EditorPane onFileClick={handleFileClick} />
-        </div>
-        <div class="absolute bottom-4 inset-x-0 p-2 flex flex-col justify-center items-center gap-2 z-50">
-          <PromptForm
-            class="w-xl"
-            onSubmit={handlePromptSubmit}
-            onOpenModelSelect={() => setStore("modelSelectOpen", true)}
-            onInputRefChange={(element: HTMLTextAreaElement | undefined) => {
-              inputRef = element ?? undefined
-            }}
-          />
+        <div class="relative grid grid-cols-2">
+          <div class="min-w-0 overflow-y-auto no-scrollbar flex justify-center">
+            <Show when={local.session.active()}>
+              {(activeSession) => <SessionTimeline session={activeSession().id} class="w-full" />}
+            </Show>
+          </div>
+          <div class="p-1.5 pl-px flex flex-col items-center justify-center overflow-y-auto no-scrollbar">
+            <EditorPane onFileClick={handleFileClick} />
+          </div>
+          <div class="absolute bottom-4 inset-x-0 p-2 flex flex-col justify-center items-center z-50">
+            <PromptInput onSubmit={handlePromptSubmit2} />
+            {/* <PromptForm */}
+            {/*   class="w-2xl" */}
+            {/*   onSubmit={handlePromptSubmit} */}
+            {/*   onOpenModelSelect={() => setStore("modelSelectOpen", true)} */}
+            {/*   onInputRefChange={(element: HTMLTextAreaElement | undefined) => { */}
+            {/*     inputRef = element ?? undefined */}
+            {/*   }} */}
+            {/* /> */}
+          </div>
+          <div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
+            <FileTree path="" onFileClick={handleFileClick} />
+          </div>
+          <div class="hidden shrink-0 w-56 p-2">
+            <Show
+              when={local.file.changes().length}
+              fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
+            >
+              <ul class="">
+                <For each={local.file.changes()}>
+                  {(path) => (
+                    <li>
+                      <button
+                        onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
+                        class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 cursor-pointer hover:bg-background-element"
+                      >
+                        <FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
+                        <span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
+                        <span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
+                          {getDirectory(path)}
+                        </span>
+                      </button>
+                    </li>
+                  )}
+                </For>
+              </ul>
+            </Show>
+          </div>
         </div>
       </main>
       <Show when={store.modelSelectOpen}>
@@ -343,8 +346,7 @@ export default function Page() {
             </div>
           )}
           onClose={() => setStore("fileSelectOpen", false)}
-          onSelect={(x) => (x ? local.context.openFile(x) : undefined)}
-          // onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
+          onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
         />
       </Show>
     </div>

+ 0 - 5
packages/desktop/src/pages/layout.tsx

@@ -1,5 +0,0 @@
-import { type ParentProps } from "solid-js"
-
-export default function Layout(props: ParentProps) {
-  return <main class="">{props.children}</main>
-}

+ 0 - 36
packages/desktop/src/ui/button.tsx

@@ -1,36 +0,0 @@
-import { Button as Kobalte } from "@kobalte/core/button"
-import { type ComponentProps, splitProps } from "solid-js"
-
-export interface ButtonProps {
-  variant?: "primary" | "secondary" | "ghost"
-  size?: "sm" | "md" | "lg"
-}
-
-export function Button(props: ComponentProps<"button"> & ButtonProps) {
-  const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"])
-  return (
-    <Kobalte
-      {...rest}
-      data-size={split.size || "sm"}
-      data-variant={split.variant || "secondary"}
-      class="inline-flex items-center justify-center rounded-md cursor-pointer font-medium transition-colors
-             min-w-0 whitespace-nowrap truncate
-             data-[size=sm]:h-6 data-[size=sm]:pl-2 data-[size=sm]:text-xs
-             data-[size=md]:h-8 data-[size=md]:pl-3 data-[size=md]:text-sm
-             data-[size=lg]:h-10 data-[size=lg]:pl-4 data-[size=lg]:text-base
-             data-[variant=primary]:bg-primary data-[variant=primary]:text-background 
-             data-[variant=primary]:hover:bg-secondary data-[variant=primary]:focus-visible:ring-primary
-             data-[variant=secondary]:bg-background-element data-[variant=secondary]:text-text
-             data-[variant=secondary]:hover:bg-background-element data-[variant=secondary]:focus-visible:ring-secondary
-             data-[variant=ghost]:text-text data-[variant=ghost]:hover:bg-background-panel data-[variant=ghost]:focus-visible:ring-border-active
-             focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-transparent 
-             disabled:pointer-events-none disabled:opacity-50"
-      classList={{
-        ...(split.classList ?? {}),
-        [split.class ?? ""]: !!split.class,
-      }}
-    >
-      {props.children}
-    </Kobalte>
-  )
-}

+ 1 - 1
packages/desktop/src/ui/collapsible.tsx

@@ -1,7 +1,7 @@
 import { Collapsible as KobalteCollapsible } from "@kobalte/core/collapsible"
 import { splitProps } from "solid-js"
 import type { ComponentProps, ParentProps } from "solid-js"
-import { Icon, type IconProps } from "./icon"
+import { Icon, type IconProps } from "@opencode-ai/ui"
 
 export interface CollapsibleProps extends ComponentProps<typeof KobalteCollapsible> {}
 export interface CollapsibleTriggerProps extends ComponentProps<typeof KobalteCollapsible.Trigger> {}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 109
packages/desktop/src/ui/icon.tsx


+ 0 - 5
packages/desktop/src/ui/index.ts

@@ -1,4 +1,3 @@
-export { Button, type ButtonProps } from "./button"
 export {
   Collapsible,
   type CollapsibleProps,
@@ -6,8 +5,4 @@ export {
   type CollapsibleContentProps,
 } from "./collapsible"
 export { FileIcon, type FileIconProps } from "./file-icon"
-export { Icon, type IconProps } from "./icon"
 export { IconButton, type IconButtonProps } from "./icon-button"
-export { Link, type LinkProps } from "./link"
-export { Logo, type LogoProps } from "./logo"
-export { Tooltip, type TooltipProps } from "./tooltip"

+ 0 - 13
packages/desktop/src/ui/link.tsx

@@ -1,13 +0,0 @@
-import { A } from "@solidjs/router"
-import { splitProps } from "solid-js"
-import type { ComponentProps } from "solid-js"
-
-export interface LinkProps extends ComponentProps<typeof A> {
-  variant?: "primary" | "secondary" | "ghost"
-  size?: "sm" | "md" | "lg"
-}
-
-export function Link(props: LinkProps) {
-  const [, others] = splitProps(props, ["variant", "size", "class"])
-  return <A {...others} />
-}

+ 0 - 125
packages/desktop/src/ui/logo.tsx

@@ -1,125 +0,0 @@
-import type { ComponentProps } from "solid-js"
-
-export interface LogoProps extends ComponentProps<"svg"> {
-  variant?: "mark" | "full" | "ornate"
-  size?: number
-}
-
-export function Logo(props: LogoProps) {
-  const { variant = "mark", size = 64, ...others } = props
-
-  if (variant === "mark") {
-    return (
-      <svg
-        width={size}
-        height={size * (42 / 64)}
-        viewBox="0 0 64 42"
-        fill="none"
-        xmlns="http://www.w3.org/2000/svg"
-        class={`text-text ${props.class ?? ""}`}
-        {...others}
-      >
-        <path
-          fill-rule="evenodd"
-          clip-rule="evenodd"
-          d="M0 0H32V41.5955H0V0ZM24 8.5H8V33H24V8.5Z"
-          fill="currentColor"
-        />
-        <path d="M40 0H64V8.5H48V33H64V41.5H40V0Z" fill="currentColor" />
-      </svg>
-    )
-  }
-
-  if (variant === "full") {
-    return (
-      <svg
-        width={size * (289 / 42)}
-        height={size}
-        viewBox="0 0 289 42"
-        fill="none"
-        xmlns="http://www.w3.org/2000/svg"
-        {...others}
-      >
-        <path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="currentColor" />
-        <path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="currentColor" />
-        <path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="currentColor" />
-        <path
-          fill-rule="evenodd"
-          clip-rule="evenodd"
-          d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z"
-          fill="currentColor"
-        />
-        <path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="currentColor" />
-        <path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="currentColor" />
-        <path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="currentColor" />
-        <path
-          fill-rule="evenodd"
-          clip-rule="evenodd"
-          d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z"
-          fill="currentColor"
-        />
-        <path
-          fill-rule="evenodd"
-          clip-rule="evenodd"
-          d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z"
-          fill="currentColor"
-        />
-        <path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="currentColor" />
-      </svg>
-    )
-  }
-
-  return (
-    <svg
-      width={size * (289 / 42)}
-      height={size}
-      viewBox="0 0 289 50"
-      fill="none"
-      xmlns="http://www.w3.org/2000/svg"
-      {...others}
-    >
-      <path d="M8.5 16.5H24.5V33H8.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
-      <path d="M48.5 16.5H64.5V33H48.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
-      <path d="M120.5 16.5H136.5V33H120.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
-      <path d="M160.5 16.5H176.5V33H160.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
-      <path d="M192.5 16.5H208.5V33H192.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
-      <path d="M232.5 16.5H248.5V33H232.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
-      <path
-        d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z"
-        fill="currentColor"
-        fill-opacity="0.95"
-      />
-      <path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="currentColor" fill-opacity="0.95" />
-      <path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="currentColor" fill-opacity="0.95" />
-      <path
-        fill-rule="evenodd"
-        clip-rule="evenodd"
-        d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z"
-        fill="currentColor"
-        fill-opacity="0.95"
-      />
-      <path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="currentColor" fill-opacity="0.5" />
-      <path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="currentColor" fill-opacity="0.5" />
-      <path
-        d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z"
-        fill="currentColor"
-        fill-opacity="0.5"
-      />
-      <path
-        fill-rule="evenodd"
-        clip-rule="evenodd"
-        d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z"
-        fill="currentColor"
-        fill-opacity="0.5"
-      />
-      <path
-        fill-rule="evenodd"
-        clip-rule="evenodd"
-        d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z"
-        fill="currentColor"
-        fill-opacity="0.5"
-      />
-      <path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="currentColor" fill-opacity="0.95" />
-    </svg>
-  )
-}

+ 0 - 71
packages/desktop/src/ui/tabs.tsx

@@ -1,71 +0,0 @@
-import { Tabs as KobalteTabs } from "@kobalte/core/tabs"
-import { splitProps } from "solid-js"
-import type { ComponentProps, ParentProps } from "solid-js"
-
-export interface TabsProps extends ComponentProps<typeof KobalteTabs> {}
-export interface TabsListProps extends ComponentProps<typeof KobalteTabs.List> {}
-export interface TabsTriggerProps extends ComponentProps<typeof KobalteTabs.Trigger> {}
-export interface TabsContentProps extends ComponentProps<typeof KobalteTabs.Content> {}
-
-function TabsRoot(props: TabsProps) {
-  return <KobalteTabs {...props} />
-}
-
-function TabsList(props: TabsListProps) {
-  const [local, others] = splitProps(props, ["class"])
-  return (
-    <KobalteTabs.List
-      classList={{
-        "relative flex items-center bg-background overflow-x-auto no-scrollbar": true,
-        "divide-x divide-border-subtle/40": true,
-        "after:content-[''] after:block after:grow after:h-8": true,
-        "after:border-l empty:after:border-l-0! after:border-b after:border-border-subtle/40": true,
-        [local.class ?? ""]: !!local.class,
-      }}
-      {...others}
-    />
-  )
-}
-
-function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
-  const [local, others] = splitProps(props, ["class", "children"])
-  return (
-    <KobalteTabs.Trigger
-      classList={{
-        "relative px-3 h-8 flex items-center": true,
-        "text-sm font-medium text-text-muted/60 cursor-pointer": true,
-        "whitespace-nowrap shrink-0 border-b border-border-subtle/40": true,
-        "disabled:pointer-events-none disabled:opacity-50": true,
-        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring": true,
-        "data-[selected]:text-text data-[selected]:bg-background-panel": true,
-        "data-[selected]:!border-b-transparent": true,
-        [local.class ?? ""]: !!local.class,
-      }}
-      {...others}
-    >
-      {local.children}
-    </KobalteTabs.Trigger>
-  )
-}
-
-function TabsContent(props: ParentProps<TabsContentProps>) {
-  const [local, others] = splitProps(props, ["class", "children"])
-  return (
-    <KobalteTabs.Content
-      classList={{
-        "bg-background-panel overflow-y-auto h-full no-scrollbar": true,
-        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring": true,
-        [local.class ?? ""]: !!local.class,
-      }}
-      {...others}
-    >
-      {local.children}
-    </KobalteTabs.Content>
-  )
-}
-
-export const Tabs = Object.assign(TabsRoot, {
-  List: TabsList,
-  Trigger: TabsTrigger,
-  Content: TabsContent,
-})

+ 0 - 2
packages/desktop/vite.config.ts

@@ -3,7 +3,6 @@ import solidPlugin from "vite-plugin-solid"
 import tailwindcss from "@tailwindcss/vite"
 import path from "path"
 import { iconsSpritesheet } from "vite-plugin-icons-spritesheet"
-import { generateThemeCSS } from "./scripts/vite-theme-plugin"
 
 export default defineConfig({
   resolve: {
@@ -12,7 +11,6 @@ export default defineConfig({
     },
   },
   plugins: [
-    generateThemeCSS(),
     tailwindcss(),
     solidPlugin(),
     iconsSpritesheet({

+ 2 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/ui",
-  "version": "0.15.5",
+  "version": "0.15.4",
   "type": "module",
   "exports": {
     ".": "./src/components/index.ts",
@@ -17,6 +17,7 @@
   "dependencies": {
     "remeda": "catalog:",
     "solid-js": "catalog:",
+    "@solidjs/meta": "catalog:",
     "@kobalte/core": "catalog:",
     "@opencode-ai/css": "workspace:*"
   }

+ 32 - 6
packages/ui/src/app.tsx

@@ -1,6 +1,5 @@
 import type { Component } from "solid-js"
-import { Button, Select, Tabs } from "./components"
-import "@opencode-ai/css"
+import { Button, Select, Tabs, Tooltip, Fonts } from "./components"
 import "./index.css"
 
 const App: Component = () => {
@@ -17,6 +16,9 @@ const App: Component = () => {
         <Button variant="ghost" size="normal">
           Normal Ghost
         </Button>
+        <Button variant="secondary" size="normal" disabled>
+          Normal Disabled
+        </Button>
         <Button variant="primary" size="large">
           Large Primary
         </Button>
@@ -26,6 +28,9 @@ const App: Component = () => {
         <Button variant="ghost" size="large">
           Large Ghost
         </Button>
+        <Button variant="secondary" size="large" disabled>
+          Large Disabled
+        </Button>
       </section>
       <h3>Select</h3>
       <section>
@@ -88,14 +93,35 @@ const App: Component = () => {
           </Tabs.Content>
         </Tabs>
       </section>
+      <h3>Tooltips</h3>
+      <section>
+        <Tooltip value="This is a top tooltip" placement="top">
+          <Button variant="secondary">Top Tooltip</Button>
+        </Tooltip>
+        <Tooltip value="This is a bottom tooltip" placement="bottom">
+          <Button variant="secondary">Bottom Tooltip</Button>
+        </Tooltip>
+        <Tooltip value="This is a left tooltip" placement="left">
+          <Button variant="secondary">Left Tooltip</Button>
+        </Tooltip>
+        <Tooltip value="This is a right tooltip" placement="right">
+          <Button variant="secondary">Right Tooltip</Button>
+        </Tooltip>
+        <Tooltip value={() => `Dynamic tooltip: ${new Date().toLocaleTimeString()}`} placement="top">
+          <Button variant="primary">Dynamic Tooltip</Button>
+        </Tooltip>
+      </section>
     </div>
   )
 
   return (
-    <main>
-      <Content />
-      <Content dark />
-    </main>
+    <>
+      <Fonts />
+      <main>
+        <Content />
+        <Content dark />
+      </main>
+    </>
   )
 }
 

+ 44 - 0
packages/ui/src/components/fonts.tsx

@@ -0,0 +1,44 @@
+import { Style, Link } from "@solidjs/meta"
+import geist from "@opencode-ai/css/fonts/geist.woff2"
+import geistMono from "@opencode-ai/css/fonts/geist-mono.woff2"
+
+export const Fonts = () => {
+  return (
+    <>
+      <Style>{`
+        @font-face {
+          font-family: "geist";
+          src: url("${geist}") format("woff2-variations");
+          font-display: swap;
+          font-style: normal;
+          font-weight: 100 900;
+        }
+        @font-face {
+          font-family: "geist-fallback";
+          src: local("Arial");
+          size-adjust: 100%;
+          ascent-override: 97%;
+          descent-override: 25%;
+          line-gap-override: 1%;
+        }
+        @font-face {
+          font-family: "geist-mono";
+          src: url("${geistMono}") format("woff2-variations");
+          font-display: swap;
+          font-style: normal;
+          font-weight: 100 900;
+        }
+        @font-face {
+          font-family: "geist-mono-fallback";
+          src: local("Courier New");
+          size-adjust: 100%;
+          ascent-override: 97%;
+          descent-override: 25%;
+          line-gap-override: 1%;
+        }
+      `}</Style>
+      <Link rel="preload" href={geist} as="font" type="font/woff2" crossorigin="anonymous" />
+      <Link rel="preload" href={geistMono} as="font" type="font/woff2" crossorigin="anonymous" />
+    </>
+  )
+}

+ 2 - 0
packages/ui/src/components/index.ts

@@ -1,4 +1,6 @@
 export * from "./button"
 export * from "./icon"
+export * from "./fonts"
 export * from "./select"
 export * from "./tabs"
+export * from "./tooltip"

+ 4 - 18
packages/desktop/src/ui/tooltip.tsx → packages/ui/src/components/tooltip.tsx

@@ -9,7 +9,7 @@ export interface TooltipProps extends ComponentProps<typeof KobalteTooltip> {
 
 export function Tooltip(props: TooltipProps) {
   const [open, setOpen] = createSignal(false)
-  const [local, others] = splitProps(props, ["class", "children"])
+  const [local, others] = splitProps(props, ["children", "class"])
 
   const c = children(() => local.children)
 
@@ -30,27 +30,13 @@ export function Tooltip(props: TooltipProps) {
 
   return (
     <KobalteTooltip forceMount {...others} open={open()} onOpenChange={setOpen}>
-      <KobalteTooltip.Trigger as={"div"} class="flex items-center">
+      <KobalteTooltip.Trigger as={"div"} data-component="tooltip-trigger">
         {c()}
       </KobalteTooltip.Trigger>
       <KobalteTooltip.Portal>
-        <KobalteTooltip.Content
-          classList={{
-            "z-[1000] max-w-[320px] rounded-md bg-background-element px-2 py-1": true,
-            "text-xs font-medium text-text shadow-md pointer-events-none!": true,
-            "transition-all duration-150 ease-out": true,
-            "transform-gpu transform-origin-[var(--kb-tooltip-content-transform-origin)]": true,
-            "data-closed:opacity-0": true,
-            "data-expanded:opacity-100 data-expanded:translate-y-0 data-expanded:translate-x-0": true,
-            "data-closed:translate-y-1": props.placement === "top",
-            "data-closed:-translate-y-1": props.placement === "bottom",
-            "data-closed:translate-x-1": props.placement === "left",
-            "data-closed:-translate-x-1": props.placement === "right",
-            [local.class ?? ""]: !!local.class,
-          }}
-        >
+        <KobalteTooltip.Content data-component="tooltip" data-placement={props.placement} class={local.class}>
           {typeof others.value === "function" ? others.value() : others.value}
-          <KobalteTooltip.Arrow size={18} />
+          <KobalteTooltip.Arrow data-slot="arrow" size={18} />
         </KobalteTooltip.Content>
       </KobalteTooltip.Portal>
     </KobalteTooltip>

+ 6 - 4
packages/ui/src/index.css

@@ -1,8 +1,10 @@
+@import "@opencode-ai/css";
+
 :root {
   body {
     margin: 0;
-    background-color: var(--background-background);
-    color: var(--text-default-text);
+    background-color: var(--background-base);
+    color: var(--text-base);
   }
   main {
     display: flex;
@@ -35,6 +37,6 @@
 }
 
 .dark {
-  background-color: var(--background-background);
-  color: var(--text-default-text);
+  background-color: var(--background-base);
+  color: var(--text-base);
 }

+ 9 - 1
packages/ui/src/index.tsx

@@ -1,5 +1,6 @@
 /* @refresh reload */
 import { render } from "solid-js/web"
+import { MetaProvider } from "@solidjs/meta"
 
 import App from "./app"
 
@@ -11,4 +12,11 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
   )
 }
 
-render(() => <App />, root!)
+render(
+  () => (
+    <MetaProvider>
+      <App />
+    </MetaProvider>
+  ),
+  root!,
+)

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно