mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 07:29:42 +02:00
Merge pull request #357 from dignfei/feat/docker-adopt-existing-container
feat(docker): attach a case to an already-running container Conflicts came from work that landed after the PR was opened, and each is resolved onto the newer abstraction rather than by keeping the older code: - `defaultDockerCommandForMode` is registry-driven since #347, so the PR's `runsAsRoot` arm became `overlays.docker.rootCommand` (claude only). Claude Code still refuses `--dangerously-skip-permissions` as root in 2.1.261 and the refusal is visible only inside the container, so an adopted root container otherwise just shows a dead pane. Which flag to drop is a per-CLI fact, and `test/cli-registry-no-id-branching.test.ts` forbids expressing it as a branch. - The probe's mode list and its mode -> binary table both duplicated the registry. They now read `enabledCliIds()` / `discovery.binaries[0]`, which is also what fixes the merge's silent regression: the hand-written list predates `omp`, and the run menu gates every docker case on this probe, so owned containers would have lost that mode. `shell` needs no arm — it declares no binary, so it is dropped from the lookup and reported available regardless. - The per-mode `mode === 'claude' && !cliDir` chain in `tmux-manager.ts` is one `missingCliMessage(mode)` gate since #347; the PR's docker exemption moved onto it. Its test now pins the single gate instead of counting seven arms. - The create arm keeps #349's swap-limit warning filter, which the adopted arm never reaches; the run-mode list gains `omp` from #353. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TecFD9hvPYJ1mkkMtBQbT1
This commit is contained in:
+40
-13
@@ -2270,9 +2270,11 @@ class CodemanApp {
|
||||
? 'Grok'
|
||||
: mode === 'deepseek'
|
||||
? 'DeepSeek'
|
||||
: mode === 'opencode'
|
||||
? 'OpenCode'
|
||||
: 'Claude';
|
||||
: mode === 'omp'
|
||||
? 'OMP'
|
||||
: mode === 'opencode'
|
||||
? 'OpenCode'
|
||||
: 'Claude';
|
||||
}
|
||||
|
||||
async toggleResponseViewer() {
|
||||
@@ -2632,29 +2634,52 @@ class CodemanApp {
|
||||
// string field (e.g. modelDisplayName, which the route also broadcasts) is
|
||||
// ever shown in this chip, render it via textContent — never interpolate an
|
||||
// untrusted string into this template.
|
||||
const seg = (label, p) => {
|
||||
if (p === null) return '';
|
||||
// `idle: true` keeps a missing window's SLOT with a dimmed em dash instead of
|
||||
// dropping it. Claude only: Claude Code documents `five_hour` as "present
|
||||
// only while the API reports it and its resets_at has not passed", so that
|
||||
// key leaves the statusline payload whenever no 5-hour session window is
|
||||
// open, and a chip that silently shrank from two windows to one read as a
|
||||
// broken feature rather than as an idle window (reported 2026-09-01). A
|
||||
// missing CODEX bucket means the opposite — that plan has no such limit —
|
||||
// so those stay omitted rather than showing a dash forever.
|
||||
const seg = (label, p, idle) => {
|
||||
if (p === null) {
|
||||
if (!idle) return '';
|
||||
return `<span class="pu-win pu-win-idle"><span class="pu-label">${label}</span><span class="pu-val">—</span></span>`;
|
||||
}
|
||||
const n = Math.round(Number(p));
|
||||
if (!Number.isFinite(n)) return '';
|
||||
return `<span class="pu-win"><span class="pu-label">${label}</span><span class="pu-val ${colorClass(n)}">${n}%</span></span>`;
|
||||
};
|
||||
const row = (provider, usage) => {
|
||||
const windows = [seg('5h', pct(usage?.fiveHour)), seg('7d', pct(usage?.sevenDay))].filter(Boolean);
|
||||
// The provider label only earns its space when there is more than one
|
||||
// provider to tell apart: a machine with Claude alone shows bare windows.
|
||||
const hasWindows = (usage) => pct(usage?.fiveHour) !== null || pct(usage?.sevenDay) !== null;
|
||||
const labelled = hasWindows(data) && hasWindows(data.codex);
|
||||
const row = (provider, usage, idle) => {
|
||||
// hasWindows() gates the row, so a placeholder can only ever appear
|
||||
// ALONGSIDE a real reading — a provider reporting nothing still renders
|
||||
// nothing, never a row of em dashes.
|
||||
if (!hasWindows(usage)) return '';
|
||||
const windows = [seg('5h', pct(usage?.fiveHour), idle), seg('7d', pct(usage?.sevenDay), idle)].filter(Boolean);
|
||||
if (!windows.length) return '';
|
||||
return `<span class="pu-row"><span class="pu-provider">${provider}</span><span class="pu-windows">${windows.join('<span class="pu-sep">·</span>')}</span></span>`;
|
||||
const label = labelled ? `<span class="pu-provider">${provider}</span>` : '';
|
||||
return `<span class="pu-row">${label}<span class="pu-windows">${windows.join('<span class="pu-sep">·</span>')}</span></span>`;
|
||||
};
|
||||
const rows = [row('Claude', data), row('Codex', data.codex)].filter(Boolean);
|
||||
const rows = [row('Claude', data, true), row('Codex', data.codex, false)].filter(Boolean);
|
||||
chip.innerHTML = rows.length ? rows.join('') : '—';
|
||||
const resetStr = (w) => (w && w.resetAt ? new Date(w.resetAt).toLocaleString() : '—');
|
||||
const details = (provider, usage) => {
|
||||
const details = (provider, usage, idle) => {
|
||||
const lines = [];
|
||||
const five = pct(usage?.fiveHour);
|
||||
const seven = pct(usage?.sevenDay);
|
||||
if (five !== null) lines.push(`5-hour limit: ${five}% used (resets ${resetStr(usage.fiveHour)})`);
|
||||
else if (idle && seven !== null) lines.push('5-hour limit: no active session window');
|
||||
if (seven !== null) lines.push(`Weekly limit: ${seven}% used (resets ${resetStr(usage.sevenDay)})`);
|
||||
return lines.length ? `${provider} plan usage\n${lines.join('\n')}` : '';
|
||||
};
|
||||
chip.title = [details('Claude', data), details('Codex', data.codex)].filter(Boolean).join('\n\n') || 'Plan usage limits';
|
||||
chip.title =
|
||||
[details('Claude', data, true), details('Codex', data.codex, false)].filter(Boolean).join('\n\n') ||
|
||||
'Plan usage limits';
|
||||
}
|
||||
|
||||
// Scheduled runs
|
||||
@@ -4896,7 +4921,7 @@ class CodemanApp {
|
||||
<span class="tab-status ${status}" aria-hidden="true"></span>
|
||||
<span class="tab-info">
|
||||
<span class="tab-name-row">
|
||||
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : mode === 'opencode' ? '<span class="tab-mode opencode" aria-hidden="true">oc</span>' : mode === 'codex' ? '<span class="tab-mode codex" aria-hidden="true">cx</span>' : mode === 'gemini' ? '<span class="tab-mode gemini" aria-hidden="true">gm</span>' : mode === 'antigravity' ? '<span class="tab-mode antigravity" aria-hidden="true">ag</span>' : mode === 'pi' ? '<span class="tab-mode pi" aria-hidden="true">pi</span>' : mode === 'grok' ? '<span class="tab-mode grok" aria-hidden="true">gk</span>' : mode === 'deepseek' ? '<span class="tab-mode deepseek" aria-hidden="true">ds</span>' : ''}
|
||||
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : mode === 'opencode' ? '<span class="tab-mode opencode" aria-hidden="true">oc</span>' : mode === 'codex' ? '<span class="tab-mode codex" aria-hidden="true">cx</span>' : mode === 'gemini' ? '<span class="tab-mode gemini" aria-hidden="true">gm</span>' : mode === 'antigravity' ? '<span class="tab-mode antigravity" aria-hidden="true">ag</span>' : mode === 'pi' ? '<span class="tab-mode pi" aria-hidden="true">pi</span>' : mode === 'grok' ? '<span class="tab-mode grok" aria-hidden="true">gk</span>' : mode === 'deepseek' ? '<span class="tab-mode deepseek" aria-hidden="true">ds</span>' : mode === 'omp' ? '<span class="tab-mode omp" aria-hidden="true">om</span>' : ''}
|
||||
<span class="tab-name" data-session-id="${id}" data-full-name="${escapeHtml(name)}">${tabLabel}</span>
|
||||
${inlineSessionActions ? tabActionsHtml : ''}
|
||||
<span class="tab-detached-badge" aria-hidden="true">detached</span>
|
||||
@@ -6344,7 +6369,9 @@ class CodemanApp {
|
||||
? 'Kill Tmux & Grok'
|
||||
: session.mode === 'deepseek'
|
||||
? 'Kill Tmux & DeepSeek'
|
||||
: 'Kill Tmux & Claude Code';
|
||||
: session.mode === 'omp'
|
||||
? 'Kill Tmux & OMP'
|
||||
: 'Kill Tmux & Claude Code';
|
||||
}
|
||||
|
||||
document.getElementById('closeConfirmModal').classList.add('active');
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* @globals {function} scheduleBackground - scheduler.postTask wrapper (background priority)
|
||||
* @globals {function} getEventCoords - Unified mouse/touch coordinate extractor
|
||||
* @globals {function} escapeHtml - XSS-safe HTML escaping
|
||||
* @globals {object} SSE_EVENTS - Centralized SSE event type constants (156 event types; must match backend src/web/sse-events.ts)
|
||||
* @globals {object} SSE_EVENTS - Centralized SSE event type constants (157 event types; must match backend src/web/sse-events.ts)
|
||||
* @globals {Array} BUILTIN_RESPAWN_PRESETS - Built-in respawn configuration presets
|
||||
*
|
||||
* @dependency None (first in load order)
|
||||
|
||||
@@ -80,6 +80,7 @@ const HOME_SESSIONS_MODE_BADGE = {
|
||||
pi: 'pi',
|
||||
grok: 'gk',
|
||||
deepseek: 'ds',
|
||||
omp: 'om',
|
||||
};
|
||||
|
||||
Object.assign(CodemanApp.prototype, {
|
||||
|
||||
@@ -452,6 +452,10 @@
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
Run DeepSeek
|
||||
</button>
|
||||
<button class="welcome-btn welcome-btn-omp" id="welcomeOmpBtn" style="display: none;" onclick="app.setRunMode('omp'); app.runOmp()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
Run OMP
|
||||
</button>
|
||||
</div>
|
||||
<div class="welcome-qr" id="welcomeQr" onclick="app.toggleWelcomeQrSize()">
|
||||
<div class="welcome-qr-inner" id="welcomeQrInner"></div>
|
||||
@@ -643,6 +647,9 @@
|
||||
<button class="run-mode-option run-mode-option-install" data-action="deepseek-install" id="runModeDeepSeekInstall" style="display: none;" onclick="app.installDeepSeekProfile()">
|
||||
<span class="run-mode-dot deepseek"></span>DeepSeek — add a terminal profile…
|
||||
</button>
|
||||
<button class="run-mode-option" data-mode="omp" onclick="app.setRunMode('omp')">
|
||||
<span class="run-mode-dot omp"></span>OMP
|
||||
</button>
|
||||
<div class="run-mode-sep"></div>
|
||||
<button class="run-mode-option" data-mode="shell" onclick="app.setRunMode('shell')">
|
||||
<span class="run-mode-dot shell"></span>Terminal / Shell
|
||||
@@ -930,6 +937,7 @@
|
||||
<option value="pi">Pi</option>
|
||||
<option value="grok">Grok</option>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="omp">OMP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row"><label>Working Directory</label><input type="text" id="schWorkingDir" placeholder="/absolute/path"></div>
|
||||
@@ -2020,6 +2028,8 @@
|
||||
<div class="set-modelgrid" id="appSettingsModelCards" role="radiogroup" aria-label="Model for new Claude sessions" data-search="model opus sonnet haiku fable claude"></div>
|
||||
<select id="appSettingsClaudeModel" class="set-select set-field-hidden" aria-hidden="true" tabindex="-1">
|
||||
<option value="" data-meta="Whatever the CLI picks">Default (CLI setting)</option>
|
||||
<option value="claude-fable-5-1" data-meta="Latest" data-base="claude-fable-5-1" data-ctx="1">Fable 5.1</option>
|
||||
<option value="claude-fable-5-1[1m]" data-variant="1m" data-base="claude-fable-5-1">Fable 5.1 (1M context)</option>
|
||||
<option value="claude-fable-5" data-meta="Most powerful" data-base="claude-fable-5" data-ctx="1">Fable 5</option>
|
||||
<option value="claude-fable-5[1m]" data-variant="1m" data-base="claude-fable-5">Fable 5 (1M context)</option>
|
||||
<option value="opus" data-meta="Most capable" data-base="opus" data-ctx="1">Opus</option>
|
||||
@@ -2032,7 +2042,7 @@
|
||||
<div class="set-row" id="appSettingsContextRow" data-search="1m context window opus long">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">1M context window</span>
|
||||
<span class="set-row-desc" id="appSettingsContextDesc">Available for Fable 5, Opus and Opus 4.6.</span>
|
||||
<span class="set-row-desc" id="appSettingsContextDesc">Available for Fable 5.1, Fable 5, Opus and Opus 4.6.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="appSettingsOpusContext1m"><span class="slider"></span></label>
|
||||
</div>
|
||||
@@ -2072,6 +2082,7 @@
|
||||
</div>
|
||||
<select id="appSettingsDefaultModel" class="set-select">
|
||||
<option value="">Default (CLI default)</option>
|
||||
<option value="claude-fable-5-1">Fable 5.1 (Latest)</option>
|
||||
<option value="claude-fable-5">Fable 5 (Most powerful)</option>
|
||||
<option value="opus">Opus (Most capable)</option>
|
||||
<option value="sonnet">Sonnet (Balanced)</option>
|
||||
@@ -2086,6 +2097,7 @@
|
||||
<option value="haiku">Haiku</option>
|
||||
<option value="sonnet">Sonnet</option>
|
||||
<option value="opus">Opus</option>
|
||||
<option value="claude-fable-5-1">Fable 5.1</option>
|
||||
<option value="claude-fable-5">Fable 5</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -2096,6 +2108,7 @@
|
||||
<option value="haiku">Haiku</option>
|
||||
<option value="sonnet">Sonnet</option>
|
||||
<option value="opus">Opus</option>
|
||||
<option value="claude-fable-5-1">Fable 5.1</option>
|
||||
<option value="claude-fable-5">Fable 5</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -2106,6 +2119,7 @@
|
||||
<option value="haiku">Haiku</option>
|
||||
<option value="sonnet">Sonnet</option>
|
||||
<option value="opus">Opus</option>
|
||||
<option value="claude-fable-5-1">Fable 5.1</option>
|
||||
<option value="claude-fable-5">Fable 5</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -2116,6 +2130,7 @@
|
||||
<option value="haiku">Haiku</option>
|
||||
<option value="sonnet">Sonnet</option>
|
||||
<option value="opus">Opus</option>
|
||||
<option value="claude-fable-5-1">Fable 5.1</option>
|
||||
<option value="claude-fable-5">Fable 5</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -2710,6 +2725,7 @@
|
||||
<option value="pi" data-cli="pi">Pi</option>
|
||||
<option value="grok" data-cli="grok">Grok</option>
|
||||
<option value="deepseek" data-cli="deepseek">DeepSeek</option>
|
||||
<option value="omp" data-cli="omp">OMP</option>
|
||||
<option value="shell">Shell (no agent)</option>
|
||||
</select>
|
||||
<span class="form-hint">Which CLI to point the Run button at once the clone finishes. Changeable any time from the Run dropdown.</span>
|
||||
|
||||
@@ -56,6 +56,7 @@ const MOBILE_OVERVIEW_RUN_MODES = [
|
||||
{ mode: 'pi', label: 'Pi', short: 'Pi' },
|
||||
{ mode: 'grok', label: 'Grok', short: 'Grok' },
|
||||
{ mode: 'deepseek', label: 'DeepSeek', short: 'DeepSeek' },
|
||||
{ mode: 'omp', label: 'OMP', short: 'OMP' },
|
||||
{ mode: 'shell', label: 'Terminal / Shell', short: 'Shell' },
|
||||
];
|
||||
|
||||
@@ -388,7 +389,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
async resumeMobileOverviewSession(sessionId) {
|
||||
const row = (this._mobileOverviewPastRows || []).find((r) => r.id === sessionId);
|
||||
if (!row || !row.workingDir) return;
|
||||
await this.resumeHistorySession(row.claudeSessionId || row.id, row.workingDir, row.name || undefined);
|
||||
await this.resumeHistorySession(row.claudeSessionId || row.id, row.workingDir, row.name || undefined, row.mode);
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -1003,6 +1003,20 @@ html.mobile-init .file-browser-panel {
|
||||
border-color: rgba(150, 170, 255, 0.55) !important;
|
||||
}
|
||||
|
||||
/* OMP mode colors on mobile. Same `!important` rationale as the pi/grok/deepseek blocks above. */
|
||||
.btn-toolbar.btn-run.mode-omp,
|
||||
.btn-toolbar.btn-run-gear.mode-omp {
|
||||
background: #312e81 !important;
|
||||
border-color: rgba(129, 140, 248, 0.3) !important;
|
||||
color: #e0e7ff !important;
|
||||
}
|
||||
|
||||
.btn-toolbar.btn-run.mode-omp:active,
|
||||
.btn-toolbar.btn-run-gear.mode-omp:active {
|
||||
background: #4f46e5 !important;
|
||||
border-color: rgba(129, 140, 248, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Run mode dropdown menu — positioned above toolbar on mobile */
|
||||
.run-mode-menu {
|
||||
bottom: 100%;
|
||||
@@ -3086,6 +3100,12 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(.btn-toolbar.btn-run.mode-omp, .btn-toolbar.btn-run-gear.mode-omp) {
|
||||
background: linear-gradient(135deg, #4f46e5, #6366f1);
|
||||
border-color: #4338ca;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(.btn-toolbar.btn-run.mode-grok, .btn-toolbar.btn-run-gear.mode-grok) {
|
||||
background: linear-gradient(135deg, #27272a, #52525b);
|
||||
border-color: #18181b;
|
||||
|
||||
@@ -432,7 +432,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
_buildCommandPaletteNewSessionItem(query = '') {
|
||||
const mode = this.runMode || this._runMode || 'claude';
|
||||
const labels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', gemini: 'Gemini', antigravity: 'Antigravity', pi: 'Pi', grok: 'Grok', deepseek: 'DeepSeek' };
|
||||
const labels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', gemini: 'Gemini', antigravity: 'Antigravity', pi: 'Pi', grok: 'Grok', deepseek: 'DeepSeek', omp: 'OMP' };
|
||||
const caseName = this._findCommandPaletteCaseMatch(query) || document.getElementById('quickStartCase')?.value || 'testcase';
|
||||
return {
|
||||
id: 'new-session',
|
||||
@@ -670,7 +670,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
} else if (record.workingDir) {
|
||||
// History rows are keyed by the Claude conversation UUID; resumed
|
||||
// sessions carry theirs separately as claudeSessionId.
|
||||
void this.resumeHistorySession(s.claudeSessionId || s.sessionId, record.workingDir);
|
||||
void this.resumeHistorySession(s.claudeSessionId || s.sessionId, record.workingDir, undefined, s.mode);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -407,6 +407,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (mode === 'antigravity') {
|
||||
return await this.runAntigravity();
|
||||
}
|
||||
if (mode === 'omp') {
|
||||
return await this.runOmp();
|
||||
}
|
||||
if (mode === 'pi') {
|
||||
return await this.runPi();
|
||||
}
|
||||
@@ -501,7 +504,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// An unreachable container hides every agent mode and explains why, instead
|
||||
// of silently offering modes that cannot start.
|
||||
const probeError = isDocker ? this._dockerCaseProbeError?.[caseName] : null;
|
||||
for (const mode of ['claude', 'opencode', 'codex', 'gemini', 'antigravity', 'pi', 'grok', 'deepseek']) {
|
||||
for (const mode of ['claude', 'opencode', 'codex', 'gemini', 'antigravity', 'pi', 'grok', 'deepseek', 'omp']) {
|
||||
const btn = menu.querySelector(`.run-mode-option[data-mode="${mode}"]`);
|
||||
if (!btn) continue;
|
||||
let available;
|
||||
@@ -789,7 +792,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
btn.append(...parts);
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.resumeHistorySession(s.sessionId, s.workingDir, s.name);
|
||||
this.resumeHistorySession(s.sessionId, s.workingDir, s.name, s.mode);
|
||||
});
|
||||
container.appendChild(btn);
|
||||
}
|
||||
@@ -810,7 +813,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
gearBtn.className = `btn-toolbar btn-run-gear mode-${mode}`;
|
||||
}
|
||||
if (label) {
|
||||
label.textContent = mode === 'opencode' ? 'Run OC' : mode === 'codex' ? 'Run CX' : mode === 'gemini' ? 'Run GM' : mode === 'antigravity' ? 'Run AG' : mode === 'pi' ? 'Run PI' : mode === 'grok' ? 'Run GK' : mode === 'deepseek' ? 'Run DS' : mode === 'shell' ? 'Run SH' : 'Run';
|
||||
label.textContent = mode === 'opencode' ? 'Run OC' : mode === 'codex' ? 'Run CX' : mode === 'gemini' ? 'Run GM' : mode === 'antigravity' ? 'Run AG' : mode === 'pi' ? 'Run PI' : mode === 'grok' ? 'Run GK' : mode === 'deepseek' ? 'Run DS' : mode === 'omp' ? 'Run OMP' : mode === 'shell' ? 'Run SH' : 'Run';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1523,6 +1526,56 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
async runOmp() {
|
||||
const caseName = document.getElementById('quickStartCase').value || 'testcase';
|
||||
// Remote/docker cases run omp on the OTHER side — skip the local status probe
|
||||
// and the local-only config below (quick-start rejects them for remote cases).
|
||||
const _runLoc = (this.cases || []).find(c => c.name === caseName)?.location;
|
||||
const isRemote = _runLoc === 'remote' || _runLoc === 'docker';
|
||||
|
||||
const ownsLaunchTerminal = this._beginSessionLaunchStatus(`Starting OMP session in ${caseName}...`);
|
||||
this.terminal.focus();
|
||||
|
||||
try {
|
||||
if (!isRemote) {
|
||||
const statusRes = await fetch('/api/omp/status');
|
||||
const status = (await statusRes.json()).data;
|
||||
if (!status.available) {
|
||||
this._reportSessionLaunchError(
|
||||
ownsLaunchTerminal,
|
||||
'OMP CLI not found. Install with: curl -fsSL https://omp.sh/install | sh'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), this.loadAppSettingsFromStorage());
|
||||
const res = await fetch('/api/quick-start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
caseName,
|
||||
mode: 'omp',
|
||||
sessionName: `w${this._nextCaseSessionStartNumber(caseName)}-${caseName}`,
|
||||
...(isRemote ? {} : {
|
||||
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
||||
}),
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.error || 'Failed to start OMP');
|
||||
await this._ensureCreatedSessionVisible(data.data.sessionId, data.data.session);
|
||||
|
||||
if (data.data.sessionId) {
|
||||
await this.selectSession(data.data.sessionId);
|
||||
}
|
||||
|
||||
this.terminal.focus();
|
||||
} catch (err) {
|
||||
this._reportSessionLaunchError(ownsLaunchTerminal, err.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Launch a Grok Build (xAI `grok`) session.
|
||||
*
|
||||
@@ -1726,7 +1779,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (detachToggle) detachToggle.checked = this.hasTabDetachOverride(sessionId);
|
||||
|
||||
// Reset to an appropriate tab — Summary for external CLIs (Respawn/Ralph are Claude-only)
|
||||
const isAltMode = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi' || session.mode === 'grok' || session.mode === 'deepseek';
|
||||
const isAltMode = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi' || session.mode === 'grok' || session.mode === 'deepseek' || session.mode === 'omp';
|
||||
this.switchOptionsTab(isAltMode ? 'summary' : 'respawn');
|
||||
|
||||
// Update respawn status display and buttons
|
||||
@@ -1756,7 +1809,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
// Hide Claude-specific options for external CLI sessions
|
||||
const isExternalCli = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi' || session.mode === 'grok' || session.mode === 'deepseek';
|
||||
const isExternalCli = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi' || session.mode === 'grok' || session.mode === 'deepseek' || session.mode === 'omp';
|
||||
const claudeOnlyEls = document.querySelectorAll('[data-claude-only]');
|
||||
claudeOnlyEls.forEach(el => { el.style.display = isExternalCli ? 'none' : ''; });
|
||||
|
||||
@@ -3733,7 +3786,7 @@ Object.defineProperty(CodemanApp.prototype, 'runMode', {
|
||||
},
|
||||
set(mode) {
|
||||
this._runMode =
|
||||
mode === 'opencode' || mode === 'codex' || mode === 'gemini' || mode === 'antigravity' || mode === 'pi' || mode === 'grok' || mode === 'deepseek' || mode === 'claude'
|
||||
mode === 'opencode' || mode === 'codex' || mode === 'gemini' || mode === 'antigravity' || mode === 'pi' || mode === 'grok' || mode === 'deepseek' || mode === 'omp' || mode === 'claude'
|
||||
? mode
|
||||
: 'claude';
|
||||
},
|
||||
|
||||
@@ -922,7 +922,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
desc.textContent = inert
|
||||
? 'The selected model has no 1M variant.'
|
||||
: base
|
||||
? 'Available for Fable 5, Opus and Opus 4.6.'
|
||||
? 'Available for Fable 5.1, Fable 5, Opus and Opus 4.6.'
|
||||
: 'With no model pinned, this starts new sessions on Opus with a 1M window.';
|
||||
}
|
||||
},
|
||||
@@ -1079,10 +1079,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
const verEl = this.$('updateCurrentVersion');
|
||||
if (verEl && data.currentVersion) verEl.textContent = `v${data.currentVersion}`;
|
||||
|
||||
if (data.installKind && data.installKind !== 'git') {
|
||||
this._setUpdateResult(
|
||||
`This install can't update itself (${escapeHtml(data.installKind)}). Update with <code>npm i -g aicodeman@latest</code>.`
|
||||
);
|
||||
// `docker-compose` self-updates in place like `git` does — the container
|
||||
// restarts itself. Anything else cannot.
|
||||
if (data.installKind && data.installKind !== 'git' && data.installKind !== 'docker-compose') {
|
||||
const hint =
|
||||
data.supervisor === 'docker-compose'
|
||||
? 'Update from the Docker host with <code>docker/Start-Codeman.sh</code>.'
|
||||
: 'Update with <code>npm i -g aicodeman@latest</code>.';
|
||||
this._setUpdateResult(`This install can't update itself (${escapeHtml(data.installKind)}). ${hint}`);
|
||||
return;
|
||||
}
|
||||
if (data.selfUpdateEnabled === false) {
|
||||
@@ -1093,6 +1097,30 @@ Object.assign(CodemanApp.prototype, {
|
||||
this._setUpdateResult(escapeHtml(data.error));
|
||||
return;
|
||||
}
|
||||
// A container release that changes the ENVIRONMENT (Dockerfile, compose file
|
||||
// or new .env keys) cannot be applied by the container restarting itself, so
|
||||
// the update button is never offered — the host command is, instead. The
|
||||
// server re-checks this on POST, so hiding the button is UX, not the gate.
|
||||
const blockers = data.environment?.blockers || [];
|
||||
if (data.updateAvailable && blockers.length > 0) {
|
||||
const reasons = blockers
|
||||
.map((b) => {
|
||||
const details = b.details?.length ? `<br><code>${escapeHtml(b.details.join(' '))}</code>` : '';
|
||||
return `<li>${escapeHtml(b.message)}${details}</li>`;
|
||||
})
|
||||
.join('');
|
||||
this._setUpdateResult(
|
||||
`<strong>v${escapeHtml(data.latestVersion || '')}</strong> needs a rebuild on the Docker host` +
|
||||
` (current v${escapeHtml(data.currentVersion || '')}):<ul>${reasons}</ul>` +
|
||||
`Run <code>${escapeHtml(data.environment?.hostCommand || 'docker/Start-Codeman.sh')}</code> there to apply it.`
|
||||
);
|
||||
if (notes && data.notes) {
|
||||
notes.style.display = 'block';
|
||||
notes.textContent = data.notes;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.updateAvailable && data.latestVersion) {
|
||||
this._setUpdateResult(
|
||||
`Update available: <strong>v${escapeHtml(data.latestVersion)}</strong> (current v${escapeHtml(data.currentVersion || '')})`
|
||||
@@ -1230,6 +1258,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
['welcomeClaudeBtn', 'claude'],
|
||||
['welcomeOpencodeBtn', 'opencode'],
|
||||
['welcomeAntigravityBtn', 'antigravity'],
|
||||
['welcomeOmpBtn', 'omp'],
|
||||
['welcomeGeminiBtn', 'gemini'],
|
||||
['welcomePiBtn', 'pi'],
|
||||
['welcomeGrokBtn', 'grok'],
|
||||
|
||||
@@ -349,7 +349,8 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
.session-tab .tab-mode.gemini,
|
||||
.session-tab .tab-mode.antigravity,
|
||||
.session-tab .tab-mode.pi,
|
||||
.session-tab .tab-mode.grok
|
||||
.session-tab .tab-mode.grok,
|
||||
.session-tab .tab-mode.omp
|
||||
) {
|
||||
color: var(--accent-d);
|
||||
}
|
||||
@@ -2499,6 +2500,10 @@ body.solo-mode .btn-lifecycle-log {
|
||||
background: rgba(34, 211, 238, 0.2);
|
||||
color: #22d3ee;
|
||||
}
|
||||
.session-tab .tab-mode.omp {
|
||||
background: rgba(129, 140, 248, 0.2);
|
||||
color: #818cf8;
|
||||
}
|
||||
|
||||
.session-tab .tab-mode.pi {
|
||||
background: rgba(244, 114, 182, 0.2);
|
||||
@@ -3883,6 +3888,22 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
||||
color: #fff1f7;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
/* OMP: indigo identity, matching .btn-toolbar.btn-run.mode-omp and
|
||||
.run-mode-dot.omp so the welcome action reads as the same backend. */
|
||||
.welcome-btn-omp {
|
||||
background: linear-gradient(135deg, #1e1b4b 0%, #4f46e5 55%, #6366f1 100%);
|
||||
border-color: rgba(129, 140, 248, 0.4);
|
||||
color: #e0e7ff;
|
||||
box-shadow: 0 2px 8px rgba(129, 140, 248, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.welcome-btn-omp:hover {
|
||||
background: linear-gradient(135deg, #312e81 0%, #6366f1 55%, #818cf8 100%);
|
||||
box-shadow: 0 4px 20px rgba(129, 140, 248, 0.3), 0 0 40px rgba(79, 70, 229, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(165, 180, 252, 0.5);
|
||||
color: #eef2ff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Grok (xAI): monochrome charcoal identity, matching .btn-toolbar.btn-run.mode-grok
|
||||
and .run-mode-dot.grok so the welcome action reads as the same backend. */
|
||||
@@ -4992,6 +5013,21 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
||||
border-color: rgba(249, 168, 212, 0.6);
|
||||
color: #fff1f7;
|
||||
}
|
||||
/* OMP mode colors */
|
||||
.btn-toolbar.btn-run.mode-omp,
|
||||
.btn-toolbar.btn-run-gear.mode-omp {
|
||||
background: linear-gradient(135deg, #312e81 0%, #4f46e5 55%, #6366f1 100%);
|
||||
border-color: rgba(129, 140, 248, 0.5);
|
||||
color: #e0e7ff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.btn-toolbar.btn-run.mode-omp:hover,
|
||||
.btn-toolbar.btn-run-gear.mode-omp:hover {
|
||||
background: linear-gradient(135deg, #3730a3 0%, #6366f1 55%, #818cf8 100%);
|
||||
box-shadow: 0 0 12px rgba(129, 140, 248, 0.35), 0 2px 8px rgba(79, 70, 229, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(165, 180, 252, 0.6);
|
||||
color: #eef2ff;
|
||||
}
|
||||
|
||||
/* Grok mode colors. Same cascade note as pi above: this base-sheet pair only
|
||||
renders on the `og` skin — the nested `html:not([data-skin="og"])` block
|
||||
@@ -5116,6 +5152,7 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
||||
.run-mode-dot.pi { background: #f472b6; }
|
||||
.run-mode-dot.grok { background: #a1a1aa; }
|
||||
.run-mode-dot.deepseek { background: #4d6bfe; }
|
||||
.run-mode-dot.omp { background: #818cf8; }
|
||||
.run-mode-dot.shell { background: #94a3b8; }
|
||||
|
||||
/* Phone-only Enter button (see index.html). Hidden by default at every width;
|
||||
@@ -12433,6 +12470,13 @@ kbd {
|
||||
color: var(--text-dim);
|
||||
opacity: 0.45;
|
||||
}
|
||||
/* A window Claude is not currently reporting: the slot stays, dimmed, so the
|
||||
chip keeps its shape instead of looking like half of it broke. */
|
||||
.header-plan-usage .pu-win-idle .pu-label,
|
||||
.header-plan-usage .pu-win-idle .pu-val {
|
||||
color: var(--text-dim);
|
||||
opacity: 0.55;
|
||||
}
|
||||
/* Green/yellow/red by how much of the window is used up. */
|
||||
.header-plan-usage .pu-green {
|
||||
color: #3fb950;
|
||||
|
||||
@@ -2193,7 +2193,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (isLive && this.sessions.has(s.sessionId)) {
|
||||
this.selectSession(s.sessionId);
|
||||
} else {
|
||||
this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name);
|
||||
this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name, s.mode);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -2436,7 +2436,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
} else {
|
||||
// Resume by the Claude conversation UUID when present (resumed sessions
|
||||
// carry theirs separately from their Codeman id).
|
||||
this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name);
|
||||
this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name, s.mode);
|
||||
}
|
||||
this.closeSessionManager?.();
|
||||
closeMenu();
|
||||
@@ -2904,7 +2904,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
return `w${startNumber}-${dirName}`;
|
||||
},
|
||||
|
||||
async resumeHistorySession(sessionId, workingDir, existingName) {
|
||||
async resumeHistorySession(sessionId, workingDir, existingName, mode) {
|
||||
// Close the run mode menu if open
|
||||
document.getElementById('runModeMenu')?.classList.remove('active');
|
||||
// Close folder history modal if open
|
||||
@@ -2925,13 +2925,45 @@ Object.assign(CodemanApp.prototype, {
|
||||
const globalSettings = this.loadAppSettingsFromStorage();
|
||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings);
|
||||
const effort = this.getEffortSetting(globalSettings);
|
||||
// `resumeSessionId` is a Claude conversation UUID (server reads it from
|
||||
// ~/.claude/projects); an external-CLI row has no such thing, so sending
|
||||
// it there gets silently ignored while the OMITTED `mode` field defaults
|
||||
// the create to plain claude — reproducing whatever conversation THAT
|
||||
// uuid happens to collide with instead of the row's own backend. Row mode
|
||||
// wins here. Codeman has no cross-restart PTY-reattach outside server
|
||||
// boot, so "resume" for a non-claude row means relaunching the CLI's own
|
||||
// continue-most-recent flag (opencode/pi/grok/omp --continue, deepseek
|
||||
// resumeSession) in the same directory — real conversation continuity,
|
||||
// just not the literal old process.
|
||||
const effectiveMode = mode || 'claude';
|
||||
const modeConfigKey = {
|
||||
opencode: 'openCodeConfig',
|
||||
pi: 'piConfig',
|
||||
grok: 'grokConfig',
|
||||
omp: 'ompConfig',
|
||||
}[effectiveMode];
|
||||
// codex/gemini/antigravity have no wired continuation here yet (their
|
||||
// configs use an exact conversation id, not a "continue most recent"
|
||||
// flag, and the row's own `sessionId` is not verified to carry that
|
||||
// id for these three modes) — `continuesSomething` below is what keeps
|
||||
// their row from being retired for a resume that didn't actually
|
||||
// continue anything.
|
||||
const modeConfig =
|
||||
modeConfigKey
|
||||
? { [modeConfigKey]: { continueSession: true } }
|
||||
: effectiveMode === 'deepseek'
|
||||
? { deepSeekConfig: { resumeSession: true } }
|
||||
: {};
|
||||
const continuesSomething = Boolean(modeConfigKey) || effectiveMode === 'deepseek';
|
||||
const createRes = await fetch('/api/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workingDir,
|
||||
name,
|
||||
resumeSessionId: sessionId,
|
||||
mode: effectiveMode,
|
||||
...(effectiveMode === 'claude' ? { resumeSessionId: sessionId } : {}),
|
||||
...modeConfig,
|
||||
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
||||
...(effort ? { effort } : {}),
|
||||
}),
|
||||
@@ -2944,6 +2976,21 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Start interactive
|
||||
await fetch(`/api/sessions/${newSessionId}/interactive`, { method: 'POST' });
|
||||
|
||||
// Retire the row being resumed: a non-claude "resume" is really a brand
|
||||
// new Codeman session pointed at the same directory (there is no id to
|
||||
// reattach to), so without this every resume leaves the old row behind
|
||||
// as a duplicate — click it 3 times, see the same name 3 times. Claude
|
||||
// rows are left alone: `sessionId` there is a claudeSessionId, which
|
||||
// usually has no live/persisted Codeman session of its own to delete.
|
||||
// Gated on `continuesSomething`: for codex/gemini/antigravity (no
|
||||
// continuation wired above), this is really a FRESH session with no
|
||||
// relation to the old row's conversation, so retiring it would discard
|
||||
// the old conversation with no recovery — worse than the duplicate row
|
||||
// this guard exists to prevent for the modes that DO continue.
|
||||
if (effectiveMode !== 'claude' && continuesSomething && sessionId !== newSessionId) {
|
||||
fetch(`/api/sessions/${sessionId}?killMux=true`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
|
||||
this.terminal.writeln(`\x1b[90m Session ${name} ready\x1b[0m`);
|
||||
await this.selectSession(newSessionId);
|
||||
this.terminal.focus();
|
||||
|
||||
Reference in New Issue
Block a user