Merge #650: jcode as a run mode

Conflict in skills/codeman/SKILL.md (and its plugin mirror): the list of
modes with neither stop nor blocked gained jcode here and the omp
exception from #798 on the release branch; both are kept.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-11 20:27:26 +01:00
commit d45b1ff248
66 files changed
+1304 -127

No files matched your search

+8 -4
View File
@@ -3186,9 +3186,11 @@ class CodemanApp {
? 'OMP'
: mode === 'copilot'
? 'GitHub Copilot'
: mode === 'opencode'
? 'OpenCode'
: 'Claude';
: mode === 'jcode'
? 'jcode'
: mode === 'opencode'
? 'OpenCode'
: 'Claude';
}
async toggleResponseViewer() {
@@ -11023,7 +11025,9 @@ class CodemanApp {
? 'Kill Tmux & OMP'
: session.mode === 'copilot'
? 'Kill Tmux & GitHub Copilot'
: 'Kill Tmux & Claude Code';
: session.mode === 'jcode'
? 'Kill Tmux & jcode'
: 'Kill Tmux & Claude Code';
}
document.getElementById('closeConfirmModal').classList.add('active');
+1
View File
@@ -276,6 +276,7 @@
'Run DeepSeek': '运行 DeepSeek',
'Run OMP': '运行 OMP',
'Run GitHub Copilot': '运行 GitHub Copilot',
'Run jcode': '运行 jcode',
'Run Shell': '运行 Shell',
'More tools': '更多工具',
'Select AI backend': '选择 AI 后端',
+3 -1
View File
@@ -1177,6 +1177,7 @@
<option value="deepseek">DeepSeek</option>
<option value="omp">OMP</option>
<option value="copilot">GitHub Copilot</option>
<option value="jcode">jcode</option>
</select>
</div>
<div class="form-row"><label>Working Directory</label><input type="text" id="schWorkingDir" placeholder="/absolute/path"></div>
@@ -3913,6 +3914,7 @@
<option value="deepseek" data-cli="deepseek">DeepSeek</option>
<option value="omp" data-cli="omp">OMP</option>
<option value="copilot" data-cli="copilot">GitHub Copilot</option>
<option value="jcode" data-cli="jcode">jcode</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>
@@ -4091,7 +4093,7 @@
<div class="form-row docker-create-only">
<label>Image</label>
<input type="text" id="dockerImage" placeholder="codeman/agent:base" autocomplete="off" autocapitalize="off" spellcheck="false">
<span class="form-hint">Build it once with <code>node scripts/build-agent-image.mjs</code>. Contains node + claude/opencode/codex/gemini/agy/pi/grok/dsh/omp/copilot + tmux.</span>
<span class="form-hint">Build it once with <code>node scripts/build-agent-image.mjs</code>. Contains node + claude/opencode/codex/gemini/agy/pi/grok/dsh/omp/copilot/jcode + tmux.</span>
</div>
<div class="form-row docker-create-only">
<label>Network</label>
+1
View File
@@ -59,6 +59,7 @@ const MOBILE_OVERVIEW_RUN_MODES = [
{ mode: 'deepseek', label: 'DeepSeek', short: 'DeepSeek' },
{ mode: 'omp', label: 'OMP', short: 'OMP' },
{ mode: 'copilot', label: 'GitHub Copilot', short: 'GitHub Copilot' },
{ mode: 'jcode', label: 'jcode', short: 'jcode' },
{ mode: 'shell', label: 'Terminal / Shell', short: 'Shell' },
];
+20
View File
@@ -1279,6 +1279,20 @@ html.mobile-init .file-browser-panel {
border-color: rgba(163, 113, 247, 0.5) !important;
}
/* jcode mode colors on mobile. Same `!important` rationale as the pi/grok/deepseek/omp blocks above. */
.btn-toolbar.btn-run.mode-jcode,
.btn-toolbar.btn-run-gear.mode-jcode {
background: #78350f !important;
border-color: rgba(245, 158, 11, 0.3) !important;
color: #fffbeb !important;
}
.btn-toolbar.btn-run.mode-jcode:active,
.btn-toolbar.btn-run-gear.mode-jcode:active {
background: #b45309 !important;
border-color: rgba(245, 158, 11, 0.5) !important;
}
/* Run mode dropdown menu — positioned above toolbar on mobile */
.run-mode-menu {
bottom: 100%;
@@ -3442,6 +3456,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-jcode, .btn-toolbar.btn-run-gear.mode-jcode) {
background: linear-gradient(135deg, #92400e, #d97706);
border-color: #78350f;
color: #ffffff;
}
html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) .btn-toolbar.btn-run-gear {
border-left-color: var(--control-border-hover) !important;
}
+1 -1
View File
@@ -559,7 +559,7 @@ Object.assign(CodemanApp.prototype, {
const caseName = this._commandPaletteCaseName(query);
const agent = this._commandPaletteAgent(caseName);
const mode = agent?.id || this.runMode || this._runMode || 'claude';
const labels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', gemini: 'Gemini', antigravity: 'Antigravity', pi: 'Pi', grok: 'Grok', deepseek: 'DeepSeek', omp: 'OMP', copilot: 'Copilot' };
const labels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', gemini: 'Gemini', antigravity: 'Antigravity', pi: 'Pi', grok: 'Grok', deepseek: 'DeepSeek', omp: 'OMP', copilot: 'Copilot', jcode: 'jcode' };
return {
id: 'new-session',
type: 'new-session',
+13
View File
@@ -89,6 +89,15 @@ const RUN_MODE_LAUNCH = {
// The multi-user clamp forces it back off for non-granted owners server-side.
buildConfig: () => ({ copilotConfig: { allowAll: true } }),
},
jcode: {
label: 'jcode',
installHint: 'jcode not found. Install with: curl -fsSL https://jcode.sh/install | bash',
// No custom endpoint recipe yet: jcode's provider lives in its shared daemon, not the pane.
supportsCustomModel: false,
// Deliberately no jcodeConfig: jcode has no approval prompts, so there is no bypass
// switch to opt into (same as pi).
buildConfig: () => null,
},
grok: {
label: 'Grok',
installHint: 'Grok CLI not found. Install with: curl -fsSL https://x.ai/cli/install.sh | bash',
@@ -2717,6 +2726,10 @@ Object.assign(CodemanApp.prototype, {
return this._runCliMode('copilot');
},
async runJcode() {
return this._runCliMode('jcode');
},
// ═══════════════════════════════════════════════════════════════
// Session Options Modal
+30 -1
View File
@@ -6023,6 +6023,24 @@ html[data-skin="og"] .welcome-primary:hover {
color: #f8faff;
}
/* jcode mode colors. Same cascade note as pi/grok/deepseek above: this base-sheet
pair only renders on the `og` skin, so jcode also carries a rule inside the nested
`html:not([data-skin="og"])` block (search `.btn-toolbar.btn-run.mode-jcode`). */
.btn-toolbar.btn-run.mode-jcode,
.btn-toolbar.btn-run-gear.mode-jcode {
background: linear-gradient(135deg, #451a03 0%, #92400e 55%, #b45309 100%);
border-color: rgba(245, 158, 11, 0.5);
color: #fffbeb;
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-jcode:hover,
.btn-toolbar.btn-run-gear.mode-jcode:hover {
background: linear-gradient(135deg, #5c2405 0%, #b45309 55%, #d97706 100%);
box-shadow: 0 0 12px rgba(245, 158, 11, 0.35), 0 2px 8px rgba(180, 83, 9, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.08);
border-color: rgba(252, 211, 77, 0.6);
color: #ffffff;
}
/* Dropdown menu */
.run-mode-menu {
display: none;
@@ -6134,7 +6152,8 @@ html[data-skin="og"] .welcome-primary:hover {
~/.codeman/clis.json, say) keeps the plain 8px dot.
Logos: @lobehub/icons-static-svg 1.95.1 (MIT, (c) LobeHub); the OMP mark is
the one omp.sh draws in its own header (oh-my-pi, MIT). The marks are their
owners' trademarks, shown only to name the CLI a row launches. */
owners' trademarks, shown only to name the CLI a row launches. jcode's own mark
is a 60 KB halftone torus, so its slot draws a plain ring in its place. */
.run-mode-dot.claude,
.run-mode-dot.gemini,
.run-mode-dot.antigravity,
@@ -6145,6 +6164,7 @@ html[data-skin="og"] .welcome-primary:hover {
.run-mode-dot.pi,
.run-mode-dot.grok,
.run-mode-dot.copilot,
.run-mode-dot.jcode,
.run-mode-dot.shell,
.run-mode-dot.web {
width: 1.15em;
@@ -6166,6 +6186,7 @@ html[data-skin="og"] .welcome-primary:hover {
.run-mode-dot.pi,
.run-mode-dot.grok,
.run-mode-dot.copilot,
.run-mode-dot.jcode,
.run-mode-dot.shell,
.run-mode-dot.web {
background-color: currentColor;
@@ -6182,6 +6203,7 @@ html[data-skin="og"] .welcome-primary:hover {
.run-mode-dot.pi { --run-mode-logo: url("data:image/svg+xml,%3Csvg fill-rule='evenodd' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath clip-rule='evenodd' d='M1 1h16.5v11H12v5.5H6.5V23H1V1zm5.5 5.5V12H12V6.5H6.5z'/%3E%3Cpath d='M17.5 12H23v11h-5.5V12z'/%3E%3C/svg%3E"); }
.run-mode-dot.grok { --run-mode-logo: url("data:image/svg+xml,%3Csvg fill-rule='evenodd' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815'/%3E%3C/svg%3E"); }
.run-mode-dot.copilot { --run-mode-logo: url("data:image/svg+xml,%3Csvg fill-rule='evenodd' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 3C7 3 3 6.5 3 11v4a3 3 0 003 3h12a3 3 0 003-3v-4c0-4.5-4-8-9-8zM8.5 11.5a1.5 2 0 110 4 1.5 2 0 010-4zm7 0a1.5 2 0 110 4 1.5 2 0 010-4z'/%3E%3C/svg%3E"); }
.run-mode-dot.jcode { --run-mode-logo: url("data:image/svg+xml,%3Csvg fill-rule='evenodd' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 2a10 10 0 110 20 10 10 0 010-20zm0 6a4 4 0 100 8 4 4 0 000-8z'/%3E%3C/svg%3E"); }
.run-mode-dot.shell { --run-mode-logo: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='3.5' width='19' height='17' rx='3'/%3E%3Cpath d='M7 9.5l3 2.5-3 2.5M12.5 15h4.5'/%3E%3C/svg%3E"); }
.run-mode-dot.web { --run-mode-logo: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20'/%3E%3C/svg%3E"); }
@@ -17207,6 +17229,13 @@ html:not([data-skin="og"]) {
color: #f8faff;
}
.btn-toolbar.btn-run.mode-deepseek:hover { box-shadow: 0 0 14px -2px rgba(77, 107, 254, 0.55); }
/* jcode keeps its amber on the non-og skins, same specificity trap as above. */
.btn-toolbar.btn-run.mode-jcode {
background: linear-gradient(135deg, #92400e, #d97706);
border-color: #78350f;
color: #fffbeb;
}
.btn-toolbar.btn-run.mode-jcode:hover { box-shadow: 0 0 14px -2px rgba(245, 158, 11, 0.5); }
.btn-toolbar.btn-run-gear {
background: var(--accent-d);
border-color: var(--accent);
+26 -2
View File
@@ -1483,6 +1483,8 @@ export function registerSessionRoutes(
piConfig: mode === 'pi' ? gatedPiConfig : undefined,
grokConfig: mode === 'grok' ? gatedGrokConfig : undefined,
copilotConfig: mode === 'copilot' ? gatedCopilotConfig : undefined,
// No bypass switch to clamp (jcode has no approval prompts), so the body's own config.
jcodeConfig: mode === 'jcode' ? launchBody.jcodeConfig : undefined,
deepSeekConfig: mode === 'deepseek' ? gatedDeepSeekConfig : undefined,
ompConfig: resolveOmpConfigForCreate(mode, workingDir, launchBody.ompConfig),
resumeSessionId: validatedResumeId,
@@ -3247,6 +3249,21 @@ export function registerSessionRoutes(
};
}
// An external CLI with no transcript reader that the pane segmenter cannot read either
// (jcode: a repainting alternate-screen TUI, whose segmented stream served its welcome
// banner as "the answer", measured) has nothing honest to return. Empty, "nothing said
// yet", like the DeepSeek reader above: the Claude scan below would otherwise go looking
// in ~/.claude/projects and could adopt a Claude conversation from the same folder.
// `codeman agent read --source terminal` / MCP `read_output` still read the screen.
{
const caps = getCli(session.mode)?.capabilities;
if (caps?.external && caps.transcript === 'none') {
return context === 'full' || context === 'turn'
? { text: '', timestamp: '', messages: [] }
: { text: '', timestamp: '' };
}
}
// Scan ~/.claude/projects/*/ for the transcript file
const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
@@ -4269,6 +4286,7 @@ export function registerSessionRoutes(
piConfig,
grokConfig,
copilotConfig,
jcodeConfig,
deepSeekConfig,
ompConfig,
envOverrides,
@@ -4341,6 +4359,7 @@ export function registerSessionRoutes(
piConfig ||
grokConfig ||
copilotConfig ||
jcodeConfig ||
deepSeekConfig ||
ompConfig ||
openCodeConfig ||
@@ -4412,6 +4431,7 @@ export function registerSessionRoutes(
piConfig ||
grokConfig ||
copilotConfig ||
jcodeConfig ||
deepSeekConfig ||
ompConfig ||
openCodeConfig ||
@@ -4513,6 +4533,7 @@ export function registerSessionRoutes(
piConfig,
grokConfig,
copilotConfig,
jcodeConfig,
deepSeekConfig,
} as unknown as Record<string, unknown>)
);
@@ -4600,7 +4621,7 @@ export function registerSessionRoutes(
writeFileSync(join(resolvedCasePath, 'CLAUDE.md'), claudeMd);
// Write .claude/settings.local.json with hooks for desktop notifications
// (Claude-specific — OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek and OMP use their own systems)
// (Claude-specific — OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP and jcode use their own systems)
if (
mode !== 'opencode' &&
mode !== 'codex' &&
@@ -4608,7 +4629,8 @@ export function registerSessionRoutes(
mode !== 'antigravity' &&
mode !== 'pi' &&
mode !== 'grok' &&
mode !== 'omp'
mode !== 'omp' &&
mode !== 'jcode'
) {
await writeHooksConfig(resolvedCasePath);
}
@@ -4723,6 +4745,7 @@ export function registerSessionRoutes(
piConfig,
grokConfig,
copilotConfig,
jcodeConfig,
deepSeekConfig,
ompConfig,
};
@@ -4942,6 +4965,7 @@ export function registerSessionRoutes(
piConfig: mode === 'pi' ? qsGatedPiConfig : undefined,
grokConfig: mode === 'grok' ? qsGatedGrokConfig : undefined,
copilotConfig: mode === 'copilot' ? qsGatedCopilotConfig : undefined,
jcodeConfig: mode === 'jcode' ? qsLaunchConfigs.jcodeConfig : undefined,
deepSeekConfig: mode === 'deepseek' ? qsGatedDeepSeekConfig : undefined,
ompConfig: qsResolvedOmpConfig,
envOverrides: qsGatedEnvOverrides,
+14
View File
@@ -587,6 +587,20 @@ export function registerSystemRoutes(
};
});
// ========== jcode ==========
// The sibling shape (path + version), answered by the GENERIC registry resolver rather than a
// module of its own: it is what a jcode spawn resolves the binary with, and it already applies
// the entry's `jcode v<semver>` version anchor (npm carries an unrelated `jcode` package).
app.get('/api/jcode/status', async () => {
const { isCliAvailable, resolveCliBinDir, resolveCliVersion } = await import('../../utils/cli-resolver.js');
return {
available: isCliAvailable('jcode'),
path: resolveCliBinDir('jcode'),
version: resolveCliVersion('jcode'),
};
});
// ========== DeepSeek Harness ==========
// The widest of the per-CLI status shapes, because this mode has the widest
+27
View File
@@ -493,6 +493,30 @@ const CopilotConfigSchema = z
})
.optional();
/**
* Schema for jcode (1jehuang/jcode)-specific configuration.
*
* jcode has no approval prompts and no bypass flag, so there is nothing for the
* multi-user clamp to touch.
*/
const JcodeConfigSchema = z
.object({
model: z
.string()
.max(100)
.regex(/^[a-zA-Z0-9._\-/]+$/)
.optional(),
// A session id (`session_fox_<ms>_<hex>`) or short name (`fox`). Leading
// alphanumeric: --resume's value is optional, so a value starting with `-`
// would be parsed as a flag of its own.
resumeSessionId: z
.string()
.max(100)
.regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/)
.optional(),
})
.optional();
/**
* Schema for OMP CLI-specific configuration.
*/
@@ -651,6 +675,7 @@ export const CreateSessionSchema = z.object({
piConfig: PiConfigSchema,
grokConfig: GrokConfigSchema,
copilotConfig: CopilotConfigSchema,
jcodeConfig: JcodeConfigSchema,
deepSeekConfig: DeepSeekConfigSchema,
ompConfig: OmpConfigSchema,
/** Resume a previous Claude conversation by its session ID (used for reboot recovery) */
@@ -871,6 +896,7 @@ const RemoteCommandOverridesSchema = z
deepseek: z.string().min(1).max(300).optional(),
omp: z.string().min(1).max(300).optional(),
copilot: z.string().min(1).max(300).optional(),
jcode: z.string().min(1).max(300).optional(),
})
.strict()
.optional();
@@ -1260,6 +1286,7 @@ export const QuickStartSchema = z.object({
piConfig: PiConfigSchema,
grokConfig: GrokConfigSchema,
copilotConfig: CopilotConfigSchema,
jcodeConfig: JcodeConfigSchema,
deepSeekConfig: DeepSeekConfigSchema,
ompConfig: OmpConfigSchema,
envOverrides: safeEnvOverridesSchema,
+1
View File
@@ -4171,6 +4171,7 @@ export class WebServer extends EventEmitter {
piConfig: muxSession.mode === 'pi' ? savedState?.piConfig : undefined,
grokConfig: muxSession.mode === 'grok' ? savedState?.grokConfig : undefined,
copilotConfig: muxSession.mode === 'copilot' ? savedState?.copilotConfig : undefined,
jcodeConfig: muxSession.mode === 'jcode' ? savedState?.jcodeConfig : undefined,
deepSeekConfig: muxSession.mode === 'deepseek' ? savedState?.deepSeekConfig : undefined,
ompConfig: muxSession.mode === 'omp' ? savedState?.ompConfig : undefined,
envOverrides: savedEnvOverrides,