diff --git a/ui/package.json b/ui/package.json
index 9e66b19..28eb859 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -4,6 +4,7 @@
"type": "module",
"scripts": {
"build": "tsdown && node scripts/copy-static.mjs",
+ "dev": "node scripts/dev-server.mjs",
"typecheck": "tsc --noEmit -p tsconfig.json",
"test": "node --test entrypoint.test.mjs nginx-config.test.mjs public/*.test.mjs"
},
diff --git a/ui/public/acp-page-layout.test.mjs b/ui/public/acp-page-layout.test.mjs
index cf31e3e..78feedb 100644
--- a/ui/public/acp-page-layout.test.mjs
+++ b/ui/public/acp-page-layout.test.mjs
@@ -155,7 +155,8 @@ test('ACP page renders a two-pane shell with a single sidebar rail', async () =>
assert.equal(shellEl.children.length, 1);
const card = shellEl.children[0];
assert.equal(card.className.includes('acp-shell'), true);
- assert.equal(card.children.length, 2);
+ assert.ok(card.children.length >= 2);
assert.equal(card.children[0].className.includes('acp-sidebar'), true);
- assert.equal(card.children[1].className.includes('acp-main'), true);
+ const mainEl = card.children.find((c) => c.className && c.className.includes('acp-main'));
+ assert.ok(mainEl);
});
diff --git a/ui/public/acp-page-session-binding.test.mjs b/ui/public/acp-page-session-binding.test.mjs
index d2d5a97..79e6997 100644
--- a/ui/public/acp-page-session-binding.test.mjs
+++ b/ui/public/acp-page-session-binding.test.mjs
@@ -200,7 +200,7 @@ test('ACP page rebinds the selected conversation before sending on a stale clien
await new Promise((resolve) => setTimeout(resolve, 0));
const composer = walk(shellEl, (node) => node.tagName === 'textarea');
- const sendButton = walk(shellEl, (node) => node.tagName === 'button' && node.textContent === 'Send');
+ const sendButton = walk(shellEl, (node) => node.tagName === 'button' && (node.textContent === 'Send' || node.className === 'acp-composer-send'));
assert.ok(composer);
assert.ok(sendButton);
@@ -327,7 +327,7 @@ test('ACP page repairs a missing session by bootstrapping once and reconnecting'
await new Promise((resolve) => setTimeout(resolve, 0));
const composer = walk(shellEl, (node) => node.tagName === 'textarea');
- const sendButton = walk(shellEl, (node) => node.tagName === 'button' && node.textContent === 'Send');
+ const sendButton = walk(shellEl, (node) => node.tagName === 'button' && (node.textContent === 'Send' || node.className === 'acp-composer-send'));
assert.ok(composer);
assert.ok(sendButton);
diff --git a/ui/public/app-list-actions.test.mjs b/ui/public/app-list-actions.test.mjs
index 24cb923..36d0a5a 100644
--- a/ui/public/app-list-actions.test.mjs
+++ b/ui/public/app-list-actions.test.mjs
@@ -121,7 +121,7 @@ test('spritz list shows a transitional chat action while workspace chat is still
},
},
location: {
- hash: '',
+ hash: '#create',
pathname: '/',
search: '',
origin: 'http://example.test',
diff --git a/ui/public/index.html b/ui/public/index.html
index 45e9efd..10e56f6 100644
--- a/ui/public/index.html
+++ b/ui/public/index.html
@@ -8,55 +8,69 @@
-
diff --git a/ui/public/styles.css b/ui/public/styles.css
index b45911b..71d6bcc 100644
--- a/ui/public/styles.css
+++ b/ui/public/styles.css
@@ -1,10 +1,81 @@
-@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
+
+html {
+ overflow-y: scroll;
+ scrollbar-gutter: stable;
+}
+
+/* Tooltip */
+[data-tooltip] {
+ position: relative;
+}
+
+[data-tooltip]::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: calc(100% + 6px);
+ left: 50%;
+ transform: translateX(-50%) translateY(4px);
+ padding: 5px 10px;
+ border-radius: 8px;
+ background: #1a1a1a;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 400;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.15s ease, transform 0.15s ease;
+ z-index: 9999;
+}
+
+[data-tooltip]:hover::after {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+}
+
+[data-tooltip=""]::after {
+ display: none;
+}
+
+[data-tooltip-pos="bottom"]::after {
+ bottom: auto;
+ top: calc(100% + 6px);
+ transform: translateX(-50%) translateY(-4px);
+}
+
+[data-tooltip-pos="bottom"]:hover::after {
+ transform: translateX(-50%) translateY(0);
+}
+
+[data-tooltip-pos="bottom-end"]::after {
+ bottom: auto;
+ top: calc(100% + 6px);
+ left: auto;
+ right: 0;
+ transform: translateY(-4px);
+}
+
+[data-tooltip-pos="bottom-end"]:hover::after {
+ transform: translateY(0);
+}
+
+[data-tooltip-pos="right"]::after {
+ bottom: auto;
+ top: 50%;
+ left: calc(100% + 6px);
+ transform: translateY(-50%) translateX(-4px);
+}
+
+[data-tooltip-pos="right"]:hover::after {
+ transform: translateY(-50%) translateX(0);
+}
:root {
color-scheme: light;
- font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
- background: radial-gradient(circle at top left, #fdf4e3 0%, #f8f7f3 35%, #eef0f3 100%);
- color: #1f2a33;
+ font-family: "Inter", system-ui, sans-serif;
+ background: #ffffff;
+ color: #000000;
}
body {
@@ -44,9 +115,9 @@ header p {
justify-content: center;
padding: 10px 16px;
border-radius: 999px;
- background: rgba(46, 58, 70, 0.08);
- border: 1px solid rgba(46, 58, 70, 0.14);
- color: #2e3a46;
+ background: rgba(0, 0, 0, 0.06);
+ border: 1px solid rgba(0, 0, 0, 0.12);
+ color: #000000;
text-decoration: none;
white-space: nowrap;
}
@@ -84,9 +155,9 @@ header p {
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(255, 90, 90, 0.28);
- background: rgba(29, 37, 46, 0.94);
- color: #f5f7fa;
- box-shadow: 0 18px 40px rgba(12, 18, 28, 0.24);
+ background: rgba(0, 0, 0, 0.92);
+ color: #ffffff;
+ box-shadow: 0 18px 40px rgba(0, 0, 0, 0.2);
pointer-events: auto;
}
@@ -103,37 +174,199 @@ header p {
padding: 0;
border: none;
background: transparent;
- color: rgba(245, 247, 250, 0.84);
+ color: rgba(255, 255, 255, 0.84);
font: inherit;
cursor: pointer;
}
.card {
- background: white;
+ background: #ffffff;
border-radius: 20px;
- padding: 24px;
- box-shadow: 0 18px 40px rgba(12, 18, 28, 0.08);
+ padding: 32px;
+ border: 1px solid #e5e5e5;
}
form {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+#create-section,
+#list-section {
+ display: contents;
+}
+
+#create-section[hidden],
+#list-section[hidden] {
+ display: none;
+}
+
+.page-title {
+ font-size: 24px;
+ font-weight: 600;
+ margin: 0;
+}
+
+.page-subtitle {
+ margin: -16px 0 0;
+ opacity: 0.6;
+ font-size: 15px;
+}
+
+.form-row {
+ display: flex;
+ gap: 24px;
+}
+
+.form-row > label {
+ flex: 1;
+}
+
+@media (max-width: 540px) {
+ .shell {
+ padding: 24px 16px;
+ gap: 16px;
+ }
+
+ .card {
+ padding: 20px;
+ border-radius: 16px;
+ }
+
+ .page-title {
+ font-size: 20px;
+ }
+
+ .page-subtitle {
+ margin-top: -10px;
+ font-size: 13px;
+ }
+
+ form {
+ gap: 18px;
+ }
+
+ .form-row {
+ flex-direction: column;
+ gap: 18px;
+ }
+
+ .name-field {
+ gap: 0;
+ }
+
+ .name-field input {
+ border-radius: 10px 0 0 10px;
+ min-width: 0;
+ }
+
+ .name-field .ghost {
+ border-radius: 0 10px 10px 0;
+ white-space: nowrap;
+ }
+
+ .spritz-item {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 14px 0;
+ }
+
+ .actions {
+ flex-wrap: wrap;
+ }
+
+ .list-header {
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ form > button[type="submit"] {
+ align-self: stretch;
+ justify-content: center;
+ }
+}
+
+.advanced-config {
+ border-top: 1px solid #e5e5e5;
+ padding-top: 24px;
+}
+
+.advanced-config-toggle {
+ background: none;
+ border: none;
+ padding: 0;
+ color: #000000;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ user-select: none;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.advanced-config-toggle::before {
+ content: '';
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-right: 2px solid currentColor;
+ border-bottom: 2px solid currentColor;
+ transform: rotate(-45deg);
+ transition: transform 0.25s ease;
+}
+
+.advanced-config.open .advanced-config-toggle::before {
+ transform: rotate(45deg);
+}
+
+.advanced-config-body {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 16px;
+ grid-template-rows: 0fr;
+ transition: grid-template-rows 0.3s ease;
+}
+
+.advanced-config.open .advanced-config-body {
+ grid-template-rows: 1fr;
+}
+
+.advanced-config-inner {
+ overflow: hidden;
+ padding-top: 0;
+}
+
+.advanced-config-inner::before {
+ content: '';
+ display: block;
+ height: 16px;
}
.preset-panel {
- grid-column: 1 / -1;
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: 10px;
+ padding-bottom: 4px;
}
.preset-panel select {
- padding: 10px 12px;
+ padding: 12px 14px;
border-radius: 10px;
- border: 1px solid #d7dbe0;
+ border: 1px solid #e5e5e5;
font-size: 14px;
+ font-weight: 400;
background: white;
+ transition: border-color 0.15s ease;
+}
+
+.preset-panel select:hover {
+ border-color: #cccccc;
+}
+
+.preset-panel select:focus-visible {
+ outline: none;
+ border-color: #000000;
}
.preset-help {
@@ -144,90 +377,294 @@ form {
label {
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: 10px;
font-size: 14px;
+ font-weight: 500;
}
input {
- padding: 10px 12px;
+ padding: 12px 14px;
border-radius: 10px;
- border: 1px solid #d7dbe0;
+ border: 1px solid #e5e5e5;
font-size: 14px;
+ font-weight: 400;
+ transition: border-color 0.15s ease;
+}
+
+input:hover {
+ border-color: #cccccc;
+}
+
+input:focus-visible {
+ outline: none;
+ border-color: #000000;
+}
+
+input::placeholder {
+ color: #999999;
}
.name-field {
display: flex;
- gap: 8px;
+ gap: 4px;
align-items: center;
}
.name-field input {
flex: 1;
+ border-radius: 10px 4px 4px 10px;
+}
+
+.name-field .ghost {
+ border-radius: 4px 10px 10px 4px;
}
textarea {
- padding: 10px 12px;
+ padding: 12px 14px;
border-radius: 10px;
- border: 1px solid #d7dbe0;
+ border: 1px solid #e5e5e5;
font-size: 13px;
font-family: "SFMono-Regular", "JetBrains Mono", "Menlo", monospace;
resize: vertical;
min-height: 140px;
+ transition: border-color 0.15s ease;
}
-small.hint {
- font-size: 12px;
- opacity: 0.7;
+textarea:hover {
+ border-color: #cccccc;
+}
+
+textarea:focus-visible {
+ outline: none;
+ border-color: #000000;
+}
+
+textarea::placeholder {
+ color: #999999;
}
-.span-all {
- grid-column: 1 / -1;
+small.hint {
+ font-size: 12px;
+ font-weight: 400;
+ opacity: 0.6;
}
button {
- padding: 10px 16px;
+ padding: 16px 22px;
border: none;
border-radius: 999px;
- background: #2e3a46;
+ background: #000000;
color: white;
cursor: pointer;
+ font-size: 14px;
+ font-weight: 400;
+ transition: opacity 0.15s ease;
+}
+
+button:hover {
+ opacity: 0.8;
+}
+
+button:active {
+ opacity: 0.7;
+}
+
+button:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+form > button[type="submit"] {
+ align-self: flex-start;
+ margin-top: 4px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+#name-random{
+ padding:12px 20px;
}
button.ghost {
background: white;
- border: 1px solid #d7dbe0;
- color: #2e3a46;
+ border: 1px solid #e5e5e5;
+ color: #000000;
+ transition: border-color 0.15s ease, background-color 0.15s ease;
+}
+
+button.ghost:hover {
+ border-color: #cccccc;
+ background: #fafafa;
+ opacity: 1;
+}
+
+button.ghost:active {
+ background: #f5f5f5;
+ opacity: 1;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 12px;
+}
+
+.list-header .ghost {
+ padding: 8px 14px;
+ font-size: 13px;
+}
+
+.icon-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.icon-btn svg {
+ flex-shrink: 0;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+button.loading {
+ pointer-events: none;
+ opacity: 0.6;
+}
+
+button.loading svg {
+ animation: spin 0.8s linear infinite;
}
#list {
- display: grid;
- gap: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.list-empty {
+ padding: 32px 0;
+ text-align: center;
+}
+
+.list-empty p {
+ margin: 0;
+ font-size: 14px;
+ opacity: 0.4;
+}
+
+.list-empty p:first-child {
+ font-weight: 500;
+ opacity: 0.6;
+ margin-bottom: 4px;
}
.spritz-item {
display: flex;
justify-content: space-between;
- align-items: center;
- border: 1px solid #e5e8ec;
- padding: 12px 16px;
- border-radius: 12px;
+ gap:16px;
+ align-items: start;
+ padding: 16px 0;
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.spritz-item:last-child {
+ border-bottom: none;
+}
+
+.spritz-item strong {
+ font-size: 14px;
+ font-weight: 500;
}
.spritz-item small {
display: block;
- opacity: 0.6;
+ opacity: 0.5;
+ font-size: 13px;
+ margin-top: 2px;
}
.actions {
display: flex;
- gap: 8px;
+ gap: 6px;
+ flex-shrink: 0;
+}
+
+.actions button {
+ padding: 8px 14px;
+ font-size: 13px;
+ background: white;
+ border: 1px solid #e5e5e5;
+ color: #000000;
+ transition: border-color 0.15s ease, background-color 0.15s ease;
+}
+
+.actions button:hover {
+ border-color: #cccccc;
+ background: #fafafa;
+ opacity: 1;
+}
+
+.actions button:active {
+ background: #f5f5f5;
+ opacity: 1;
+}
+
+.actions button.destructive {
+ color: #dc2626;
+ border-color: rgba(220, 38, 38, 0.25);
+}
+
+.actions button.destructive:hover {
+ background: rgba(220, 38, 38, 0.06);
+ border-color: rgba(220, 38, 38, 0.4);
+}
+
+.actions button.destructive:active {
+ background: rgba(220, 38, 38, 0.1);
+}
+
+@keyframes shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+
+.skeleton-line {
+ height: 14px;
+ border-radius: 6px;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s ease infinite;
+}
+
+.skeleton-name {
+ width: 120px;
+ margin-bottom: 6px;
+}
+
+.skeleton-meta {
+ width: 200px;
+ height: 12px;
+}
+
+.skeleton-info {
+ flex: 1;
+}
+
+.skeleton-actions {
+ display: flex;
+ gap: 6px;
+}
+
+.skeleton-btn {
+ width: 64px;
+ height: 32px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s ease infinite;
}
.terminal-card {
@@ -243,10 +680,10 @@ button.ghost {
max-width: none;
height: 100dvh;
min-height: 100dvh;
- padding: 20px;
+ padding: 0;
box-sizing: border-box;
overflow: hidden;
- gap: 12px;
+ gap: 0;
}
.shell[data-view="chat"] > header {
@@ -261,17 +698,18 @@ button.ghost {
.acp-shell {
flex: 1;
display: grid;
- grid-template-columns: 320px minmax(0, 1fr);
+ grid-template-columns: 260px minmax(0, 1fr);
min-height: 0;
padding: 0;
overflow: hidden;
+ transition: grid-template-columns 0.2s ease;
}
.acp-sidebar {
display: flex;
flex-direction: column;
- border-right: 1px solid #e5e8ec;
- background: rgba(247, 247, 248, 0.96);
+ border-right: 1px solid #e5e5e5;
+ background: #fafafa;
min-height: 0;
overflow: hidden;
}
@@ -279,124 +717,262 @@ button.ghost {
.acp-sidebar-top {
display: flex;
flex-direction: column;
- gap: 16px;
- padding: 20px;
- border-bottom: 1px solid #e5e8ec;
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(247, 247, 248, 0.94));
+ gap: 8px;
+ padding: 12px;
+ background: #fafafa;
flex-shrink: 0;
+ border-bottom: 1px solid #e5e5e5;
}
-.acp-sidebar-nav {
+.acp-sidebar-select-row {
display: flex;
align-items: center;
- justify-content: space-between;
- gap: 12px;
+ gap: 8px;
+}
+
+.acp-sidebar-select-row .acp-agent-select {
+ flex: 1;
+ min-width: 0;
}
-.acp-sidebar-title {
+.acp-sidebar-actions {
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: 8px;
+}
+
+.acp-sidebar-nav {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.acp-nav-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ border-radius: 10px;
+ border: 1px solid #e5e5e5;
+ background: white;
+ color: #000000;
+ cursor: pointer;
+ text-decoration: none;
+ transition: background-color 0.15s ease, border-color 0.15s ease;
+}
+
+.acp-nav-icon:hover {
+ background: #f5f5f5;
+ border-color: #cccccc;
+ opacity: 1;
}
-.acp-sidebar-title h2,
.acp-main-copy h2 {
margin: 0;
}
-.acp-sidebar-title p,
.acp-main-copy p {
margin: 0;
- opacity: 0.68;
+ opacity: 0.6;
}
.acp-agent-select {
width: 100%;
- padding: 12px 14px;
- border-radius: 16px;
- border: 1px solid #d7dbe0;
+ padding: 8px 10px;
+ border-radius: 8px;
+ border: 1px solid #e5e5e5;
background: white;
- color: #1f2a33;
+ color: #000000;
font: inherit;
+ font-size: 13px;
}
-.acp-thread-list {
- flex: 1;
- min-height: 0;
- overflow: auto;
- padding: 12px;
+.acp-new-chat-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ width: 100%;
+ padding: 8px 10px;
+ border-radius: 8px;
+ border: 1px solid #e5e5e5;
+ background: white;
+ color: #000000;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.12s ease, border-color 0.12s ease;
+ flex-shrink: 0;
+ box-sizing: border-box;
+}
+
+.acp-new-chat-item:hover {
+ background: #f5f5f5;
+ border-color: #cccccc;
+ opacity: 1;
+}
+
+.acp-new-chat-item svg {
+ flex-shrink: 0;
+ opacity: 0.6;
+}
+
+.acp-back-link {
+ border: 1px dashed #e5e5e5;
+ background: transparent;
+ font-weight: 400;
+ text-decoration: none;
+ margin-bottom: 0;
+}
+
+.acp-back-link svg {
+ opacity: 0.4;
+}
+
+/* Agent groups */
+.acp-agent-group {
display: flex;
flex-direction: column;
- gap: 8px;
- overscroll-behavior: contain;
}
-.acp-thread-item {
+.acp-agent-group + .acp-agent-group {
+ margin-top: 8px;
+}
+
+.acp-agent-header {
display: flex;
- align-items: flex-start;
- gap: 12px;
+ align-items: center;
+ gap: 8px;
width: 100%;
- padding: 12px 14px;
- border-radius: 18px;
+ padding: 8px 12px;
border: 0;
+ border-radius: 8px;
background: transparent;
- color: inherit;
+ color: #000000;
+ font-size: 13px;
+ font-weight: 500;
text-align: left;
+ cursor: pointer;
+ transition: background-color 0.12s ease;
}
-.acp-thread-item:hover,
-.acp-thread-item[data-active="true"] {
- background: white;
- box-shadow: 0 8px 28px rgba(15, 23, 42, 0.08);
+.acp-agent-header:hover {
+ background: #f0f0f0;
+ opacity: 1;
}
-.acp-thread-avatar {
- width: 42px;
- height: 42px;
+.acp-agent-header[data-active="true"] {
+ color: #000000;
+}
+
+.acp-agent-header-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.acp-agent-chevron {
+ display: inline-block;
+ width: 5px;
+ height: 5px;
+ border-right: 1.5px solid #999999;
+ border-bottom: 1.5px solid #999999;
+ transform: rotate(-45deg);
+ transition: transform 0.15s ease;
flex-shrink: 0;
- border-radius: 50%;
+}
+
+.acp-agent-group[data-expanded="true"] .acp-agent-chevron {
+ transform: rotate(45deg);
+}
+
+.acp-agent-add-btn {
display: inline-flex;
align-items: center;
justify-content: center;
- background: #2e3a46;
- color: white;
- font-size: 13px;
- font-weight: 600;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: none;
+ border-radius: 6px;
+ background: transparent;
+ color: #999999;
+ cursor: pointer;
+ flex-shrink: 0;
+ opacity: 0;
+ transition: opacity 0.12s ease, background-color 0.12s ease;
}
-.acp-thread-item-body {
- min-width: 0;
- flex: 1;
- display: flex;
+.acp-agent-header:hover .acp-agent-add-btn {
+ opacity: 1;
+}
+
+.acp-agent-add-btn:hover {
+ background: #e5e5e5;
+ color: #000000;
+ opacity: 1;
+}
+
+.acp-agent-convos {
+ display: none;
flex-direction: column;
- gap: 4px;
+ gap: 2px;
+ padding-left: 8px;
+ margin-top: 2px;
}
-.acp-thread-item-top {
+.acp-agent-group[data-expanded="true"] .acp-agent-convos {
display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
}
-.acp-thread-item-title {
- font-size: 15px;
+.acp-agent-convos .acp-thread-item {
+ padding: 6px 12px;
+ font-size: 13px;
+ border-radius: 6px;
}
-.acp-thread-item-time,
-.acp-thread-item-meta {
- font-size: 12px;
- opacity: 0.62;
+.acp-thread-list {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ overscroll-behavior: contain;
}
-.acp-thread-item-preview {
- margin: 0;
- font-size: 13px;
- color: #5d6772;
+.acp-thread-item {
+ display: block;
+ width: 100%;
+ padding: 10px 12px;
+ border-radius: 10px;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
+ transition: background-color 0.12s ease;
}
-.acp-thread-item-meta {
- margin: 0;
+.acp-thread-item:hover {
+ background: #f0f0f0;
+}
+
+.acp-thread-item[data-active="true"] {
+ background: white;
+}
+
+.acp-thread-item-title {
+ font-size: 14px;
+ font-weight: 400;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.acp-main {
@@ -405,36 +981,63 @@ button.ghost {
min-width: 0;
min-height: 0;
overflow: hidden;
- background:
- radial-gradient(circle at top left, rgba(120, 177, 255, 0.08), transparent 28%),
- linear-gradient(180deg, #f7f8fa 0%, #eef1f5 100%);
+ background: #ffffff;
}
.acp-main-header {
flex-shrink: 0;
- padding: 20px 24px 16px;
- border-bottom: 1px solid rgba(229, 232, 236, 0.9);
- background: rgba(255, 255, 255, 0.82);
- backdrop-filter: blur(16px);
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 12px 20px;
+ border-bottom: 1px solid #e5e5e5;
+ background: #fafafa;
}
.acp-main-header-top {
display: flex;
- align-items: flex-start;
+ align-items: center;
justify-content: space-between;
- gap: 16px;
+ gap: 12px;
+}
+
+.acp-main-copy {
+ min-width: 0;
+ flex: 1;
+}
+
+.acp-main-copy h2 {
+ font-size: 14px;
+ font-weight: 500;
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.acp-main-copy p {
+ font-size: 12px;
+ margin: 2px 0 0;
+ opacity: 0.5;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.acp-main-copy p:empty {
+ display: none;
}
.acp-main-actions {
display: flex;
gap: 8px;
+ flex-shrink: 0;
}
.acp-command-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
- padding-top: 14px;
}
.acp-command-pill,
@@ -444,65 +1047,202 @@ button.ghost {
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
- border: 1px solid rgba(46, 58, 70, 0.12);
- background: rgba(255, 255, 255, 0.84);
+ border: 1px solid #e5e5e5;
+ background: #ffffff;
font-size: 12px;
}
-.acp-main-body {
- flex: 1;
- min-height: 0;
- overflow: auto;
- padding: 28px 24px 12px;
- overscroll-behavior: contain;
+.acp-main-body {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding: 28px 24px 12px;
+ overscroll-behavior: contain;
+ display: flex;
+ flex-direction: column;
+ scrollbar-gutter: stable;
+}
+
+.acp-stream {
+ max-width: 880px;
+ width: 100%;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ flex: 1;
+}
+
+.acp-empty {
+ margin: auto;
+ max-width: 420px;
+ text-align: center;
+ opacity: 0.7;
+}
+
+.acp-empty--sidebar {
+ margin: 24px 12px;
+ text-align: left;
+}
+
+.acp-welcome-card {
+ max-width: 540px;
+ margin: auto;
+ padding: 0;
+ border: none;
+ background: transparent;
+ text-align: center;
+}
+
+.acp-welcome-card strong {
+ display: block;
+ font-size: 16px;
+ font-weight: 500;
+ margin-bottom: 6px;
+}
+
+.acp-welcome-card p {
+ margin: 0;
+ font-size: 14px;
+ color: #999999;
+}
+
+/* Preset gallery */
+.acp-preset-gallery {
+ margin: auto;
+ max-width: 560px;
+ text-align: center;
+}
+
+.acp-preset-gallery-title {
+ font-size: 18px;
+ font-weight: 600;
+ margin: 0 0 20px;
+}
+
+.acp-preset-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ gap: 12px;
+}
+
+.acp-preset-card {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 6px;
+ padding: 16px;
+ border-radius: 12px;
+ border: 1px solid #e5e5e5;
+ background: white;
+ cursor: pointer;
+ text-align: left;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.acp-preset-card:hover {
+ border-color: #cccccc;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+ opacity: 1;
+}
+
+.acp-preset-card strong {
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.acp-preset-card p {
+ margin: 0;
+ font-size: 12px;
+ color: #999999;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 100%;
+}
+
+.acp-preset-card--custom {
+ border-style: dashed;
+ background: transparent;
+}
+
+.acp-preset-card:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Create modal */
+.acp-create-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 50;
+ background: rgba(0, 0, 0, 0.3);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+}
+
+.acp-create-modal {
+ background: white;
+ border-radius: 16px;
+ width: 100%;
+ max-width: 480px;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.12);
+}
+
+.acp-create-modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.acp-create-modal-header h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
}
-.acp-stream {
- max-width: 880px;
- margin: 0 auto;
+.acp-create-modal-form {
+ padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
-.acp-empty {
- margin: auto;
- max-width: 420px;
- text-align: center;
- opacity: 0.7;
-}
-
-.acp-empty--sidebar {
- margin: 24px 12px;
- text-align: left;
+.acp-create-modal-form label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ font-size: 13px;
+ font-weight: 500;
}
-.acp-welcome-card {
- max-width: 680px;
- margin: auto;
- padding: 24px;
- border-radius: 24px;
- border: 1px solid rgba(46, 58, 70, 0.08);
- background: rgba(255, 255, 255, 0.88);
- box-shadow: 0 18px 44px rgba(15, 23, 42, 0.08);
+.acp-create-modal-form input {
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1px solid #e5e5e5;
+ font-size: 14px;
+ transition: border-color 0.15s ease;
}
-.acp-welcome-card strong {
- display: block;
- font-size: 20px;
- margin-bottom: 8px;
+.acp-create-modal-form input:focus-visible {
+ outline: none;
+ border-color: #000000;
}
-.acp-welcome-card p {
- margin: 0;
- color: #5d6772;
+.acp-create-modal-form button[type="submit"] {
+ margin-top: 4px;
+ align-self: flex-start;
}
.acp-pending-card {
- background:
- linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(244, 247, 251, 0.94)),
- white;
- border-color: rgba(55, 130, 255, 0.18);
+ background: #ffffff;
+ border-color: rgba(55, 130, 255, 0.25);
}
.acp-message {
@@ -523,47 +1263,119 @@ button.ghost {
.acp-message--tool,
.acp-message--plan,
.acp-message--system {
- align-self: stretch;
- max-width: 100%;
+ align-self: flex-start;
+ max-width: min(820px, 86%);
+}
+
+.acp-message--thinking {
+ align-self: flex-start;
+ max-width: min(820px, 86%);
+}
+
+/* Thinking block */
+.acp-thinking-block {
+ border-radius: 12px;
+ font-size: 13px;
+}
+
+.acp-thinking-summary {
+ cursor: pointer;
+ user-select: none;
+ color: #999999;
+ font-style: italic;
+ font-size: 13px;
+ padding: 4px 0;
+ list-style: none;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.acp-thinking-summary::-webkit-details-marker {
+ display: none;
+}
+
+.acp-thinking-summary::before {
+ content: '';
+ display: inline-block;
+ width: 5px;
+ height: 5px;
+ border-right: 1.5px solid #999999;
+ border-bottom: 1.5px solid #999999;
+ transform: rotate(-45deg);
+ transition: transform 0.15s ease;
+ flex-shrink: 0;
+}
+
+.acp-thinking-block[open] .acp-thinking-summary::before {
+ transform: rotate(45deg);
+}
+
+.acp-thinking-content {
+ padding: 8px 0 4px 12px;
+ border-left: 2px solid #e5e5e5;
+ margin-left: 2px;
+ margin-top: 4px;
+ color: #666666;
+ font-size: 13px;
+}
+
+.acp-thinking-content .acp-rich-paragraph {
+ font-size: 13px;
+ color: #666666;
+}
+
+/* Compact tool calls */
+.acp-event-card .acp-details-body {
+ max-height: 200px;
+ overflow: auto;
}
.acp-bubble,
.acp-event-card {
- border-radius: 24px;
- box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
+ border-radius: 20px;
}
.acp-bubble {
- padding: 16px 18px;
- border: 1px solid rgba(229, 232, 236, 0.9);
+ padding: 8px 16px;
+ border: 1px solid #e5e5e5;
background: white;
}
.acp-message--user .acp-bubble {
- background: #2e3a46;
+ background: #000000;
color: white;
border-color: transparent;
- border-bottom-right-radius: 10px;
+ border-bottom-right-radius: 6px;
}
.acp-message--assistant .acp-bubble {
- border-bottom-left-radius: 10px;
+ background: transparent;
+ border: none;
+ padding: 4px 0;
+ border-radius: 0;
}
.acp-event-card {
max-width: 880px;
margin: 0 auto;
- padding: 16px 18px;
- border: 1px solid rgba(46, 58, 70, 0.1);
- background: rgba(255, 255, 255, 0.9);
+ padding: 12px 16px;
+ border: 1px solid #f0f0f0;
+ background: #fafafa;
+ font-size: 13px;
}
.acp-message-meta {
display: flex;
- align-items: flex-start;
+ align-items: center;
justify-content: space-between;
gap: 12px;
- margin-bottom: 10px;
+ margin-bottom: 6px;
+}
+
+.acp-event-card .acp-message-meta strong {
+ font-size: 13px;
+ font-weight: 500;
}
.acp-meta-stack {
@@ -576,7 +1388,7 @@ button.ghost {
.acp-message-meta-text {
font-size: 12px;
- opacity: 0.64;
+ opacity: 0.6;
}
.acp-status-pill {
@@ -608,15 +1420,66 @@ button.ghost {
.acp-rich-paragraph {
margin: 0;
- line-height: 1.6;
+ line-height: 1.7;
+ font-size: 14px;
white-space: pre-wrap;
}
+.acp-rich-paragraph a {
+ color: #000000;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.acp-rich-paragraph a:hover {
+ opacity: 0.6;
+}
+
+.acp-inline-code {
+ padding: 2px 6px;
+ border-radius: 4px;
+ background: #f0f0f0;
+ font-family: "SFMono-Regular", "JetBrains Mono", "Menlo", monospace;
+ font-size: 0.9em;
+}
+
+.acp-md-heading {
+ margin: 16px 0 6px;
+ font-weight: 600;
+ line-height: 1.4;
+}
+
+h2.acp-md-heading { font-size: 18px; }
+h3.acp-md-heading { font-size: 16px; }
+h4.acp-md-heading { font-size: 15px; }
+h5.acp-md-heading { font-size: 14px; }
+
+.acp-md-heading:first-child {
+ margin-top: 0;
+}
+
+.acp-md-list {
+ margin: 4px 0;
+ padding-left: 20px;
+ line-height: 1.7;
+ font-size: 14px;
+}
+
+.acp-md-list li + li {
+ margin-top: 2px;
+}
+
+.acp-block--text hr {
+ border: none;
+ border-top: 1px solid #e5e5e5;
+ margin: 12px 0;
+}
+
.acp-code-block,
.acp-details-body {
margin: 0;
padding: 14px 16px;
- border-radius: 16px;
+ border-radius: 10px;
background: #101827;
color: #f8fafc;
overflow: auto;
@@ -628,12 +1491,19 @@ button.ghost {
.acp-details {
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: 6px;
}
.acp-details summary {
cursor: pointer;
- font-weight: 600;
+ font-size: 13px;
+ font-weight: 500;
+ opacity: 0.6;
+ user-select: none;
+}
+
+.acp-details summary:hover {
+ opacity: 0.9;
}
.acp-plan-list {
@@ -658,7 +1528,7 @@ button.ghost {
.acp-key-value dd {
margin: 0;
- opacity: 0.74;
+ opacity: 0.7;
}
.acp-tag-row {
@@ -678,27 +1548,29 @@ button.ghost {
.acp-main-footer {
flex-shrink: 0;
- border-top: 1px solid rgba(229, 232, 236, 0.9);
- background: rgba(255, 255, 255, 0.92);
+ background: #ffffff;
}
.acp-footer-inner {
max-width: 880px;
margin: 0 auto;
- padding: 16px 24px 22px;
+ padding: 12px 0 16px;
display: flex;
flex-direction: column;
- gap: 12px;
+ gap: 10px;
+ width: 100%;
+ box-sizing: border-box;
}
.acp-permission {
padding: 14px 16px;
border-radius: 16px;
- background: rgba(251, 191, 36, 0.14);
- border: 1px solid rgba(180, 120, 0, 0.18);
+ background: #fafafa;
+ border: 1px solid #e5e5e5;
display: flex;
flex-direction: column;
- gap: 12px;
+ gap: 10px;
+ font-size: 14px;
}
.acp-permission[hidden] {
@@ -707,10 +1579,37 @@ button.ghost {
.acp-permission-options {
display: flex;
- gap: 8px;
+ gap: 6px;
flex-wrap: wrap;
}
+.acp-permission-options button {
+ padding: 8px 16px;
+ font-size: 13px;
+ border-radius: 999px;
+ background: white;
+ color: #000000;
+ border: 1px solid #e5e5e5;
+ font-weight: 400;
+ transition: background-color 0.12s ease, border-color 0.12s ease;
+}
+
+.acp-permission-options button:hover {
+ background: #f5f5f5;
+ border-color: #cccccc;
+ opacity: 1;
+}
+
+.acp-permission-options button:first-child {
+ background: #000000;
+ color: white;
+ border-color: transparent;
+}
+
+.acp-permission-options button:first-child:hover {
+ opacity: 0.8;
+}
+
.acp-status-row {
display: grid;
grid-template-columns: auto auto 1fr;
@@ -720,7 +1619,40 @@ button.ghost {
}
.acp-status {
- opacity: 0.72;
+ opacity: 0.7;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+/* Grid loader */
+.grid-loader {
+ display: inline-grid;
+ grid-template-columns: repeat(3, 4px);
+ grid-template-rows: repeat(3, 4px);
+ gap: 1.5px;
+ flex-shrink: 0;
+}
+
+.grid-loader > span {
+ border-radius: 1px;
+ background: #d4d4d4;
+ animation: grid-pulse 1.2s ease-in-out infinite;
+}
+
+.grid-loader > span:nth-child(1) { animation-delay: 0s; }
+.grid-loader > span:nth-child(2) { animation-delay: 0.1s; }
+.grid-loader > span:nth-child(3) { animation-delay: 0.2s; }
+.grid-loader > span:nth-child(6) { animation-delay: 0.3s; }
+.grid-loader > span:nth-child(9) { animation-delay: 0.4s; }
+.grid-loader > span:nth-child(8) { animation-delay: 0.5s; }
+.grid-loader > span:nth-child(7) { animation-delay: 0.6s; }
+.grid-loader > span:nth-child(4) { animation-delay: 0.7s; }
+.grid-loader > span:nth-child(5) { animation-delay: 0.8s; }
+
+@keyframes grid-pulse {
+ 0%, 60%, 100% { background: #d4d4d4; }
+ 20% { background: #000000; }
}
.acp-status-meta {
@@ -733,28 +1665,88 @@ button.ghost {
.acp-composer {
display: flex;
flex-direction: column;
- gap: 12px;
+ border: 1px solid #e5e5e5;
+ border-radius: 28px;
+ background: white;
+ transition: border-color 0.15s ease;
+ box-shadow: rgba(99, 99, 99, 0.19) 0px 2px 8px 0px;
+}
+
+.acp-composer:focus-within {
+ border-color: #cccccc;
}
.acp-composer textarea {
- min-height: 120px;
+ display: block;
+ width: 100%;
+ min-height: 24px;
+ max-height: 180px;
font-family: inherit;
- font-size: 16px;
+ font-size: 14px;
line-height: 1.55;
- border-radius: 18px;
- padding: 14px 16px;
+ border: none;
+ border-radius: 28px 28px 0 0;
+ padding: 16px 20px 4px;
+ background: transparent;
+ resize: none;
+ box-sizing: border-box;
+ overflow-y: auto;
+}
+
+.acp-composer textarea:hover {
+ border-color: transparent;
+}
+
+.acp-composer textarea:focus-visible {
+ outline: none;
+ border-color: transparent;
+}
+
+/* Hide scrollbar but keep scrollable */
+.acp-composer textarea::-webkit-scrollbar {
+ display: none;
+}
+
+.acp-composer textarea {
+ scrollbar-width: none;
}
.acp-composer-actions {
display: flex;
- justify-content: space-between;
- gap: 12px;
+ justify-content: flex-end;
+ gap: 8px;
+ align-items: center;
+ padding: 8px 12px 12px;
+ background: linear-gradient(to bottom, transparent, white 60%);
+ border-radius: 0 0 28px 28px;
+ margin-top: -16px;
+ position: relative;
+ z-index: 1;
+}
+
+.acp-composer-send {
+ display: inline-flex;
align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ border-radius: 50%;
+ border: none;
+ background: #000000;
+ color: white;
+ cursor: pointer;
+ transition: opacity 0.15s ease;
+}
+
+.acp-composer-send:hover {
+ opacity: 0.8;
}
+
.acp-hint {
font-size: 12px;
- opacity: 0.65;
+ opacity: 0.6;
justify-self: end;
}
@@ -766,7 +1758,7 @@ button.ghost {
.terminal-back {
font-size: 14px;
- color: #2e3a46;
+ color: #000000;
text-decoration: none;
}
@@ -783,6 +1775,52 @@ button.ghost {
padding: 8px;
}
+/* Collapsed sidebar */
+.acp-shell[data-collapsed="true"] {
+ grid-template-columns: 56px minmax(0, 1fr);
+}
+
+.acp-shell[data-collapsed="true"] .acp-sidebar-top {
+ padding: 8px;
+ align-items: center;
+}
+
+.acp-shell[data-collapsed="true"] .acp-sidebar-select-row {
+ justify-content: center;
+}
+
+.acp-shell[data-collapsed="true"] .acp-agent-select {
+ display: none;
+}
+
+.acp-shell[data-collapsed="true"] .acp-new-chat-item {
+ justify-content: center;
+ padding: 8px;
+}
+
+.acp-shell[data-collapsed="true"] .acp-new-chat-item span {
+ display: none;
+}
+
+.acp-shell[data-collapsed="true"] .acp-thread-list {
+ display: none;
+}
+
+.acp-shell[data-collapsed="true"] .acp-sidebar-actions {
+ align-items: center;
+ gap: 4px;
+}
+
+/* Mobile menu button — hidden on desktop */
+.acp-mobile-menu {
+ display: none;
+}
+
+/* Mobile sidebar backdrop — hidden by default */
+.acp-sidebar-backdrop {
+ display: none;
+}
+
@media (max-width: 640px) {
header h1 {
font-size: 36px;
@@ -799,27 +1837,89 @@ button.ghost {
.acp-shell {
grid-template-columns: 1fr;
+ grid-template-rows: 1fr;
height: 100%;
min-height: 0;
border-radius: 0;
+ position: relative;
}
.acp-sidebar {
- min-height: 0;
- border-right: 0;
- border-bottom: 1px solid #e5e8ec;
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 260px;
+ z-index: 40;
+ transform: translateX(-100%);
+ transition: transform 0.25s ease;
+ border-right: 1px solid #e5e5e5;
+ }
+
+ .acp-shell[data-mobile-open="true"] .acp-sidebar {
+ transform: translateX(0);
+ }
+
+ .acp-sidebar-backdrop {
+ display: block;
+ position: fixed;
+ inset: 0;
+ z-index: 39;
+ background: rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.25s ease;
+ }
+
+ .acp-shell[data-mobile-open="true"] .acp-sidebar-backdrop {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .acp-sidebar-toggle {
+ display: none;
+ }
+
+ .acp-mobile-menu {
+ display: inline-flex;
}
.acp-main {
min-height: 0;
+ grid-column: 1;
+ grid-row: 1;
}
- .acp-main-header-top,
- .acp-status-row,
- .acp-composer-actions {
+ .acp-main-header {
+ overflow: hidden;
+ }
+
+ .acp-command-bar {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ padding-bottom: 2px;
+ }
+
+ .acp-command-bar::-webkit-scrollbar {
+ display: none;
+ }
+
+ .acp-command-pill {
+ white-space: nowrap;
+ flex-shrink: 0;
+ }
+
+ .acp-main-header-top {
display: flex;
- flex-direction: column;
- align-items: flex-start;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ .acp-status-row {
+ font-size: 12px;
+ gap: 8px;
}
.acp-hint {
@@ -829,4 +1929,12 @@ button.ghost {
.acp-message {
max-width: 100%;
}
+
+ .acp-footer-inner {
+ padding: 8px 12px 12px;
+ }
+
+ .acp-composer {
+ border-radius: 20px;
+ }
}
diff --git a/ui/scripts/dev-server.mjs b/ui/scripts/dev-server.mjs
new file mode 100644
index 0000000..410fa9a
--- /dev/null
+++ b/ui/scripts/dev-server.mjs
@@ -0,0 +1,290 @@
+import { spawn } from 'node:child_process';
+import fs from 'node:fs';
+import http from 'node:http';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const uiDir = path.dirname(__dirname);
+const distDir = path.join(uiDir, 'dist');
+
+const port = Number.parseInt(process.env.PORT || '8081', 10);
+const apiOrigin = process.env.SPRITZ_UI_DEV_API_ORIGIN || 'http://127.0.0.1:8090';
+const ownerId = process.env.SPRITZ_UI_OWNER_ID || 'local-dev';
+
+let assetVersion = `${Date.now()}`;
+let buildInFlight = false;
+let buildQueued = false;
+let buildOk = false;
+let lastBuildError = '';
+let watchTimer = null;
+const liveReloadClients = new Set();
+
+function runtimeReplacements() {
+ return {
+ '__SPRITZ_API_BASE_URL__': '/api',
+ '__SPRITZ_OWNER_ID__': ownerId,
+ '__SPRITZ_UI_PRESETS__': process.env.SPRITZ_UI_PRESETS || 'null',
+ '__SPRITZ_UI_DEFAULT_REPO_URL__': process.env.SPRITZ_UI_DEFAULT_REPO_URL || '',
+ '__SPRITZ_UI_DEFAULT_REPO_DIR__': process.env.SPRITZ_UI_DEFAULT_REPO_DIR || '',
+ '__SPRITZ_UI_DEFAULT_REPO_BRANCH__': process.env.SPRITZ_UI_DEFAULT_REPO_BRANCH || '',
+ '__SPRITZ_UI_HIDE_REPO_INPUTS__': process.env.SPRITZ_UI_HIDE_REPO_INPUTS || '',
+ '__SPRITZ_UI_LAUNCH_QUERY_PARAMS__': process.env.SPRITZ_UI_LAUNCH_QUERY_PARAMS || '',
+ '__SPRITZ_UI_AUTH_MODE__': process.env.SPRITZ_UI_AUTH_MODE || '',
+ '__SPRITZ_UI_AUTH_TOKEN_STORAGE__': process.env.SPRITZ_UI_AUTH_TOKEN_STORAGE || '',
+ '__SPRITZ_UI_AUTH_TOKEN_STORAGE_KEYS__': process.env.SPRITZ_UI_AUTH_TOKEN_STORAGE_KEYS || '',
+ '__SPRITZ_UI_AUTH_BEARER_TOKEN_PARAM__': process.env.SPRITZ_UI_AUTH_BEARER_TOKEN_PARAM || '',
+ '__SPRITZ_UI_AUTH_LOGIN_URL__': process.env.SPRITZ_UI_AUTH_LOGIN_URL || '',
+ '__SPRITZ_UI_AUTH_RETURN_TO_MODE__': process.env.SPRITZ_UI_AUTH_RETURN_TO_MODE || '',
+ '__SPRITZ_UI_AUTH_RETURN_TO_PARAM__': process.env.SPRITZ_UI_AUTH_RETURN_TO_PARAM || '',
+ '__SPRITZ_UI_AUTH_REDIRECT_ON_UNAUTHORIZED__': process.env.SPRITZ_UI_AUTH_REDIRECT_ON_UNAUTHORIZED || '',
+ '__SPRITZ_UI_AUTH_REFRESH_ENABLED__': process.env.SPRITZ_UI_AUTH_REFRESH_ENABLED || '',
+ '__SPRITZ_UI_AUTH_REFRESH_URL__': process.env.SPRITZ_UI_AUTH_REFRESH_URL || '',
+ '__SPRITZ_UI_AUTH_REFRESH_METHOD__': process.env.SPRITZ_UI_AUTH_REFRESH_METHOD || '',
+ '__SPRITZ_UI_AUTH_REFRESH_CREDENTIALS__': process.env.SPRITZ_UI_AUTH_REFRESH_CREDENTIALS || '',
+ '__SPRITZ_UI_AUTH_REFRESH_TOKEN_STORAGE_KEYS__': process.env.SPRITZ_UI_AUTH_REFRESH_TOKEN_STORAGE_KEYS || '',
+ '__SPRITZ_UI_AUTH_REFRESH_TIMEOUT_MS__': process.env.SPRITZ_UI_AUTH_REFRESH_TIMEOUT_MS || '',
+ '__SPRITZ_UI_AUTH_REFRESH_COOLDOWN_MS__': process.env.SPRITZ_UI_AUTH_REFRESH_COOLDOWN_MS || '',
+ '__SPRITZ_UI_AUTH_REFRESH_HEADERS__': process.env.SPRITZ_UI_AUTH_REFRESH_HEADERS || '',
+ '__SPRITZ_UI_ASSET_VERSION__': assetVersion,
+ };
+}
+
+function transformRuntimeContent(fileName, content) {
+ let next = content;
+ for (const [placeholder, value] of Object.entries(runtimeReplacements())) {
+ next = next.split(placeholder).join(value);
+ }
+ if (fileName === 'index.html') {
+ next = next.replace(
+ '',
+ `