Users, file manager, image generation, budgets and agent workflow

- User management: local accounts (Argon2) and OpenID Connect, admin/user
  roles, per-user folders plus admin-shared folders, read-only session
  sharing, event filtering per user
- File manager with CodeMirror editor and git client (status, diff,
  stage, commit, branches, fetch/pull/push, log, clone/init)
- Image generation via fal.ai: generate_image tool and image page
- Budgets: monthly limits per provider, provider balances/costs
  (OpenRouter, Anthropic admin cost report, fal.ai), price discovery
- Subagents on other models, background tasks, concurrency limits per
  host/model/subagent
- MCP tools loaded on demand (mcp_search/mcp_call), read-only hints
- Permission mode "auto", safe read-only commands always allowed
- Persistent message queue with attachments, deletable
- Live reasoning incl. <think> tags, activity animations, CSS themes,
  mobile layout, statistics per user, upload size handling
- Docker: named volume for state, localhost-bound port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 16:54:08 +01:00
1 parent f3a2fe3816
commit 49262c612c
70 files changed
+8495 -714

No files matched your search

+772 -137
View File
@@ -1,126 +1,6 @@
/* CodeCraft – Gruvbox theme (dark default, light via data-theme or OS). */
/* CodeCraft – layout and components. Colours come exclusively from the theme
variables defined in src/themes/*.css. */
:root {
color-scheme: dark;
--bg0: #282828;
--bg1: #32302f;
--bg2: #3c3836;
--bg3: #504945;
--fg0: #fbf1c7;
--fg1: #ebdbb2;
--fg2: #d5c4a1;
--fg3: #a89984;
--muted: #928374;
--accent: #fabd2f;
--accent2: #fe8019;
--red: #fb4934;
--green: #b8bb26;
--aqua: #8ec07c;
--blue: #83a598;
--purple: #d3869b;
--diff-add: rgba(184, 187, 38, 0.16);
--diff-del: rgba(251, 73, 52, 0.16);
--shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
/* Data viz: validated categorical palette (dark steps on #282828). */
--series-1: #3987e5;
--series-2: #d95926;
--series-3: #199e70;
--series-4: #c98500;
--series-5: #d55181;
--series-6: #008300;
--series-7: #9085e9;
--series-8: #e66767;
/* Sequential (heatmap), blue ramp: low → high on dark. */
--seq-0: #32302f;
--seq-1: #104281;
--seq-2: #184f95;
--seq-3: #256abf;
--seq-4: #3987e5;
--seq-5: #6da7ec;
--seq-6: #9ec5f4;
--radius: 8px;
--font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: "JetBrains Mono", "Fira Code", ui-monospace, monospace;
}
@media (prefers-color-scheme: light) {
:root:where(:not([data-theme="dark"])) {
color-scheme: light;
--bg0: #fbf1c7;
--bg1: #f2e5bc;
--bg2: #ebdbb2;
--bg3: #d5c4a1;
--fg0: #282828;
--fg1: #3c3836;
--fg2: #504945;
--fg3: #665c54;
--muted: #7c6f64;
--accent: #b57614;
--accent2: #af3a03;
--red: #9d0006;
--green: #79740e;
--aqua: #427b58;
--blue: #076678;
--purple: #8f3f71;
--diff-add: rgba(121, 116, 14, 0.16);
--diff-del: rgba(157, 0, 6, 0.12);
--shadow: 0 6px 24px rgba(60, 56, 54, 0.18);
--series-1: #2a78d6;
--series-2: #eb6834;
--series-3: #1baf7a;
--series-4: #eda100;
--series-5: #e87ba4;
--series-6: #008300;
--series-7: #6250d6;
--series-8: #e34948;
--seq-0: #f2e5bc;
--seq-1: #cde2fb;
--seq-2: #9ec5f4;
--seq-3: #6da7ec;
--seq-4: #3987e5;
--seq-5: #256abf;
--seq-6: #184f95;
}
}
:root[data-theme="light"] {
color-scheme: light;
--bg0: #fbf1c7;
--bg1: #f2e5bc;
--bg2: #ebdbb2;
--bg3: #d5c4a1;
--fg0: #282828;
--fg1: #3c3836;
--fg2: #504945;
--fg3: #665c54;
--muted: #7c6f64;
--accent: #b57614;
--accent2: #af3a03;
--red: #9d0006;
--green: #79740e;
--aqua: #427b58;
--blue: #076678;
--purple: #8f3f71;
--diff-add: rgba(121, 116, 14, 0.16);
--diff-del: rgba(157, 0, 6, 0.12);
--shadow: 0 6px 24px rgba(60, 56, 54, 0.18);
--series-1: #2a78d6;
--series-2: #eb6834;
--series-3: #1baf7a;
--series-4: #eda100;
--series-5: #e87ba4;
--series-6: #008300;
--series-7: #6250d6;
--series-8: #e34948;
--seq-0: #f2e5bc;
--seq-1: #cde2fb;
--seq-2: #9ec5f4;
--seq-3: #6da7ec;
--seq-4: #3987e5;
--seq-5: #256abf;
--seq-6: #184f95;
}
* {
box-sizing: border-box;
@@ -176,7 +56,7 @@ button:disabled {
button.primary {
background: var(--accent);
border-color: var(--accent);
color: #282828;
color: var(--on-accent);
font-weight: 600;
}
button.danger {
@@ -335,7 +215,8 @@ textarea:focus {
border-top: 1px solid var(--bg2);
padding: 8px;
padding-bottom: max(8px, env(safe-area-inset-bottom));
display: flex;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
.sidebar-foot button {
@@ -503,13 +384,41 @@ textarea:focus {
padding-left: 10px;
margin: 6px 0;
}
.reasoning summary {
cursor: pointer;
user-select: none;
.reasoning-head {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
padding: 2px 0;
color: var(--muted);
font-size: 12.5px;
}
.reasoning-head:hover {
color: var(--fg2);
}
.reasoning .body {
white-space: pre-wrap;
margin-top: 4px;
font-style: italic;
line-height: 1.5;
}
.reasoning.live .body {
max-height: 260px;
overflow-y: auto;
}
.reasoning.live {
border-left-color: var(--accent);
}
.streaming-text::after {
content: "";
display: inline-block;
width: 0.5em;
height: 1em;
margin-left: 2px;
vertical-align: text-bottom;
background: var(--muted);
animation: blink 1s steps(1) infinite;
}
.tool {
@@ -750,8 +659,29 @@ textarea:focus {
font-size: 12.5px;
padding: 3px 6px;
}
.composer-bar .grow {
.composer-options {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
flex: 1;
min-width: 0;
}
.composer-options > * {
flex-shrink: 0;
}
.composer-actions {
display: flex;
gap: 6px;
margin-left: auto;
}
.todo-line {
font-size: 12.5px;
color: var(--muted);
margin: 4px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mode-tabs {
display: inline-flex;
@@ -768,7 +698,7 @@ textarea:focus {
}
.mode-tabs button.active {
background: var(--accent);
color: #282828;
color: var(--on-accent);
font-weight: 600;
}
.attachments {
@@ -916,6 +846,9 @@ table.grid input {
width: 100%;
min-width: 60px;
}
table.grid input[type="number"] {
min-width: 92px;
}
.raw-editor {
width: 100%;
min-height: 60vh;
@@ -1123,46 +1056,83 @@ table.grid input {
.chat {
padding: 12px 10px 20px;
}
.hide-mobile {
display: none !important;
}
.topbar {
flex-wrap: nowrap;
gap: 4px;
padding-left: 6px;
padding-right: 6px;
}
.topbar .path {
display: none;
}
.topbar button.small {
padding: 2px 5px;
}
/* One scrollable row of options, actions stay visible. */
.composer-bar {
flex-wrap: nowrap;
}
.composer-options {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: none;
}
.composer-options::-webkit-scrollbar {
display: none;
}
.composer-options select {
max-width: 150px;
}
/* Avoid iOS zoom on focus. */
.composer textarea,
input,
select,
textarea {
font-size: 16px;
}
}
/* --- syntax highlighting (highlight.js classes, Gruvbox colours) ----------- */
.hljs-keyword,
.hljs-selector-tag,
.hljs-built_in {
color: var(--red);
color: var(--syntax-keyword);
}
.hljs-string,
.hljs-attr,
.hljs-regexp {
color: var(--green);
color: var(--syntax-string);
}
.hljs-number,
.hljs-literal {
color: var(--purple);
color: var(--syntax-number);
}
.hljs-comment,
.hljs-quote {
color: var(--muted);
color: var(--syntax-comment);
font-style: italic;
}
.hljs-title,
.hljs-section,
.hljs-function {
color: var(--aqua);
color: var(--syntax-function);
}
.hljs-type,
.hljs-class,
.hljs-tag {
color: var(--accent);
color: var(--syntax-type);
}
.hljs-variable,
.hljs-template-variable,
.hljs-params {
color: var(--blue);
color: var(--syntax-variable);
}
.hljs-meta,
.hljs-symbol {
color: var(--accent2);
color: var(--syntax-meta);
}
.hljs-addition {
background: var(--diff-add);
@@ -1170,3 +1140,668 @@ table.grid input {
.hljs-deletion {
background: var(--diff-del);
}
a.button {
display: inline-block;
text-align: center;
text-decoration: none;
color: var(--fg1);
border: 1px solid var(--bg3);
background: var(--bg2);
border-radius: 6px;
padding: 5px 12px;
}
a.button:hover {
border-color: var(--fg3);
}
.sidebar-user {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 12px;
border-top: 1px solid var(--bg2);
font-size: 13px;
color: var(--fg2);
}
.sidebar-user > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 30;
padding: 16px;
}
.modal {
background: var(--bg1);
border: 1px solid var(--bg3);
border-radius: 12px;
padding: 18px;
width: min(440px, 100%);
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow);
}
.modal h3 {
margin: 0 0 10px;
}
.check-list label {
display: flex;
gap: 8px;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid var(--bg2);
}
.readonly-bar {
border-top: 1px solid var(--bg2);
padding: 10px 14px;
text-align: center;
color: var(--muted);
padding-bottom: max(10px, env(safe-area-inset-bottom));
}
/* --- activity animations ---------------------------------------------------- */
.activity {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0;
font-size: 13px;
color: var(--fg3);
}
.activity.paused {
color: var(--accent2);
}
.dots {
display: inline-flex;
gap: 4px;
}
.dots i {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
animation: dot-bounce 1.2s infinite ease-in-out;
}
.dots i:nth-child(2) {
animation-delay: 0.15s;
}
.dots i:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes dot-bounce {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.35;
}
40% {
transform: translateY(-5px);
opacity: 1;
}
}
.pause-icon {
width: 10px;
height: 12px;
border-left: 3px solid var(--accent2);
border-right: 3px solid var(--accent2);
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid var(--bg3);
border-top-color: var(--accent);
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
.spinner.small {
width: 12px;
height: 12px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.tool.running {
border-color: var(--accent);
animation: glow 1.6s ease-in-out infinite;
}
@keyframes glow {
50% {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}
}
.busy-ring {
flex-shrink: 0;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--bg3);
border-top-color: var(--accent);
animation: spin 0.8s linear infinite;
}
/* Blinking caret at the end of streaming text. */
.streaming .markdown > :last-child::after {
content: "";
display: inline-block;
width: 0.55em;
height: 1.05em;
margin-left: 2px;
vertical-align: text-bottom;
background: var(--accent);
animation: blink 1s steps(1) infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
/* Indeterminate progress bar under the top bar. */
.workbar {
height: 2px;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
.workbar.on::before {
content: "";
position: absolute;
inset: 0;
width: 35%;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
animation: slide 1.4s ease-in-out infinite;
}
@keyframes slide {
from {
transform: translateX(-100%);
}
to {
transform: translateX(300%);
}
}
@media (prefers-reduced-motion: reduce) {
.dots i,
.spinner,
.busy-ring,
.tool.running,
.streaming .markdown > :last-child::after,
.streaming-text::after,
.workbar.on::before {
animation: none;
}
.dots i {
opacity: 1;
}
.workbar.on::before {
width: 100%;
opacity: 0.6;
}
}
/* --- image gallery --------------------------------------------------------- */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
}
.gallery.small {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
margin-top: 6px;
}
.gallery-item {
margin: 0;
background: var(--bg1);
border: 1px solid var(--bg2);
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
}
.gallery-item img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
cursor: zoom-in;
background: var(--bg2);
margin: 0;
border-radius: 0;
}
.gallery-item.placeholder {
aspect-ratio: 4 / 3;
align-items: center;
justify-content: center;
}
.gallery-item figcaption {
padding: 8px;
display: grid;
gap: 4px;
font-size: 12.5px;
}
.gallery-item .prompt {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color: var(--fg1);
}
.gallery-item .meta,
.viewer .meta {
color: var(--muted);
font-size: 11.5px;
}
a.button.small {
padding: 2px 8px;
font-size: 12px;
}
.viewer {
margin: 0;
max-width: min(1200px, 96vw);
max-height: 92vh;
display: flex;
flex-direction: column;
gap: 8px;
color: var(--fg0);
}
.viewer img {
max-width: 100%;
max-height: 80vh;
object-fit: contain;
border-radius: var(--radius);
}
.budget-bar {
height: 8px;
border-radius: 4px;
background: var(--bg2);
overflow: hidden;
}
.budget-bar > div {
height: 100%;
border-radius: 4px;
}
.muted {
color: var(--muted);
}
/* --- message queue --------------------------------------------------------- */
.queue {
max-width: 900px;
margin: 0 auto;
padding: 6px 12px 0;
width: 100%;
}
.queue-head {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: var(--muted);
margin-bottom: 4px;
}
.queue-item {
display: flex;
align-items: center;
gap: 8px;
border: 1px dashed var(--bg3);
border-radius: var(--radius);
padding: 5px 8px;
margin-bottom: 4px;
font-size: 13px;
background: var(--bg1);
}
.queue-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--fg2);
}
.queue-item img {
height: 28px;
border-radius: 4px;
}
/* --- file manager ------------------------------------------------------------ */
.files-layout {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: minmax(260px, 360px) 1fr;
}
.files-side {
border-right: 1px solid var(--bg2);
display: flex;
flex-direction: column;
min-height: 0;
overflow-y: auto;
}
.files-main {
min-width: 0;
min-height: 0;
display: flex;
}
.files-list {
display: flex;
flex-direction: column;
min-height: 100%;
}
.files-list.drag {
outline: 2px dashed var(--accent);
outline-offset: -4px;
}
.files-toolbar {
display: flex;
gap: 4px;
padding: 8px;
flex-wrap: wrap;
align-items: center;
}
.files-toolbar button.active {
border-color: var(--accent);
color: var(--accent);
}
.files-search {
flex: 1;
min-width: 100px;
}
.crumbs {
padding: 0 10px 6px;
font-size: 12.5px;
color: var(--muted);
word-break: break-all;
}
.entries {
flex: 1;
}
.entry {
display: flex;
align-items: center;
width: 100%;
border: none;
border-radius: 0;
background: transparent;
padding: 0;
text-align: left;
}
button.entry {
padding: 6px 10px;
gap: 8px;
}
.entry:hover,
.entry.active {
background: var(--bg2);
}
.entry-main {
flex: 1;
min-width: 0;
display: flex;
gap: 8px;
align-items: center;
border: none;
background: transparent;
border-radius: 0;
padding: 6px 4px 6px 10px;
text-align: left;
}
.entry .name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.entry .size {
font-size: 11px;
color: var(--muted);
}
.entry-menu {
position: relative;
}
.entry-menu summary {
list-style: none;
cursor: pointer;
padding: 4px 10px;
color: var(--muted);
}
.entry-menu summary::-webkit-details-marker {
display: none;
}
.entry-menu .menu {
position: absolute;
right: 4px;
top: 100%;
z-index: 5;
background: var(--bg1);
border: 1px solid var(--bg3);
border-radius: var(--radius);
box-shadow: var(--shadow);
display: grid;
min-width: 150px;
padding: 4px;
}
.entry-menu .menu > * {
text-align: left;
border: none;
background: transparent;
padding: 6px 10px;
}
.entry-menu .menu > *:hover {
background: var(--bg2);
}
.drop-hint {
font-size: 11.5px;
color: var(--muted);
padding: 8px 10px;
text-align: center;
}
.git-mark {
font-family: var(--mono);
font-size: 11px;
font-weight: 700;
min-width: 14px;
text-align: center;
color: var(--accent2);
}
.git-mark.m-A,
.git-mark.m-U {
color: var(--green);
}
.git-mark.m-D {
color: var(--red);
}
.git-mark.m-dot {
color: var(--muted);
}
.editor-pane {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.editor-pane.empty {
align-items: center;
justify-content: center;
}
.editor-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-bottom: 1px solid var(--bg2);
}
.editor-bar .grow {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dirty {
color: var(--accent);
}
.code-editor {
flex: 1;
min-height: 0;
overflow: hidden;
}
.code-editor .cm-editor {
height: 100%;
}
.pane-scroll {
flex: 1;
min-height: 0;
overflow: auto;
}
.image-preview {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: repeating-conic-gradient(var(--bg1) 0% 25%, var(--bg0) 0% 50%) 50% / 20px 20px;
}
.image-preview img {
max-width: 100%;
max-height: 100%;
}
.files-status {
position: fixed;
right: 12px;
bottom: 8px;
font-size: 11px;
color: var(--muted);
pointer-events: none;
}
.git-init {
display: flex;
gap: 6px;
padding: 8px 10px;
border-top: 1px solid var(--bg2);
}
.mobile-only {
display: none;
}
/* git panel */
.git-panel {
padding: 8px 10px;
}
.git-branch {
display: flex;
gap: 4px;
align-items: center;
flex-wrap: wrap;
}
.git-branch select {
max-width: 160px;
}
.git-output {
font-size: 12px;
background: var(--bg1);
border: 1px solid var(--bg2);
border-radius: 6px;
padding: 6px 8px;
white-space: pre-wrap;
max-height: 160px;
overflow: auto;
margin: 6px 0 0;
}
.git-output.err {
color: var(--red);
border-color: var(--red);
}
.git-section {
margin-bottom: 10px;
}
.git-head {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11.5px;
text-transform: uppercase;
color: var(--muted);
padding: 4px 0;
}
.git-file {
display: flex;
align-items: center;
gap: 4px;
}
.git-name {
flex: 1;
min-width: 0;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 3px 4px;
font-size: 13px;
}
.git-commit textarea {
width: 100%;
margin-bottom: 6px;
}
.git-log {
display: grid;
}
.git-commit-row {
display: grid;
text-align: left;
border: none;
border-bottom: 1px solid var(--bg2);
border-radius: 0;
background: transparent;
padding: 6px 2px;
}
.git-commit-row:hover {
background: var(--bg2);
}
.git-commit-row .subject {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.git-commit-row .meta {
font-size: 11.5px;
color: var(--muted);
}
@media (max-width: 800px) {
.files-layout {
grid-template-columns: 1fr;
}
.files-main {
display: none;
}
.files-layout.show-editor .files-side {
display: none;
}
.files-layout.show-editor .files-main {
display: flex;
}
.mobile-only {
display: inline-block;
}
}