)
+ paints over the popped-up Run menu: with no session open, every item in that
+ menu is unclickable. Must stay below .modal (1000). */
+ z-index: 20;
}
/* backdrop-filter creates a stacking context, trapping the popover's
@@ -3323,10 +3538,10 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
bottom: 100%;
left: 0;
margin-bottom: 6px;
- background: rgba(22, 22, 28, 0.95);
+ background: var(--floating-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
- border: 1px solid rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--control-border);
border-radius: 10px;
padding: 4px;
z-index: 1000;
@@ -3370,6 +3585,14 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
.run-mode-dot.opencode { background: #10b981; }
.run-mode-dot.codex { background: #a855f7; }
.run-mode-dot.gemini { background: #8ab4f8; }
+.run-mode-dot.shell { background: #94a3b8; }
+
+/* Phone-only Enter button (see index.html). Hidden by default at every width;
+ mobile.css turns it on inside @media (max-width: 430px), where it takes over
+ the slot the Shell button occupies on wider screens. */
+.btn-toolbar.btn-enter {
+ display: none;
+}
.run-mode-sep {
height: 1px;
@@ -3965,10 +4188,10 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
bottom: 100%;
right: 0;
margin-bottom: 6px;
- background: rgba(22, 22, 28, 0.95);
+ background: var(--floating-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
- border: 1px solid rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--control-border);
border-radius: 10px;
padding: 0.6rem 0.7rem;
width: 220px;
@@ -4293,10 +4516,9 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
transition: border-color var(--transition-smooth), box-shadow var(--transition-smooth);
}
-/* Selects: strip the native (light) control so the field follows the active
- skin instead of rendering as a white OS widget. Opaque --bg-input fill +
- --border, a custom chevron, and color-scheme:dark (set on :root) for a dark
- option popup. Shared by App Settings, Cron, and every other .form-select. */
+/* Selects: strip the native control so the field follows the active skin.
+ Opaque --bg-input fill + --border, a custom chevron, and the skin's
+ color-scheme keep option popups coherent in both modes. */
.form-select {
padding-right: 2rem;
background-color: var(--bg-input);
@@ -4749,21 +4971,21 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
.modal-backdrop {
position: absolute;
inset: 0;
- background: rgba(0, 0, 0, 0.6);
+ background: var(--modal-backdrop);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.modal-content {
position: relative;
- background: rgba(19, 19, 22, 0.95);
+ background: var(--floating-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
- border: 1px solid rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--control-border);
border-radius: 12px;
width: 90%;
max-width: 400px;
- box-shadow: 0 16px 64px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.3);
+ box-shadow: var(--elevated-shadow);
/* Performance: isolate modal paint operations */
contain: layout style paint;
}
@@ -4797,7 +5019,7 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
bottom: 0;
left: 0;
right: 0;
- background: #1a1a1a;
+ background: var(--floating-bg);
border-radius: 16px 16px 0 0;
max-height: 70vh;
display: flex;
@@ -5717,8 +5939,8 @@ kbd {
display: flex;
flex-direction: column;
overflow: hidden;
- background: rgba(19, 19, 22, 0.97);
- border: 1px solid rgba(255, 255, 255, 0.1);
+ background: var(--floating-bg);
+ border: 1px solid var(--control-border);
border-radius: 10px;
box-shadow: 0 20px 70px rgba(0, 0, 0, 0.55), 0 4px 18px rgba(0, 0, 0, 0.35);
}
@@ -5893,7 +6115,7 @@ kbd {
flex-direction: column;
gap: 2px;
padding: 4px;
- background: rgba(22, 22, 28, 0.97);
+ background: var(--floating-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border);
@@ -7499,7 +7721,7 @@ kbd {
.subagent-id {
font-family: var(--font-mono);
font-size: 0.75rem;
- color: white;
+ color: var(--text);
}
.subagent-status {
@@ -7802,7 +8024,7 @@ kbd {
.subagent-window-title .id {
font-family: var(--font-mono);
font-size: 0.8rem;
- color: white;
+ color: var(--text);
}
.subagent-window-title .status {
@@ -7851,8 +8073,8 @@ kbd {
padding: 0.5rem;
font-family: var(--font-mono);
font-size: 0.75rem;
- background: #111;
- color: #c8c8c8;
+ background: var(--bg-dark);
+ color: var(--text);
min-height: 0; /* Allow flex child to shrink below content size */
scroll-behavior: smooth;
}
@@ -8036,10 +8258,10 @@ kbd {
height: 350px;
min-width: 300px;
min-height: 200px;
- background: rgba(19, 19, 22, 0.95);
+ background: var(--floating-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
- border: 1px solid rgba(255, 255, 255, 0.07);
+ border: 1px solid var(--control-border);
border-radius: 12px;
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.2);
z-index: 1000;
@@ -8188,10 +8410,10 @@ kbd {
max-height: 600px;
min-width: 200px;
min-height: 300px;
- background: rgba(19, 19, 22, 0.95);
+ background: var(--floating-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
- border: 1px solid rgba(255, 255, 255, 0.07);
+ border: 1px solid var(--control-border);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 100;
@@ -8391,7 +8613,7 @@ kbd {
.file-preview-overlay {
position: fixed;
inset: 0;
- background: rgba(0, 0, 0, 0.6);
+ background: var(--modal-backdrop);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
z-index: 2000;
@@ -8409,12 +8631,12 @@ kbd {
max-width: 900px;
height: 80vh;
max-height: 700px;
- background: rgba(19, 19, 22, 0.95);
+ background: var(--floating-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
- border: 1px solid rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--control-border);
border-radius: 12px;
- box-shadow: 0 16px 64px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.3);
+ box-shadow: var(--elevated-shadow);
display: flex;
flex-direction: column;
overflow: hidden;
@@ -8448,7 +8670,7 @@ kbd {
flex: 1;
overflow: auto;
padding: 0;
- background: #111;
+ background: var(--bg-dark);
}
.file-preview-body pre {
@@ -8457,7 +8679,7 @@ kbd {
font-family: var(--font-mono);
font-size: 0.8rem;
line-height: 1.5;
- color: #d4d4d4;
+ color: var(--text);
white-space: pre-wrap;
word-break: break-all;
}
@@ -8544,7 +8766,7 @@ kbd {
.log-viewer-window-title .filename {
font-family: var(--font-mono);
font-size: 0.8rem;
- color: white;
+ color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -9541,7 +9763,7 @@ kbd {
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.75rem;
- color: var(--green);
+ color: var(--text);
}
/* Advanced Options */
@@ -10072,10 +10294,10 @@ kbd {
left: 0;
right: 0;
max-height: 88vh;
- background: #14141f;
- border-top: 1px solid #2a2a3a;
+ background: var(--floating-bg);
+ border-top: 1px solid var(--border);
border-radius: 14px 14px 0 0;
- box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.45);
+ box-shadow: var(--elevated-shadow);
z-index: 5000;
flex-direction: column;
transform: translateY(100%);
@@ -10092,11 +10314,11 @@ kbd {
align-items: center;
justify-content: space-between;
padding: 14px 20px;
- border-bottom: 1px solid #2a2a3a;
+ border-bottom: 1px solid var(--border);
flex-shrink: 0;
font-size: 14px;
font-weight: 600;
- color: #e8e8ec;
+ color: var(--text);
letter-spacing: 0.2px;
}
@@ -10107,9 +10329,9 @@ kbd {
}
.response-viewer-more {
- background: #2a2a4a;
- border: 1px solid #444;
- color: #aaa;
+ background: var(--control-bg);
+ border: 1px solid var(--control-border);
+ color: var(--text-dim);
font-size: 12px;
padding: 3px 10px;
border-radius: 4px;
@@ -10117,13 +10339,13 @@ kbd {
}
.response-viewer-more:active {
- background: #3a3a5a;
+ background: var(--control-bg-hover);
}
.response-viewer-close {
background: none;
border: none;
- color: #888;
+ color: var(--text-muted);
font-size: 22px;
cursor: pointer;
padding: 0 4px;
@@ -10135,9 +10357,9 @@ kbd {
margin: 0 0 18px;
padding: 14px 16px 16px;
border-radius: 10px;
- border: 1px solid #252538;
+ border: 1px solid var(--border);
border-left-width: 3px;
- background: #181826;
+ background: var(--bg-card);
position: relative;
}
@@ -10148,14 +10370,14 @@ kbd {
/* Distinct accent per role so threads are scannable at a glance */
.rv-message:has(.rv-role-user),
.rv-message.rv-msg-user {
- border-left-color: #7aa2ff;
- background: #16182a;
+ border-left-color: var(--accent);
+ background: rgba(var(--accent-rgb), 0.055);
}
.rv-message:has(.rv-role-assistant),
.rv-message.rv-msg-assistant {
- border-left-color: #6ddb7f;
- background: #161f1a;
+ border-left-color: var(--green);
+ background: color-mix(in srgb, var(--green) 6%, var(--bg-card));
}
.rv-role {
@@ -10167,17 +10389,17 @@ kbd {
margin-bottom: 10px;
padding: 2px 8px;
border-radius: 10px;
- background: rgba(255, 255, 255, 0.04);
+ background: var(--control-bg);
}
.rv-role-user {
- color: #7aa2ff;
- background: rgba(122, 162, 255, 0.12);
+ color: var(--accent);
+ background: rgba(var(--accent-rgb), 0.12);
}
.rv-role-assistant {
- color: #6ddb7f;
- background: rgba(109, 219, 127, 0.12);
+ color: var(--green);
+ background: color-mix(in srgb, var(--green) 12%, transparent);
}
/* Markdown rendered content inside response viewer.
@@ -10202,7 +10424,7 @@ kbd {
.rv-text h1, .rv-text h2, .rv-text h3, .rv-text h4,
.response-viewer-body > h1, .response-viewer-body > h2,
.response-viewer-body > h3, .response-viewer-body > h4 {
- color: #f2f2f6;
+ color: var(--text);
margin: 1.4em 0 0.5em;
line-height: 1.3;
font-weight: 700;
@@ -10217,27 +10439,27 @@ kbd {
.rv-text h1, .response-viewer-body > h1 {
font-size: 1.55em;
padding-bottom: 0.3em;
- border-bottom: 1px solid #2d2d40;
+ border-bottom: 1px solid var(--border);
}
.rv-text h2, .response-viewer-body > h2 {
font-size: 1.3em;
- color: #ffd27a;
+ color: var(--yellow);
}
.rv-text h3, .response-viewer-body > h3 {
font-size: 1.13em;
- color: #bfc8ff;
+ color: var(--accent);
}
.rv-text h4, .response-viewer-body > h4 {
font-size: 1em;
- color: #c9c9d5;
+ color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.rv-text code,
.response-viewer-body > :not(pre) code {
- background: #262638;
- color: #ffb4a2;
+ background: var(--bg-input);
+ color: var(--red);
padding: 1px 6px;
border-radius: 4px;
font-family: 'Fira Code', 'JetBrains Mono', 'SF Mono', Menlo, Monaco, monospace;
@@ -10250,14 +10472,14 @@ kbd {
already matched via descendant; keep both in lockstep. */
.rv-text pre,
.response-viewer-body pre {
- background: #0f0f1a;
- border: 1px solid #2a2a3d;
+ background: var(--bg-dark);
+ border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
overflow-x: auto;
margin: 1em 0;
-webkit-overflow-scrolling: touch;
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
+ box-shadow: inset 0 0 0 1px var(--control-border);
position: relative;
}
@@ -10267,7 +10489,7 @@ kbd {
.rv-text pre code,
.response-viewer-body pre code {
background: none;
- color: #e6e6f0;
+ color: var(--text);
padding: 0;
font-family: 'Fira Code', 'JetBrains Mono', 'SF Mono', Menlo, Monaco, monospace;
font-size: 12.5px;
@@ -10317,10 +10539,7 @@ kbd {
.rv-text pre.rv-diagram.rv-nowrap,
.response-viewer-body pre.rv-diagram.rv-nowrap {
- background:
- linear-gradient(to left, #0f0f1a 0, rgba(15, 15, 26, 0) 28px) right / 28px 100% no-repeat,
- linear-gradient(to left, rgba(122, 162, 255, 0.18) 0, rgba(15, 15, 26, 0) 28px) right / 28px 100% no-repeat,
- #0f0f1a;
+ background: var(--bg-dark);
}
/* Toggle button — pinned to the wrapper's top-right, NOT affected by 's
@@ -10332,10 +10551,10 @@ kbd {
width: 28px;
height: 24px;
padding: 0;
- border: 1px solid #2f2f45;
+ border: 1px solid var(--control-border);
border-radius: 5px;
- background: rgba(20, 20, 32, 0.92);
- color: #8b8b97;
+ background: var(--floating-bg);
+ color: var(--text-muted);
font-size: 11px;
line-height: 1;
cursor: pointer;
@@ -10348,8 +10567,8 @@ kbd {
.rv-wrap-toggle:hover,
.rv-wrap-toggle:active {
- color: #e0e0ec;
- border-color: #4a4a65;
+ color: var(--text);
+ border-color: var(--control-border-hover);
}
/* Default icon = "return" (wrap is active). Clicking switches to expand/scroll. */
@@ -10406,10 +10625,10 @@ kbd {
width: 28px;
height: 24px;
padding: 0;
- border: 1px solid #2f2f45;
+ border: 1px solid var(--control-border);
border-radius: 5px;
- background: rgba(20, 20, 32, 0.92);
- color: #8b8b97;
+ background: var(--floating-bg);
+ color: var(--text-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
@@ -10421,14 +10640,14 @@ kbd {
.rv-copy-btn:hover,
.rv-copy-btn:active {
- color: #e0e0ec;
- border-color: #4a4a65;
+ color: var(--text);
+ border-color: var(--control-border-hover);
}
.rv-copy-btn::before { content: '\2398'; } /* ⎘ — matches file-preview copy */
-.rv-copy-btn.rv-copied { color: #9ece6a; border-color: #3a5a3a; }
+.rv-copy-btn.rv-copied { color: var(--green); border-color: var(--green); }
.rv-copy-btn.rv-copied::before { content: '\2713'; } /* ✓ */
-.rv-copy-btn.rv-copy-failed { color: #f7768e; border-color: #5a3a3a; }
+.rv-copy-btn.rv-copy-failed { color: var(--red); border-color: var(--red); }
.rv-copy-btn.rv-copy-failed::before { content: '\2715'; } /* ✕ */
.rv-text ul, .rv-text ol,
@@ -10446,36 +10665,36 @@ kbd {
.rv-text blockquote,
.response-viewer-body > blockquote {
- border-left: 3px solid #5c7cfa;
- background: rgba(92, 124, 250, 0.06);
+ border-left: 3px solid var(--accent);
+ background: rgba(var(--accent-rgb), 0.06);
margin: 0.8em 0;
padding: 0.5em 14px;
- color: #b8b8c8;
+ color: var(--text-dim);
border-radius: 0 6px 6px 0;
}
.rv-text strong,
.response-viewer-body > p strong,
.response-viewer-body > li strong {
- color: #ffffff;
+ color: var(--text);
font-weight: 700;
}
.rv-text em,
.response-viewer-body em {
- color: #e0e0ec;
+ color: var(--text);
}
.rv-text a,
.response-viewer-body a {
- color: #7aa2ff;
+ color: var(--accent);
text-decoration: none;
- border-bottom: 1px solid rgba(122, 162, 255, 0.35);
+ border-bottom: 1px solid rgba(var(--accent-rgb), 0.35);
}
.rv-text a:hover,
.response-viewer-body a:hover {
- border-bottom-color: #7aa2ff;
+ border-bottom-color: var(--accent);
}
/* Tables — scroll wrapper keeps table proper while allowing horizontal overflow */
@@ -10483,9 +10702,9 @@ kbd {
margin: 1em 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
- border: 1px solid #2a2a3d;
+ border: 1px solid var(--border);
border-radius: 8px;
- background: #12121d;
+ background: var(--bg-card);
}
.rv-text table,
@@ -10502,8 +10721,8 @@ kbd {
.response-viewer-body > table th,
.response-viewer-body > table td,
.rv-table-wrap th, .rv-table-wrap td {
- border-bottom: 1px solid #252538;
- border-right: 1px solid #252538;
+ border-bottom: 1px solid var(--border);
+ border-right: 1px solid var(--border);
padding: 8px 12px;
text-align: left;
vertical-align: top;
@@ -10526,29 +10745,29 @@ kbd {
.rv-text th,
.response-viewer-body > table th,
.rv-table-wrap th {
- background: #20202e;
- color: #f0f0f5;
+ background: var(--bg-input);
+ color: var(--text);
font-weight: 600;
- border-bottom: 2px solid #2f2f45;
+ border-bottom: 2px solid var(--border-light);
white-space: nowrap;
}
.rv-text tbody tr:nth-child(even) td,
.response-viewer-body > table tbody tr:nth-child(even) td,
.rv-table-wrap tbody tr:nth-child(even) td {
- background: rgba(255, 255, 255, 0.022);
+ background: var(--control-bg);
}
.rv-text tbody tr:hover td,
.response-viewer-body > table tbody tr:hover td,
.rv-table-wrap tbody tr:hover td {
- background: rgba(122, 162, 255, 0.06);
+ background: rgba(var(--accent-rgb), 0.06);
}
.rv-text hr,
.response-viewer-body > hr {
border: none;
- border-top: 1px solid #2d2d40;
+ border-top: 1px solid var(--border);
margin: 1.5em 0;
}
@@ -10564,7 +10783,7 @@ kbd {
'Noto Sans CJK SC', sans-serif;
font-size: 15px;
line-height: 1.7;
- color: #d8d8e0;
+ color: var(--text);
/* Comfortable reading width on wider viewports */
--rv-content-max: 720px;
}
@@ -10583,7 +10802,7 @@ kbd {
.response-viewer-body:empty::after {
content: 'No response yet';
- color: #555;
+ color: var(--text-muted);
font-style: italic;
}
@@ -10591,7 +10810,7 @@ kbd {
display: none;
position: fixed;
inset: 0;
- background: rgba(0, 0, 0, 0.5);
+ background: var(--modal-backdrop);
z-index: 4999;
}
@@ -10609,9 +10828,9 @@ kbd {
width: 100%;
font-family: 'Fira Code', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, monospace;
font-size: 14px;
- background: #1a1a2e;
- color: #e0e0e0;
- border: 1px solid #333;
+ background: var(--bg-input);
+ color: var(--text);
+ border: 1px solid var(--border);
border-top: none;
padding: 6px 10px;
outline: none;
@@ -10621,12 +10840,12 @@ kbd {
}
#cjkInput:focus {
- border-color: #339af0;
- background: #111;
+ border-color: var(--accent);
+ background: var(--bg-card);
}
#cjkInput::placeholder {
- color: #495057;
+ color: var(--text-muted);
font-size: 12px;
}
@@ -10640,12 +10859,12 @@ kbd {
min-height: 34px;
max-height: 68px;
padding: 6px 10px;
- border: 1px solid rgba(80, 120, 190, 0.55);
+ border: 1px solid var(--control-border-hover);
border-left: none;
border-right: none;
- background: #101827;
- color: #f3f4f6;
- box-shadow: 0 -8px 20px rgba(0, 0, 0, 0.35);
+ background: var(--floating-bg);
+ color: var(--text);
+ box-shadow: var(--subtle-shadow);
transition: transform 0.15s ease-out;
will-change: transform;
}
@@ -10668,8 +10887,8 @@ body.touch-device.cjk-input-visible .main {
.keyboard-accessory-bar {
display: none;
height: 44px;
- background: #1a1a1a;
- border-top: 1px solid rgba(255, 255, 255, 0.1);
+ background: var(--floating-bg);
+ border-top: 1px solid var(--control-border);
padding: 6px 8px;
gap: 8px;
align-items: center;
@@ -10705,10 +10924,10 @@ body.touch-device.cjk-input-visible .main {
flex-shrink: 0;
gap: 4px;
padding: 6px 12px;
- background: #2a2a2a;
- border: 1px solid rgba(255, 255, 255, 0.15);
+ background: var(--control-bg);
+ border: 1px solid var(--control-border);
border-radius: 6px;
- color: #e5e5e5;
+ color: var(--text-dim);
font-size: 0.65rem;
font-weight: 500;
cursor: pointer;
@@ -10716,13 +10935,13 @@ body.touch-device.cjk-input-visible .main {
}
.accessory-btn.confirming {
- background: #6b4f00;
- border-color: #b8860b;
- color: #ffd54f;
+ background: color-mix(in srgb, var(--yellow) 18%, var(--bg-input));
+ border-color: var(--yellow);
+ color: var(--yellow);
}
.accessory-btn:active {
- background: #3a3a3a;
+ background: var(--control-bg-hover);
}
.accessory-btn svg {
@@ -10732,13 +10951,13 @@ body.touch-device.cjk-input-visible .main {
.accessory-btn-arrow {
padding: 6px 10px;
- background: #2563eb;
- border-color: rgba(59, 130, 246, 0.5);
- color: #fff;
+ background: var(--accent);
+ border-color: var(--accent);
+ color: var(--accent-ink);
}
.accessory-btn-arrow:active {
- background: #1d4ed8;
+ background: var(--accent-hover);
}
.accessory-btn-dismiss {
@@ -10746,9 +10965,9 @@ body.touch-device.cjk-input-visible .main {
flex: 1 1 0;
max-width: 100px;
padding: 10px 8px;
- background: #2563eb;
- border-color: rgba(59, 130, 246, 0.5);
- color: #fff;
+ background: var(--accent);
+ border-color: var(--accent);
+ color: var(--accent-ink);
font-weight: 600;
}
@@ -10758,7 +10977,7 @@ body.touch-device.cjk-input-visible .main {
}
.accessory-btn-dismiss:active {
- background: #1d4ed8;
+ background: var(--accent-hover);
}
/* ═══════════════════════════════════════════════════════════════
@@ -10769,7 +10988,7 @@ body.touch-device.cjk-input-visible .main {
.paste-overlay {
position: fixed;
inset: 0;
- background: rgba(0, 0, 0, 0.6);
+ background: var(--modal-backdrop);
z-index: 10000;
display: flex;
align-items: flex-start;
@@ -10778,8 +10997,8 @@ body.touch-device.cjk-input-visible .main {
}
.paste-dialog {
- background: var(--bg-secondary, #1e1e2e);
- border: 1px solid var(--border-color, #444);
+ background: var(--bg-card);
+ border: 1px solid var(--border);
border-radius: 12px;
padding: 12px;
width: calc(100% - 24px);
@@ -10790,9 +11009,9 @@ body.touch-device.cjk-input-visible .main {
width: 100%;
min-height: 80px;
max-height: 200px;
- background: var(--bg-primary, #0d0d14);
- color: var(--text-primary, #e0e0e0);
- border: 1px solid var(--border-color, #444);
+ background: var(--bg-input);
+ color: var(--text);
+ border: 1px solid var(--border);
border-radius: 8px;
padding: 8px;
font-family: inherit;
@@ -10803,7 +11022,7 @@ body.touch-device.cjk-input-visible .main {
.paste-textarea:focus {
outline: none;
- border-color: var(--accent-color, #7aa2f7);
+ border-color: var(--accent);
}
.paste-actions {
@@ -10823,28 +11042,460 @@ body.touch-device.cjk-input-visible .main {
.paste-image {
margin-right: auto;
- background: var(--bg-tertiary, #333);
- color: var(--accent-color, #7aa2f7);
- border: 1px solid var(--accent-color, #7aa2f7);
+ background: var(--bg-input);
+ color: var(--accent);
+ border: 1px solid var(--accent);
}
.paste-cancel {
- background: var(--bg-tertiary, #333);
- color: var(--text-secondary, #aaa);
+ background: var(--bg-input);
+ color: var(--text-dim);
}
.paste-new {
- background: var(--bg-tertiary, #333);
- color: var(--accent-color, #7aa2f7);
- border: 1px solid var(--accent-color, #7aa2f7);
+ background: var(--bg-input);
+ color: var(--accent);
+ border: 1px solid var(--accent);
}
.paste-send {
- background: var(--accent-color, #7aa2f7);
+ background: var(--accent);
color: #fff;
font-weight: 600;
}
+/* Shared lazy filesystem path picker (case linking + mobile input). */
+.path-input-group {
+ display: flex;
+ gap: 8px;
+ width: 100%;
+}
+
+.path-input-group input {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.path-input-browse {
+ flex: 0 0 auto;
+ min-height: 38px;
+}
+
+.path-picker-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 10020;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ background: rgba(0, 0, 0, 0.72);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+}
+
+.path-picker-dialog {
+ display: flex;
+ flex-direction: column;
+ width: min(680px, 100%);
+ height: min(720px, calc(100dvh - 32px));
+ overflow: hidden;
+ color: var(--text);
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
+}
+
+.path-picker-header,
+.path-picker-nav,
+.path-picker-roots-row,
+.path-picker-selection,
+.path-picker-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.path-picker-header {
+ justify-content: space-between;
+ padding: 14px 16px;
+ border-bottom: 1px solid var(--border);
+}
+
+.path-picker-title {
+ font-size: 0.95rem;
+}
+
+.path-picker-close {
+ width: 36px;
+ height: 36px;
+ color: var(--text-muted);
+ font-size: 1.5rem;
+ background: transparent;
+ border: 0;
+ border-radius: 8px;
+ cursor: pointer;
+}
+
+.path-picker-roots-row {
+ padding: 10px 12px 0;
+ color: var(--text-muted);
+ font-size: 0.75rem;
+}
+
+.path-picker-roots {
+ flex: 1;
+ min-width: 0;
+ padding: 8px 10px;
+ color: var(--text);
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.path-picker-nav {
+ padding: 10px 12px;
+}
+
+.path-picker-up,
+.path-picker-refresh {
+ flex: 0 0 38px;
+ height: 38px;
+ color: var(--text);
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ cursor: pointer;
+}
+
+.path-picker-up:disabled {
+ opacity: 0.35;
+ cursor: default;
+}
+
+.path-picker-current {
+ flex: 1;
+ min-width: 0;
+ padding: 9px 11px;
+ overflow-x: auto;
+ color: var(--accent);
+ font-family: var(--font-mono, monospace);
+ font-size: 0.75rem;
+ white-space: nowrap;
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.path-picker-status {
+ padding: 0 14px 8px;
+ color: var(--text-dim);
+ font-size: 0.7rem;
+}
+
+.path-picker-status.error {
+ color: var(--danger, #ef4444);
+}
+
+.path-picker-list {
+ flex: 1;
+ min-height: 140px;
+ overflow-y: auto;
+ padding: 0 10px 10px;
+}
+
+.path-picker-item {
+ display: flex;
+ align-items: stretch;
+ margin: 2px 0;
+ border: 1px solid transparent;
+ border-radius: 9px;
+}
+
+.path-picker-item:hover,
+.path-picker-item.selected {
+ background: var(--bg-hover);
+ border-color: var(--border);
+}
+
+.path-picker-item.selected {
+ border-color: var(--accent);
+}
+
+.path-picker-item.not-selectable {
+ opacity: 0.62;
+}
+
+.path-picker-item-main {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ min-width: 0;
+ padding: 10px 8px;
+ color: var(--text);
+ text-align: left;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+}
+
+.path-picker-item-name {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ font-size: 0.8rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.path-picker-item-icon {
+ margin-right: 8px;
+}
+
+.path-picker-item-link,
+.path-picker-item-chevron,
+.path-picker-item-preview {
+ margin-left: 8px;
+ color: var(--text-dim);
+}
+
+.path-picker-item-preview {
+ font-size: 0.9rem;
+}
+
+.path-picker-item-select {
+ align-self: center;
+ margin-right: 6px;
+ padding: 6px 9px;
+ color: var(--accent);
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: 7px;
+ cursor: pointer;
+}
+
+.path-picker-selection {
+ padding: 9px 14px;
+ color: var(--text-muted);
+ font-size: 0.72rem;
+ border-top: 1px solid var(--border);
+}
+
+.path-picker-selection-value {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--text);
+ font-family: var(--font-mono, monospace);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.path-picker-actions {
+ padding: 10px 12px 12px;
+}
+
+.path-picker-actions button {
+ min-height: 40px;
+ padding: 8px 13px;
+ color: var(--text);
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ cursor: pointer;
+}
+
+.path-picker-action-spacer {
+ flex: 1;
+}
+
+.path-picker-actions .path-picker-confirm {
+ color: #fff;
+ background: var(--accent);
+ border-color: var(--accent);
+ font-weight: 600;
+}
+
+.path-picker-actions .path-picker-confirm:disabled {
+ opacity: 0.4;
+ cursor: default;
+}
+
+.path-preview-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 10030;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 18px;
+ background: rgba(0, 0, 0, 0.82);
+ backdrop-filter: blur(5px);
+ -webkit-backdrop-filter: blur(5px);
+}
+
+.path-preview-dialog {
+ display: flex;
+ flex-direction: column;
+ width: min(1000px, 100%);
+ height: min(860px, calc(100dvh - 36px));
+ overflow: hidden;
+ color: var(--text);
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ box-shadow: 0 24px 72px rgba(0, 0, 0, 0.62);
+}
+
+.path-preview-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 11px 13px;
+ border-bottom: 1px solid var(--border);
+}
+
+.path-preview-heading {
+ flex: 1;
+ min-width: 0;
+}
+
+.path-preview-title,
+.path-preview-path {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.path-preview-title {
+ font-size: 0.9rem;
+}
+
+.path-preview-path {
+ margin-top: 2px;
+ color: var(--text-dim);
+ font-family: var(--font-mono, monospace);
+ font-size: 0.65rem;
+}
+
+.path-preview-open,
+.path-preview-close {
+ flex: 0 0 auto;
+ color: var(--text);
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.path-preview-open {
+ padding: 8px 11px;
+ color: var(--accent);
+ font-size: 0.75rem;
+ text-decoration: none;
+}
+
+.path-preview-close {
+ width: 38px;
+ height: 38px;
+ font-size: 1.5rem;
+ cursor: pointer;
+}
+
+.path-preview-body {
+ position: relative;
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ align-items: center;
+ justify-content: center;
+ overflow: auto;
+ background: #111;
+}
+
+.path-preview-loading,
+.path-preview-error {
+ padding: 24px;
+ color: var(--text-muted);
+ font-size: 0.8rem;
+ text-align: center;
+}
+
+.path-preview-error {
+ color: var(--danger, #ef4444);
+}
+
+.path-preview-image {
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+ margin: auto;
+ object-fit: contain;
+}
+
+.path-preview-frame {
+ width: 100%;
+ height: 100%;
+ background: #fff;
+ border: 0;
+}
+
+.path-preview-text {
+ width: 100%;
+ min-height: 100%;
+ margin: 0;
+ padding: 18px;
+ overflow: visible;
+ color: #e5e7eb;
+ font-family: var(--font-mono, monospace);
+ font-size: 0.78rem;
+ line-height: 1.55;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+
+@media (max-width: 600px) {
+ .path-picker-overlay {
+ align-items: flex-end;
+ padding: 0;
+ }
+
+ .path-picker-dialog {
+ width: 100%;
+ height: min(82dvh, 720px);
+ border-right: 0;
+ border-bottom: 0;
+ border-left: 0;
+ border-radius: 16px 16px 0 0;
+ padding-bottom: env(safe-area-inset-bottom, 0px);
+ }
+
+ .path-picker-actions {
+ flex-wrap: wrap;
+ }
+
+ .path-picker-current-select {
+ flex: 1 1 100%;
+ }
+
+ .path-preview-overlay {
+ align-items: stretch;
+ padding: 0;
+ }
+
+ .path-preview-dialog {
+ width: 100%;
+ height: 100dvh;
+ border: 0;
+ border-radius: 0;
+ padding-bottom: env(safe-area-inset-bottom, 0px);
+ }
+
+ .path-preview-open {
+ padding: 8px;
+ }
+}
+
/* ═══════════════════════════════════════════════════════════════
Orchestrator Panel
═══════════════════════════════════════════════════════════════ */
@@ -11124,7 +11775,7 @@ body.touch-device.cjk-input-visible .main {
padding: 3px 10px;
border: 1px solid var(--border-light);
border-radius: 5px;
- background: rgba(19, 19, 22, 0.96);
+ background: var(--floating-bg);
color: var(--text-dim);
font-size: 0.68rem;
font-weight: 600;
@@ -11148,9 +11799,9 @@ body.touch-device.cjk-input-visible .main {
gap: 10px;
align-items: center;
padding: 10px;
- border: 1px solid rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--control-border);
border-radius: 8px;
- background: rgba(19, 19, 22, 0.96);
+ background: var(--floating-bg);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
pointer-events: auto;
}
@@ -11186,7 +11837,7 @@ body.touch-device.cjk-input-visible .main {
color: var(--text);
font-size: 0.7rem;
font-weight: 700;
- background: #20202a;
+ background: var(--bg-input);
}
.attachment-thumbnail-fallback.visible {
@@ -11200,7 +11851,7 @@ body.touch-device.cjk-input-visible .main {
width: 44px;
height: 44px;
border-radius: 6px;
- background: #20202a;
+ background: var(--bg-input);
color: var(--text);
border: 1px solid var(--border-light);
font-size: 0.7rem;
@@ -11309,7 +11960,7 @@ body.touch-device.cjk-input-visible .main {
max-width: calc(100vw - 24px);
height: calc(100vh - var(--header-height) - var(--toolbar-height));
height: calc(100dvh - var(--header-height) - var(--toolbar-height));
- background: rgba(19, 19, 22, 0.98);
+ background: var(--floating-bg);
border-left: 1px solid var(--border);
z-index: 10000;
display: flex;
@@ -11431,7 +12082,7 @@ body.touch-device.cjk-input-visible .main {
justify-content: center;
position: absolute;
inset: 0;
- background: #20202a;
+ background: var(--bg-input);
color: var(--text);
font-size: 0.72rem;
font-weight: 700;
@@ -11518,8 +12169,8 @@ html:not([data-skin="og"]) {
box-shadow: none;
}
.session-tab .tab-number {
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.18);
+ background: var(--control-bg-hover);
+ border: 1px solid var(--control-border-hover);
color: var(--text-dim);
}
.session-tab.active .tab-number {
@@ -11540,7 +12191,7 @@ html:not([data-skin="og"]) {
/* ---- Timer banner: emerald-teal, flat ---- */
.timer-banner {
- background: linear-gradient(90deg, rgba(31, 38, 48, 0.92), rgba(22, 27, 35, 0.92));
+ background: linear-gradient(90deg, var(--banner-bg-a), var(--banner-bg-b));
border-bottom: 1px solid var(--glass-border);
}
.timer-value { color: var(--accent); }
@@ -11548,7 +12199,7 @@ html:not([data-skin="og"]) {
/* ---- Respawn banner: lifted slate, calm green hairline ---- */
.respawn-banner {
- background: linear-gradient(90deg, rgba(31, 38, 48, 0.9), rgba(22, 27, 35, 0.9));
+ background: linear-gradient(90deg, var(--banner-bg-a), var(--banner-bg-b));
border-bottom: 1px solid var(--glass-border);
}
.respawn-countdown-timer .respawn-timer-value,
@@ -11563,7 +12214,7 @@ html:not([data-skin="og"]) {
-webkit-background-clip: text;
background-clip: text;
}
-.ralph-ring-bg, .ralph-ring-track { stroke: rgba(255, 255, 255, 0.1); }
+.ralph-ring-bg, .ralph-ring-track { stroke: var(--control-border); }
/* Ralph status badges: collapse the off-token Material palette onto the system */
.ralph-status-badge { color: var(--text-dim); }
.ralph-status-dot { box-shadow: none; }
@@ -11572,13 +12223,13 @@ html:not([data-skin="og"]) {
/* ---- Bottom toolbar + neutral buttons ---- */
.toolbar { box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.22), 0 -4px 14px rgba(0, 0, 0, 0.14); }
.btn-toolbar {
- background: rgba(255, 255, 255, 0.045);
- border-color: rgba(255, 255, 255, 0.09);
+ background: var(--control-bg);
+ border-color: var(--control-border);
color: var(--text-dim);
}
.btn-toolbar:hover {
- background: rgba(255, 255, 255, 0.08);
- border-color: rgba(255, 255, 255, 0.14);
+ background: var(--control-bg-hover);
+ border-color: var(--control-border-hover);
color: var(--text);
box-shadow: none;
}
@@ -11618,6 +12269,7 @@ html:not([data-skin="og"]) {
.run-mode-dot.claude { background: var(--accent); }
.run-mode-dot.opencode { background: var(--accent-soft); }
.run-mode-dot.codex { background: var(--accent-grad-b); }
+.run-mode-dot.shell { background: var(--text-dim); }
/* ---- Shell button: quiet neutral with a calm green tint ---- */
.btn-toolbar.btn-shell {
@@ -11634,8 +12286,8 @@ html:not([data-skin="og"]) {
/* ---- Stop button: quiet neutral control (no rose glow) ---- */
.btn-toolbar.btn-stop {
- background: rgba(255, 255, 255, 0.045);
- border: 1px solid rgba(255, 255, 255, 0.12);
+ background: var(--control-bg);
+ border: 1px solid var(--control-border);
color: var(--text-dim);
text-shadow: none;
}
@@ -11653,11 +12305,11 @@ html:not([data-skin="og"]) {
/* ---- Case selector + add ---- */
.toolbar-select {
- background: rgba(255, 255, 255, 0.045);
- border-color: rgba(255, 255, 255, 0.09);
+ background: var(--control-bg);
+ border-color: var(--control-border);
color: var(--text-dim);
}
-.toolbar-select:hover { background: rgba(255, 255, 255, 0.07); border-color: rgba(255, 255, 255, 0.14); }
+.toolbar-select:hover { background: var(--control-bg-hover); border-color: var(--control-border-hover); }
.toolbar-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.25);
@@ -11671,8 +12323,8 @@ html:not([data-skin="og"]) {
/* ---- Monitor + Subagents panels: lifted frosted slate ---- */
.monitor-panel,
.subagents-panel {
- background: rgba(31, 38, 48, 0.95);
- border: 1px solid rgba(255, 255, 255, 0.08);
+ background: var(--floating-bg);
+ border: 1px solid var(--control-border);
}
/* ---- Subagent progress ring: emerald, calm glow ---- */
@@ -11702,7 +12354,7 @@ html:not([data-skin="og"]) {
}
.welcome-btn-opencode:hover { box-shadow: 0 0 28px -4px rgba(var(--accent-rgb), 0.25); }
.welcome-btn-tunnel {
- background: linear-gradient(135deg, rgba(46, 55, 67, 0.9), rgba(56, 66, 79, 0.9));
+ background: linear-gradient(135deg, var(--bg-input), var(--bg-hover));
border-color: var(--border-light);
color: var(--text);
}
@@ -11845,3 +12497,118 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
display: flex; gap: 0.5rem; justify-content: flex-end;
margin-top: 1rem; padding-top: 0.85rem; border-top: 1px solid var(--border);
}
+
+/* ═══════════════════════════════════════════════════════════════
+ Web tabs (dashboard URLs embedded as tabs)
+ ═══════════════════════════════════════════════════════════════ */
+
+/* The iframe layer sits alongside .terminal-wrap inside and only one of
+ the two is visible at a time. Frames stay in the DOM while hidden so switching
+ tabs does not reload a dashboard. */
+.webview-layer {
+ display: none;
+ flex: 1;
+ min-height: 0;
+ position: relative;
+ background: var(--term-bg, #161b23);
+}
+.main.webview-active .webview-layer { display: flex; }
+.main.webview-active .terminal-wrap { display: none; }
+
+.webview-frame {
+ display: none;
+ position: absolute;
+ inset: 0;
+}
+.webview-frame.active { display: block; }
+
+.webview-iframe {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ display: block;
+ background: #fff;
+}
+
+/* Shown only when the frame never signalled load: a refused embed or an
+ unreachable host would otherwise be an unexplained blank rectangle. */
+.webview-failure { display: none; }
+.webview-frame--failed .webview-failure {
+ display: flex;
+ position: absolute;
+ inset: 0;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg, #0f1319);
+ padding: 1.5rem;
+ text-align: center;
+}
+.webview-failure-inner { max-width: 380px; }
+.webview-failure-inner h3 { margin: 0 0 0.5rem; font-size: 1rem; color: var(--text); }
+.webview-failure-inner p { margin: 0 0 1rem; font-size: 0.85rem; color: var(--text-dim); line-height: 1.5; }
+.webview-failure-actions { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; }
+
+/* Tab styling: same shape as a session tab, distinguished by the globe icon and
+ a cool accent so a dashboard never reads as a running agent. */
+.session-tab--web .tab-web-icon {
+ display: inline-flex;
+ align-items: center;
+ color: var(--text-dim);
+ flex-shrink: 0;
+}
+.session-tab--web.active .tab-web-icon { color: var(--accent, #3ec8ee); }
+.session-tab--web.active { border-bottom-color: var(--accent, #3ec8ee); }
+
+.run-mode-dot.web { background: #38bdf8; }
+.run-mode-webviews { max-height: 180px; overflow-y: auto; }
+.run-mode-empty {
+ padding: 4px 10px 6px;
+ font-size: 0.75em;
+ color: var(--text-dim);
+ font-style: italic;
+}
+
+/* Icon picker: a compact grid above the free-text field, so the common case is a
+ click and the escape hatch (any emoji at all) stays available. */
+.webview-icon-picker {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-bottom: 6px;
+}
+.webview-icon-choice {
+ width: 34px;
+ height: 34px;
+ font-size: 1.05rem;
+ line-height: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg-soft, rgba(255, 255, 255, 0.03));
+ cursor: pointer;
+ padding: 0;
+}
+.webview-icon-choice:hover { border-color: var(--accent, #3ec8ee); }
+.webview-icon-choice.selected {
+ border-color: var(--accent, #3ec8ee);
+ box-shadow: 0 0 0 1px var(--accent, #3ec8ee) inset;
+}
+
+/* Saved-URL rows in the Run dropdown show their chosen icon in the dot's slot. */
+.run-mode-menu-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ margin-right: 6px;
+ font-size: 0.95em;
+ flex-shrink: 0;
+}
+
+.webview-probe-result.ok { color: var(--success, #10b981); }
+.webview-probe-result.bad { color: var(--danger, #ef4444); }
+/* Delete sits apart from Cancel/Save so it is not fat-fingered on the way to Save. */
+.webview-modal-actions { justify-content: space-between; }
+.webview-modal-actions .btn-danger { margin-right: auto; }
diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js
index f309a31b..3a9de819 100644
--- a/src/web/public/terminal-ui.js
+++ b/src/web/public/terminal-ui.js
@@ -40,11 +40,22 @@
og: { background: '#0d0d0d', foreground: '#e0e0e0', cursor: '#e0e0e0', cursorAccent: '#0d0d0d', selection: 'rgba(255,255,255,0.3)', black: '#0d0d0d', red: '#ff6b6b', green: '#51cf66', yellow: '#ffd43b', blue: '#339af0', magenta: '#cc5de8', cyan: '#22b8cf', white: '#e0e0e0', brightBlack: '#495057', brightRed: '#ff8787', brightGreen: '#69db7c', brightYellow: '#ffe066', brightBlue: '#5c7cfa', brightMagenta: '#da77f2', brightCyan: '#66d9e8', brightWhite: '#ffffff' },
'daylight-green': { background: '#161b23', foreground: '#dfe6ef', cursor: '#2fd3aa', cursorAccent: '#161b23', selection: 'rgba(47,211,170,0.22)', black: '#161b23', red: '#ff8585', green: '#34d8a0', yellow: '#f0c25a', blue: '#5cc6e8', magenta: '#c79af2', cyan: '#2bcbbb', white: '#dfe6ef', brightBlack: '#5b6675', brightRed: '#ffa0a0', brightGreen: '#5fe6b8', brightYellow: '#ffd884', brightBlue: '#82d4ee', brightMagenta: '#d6b3f7', brightCyan: '#5ee0d4', brightWhite: '#f3f6fa' },
'daylight-blue': { background: '#161b23', foreground: '#dfe6ef', cursor: '#38b6f0', cursorAccent: '#161b23', selection: 'rgba(56,182,240,0.22)', black: '#161b23', red: '#ff8585', green: '#34d8a0', yellow: '#f0c25a', blue: '#5cc6e8', magenta: '#c79af2', cyan: '#2bcbbb', white: '#dfe6ef', brightBlack: '#5b6675', brightRed: '#ffa0a0', brightGreen: '#5fe6b8', brightYellow: '#ffd884', brightBlue: '#82d4ee', brightMagenta: '#d6b3f7', brightCyan: '#5ee0d4', brightWhite: '#f3f6fa' },
+ 'paper-gray': { background: '#f6f8fa', foreground: '#1f2328', cursor: '#0969da', cursorAccent: '#ffffff', selection: 'rgba(9,105,218,0.2)', black: '#24292f', red: '#cf222e', green: '#1a7f37', yellow: '#9a6700', blue: '#0969da', magenta: '#8250df', cyan: '#1b7c83', white: '#59636e', brightBlack: '#6e7781', brightRed: '#a40e26', brightGreen: '#116329', brightYellow: '#7d4e00', brightBlue: '#0550ae', brightMagenta: '#6639ba', brightCyan: '#116b75', brightWhite: '#1f2328' },
+ 'solarized-light': { background: '#fdf6e3', foreground: '#586e75', cursor: '#147ba3', cursorAccent: '#fdf6e3', selection: 'rgba(38,139,210,0.2)', black: '#eee8d5', red: '#dc322f', green: '#758600', yellow: '#9b7800', blue: '#147ba3', magenta: '#d33682', cyan: '#2a9189', white: '#073642', brightBlack: '#93a1a1', brightRed: '#cb4b16', brightGreen: '#657b83', brightYellow: '#586e75', brightBlue: '#268bd2', brightMagenta: '#6c71c4', brightCyan: '#2aa198', brightWhite: '#002b36' },
+ 'catppuccin-latte': { background: '#eff1f5', foreground: '#4c4f69', cursor: '#1e66f5', cursorAccent: '#ffffff', selection: 'rgba(30,102,245,0.18)', black: '#5c5f77', red: '#d20f39', green: '#3b8f2b', yellow: '#a86605', blue: '#1e66f5', magenta: '#8839ef', cyan: '#177f86', white: '#6c6f85', brightBlack: '#7c7f93', brightRed: '#b50930', brightGreen: '#2f7622', brightYellow: '#8b5604', brightBlue: '#174fbf', brightMagenta: '#6f2bc5', brightCyan: '#116b71', brightWhite: '#4c4f69' },
+ 'rose-pine-dawn': { background: '#faf4ed', foreground: '#575279', cursor: '#286983', cursorAccent: '#fffaf3', selection: 'rgba(40,105,131,0.2)', black: '#575279', red: '#b4637a', green: '#286983', yellow: '#96681f', blue: '#477f91', magenta: '#907aa9', cyan: '#3f7f8b', white: '#6e6a86', brightBlack: '#797593', brightRed: '#984d66', brightGreen: '#1f5266', brightYellow: '#7d5417', brightBlue: '#386b7c', brightMagenta: '#765f90', brightCyan: '#326b76', brightWhite: '#575279' },
};
+ const CODEMAN_LIGHT_SKINS = new Set(['paper-gray', 'solarized-light', 'catppuccin-latte', 'rose-pine-dawn']);
+ function currentSkin() {
+ return (typeof document !== 'undefined' && document.documentElement.dataset.skin) || 'daylight-blue';
+ }
function currentXtermTheme() {
- const skin = (typeof document !== 'undefined' && document.documentElement.dataset.skin) || 'daylight-blue';
+ const skin = currentSkin();
return CODEMAN_XTERM_THEMES[skin] || CODEMAN_XTERM_THEMES['daylight-blue'];
}
+ function currentSkinIsLight(skin = currentSkin()) {
+ return CODEMAN_LIGHT_SKINS.has(skin);
+ }
global.CodemanTerminalInput = {
isTerminalQueryResponse,
@@ -54,6 +65,7 @@
};
global.CODEMAN_XTERM_THEMES = CODEMAN_XTERM_THEMES;
global.codemanCurrentXtermTheme = currentXtermTheme;
+ global.codemanCurrentSkinIsLight = currentSkinIsLight;
})(window);
Object.assign(CodemanApp.prototype, {
@@ -75,6 +87,7 @@ Object.assign(CodemanApp.prototype, {
lineHeight: 1.2,
cursorBlink: false,
cursorStyle: 'block',
+ minimumContrastRatio: window.codemanCurrentSkinIsLight() ? 4.5 : 1,
scrollback: scrollback,
allowTransparency: true,
allowProposedApi: true,
@@ -969,8 +982,66 @@ Object.assign(CodemanApp.prototype, {
return;
}
- // Get line text - translateToString handles wrapped lines
- const lineText = line.translateToString(true);
+ // Stitch the LOGICAL line back together.
+ //
+ // xterm invokes this provider per visible ROW, and translateToString returns
+ // that row alone (the old comment here claimed otherwise). A URL or path
+ // longer than the terminal is wide therefore matched only as far as the row
+ // boundary, and the link opened a PREFIX of the real target. Walk out to both
+ // ends of the continuation, match against the joined text, and map offsets
+ // back to (x, y) so a link can span rows.
+ //
+ // Two different kinds of continuation, and handling only the first is not
+ // enough:
+ // 1. SOFT wrap: the emulator ran out of columns and flags the next row
+ // `isWrapped`.
+ // 2. HARD wrap: the program did its own wrapping and emitted a real
+ // newline, so nothing is flagged. Ink does this, which is why Claude
+ // Code's own `/login` URL was cut at the window edge, and why the
+ // clickable part grew when the window was widened.
+ // A row that fills the full width is treated as continuing into the next:
+ // that is the signal a hard wrap leaves behind, and a line that genuinely
+ // ended would stop short of the last column.
+ const cols = self.terminal.cols;
+ const rowAt = (r) => buffer.getLine(r - 1);
+ const continuesPrevious = (r) => {
+ if (r <= 1) return false;
+ if (rowAt(r)?.isWrapped) return true;
+ const prev = rowAt(r - 1);
+ return !!prev && prev.translateToString(true).length >= cols;
+ };
+
+ // Bounded so a screenful of full-width output (wide tables, box drawing)
+ // cannot make every hover stitch and re-scan the entire viewport.
+ const MAX_STITCHED_ROWS = 12;
+ let startRow = bufferLineNumber;
+ while (startRow > 1 && bufferLineNumber - startRow < MAX_STITCHED_ROWS && continuesPrevious(startRow)) {
+ startRow--;
+ }
+ let endRow = bufferLineNumber;
+ while (endRow < buffer.length && endRow - startRow < MAX_STITCHED_ROWS && continuesPrevious(endRow + 1)) {
+ endRow++;
+ }
+
+ const rowTexts = [];
+ for (let r = startRow; r <= endRow; r++) {
+ const row = rowAt(r);
+ if (!row) break;
+ // Only the final row may be trimmed. Continuation rows fill the width by
+ // definition, and trimming one would shift every later offset.
+ rowTexts.push(row.translateToString(r === endRow));
+ }
+ const lineText = rowTexts.join('');
+
+ /** Map an offset in the stitched text back to a 1-based terminal cell. */
+ const coordAt = (index) => {
+ let rest = index;
+ for (let i = 0; i < rowTexts.length - 1; i++) {
+ if (rest < rowTexts[i].length) return { x: rest + 1, y: startRow + i };
+ rest -= rowTexts[i].length;
+ }
+ return { x: rest + 1, y: startRow + rowTexts.length - 1 };
+ };
if (!lineText || !lineText.includes('/')) {
callback(undefined);
@@ -980,22 +1051,27 @@ Object.assign(CodemanApp.prototype, {
const links = [];
// Pattern 0: URLs (https://, http://) — matched first so they take priority
- const urlPattern = /https?:\/\/[^\s"'<>|;&)\]\x00-\x1f]+/g;
+ //
+ // A single `&` is PART of the URL: it separates query parameters, so excluding
+ // it truncated every real query string (`?post=1479&action=edit` linked only
+ // through `1479`, landing on the wrong page). `&&` is still a boundary, since
+ // that is the shell operator and never appears inside a URL. A lone trailing
+ // `&` is trimmed below with the other trailing punctuation.
+ const urlPattern = /https?:\/\/(?:[^\s"'<>|;&)\]\x00-\x1f]|&(?!&))+/g;
const addUrlLink = (url, matchIndex) => {
// Strip trailing punctuation that's likely not part of the URL
- const cleaned = url.replace(/[.,;:!?)]+$/, '');
+ const cleaned = url.replace(/[.,;:!?)&]+$/, '');
const startCol = lineText.indexOf(cleaned, matchIndex);
if (startCol === -1) return;
- if (links.some((l) => l.range.start.x === startCol + 1)) return;
+ const start = coordAt(startCol);
+ const end = coordAt(startCol + cleaned.length);
+ if (links.some((l) => l.range.start.x === start.x && l.range.start.y === start.y)) return;
links.push({
text: cleaned,
- range: {
- start: { x: startCol + 1, y: bufferLineNumber },
- end: { x: startCol + cleaned.length + 1, y: bufferLineNumber },
- },
+ range: { start, end },
decorations: { pointerCursor: true, underline: true },
activate(_event, text) {
window.open(text, '_blank', 'noopener,noreferrer');
@@ -1017,31 +1093,43 @@ Object.assign(CodemanApp.prototype, {
// the whole tab on hover. Non-empty token + bounded reps is O(n).
const cmdPattern = /\b(tail|cat|head|less|grep|watch|vim|nano)\s+(?:[^\s\/]+\s+){0,4}(\/[^\s"'<>|;&\n\x00-\x1f]+)/g;
- // Pattern 2: Paths with common extensions
+ // Pattern 2: Paths with common extensions.
+ // Image/PDF extensions are included so pasted-attachment paths
+ // (`.claude-images/paste-*.png`) are clickable; they open the file preview
+ // rather than the log viewer (see addLink).
const extPattern =
- /(\/(?:home|tmp|var|etc|opt)[^\s"'<>|;&\n\x00-\x1f]*\.(?:log|txt|json|md|yaml|yml|csv|xml|sh|py|ts|js))\b/g;
+ /(\/(?:home|tmp|var|etc|opt)[^\s"'<>|;&\n\x00-\x1f]*\.(?:log|txt|json|md|yaml|yml|csv|xml|sh|py|ts|js|png|jpe?g|gif|webp|bmp|svg|pdf))\b/g;
// Pattern 3: Bash() tool output
const bashPattern = /Bash\([^)]*?(\/(?:home|tmp|var|etc|opt)[^\s"'<>|;&\)\n\x00-\x1f]+)/g;
+ /** Extensions that should open the image/document preview, not the log viewer. */
+ const PREVIEW_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'pdf']);
+
const addLink = (filePath, matchIndex) => {
const startCol = lineText.indexOf(filePath, matchIndex);
if (startCol === -1) return;
+ const start = coordAt(startCol);
+ const end = coordAt(startCol + filePath.length);
// Skip if already have link at this position
- if (links.some((l) => l.range.start.x === startCol + 1)) return;
+ if (links.some((l) => l.range.start.x === start.x && l.range.start.y === start.y)) return;
links.push({
text: filePath,
- range: {
- start: { x: startCol + 1, y: bufferLineNumber }, // 1-based
- end: { x: startCol + filePath.length + 1, y: bufferLineNumber },
- },
+ range: { start, end }, // 1-based, may span wrapped rows
decorations: {
pointerCursor: true,
underline: true,
},
activate(event, text) {
+ // Tailing a PNG in the log viewer shows binary noise; the file preview
+ // already renders images and PDFs inline.
+ const ext = (text.split('.').pop() || '').toLowerCase();
+ if (PREVIEW_EXTS.has(ext)) {
+ self.openFilePreview(text, self.activeSessionId);
+ return;
+ }
self.openLogViewerWindow(text, self.activeSessionId);
},
hover() {
@@ -2418,6 +2506,50 @@ Object.assign(CodemanApp.prototype, {
this.terminal.clear();
},
+ /** Insert editable text at the active prompt without pressing Enter. */
+ insertTerminalText(text) {
+ if (!this.activeSessionId || !text) return;
+ if (this._localEchoEnabled && this._localEchoOverlay) {
+ this._localEchoOverlay.appendText(text);
+ } else {
+ this.sendInput(text).catch(() => {});
+ }
+ this.terminal?.focus();
+ },
+
+ /**
+ * Clear only the current editable prompt. This is intentionally distinct
+ * from Ctrl+L (clear display) and the agent's destructive `/clear` command.
+ */
+ clearTerminalInput() {
+ if (!this.activeSessionId) return;
+
+ if (typeof CjkInput !== 'undefined') CjkInput.clear();
+ if (this._inputFlushTimeout) {
+ clearTimeout(this._inputFlushTimeout);
+ this._inputFlushTimeout = null;
+ }
+ this._pendingInput = '';
+
+ if (this._localEchoEnabled && this._localEchoOverlay) {
+ const flushed = this._localEchoOverlay.getFlushed?.() || { count: 0, text: '' };
+ this._localEchoOverlay.clear();
+ this._localEchoOverlay.suppressBufferDetection();
+ this._flushedOffsets?.delete(this.activeSessionId);
+ this._flushedTexts?.delete(this.activeSessionId);
+ if (flushed.count > 0) {
+ this.sendInput('\x7f'.repeat(flushed.count)).catch(() => {});
+ }
+ } else {
+ // In non-local-echo mode the TUI already owns the editable buffer. Ctrl+U
+ // is the conventional kill-line key supported by shells and agent TUIs.
+ this.sendInput('\x15').catch(() => {});
+ }
+
+ this.showToast?.('Input cleared', 'success');
+ this.terminal?.focus();
+ },
+
/**
* Restore terminal size to match web UI dimensions.
* Use this after mobile screen attachment has squeezed the terminal.
@@ -2846,8 +2978,14 @@ Object.assign(CodemanApp.prototype, {
// DOM and WebGL renderers) plus a belt-and-suspenders refresh().
applyTerminalSkin(skin) {
const theme = { ...(window.CODEMAN_XTERM_THEMES[skin] || window.CODEMAN_XTERM_THEMES['daylight-blue']) };
+ const minimumContrastRatio = window.codemanCurrentSkinIsLight(skin) ? 4.5 : 1;
if (this.terminal) {
+ this.terminal.options.minimumContrastRatio = minimumContrastRatio;
this.terminal.options.theme = theme;
+ // The zero-lag typing overlay caches the xterm foreground/background.
+ // Refresh it on live skin changes so typed text never keeps the prior
+ // theme's dark backing surface or foreground color.
+ this._localEchoOverlay?.refreshFont();
try {
this.terminal.refresh(0, this.terminal.rows - 1);
} catch {}
@@ -2855,6 +2993,7 @@ Object.assign(CodemanApp.prototype, {
if (this.teammateTerminals) {
for (const [, entry] of this.teammateTerminals) {
if (entry && entry.terminal) {
+ entry.terminal.options.minimumContrastRatio = minimumContrastRatio;
entry.terminal.options.theme = { ...theme };
try {
entry.terminal.refresh(0, entry.terminal.rows - 1);
diff --git a/src/web/public/webview-tabs.js b/src/web/public/webview-tabs.js
new file mode 100644
index 00000000..e092cea3
--- /dev/null
+++ b/src/web/public/webview-tabs.js
@@ -0,0 +1,445 @@
+/**
+ * @fileoverview Web tabs: saved dashboard URLs rendered as tabs beside agent
+ * sessions, so Codeman is one mission control instead of Codeman plus a pile of
+ * browser tabs.
+ *
+ * Each open dashboard is an