mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
Merge remote-tracking branch 'origin/master' into pr274-rebase
# Conflicts: # src/web/public/session-ui.js
This commit is contained in:
@@ -393,10 +393,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
// that collide with state strings on other surfaces.
|
||||
pill.setAttribute('data-i18n-skip', '');
|
||||
pill.textContent = row.pill;
|
||||
item.appendChild(pill);
|
||||
|
||||
// Wraps onto its own line (the row is flex-wrap) so it gets the full width.
|
||||
item.appendChild(this._buildHomeSessionsMeta(row));
|
||||
// The stamps line wraps onto its own full-width line (the row is flex-wrap)
|
||||
// and the pill rides along at its right end, rather than sitting beside the
|
||||
// name: that hands the whole width of the rail to the session name, which is
|
||||
// what stops it ellipsizing.
|
||||
const meta = this._buildHomeSessionsMeta(row);
|
||||
meta.appendChild(pill);
|
||||
item.appendChild(meta);
|
||||
|
||||
return item;
|
||||
},
|
||||
@@ -439,7 +443,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
pill.className = 'home-sessions-pill home-sessions-pill--web';
|
||||
pill.setAttribute('data-i18n-skip', '');
|
||||
pill.textContent = 'web';
|
||||
item.appendChild(pill);
|
||||
|
||||
// Same bottom line as a session row (minus the stamps, a dashboard has
|
||||
// none), so the pill sits in the same place on every row in the rail.
|
||||
const foot = document.createElement('span');
|
||||
foot.className = 'home-sessions-row-meta';
|
||||
foot.setAttribute('data-i18n-skip', '');
|
||||
foot.appendChild(pill);
|
||||
item.appendChild(foot);
|
||||
|
||||
return item;
|
||||
},
|
||||
|
||||
@@ -255,12 +255,15 @@
|
||||
'Read My Mind: predict your next prompt': '读心术:预测您的下一条提示',
|
||||
'Predict my next prompt': '预测我的下一条提示',
|
||||
'Reading your mind…': '正在读取您的想法…',
|
||||
'No suggestion this time. Rethink to try again.': '这次没有建议。点击「重想」再试一次。',
|
||||
'No suggestion this time. Add a steer note and Rethink to try again.':
|
||||
'这次没有建议。可添加引导备注后点击「重想」再试一次。',
|
||||
Rethink: '重想',
|
||||
Insert: '插入',
|
||||
"Put the text on the session's composer without submitting it": '将文本放入会话输入框但不提交',
|
||||
'Predicted prompt, editable': '预测的提示,可编辑',
|
||||
'Use this suggestion instead': '改用此建议',
|
||||
"Steer the rethink, e.g. 'no, I meant the mobile bug'": '引导重想,例如:"不,我是指移动端的问题"',
|
||||
'Steer note for Rethink': '重想的引导备注',
|
||||
'Select a session first': '请先选择一个会话',
|
||||
'Read My Mind works on Claude sessions only': '读心术仅适用于 Claude 会话',
|
||||
'Prompt sent': '提示已发送',
|
||||
|
||||
+616
-393
File diff suppressed because it is too large
Load Diff
+118
-119
@@ -223,24 +223,6 @@ html.mobile-init .file-browser-panel {
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.modal-tabs {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.modal-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-tab-btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Settings grid stays 2-col on tablet but tighter */
|
||||
.settings-grid {
|
||||
gap: 0.4rem 0.75rem;
|
||||
@@ -1336,20 +1318,22 @@ html.mobile-init .file-browser-panel {
|
||||
width: calc(100% - 2rem);
|
||||
}
|
||||
/* Four footer buttons on a narrow phone: let them wrap instead of clipping,
|
||||
and give buttons + alternate rows finger-sized targets. */
|
||||
and give buttons + alternate rows finger-sized targets. The flex row
|
||||
itself comes from the base rule in styles.css. */
|
||||
.readmymind-modal .modal-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.readmymind-modal .modal-footer .btn {
|
||||
.readmymind-modal .modal-footer .btn-toolbar {
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
min-height: 38px;
|
||||
}
|
||||
.readmymind-alt {
|
||||
min-height: 38px;
|
||||
}
|
||||
.readmymind-steer-input {
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
/* Modal safe area padding - all sides for full-screen modals */
|
||||
.ios-device .modal-content {
|
||||
@@ -2077,45 +2061,8 @@ html.mobile-init .file-browser-panel {
|
||||
|
||||
/* ---- Settings Modal: Mobile Optimizations ---- */
|
||||
|
||||
/* Scrollable tabs row - prevent overflow on small screens */
|
||||
.modal-tabs {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
gap: 0.25rem;
|
||||
padding: 0 0.75rem 0.5rem 0.75rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.modal-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-tab-btn {
|
||||
padding: 0.35rem 0.6rem;
|
||||
font-size: 0.65rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---- Case Modal: Mobile Touch Optimizations ---- */
|
||||
|
||||
/* Larger tab buttons for case modal - easy to tap */
|
||||
#createCaseModal .modal-tabs {
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem 0.75rem;
|
||||
}
|
||||
|
||||
#createCaseModal .modal-tab-btn {
|
||||
flex: 1;
|
||||
min-height: 44px;
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Touch-friendly form inputs in case modal */
|
||||
#createCaseModal .form-row {
|
||||
@@ -3170,21 +3117,21 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
stack of separate cards: at 390px the per-card borders were most of the pixels.
|
||||
============================================================================ */
|
||||
@media (max-width: 860px) {
|
||||
#appSettingsModal .modal-content.modal-lg {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .modal-content.modal-lg {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-body {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Rail keeps only its search field, laid out as a bar */
|
||||
#appSettingsModal .set-rail {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-rail {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-right: 0;
|
||||
@@ -3195,37 +3142,37 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-rail-items,
|
||||
#appSettingsModal .set-rail-foot {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-rail-items,
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-rail-foot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-search {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-search {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-search input {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-search input {
|
||||
padding: 9px 10px 9px 30px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Save moves into the header; the bottom action bar would cost 60px */
|
||||
#appSettingsModal .set-head-save {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-head-save {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-foot {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-foot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-doc {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-doc {
|
||||
padding: 0 14px 34px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── jump control ──────────────────────────────────────────────────── */
|
||||
#appSettingsModal .set-jump {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -3246,30 +3193,30 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-ico {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-ico {
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-label {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-label {
|
||||
font-weight: 580;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-chev {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-chev {
|
||||
margin-left: auto;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.18s;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump[aria-expanded='true'] .set-jump-chev {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump[aria-expanded='true'] .set-jump-chev {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-veil {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-veil {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -3277,7 +3224,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
background: rgba(4, 8, 13, 0.62);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-menu {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
@@ -3299,7 +3246,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
display: block;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
@@ -3314,43 +3261,43 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row svg {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-row svg {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row .set-jump-count {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-row .set-jump-count {
|
||||
margin-left: auto;
|
||||
font-size: 0.62rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row.active {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-row.active {
|
||||
background: rgba(var(--accent-rgb), 0.14);
|
||||
color: var(--text);
|
||||
font-weight: 570;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row.active svg {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-jump-row.active svg {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── sections step down: the jump pill already names the current one ── */
|
||||
#appSettingsModal .set-section {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-section {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section + .set-section {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-section + .set-section {
|
||||
border-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-head {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-section-head {
|
||||
gap: 7px;
|
||||
margin: 18px 0 2px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-head svg {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-section-head svg {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
@@ -3359,7 +3306,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-head h2 {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-section-head h2 {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 640;
|
||||
letter-spacing: 0.1em;
|
||||
@@ -3367,36 +3314,45 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-head::after {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-section-head::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--border), transparent);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-blurb {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-section-blurb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── live layout preview ───────────────────────────────────────────── */
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-preview {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-preview-stage {
|
||||
min-height: 62px;
|
||||
}
|
||||
|
||||
/* ── inset grouped list ────────────────────────────────────────────── */
|
||||
#appSettingsModal .set-group {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group + .set-group {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group + .set-group {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-head {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-head {
|
||||
margin-bottom: 7px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-hint {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-hint {
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-body {
|
||||
gap: 0;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
@@ -3404,7 +3360,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body > .set-row {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-body > .set-row {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
@@ -3412,104 +3368,147 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body > .set-row + .set-row {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-body > .set-row + .set-row {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.055);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body > .set-chips,
|
||||
#appSettingsModal .set-group-body > .set-modelgrid,
|
||||
#appSettingsModal .set-group-body > .set-minigrid,
|
||||
#appSettingsModal .set-group-body > #appSettingsShortcutsList {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-body > .set-chips,
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-body > .set-modelgrid,
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-body > .set-minigrid,
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-body > #appSettingsShortcutsList {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body > .event-type-grid {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-group-body > .event-type-grid {
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row-label {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-row-label {
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row-desc {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-row-desc {
|
||||
font-size: 0.69rem;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* Fields go full width under their label instead of fighting for the row */
|
||||
#appSettingsModal .set-row.has-field {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-row.has-field {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row.has-field .set-select,
|
||||
#appSettingsModal .set-row.has-field .set-input {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-row.has-field .set-select,
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-row.has-field .set-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row-actions-wide {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-row-actions-wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row-actions-wide .set-input {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-row-actions-wide .set-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-num {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-num {
|
||||
width: 76px;
|
||||
}
|
||||
|
||||
/* Bigger touch targets for the toggles and chips */
|
||||
#appSettingsModal .switch-sm {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .switch-sm {
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
#appSettingsModal .switch-sm .slider:before {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .switch-sm .slider:before {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
#appSettingsModal .switch-sm input:checked + .slider:before {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .switch-sm input:checked + .slider:before {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-chip {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-chip {
|
||||
font-size: 0.78rem;
|
||||
padding: 9px 14px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-modelgrid {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-modelgrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-minigrid {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-minigrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-mini .set-select {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-mini .set-select {
|
||||
width: 148px;
|
||||
}
|
||||
|
||||
/* One scrollable line beats a ragged two-row wrap for 7 effort levels */
|
||||
#appSettingsModal .set-segment {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-segment {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-segment::-webkit-scrollbar {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-segment::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-segment button {
|
||||
:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-segment button {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Session Options, compact layout (<= 860px)
|
||||
|
||||
App Settings collapses its rail and hands navigation to the sticky
|
||||
#appSettingsJump pill. Session Options has no such pill (and no search), so
|
||||
its rail stays put and becomes a horizontal, scrollable strip — which is
|
||||
what its tab bar was before the two modals started sharing a surface.
|
||||
============================================================================ */
|
||||
@media (max-width: 860px) {
|
||||
:is(#sessionOptionsModal, #createCaseModal) .set-rail {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
:is(#sessionOptionsModal, #createCaseModal) .set-rail-items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
:is(#sessionOptionsModal, #createCaseModal) .set-rail-items::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:is(#sessionOptionsModal, #createCaseModal) .set-rail-item {
|
||||
white-space: nowrap;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* The active marker is a left bar in the vertical rail; horizontally that
|
||||
reads as a stray tick, so the strip uses a filled pill instead. */
|
||||
:is(#sessionOptionsModal, #createCaseModal) .set-rail-item.active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:is(#sessionOptionsModal, #createCaseModal) .set-rail-item.active {
|
||||
background: rgba(var(--accent-rgb), 0.13);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
* suggestions render as tappable alternate rows that swap into the field
|
||||
* without losing edits. Buttons are Send (with Enter), Insert (drop on the CLI
|
||||
* composer WITHOUT Enter, for editing), Rethink (re-run with the whole shown
|
||||
* set, main + alternates, recorded as rejected), Dismiss.
|
||||
* set, main + alternates, recorded as rejected, plus the optional free-text
|
||||
* steer note, e.g. "no, I meant the mobile bug", sent as `steer`), Dismiss.
|
||||
*
|
||||
* Suggestions are NEVER auto-sent: the explicit click here is the security
|
||||
* boundary for observed/injectable predictor inputs, so suggestion text is
|
||||
@@ -47,8 +48,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.showToast('Read My Mind works on Claude sessions only', 'warning');
|
||||
return;
|
||||
}
|
||||
// Rethink memory resets on each open (a fresh open is a fresh question).
|
||||
// Rethink memory resets on each open (a fresh open is a fresh question),
|
||||
// and the steer note resets with it.
|
||||
this._rmm = { sessionId, suggestions: [], selected: 0, rejected: [], busy: false };
|
||||
const steer = document.getElementById('readMyMindSteer');
|
||||
if (steer) steer.value = '';
|
||||
document.getElementById('readMyMindModal')?.classList.add('active');
|
||||
this._readMyMindPredict();
|
||||
},
|
||||
@@ -65,7 +69,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
state.busy = true;
|
||||
this._rmmSetPhase('loading');
|
||||
|
||||
const body = state.rejected.length > 0 ? { rejected: state.rejected.slice(-10) } : {};
|
||||
const body = {};
|
||||
if (state.rejected.length > 0) body.rejected = state.rejected.slice(-10);
|
||||
// The steer note rides every re-run while it stays in the field: what the
|
||||
// user sees in the box is what the predictor gets. Empty on first open
|
||||
// (openReadMyMind clears it), so a plain predict sends neither key.
|
||||
const steer = document.getElementById('readMyMindSteer')?.value.trim() ?? '';
|
||||
if (steer) body.steer = steer.slice(0, 2000);
|
||||
const data = await this._apiJson(`/api/sessions/${state.sessionId}/readmymind`, { method: 'POST', body });
|
||||
|
||||
// The modal may have been dismissed (or reopened for another session) while
|
||||
@@ -176,7 +186,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
/** Re-run with the whole shown set (main + alternates) recorded as rejected:
|
||||
* the user saw every row and asked for something else. */
|
||||
* the user saw every row and asked for something else. The steer note (if
|
||||
* any) is read from the field by _readMyMindPredict itself. */
|
||||
rethinkReadMyMind() {
|
||||
const state = this._rmm;
|
||||
if (!state || state.busy) return;
|
||||
@@ -193,6 +204,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
modal.querySelector('.readmymind-loading').style.display = phase === 'loading' ? '' : 'none';
|
||||
modal.querySelector('.readmymind-result').style.display = phase === 'ready' ? '' : 'none';
|
||||
modal.querySelector('.readmymind-error').style.display = phase === 'error' ? '' : 'none';
|
||||
// The steer note belongs to Rethink, so it shows wherever Rethink is live:
|
||||
// the ready phase AND the empty-result phase (typed text survives the
|
||||
// loading round-trip, only the row's visibility toggles).
|
||||
const steerRow = document.getElementById('readMyMindSteerRow');
|
||||
if (steerRow) steerRow.style.display = phase === 'loading' ? 'none' : '';
|
||||
const rethinkBtn = document.getElementById('readMyMindRethink');
|
||||
if (rethinkBtn) rethinkBtn.disabled = phase === 'loading';
|
||||
},
|
||||
|
||||
@@ -489,11 +489,12 @@ Object.assign(CodemanApp.prototype, {
|
||||
const date = new Date(s.lastModified);
|
||||
const timeStr = date.toLocaleDateString('en', { month: 'short', day: 'numeric' })
|
||||
+ ' ' + date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
// Abbreviate the home prefix. This used to match `/home/<user>/` only, so
|
||||
// on macOS (`/Users/<user>/`) nothing was stripped and every row spent its
|
||||
// first ~19 characters on an identical prefix — with the tail ellipsized,
|
||||
// all rows rendered as `/Users/jordanryan/co…` and became indistinguishable.
|
||||
const shortDir = s.workingDir.replace(/^\/(?:home|Users)\/[^/]+\//, '~/');
|
||||
// Shared helper, not a local regex: the copy that used to live here
|
||||
// matched `/home/<user>/` only, so on macOS (`/Users/<user>/`) nothing was
|
||||
// stripped and every row spent its first ~19 characters on an identical
|
||||
// prefix — with the tail ellipsized, all rows rendered as
|
||||
// `/Users/jordanryan/co…` and became indistinguishable (#273).
|
||||
const shortDir = this._shortenHomePath(s.workingDir);
|
||||
// Lead with the folder that identifies the row; the parent path trails and
|
||||
// is what gets truncated. Truncation must never eat the identity.
|
||||
const lastSlash = shortDir.lastIndexOf('/');
|
||||
@@ -1310,8 +1311,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('presetDescriptionHint').textContent = '';
|
||||
|
||||
// Hide Ralph/Todo tab and Respawn tab for external CLI sessions (not supported)
|
||||
const ralphTabBtn = document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="ralph"]');
|
||||
const respawnTabBtn = document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="respawn"]');
|
||||
const ralphTabBtn = document.querySelector('#sessionOptionsModal .set-rail-item[data-tab="ralph"]');
|
||||
const respawnTabBtn = document.querySelector('#sessionOptionsModal .set-rail-item[data-tab="respawn"]');
|
||||
if (isExternalCli) {
|
||||
if (ralphTabBtn) ralphTabBtn.style.display = 'none';
|
||||
if (respawnTabBtn) respawnTabBtn.style.display = 'none';
|
||||
@@ -1335,6 +1336,18 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
const modal = document.getElementById('sessionOptionsModal');
|
||||
|
||||
// Chips mirror their checkbox onto the label, the same way App Settings does
|
||||
// (settings-ui.js: _syncSettingsChips). Registered once per page, never per
|
||||
// open, or a long-lived tab accumulates one listener per visit.
|
||||
if (modal.dataset.chipsReady !== '1') {
|
||||
modal.dataset.chipsReady = '1';
|
||||
modal.addEventListener('change', e => {
|
||||
if (e.target?.closest?.('.set-chip')) this._syncSettingsChips();
|
||||
});
|
||||
}
|
||||
this._syncSettingsChips();
|
||||
|
||||
modal.classList.add('active');
|
||||
|
||||
// Activate focus trap
|
||||
@@ -1532,18 +1545,32 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Session Options Modal Tabs
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Show one section of the Session Options modal.
|
||||
*
|
||||
* The chrome is the shared `set-*` settings surface, but unlike App Settings
|
||||
* (whose rail is a table of contents over one scrolling document) this rail
|
||||
* is a real switcher: exactly one `.set-section` is visible and the rest
|
||||
* carry `.hidden`. Summary owns its own scroller and Respawn is long, so
|
||||
* stacking them into a single document would bury both.
|
||||
*/
|
||||
switchOptionsTab(tabName) {
|
||||
// Toggle active class on tab buttons
|
||||
document.querySelectorAll('#sessionOptionsModal .modal-tab-btn').forEach(btn => {
|
||||
// Toggle active class on rail entries
|
||||
document.querySelectorAll('#sessionOptionsModal .set-rail-item').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
// Toggle hidden class on tab content
|
||||
// Toggle hidden class on the sections
|
||||
document.getElementById('respawn-tab').classList.toggle('hidden', tabName !== 'respawn');
|
||||
document.getElementById('context-tab').classList.toggle('hidden', tabName !== 'context');
|
||||
document.getElementById('ralph-tab').classList.toggle('hidden', tabName !== 'ralph');
|
||||
document.getElementById('summary-tab').classList.toggle('hidden', tabName !== 'summary');
|
||||
|
||||
// A switched-to section starts at its own top, not at the scroll offset the
|
||||
// previous one was left at.
|
||||
const doc = document.getElementById('sessionOptionsDoc');
|
||||
if (doc) doc.scrollTop = 0;
|
||||
|
||||
// Load run summary data when switching to summary tab
|
||||
if (tabName === 'summary' && this.editingSessionId) {
|
||||
this.loadRunSummary(this.editingSessionId);
|
||||
@@ -1814,7 +1841,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.switchCaseModalTab('case-create');
|
||||
// Wire up tab buttons
|
||||
const modal = document.getElementById('createCaseModal');
|
||||
modal.querySelectorAll('.modal-tabs .modal-tab-btn').forEach(btn => {
|
||||
modal.querySelectorAll('.set-rail-item').forEach(btn => {
|
||||
btn.onclick = () => this.switchCaseModalTab(btn.dataset.tab);
|
||||
});
|
||||
// Scroll-into-view on focus for mobile keyboard visibility
|
||||
@@ -1835,14 +1862,17 @@ Object.assign(CodemanApp.prototype, {
|
||||
switchCaseModalTab(tabName) {
|
||||
this.caseModalTab = tabName;
|
||||
const modal = document.getElementById('createCaseModal');
|
||||
// Toggle active class on tab buttons
|
||||
modal.querySelectorAll('.modal-tabs .modal-tab-btn').forEach(btn => {
|
||||
// Toggle active class on rail entries
|
||||
modal.querySelectorAll('.set-rail-item').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||||
});
|
||||
// Toggle hidden class on tab content
|
||||
modal.querySelectorAll('.modal-tab-content').forEach(content => {
|
||||
// Toggle hidden class on the panels
|
||||
modal.querySelectorAll('.set-section').forEach(content => {
|
||||
content.classList.toggle('hidden', content.id !== tabName);
|
||||
});
|
||||
// A switched-to panel starts at its own top.
|
||||
const doc = document.getElementById('createCaseDoc');
|
||||
if (doc) doc.scrollTop = 0;
|
||||
// Update submit button (hide for manage tab)
|
||||
const submitBtn = document.getElementById('caseModalSubmit');
|
||||
if (tabName === 'case-manage') {
|
||||
@@ -2708,7 +2738,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
cases.forEach((c, idx) => {
|
||||
const isFirst = idx === 0;
|
||||
const isLast = idx === cases.length - 1;
|
||||
const pathDisplay = c.path ? c.path.replace(/^\/Users\/[^/]+/, '~') : '';
|
||||
// Was `/Users/<user>` only, the mirror image of the Run menu's bug: every
|
||||
// case path on a Linux host rendered in full, unabbreviated.
|
||||
const pathDisplay = c.path ? this._shortenHomePath(c.path) : '';
|
||||
html += `
|
||||
<div class="case-manage-item" data-case="${escapeHtml(c.name)}">
|
||||
<div class="case-manage-info">
|
||||
|
||||
@@ -503,8 +503,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
this._syncSettingsChips();
|
||||
this._syncModelCards();
|
||||
this._syncEffortSegment();
|
||||
// Back to the top of the document (one scroll, not a tab reset).
|
||||
this.switchSettingsTab('settings-terminal');
|
||||
// Back to the top of the document (one scroll, not a tab reset). Updates is
|
||||
// first now: the version this install is running, and whether a newer one is
|
||||
// waiting, are the two things worth seeing before any preference. The rest of
|
||||
// the system settings (paths, automation, remote access) tail the document.
|
||||
this.switchSettingsTab('settings-updates');
|
||||
const modal = document.getElementById('appSettingsModal');
|
||||
modal.classList.add('active');
|
||||
|
||||
@@ -687,11 +690,89 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('appSettingsJump')?.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
},
|
||||
|
||||
/** Mirror checkbox state onto the chip labels (see _initSettingsNav). */
|
||||
/**
|
||||
* Mirror checkbox state onto the chip labels (see _initSettingsNav).
|
||||
*
|
||||
* Covers Session Options too: it shares the `set-*` surface, and its cycle-step
|
||||
* chips would otherwise depend on `:has()` alone for their checked styling.
|
||||
*/
|
||||
_syncSettingsChips() {
|
||||
document.querySelectorAll('#appSettingsModal .set-chip').forEach(chip => {
|
||||
document.querySelectorAll('#appSettingsModal .set-chip, #sessionOptionsModal .set-chip').forEach(chip => {
|
||||
chip.classList.toggle('is-on', !!chip.querySelector('input')?.checked);
|
||||
});
|
||||
this._syncLayoutPreview();
|
||||
},
|
||||
|
||||
/**
|
||||
* Redraw the Header & Panels live preview from the chips above it.
|
||||
*
|
||||
* The preview is a scale model of the app, not a second list of settings, so
|
||||
* every icon is CLONED from the chip that owns it (`.set-chip-ico`): each icon
|
||||
* has exactly ONE copy in index.html and a chip can never drift from the button
|
||||
* it previews. A chip joins the preview purely by carrying `data-preview`
|
||||
* (which slot) and `data-preview-order` (where in that slot); nothing here
|
||||
* needs to know the setting's name.
|
||||
*
|
||||
* `data-preview-text` replaces the icon with a text token for the header
|
||||
* entries that are readouts rather than buttons (plan usage, CPU, font size).
|
||||
*/
|
||||
_syncLayoutPreview() {
|
||||
const modal = document.getElementById('appSettingsModal');
|
||||
if (!modal || typeof modal.querySelectorAll !== 'function') return;
|
||||
const slots = {
|
||||
header: document.getElementById('appSettingsPreviewHeader'),
|
||||
panel: document.getElementById('appSettingsPreviewPanels'),
|
||||
toolbar: document.getElementById('appSettingsPreviewToolbar'),
|
||||
float: document.getElementById('appSettingsPreviewFloats'),
|
||||
};
|
||||
if (!slots.header) return;
|
||||
Object.values(slots).forEach(el => {
|
||||
if (el) el.innerHTML = '';
|
||||
});
|
||||
|
||||
const chips = [...modal.querySelectorAll('.set-chip[data-preview]')]
|
||||
.filter(chip => chip.querySelector('input')?.checked)
|
||||
.sort((a, b) => (Number(a.dataset.previewOrder) || 0) - (Number(b.dataset.previewOrder) || 0));
|
||||
|
||||
let shown = 0;
|
||||
for (const chip of chips) {
|
||||
const kind = chip.dataset.preview;
|
||||
const slot = slots[kind];
|
||||
if (!slot) continue;
|
||||
// The label is the chip's own text; the icon span (if any) is skipped by
|
||||
// taking the LAST span, which is always the label.
|
||||
const spans = chip.querySelectorAll('span');
|
||||
const label = (spans[spans.length - 1]?.textContent || '').trim();
|
||||
const el = document.createElement('span');
|
||||
el.title = label;
|
||||
if (kind === 'header') {
|
||||
const text = chip.dataset.previewText;
|
||||
el.className = text ? 'set-preview-chip' : 'set-preview-btn';
|
||||
if (text) el.textContent = text;
|
||||
else this._appendPreviewIcon(el, chip);
|
||||
} else {
|
||||
el.className = `set-preview-${kind}`;
|
||||
this._appendPreviewIcon(el, chip);
|
||||
const name = document.createElement('span');
|
||||
name.textContent = label;
|
||||
el.appendChild(name);
|
||||
}
|
||||
slot.appendChild(el);
|
||||
shown++;
|
||||
}
|
||||
|
||||
const empty = document.getElementById('appSettingsPreviewEmpty');
|
||||
if (empty) empty.hidden = shown > 0;
|
||||
},
|
||||
|
||||
/** Clone a chip's icon into a preview element (see _syncLayoutPreview). */
|
||||
_appendPreviewIcon(target, chip) {
|
||||
const icon = chip.querySelector('.set-chip-ico');
|
||||
if (!icon) return;
|
||||
const clone = icon.cloneNode(true);
|
||||
clone.classList.remove('set-chip-ico');
|
||||
clone.classList.add('set-preview-ico');
|
||||
target.appendChild(clone);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -860,6 +941,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
section.classList.toggle('set-hit-hidden', !!q && !hasVisible);
|
||||
});
|
||||
|
||||
// The live preview sits outside any group, so it survives the sweep above;
|
||||
// a search is asking for one row, not for the scale model around it.
|
||||
doc.querySelectorAll('.set-preview').forEach(pv => pv.classList.toggle('set-hit-hidden', !!q));
|
||||
|
||||
const empty = document.getElementById('appSettingsSearchEmpty');
|
||||
if (empty) empty.hidden = !q || anyVisible;
|
||||
if (!q) doc.querySelectorAll('.set-group-advanced').forEach(g => g.classList.remove('open'));
|
||||
|
||||
+1146
-192
File diff suppressed because it is too large
Load Diff
@@ -1610,11 +1610,21 @@ Object.assign(CodemanApp.prototype, {
|
||||
return workingDir.split('/').pop() || workingDir;
|
||||
},
|
||||
|
||||
/** Normalize home prefixes to "~/" on both Linux and macOS */
|
||||
/**
|
||||
* Normalize a home prefix to "~" on both Linux (`/home/<user>`) and macOS
|
||||
* (`/Users/<user>`). The lookahead lets the home directory ITSELF match, so a
|
||||
* path that is exactly `$HOME` renders "~" instead of being left raw.
|
||||
*
|
||||
* This is the only place that pattern belongs. Two hand-rolled copies had
|
||||
* drifted, each broken on the platform its author was not using: the Run
|
||||
* menu's matched `/home/` only, so on macOS nothing was stripped and every
|
||||
* Recent Sessions row spent its first ~19 characters on an identical
|
||||
* `/Users/<user>/` prefix (#273); the case-manage list's matched `/Users/`
|
||||
* only, so no Linux path was ever abbreviated there. Route new path labels
|
||||
* through here rather than writing a third copy.
|
||||
*/
|
||||
_shortenHomePath(p) {
|
||||
return (p || '')
|
||||
.replace(/^\/home\/[^/]+\//, '~/')
|
||||
.replace(/^\/Users\/[^/]+\//, '~/');
|
||||
return (p || '').replace(/^\/(?:home|Users)\/[^/]+(?=\/|$)/, '~');
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user