mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
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:
commit
d45b1ff248
66 files changed
+1304
-127
No files matched your search
@@ -314,6 +314,8 @@ const capabilitiesSchema = z
|
||||
// Keep the PTY's rows while a phone keyboard is up (inline renderers only).
|
||||
// Optional: absent means the keyboard resizes, the safe default.
|
||||
keyboardHold: z.literal('inline').optional(),
|
||||
// Spare the CLI's own detached daemon (another process group) when a pane is killed.
|
||||
sharedDaemon: z.boolean().optional(),
|
||||
keyboardAccessory: z.enum(['agent', 'shell']),
|
||||
privilegedCommandGate: z.boolean(),
|
||||
startMode: z.enum(['interactive', 'shell']),
|
||||
|
||||
@@ -2039,6 +2039,142 @@ const COPILOT: CliEntry = {
|
||||
},
|
||||
};
|
||||
|
||||
// jcode (1jehuang/jcode, a Rust coding-agent TUI). Measured on a live v0.98.1 pane inside tmux
|
||||
// (capture-pane every 200 ms through real turns, a recorded tmux client stream, PageUp/PageDown
|
||||
// and SGR wheel reports, a bracketed-paste prompt; 2026-10-16). Three things about it shape
|
||||
// this entry and are worth knowing before changing it:
|
||||
//
|
||||
// 1. Every jcode TUI is a CLIENT of one shared per-user daemon (`jcode serve`, started by the
|
||||
// first client in its own session, so it outlives the pane that started it). The agent and
|
||||
// its tools run in the DAEMON, with the daemon's environment: a second client's env never
|
||||
// reaches its bash tool (measured, `probe=[]`). A session's `CODEMAN_SESSION_ID` and
|
||||
// `CODEMAN_API_URL` are therefore whatever the session that started the daemon had, or
|
||||
// nothing. That is why the agent skill is not injected (below) and why `JCODE_*` is not an
|
||||
// `envOverrides` prefix: most of that namespace is read by the daemon, so a per-session
|
||||
// value would do nothing, and where it did act (the session that starts the daemon) it
|
||||
// would reach every jcode session, hook and command-path variables included.
|
||||
// 2. It has no approval prompts and no bypass flag: bash runs ungated apart from jcode's own
|
||||
// model-side reflection gate and a catastrophic-path deny. Nothing to clamp, nothing to
|
||||
// read as blocked, same shape as pi.
|
||||
// 3. A fresh install opens an onboarding wizard instead of the composer until its launch
|
||||
// counter passes 5 (or 10 sessions exist). A prompt typed into it is lost, so a new user
|
||||
// should run `jcode` once in a terminal (or press Esc in the pane) before driving it.
|
||||
const JCODE: CliEntry = {
|
||||
id: 'jcode' as CliEntry['id'],
|
||||
label: 'jcode',
|
||||
shortBadge: 'JC',
|
||||
// No brand colour to borrow (the mark is a monochrome halftone donut, and its TUI's own blue
|
||||
// is gemini's #8ab4f8), so an amber no other entry uses.
|
||||
accent: '#f59e0b',
|
||||
enabled: true,
|
||||
stock: true,
|
||||
order: 110,
|
||||
kind: 'agent',
|
||||
discovery: {
|
||||
binaries: ['jcode'],
|
||||
// The official installer links `~/.local/bin/jcode` (its `JCODE_INSTALL_DIR` default) to a
|
||||
// launcher script under `~/.jcode/builds/`; the agent image puts it in `/usr/local/bin`.
|
||||
searchDirs: [HOME_DIRS.local, HOME_DIRS.usrLocal, HOME_DIRS.homeBin],
|
||||
// `jcode --version` prints `jcode v0.98.1 (1c7426eb1)`. npm carries an unrelated `jcode`
|
||||
// package, so the probe is anchored on the `jcode v` prefix and a mismatch counts as absent.
|
||||
version: { arg: '--version', regex: '(?:^|\\s)jcode v(\\d+\\.\\d+\\.\\d+)', requireVersionMatch: true },
|
||||
install: {
|
||||
command: {
|
||||
linux: 'curl -fsSL https://jcode.sh/install | bash',
|
||||
darwin: 'curl -fsSL https://jcode.sh/install | bash',
|
||||
},
|
||||
docsUrl: 'https://jcode.sh/docs',
|
||||
agentImageLayer: {
|
||||
kind: 'dedicated',
|
||||
reason:
|
||||
'Not on npm: jcode ships a standalone Rust binary through jcode.sh/install, whose launcher is a script that execs the binary beside it.',
|
||||
},
|
||||
},
|
||||
},
|
||||
launch: {
|
||||
params: {
|
||||
model: { type: 'token', pattern: 'model' },
|
||||
// jcode ids look like `session_fox_1792160401272_f5b47ce47d631006` and `--resume` also
|
||||
// takes the short name (`fox`). Its value is OPTIONAL (bare `--resume` opens the session
|
||||
// picker), so the token must not be able to start with `-`, or it would be read as a
|
||||
// flag of its own: `path-segment` requires a leading alphanumeric.
|
||||
resumeId: { type: 'token', pattern: 'path-segment' },
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
id: 'default',
|
||||
args: [
|
||||
{ lit: 'jcode' },
|
||||
{ flag: '--model', valueFrom: 'model', when: { param: 'model', state: 'set' } },
|
||||
{ flag: '--resume', valueFrom: 'resumeId', when: { param: 'resumeId', state: 'set' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
legacyConfigAliases: { resumeId: 'resumeSessionId' },
|
||||
legacyConfigField: 'jcodeConfig',
|
||||
resumeAppend: { style: 'flag', flag: '--resume' },
|
||||
},
|
||||
env: {
|
||||
exports: [{ name: 'COLORTERM', value: 'truecolor' }],
|
||||
unset: ['NO_COLOR'],
|
||||
// Provider logins live in jcode's own files (`~/.jcode/auth.json`, `~/.config/jcode/*.env`)
|
||||
// and are read by the daemon, so there is nothing for the server to forward into the pane.
|
||||
tmuxSetenvKeys: [],
|
||||
dockerExecEnvNames: [],
|
||||
// Deliberately none: see note 1 above the entry.
|
||||
allowedPrefixes: [],
|
||||
allowedKeys: [],
|
||||
},
|
||||
capabilities: {
|
||||
...agentDefaults(),
|
||||
// A full-screen alternate-screen TUI that turns mouse tracking on itself (tmux
|
||||
// `alternate_on=1`, `mouse_any_flag=1`, `mouse_sgr_flag=1`), the opencode shape: kept, a
|
||||
// drag would be reported to jcode instead of selecting text, so Auto Copy and copy-on-select
|
||||
// would silently do nothing.
|
||||
altScreen: 'strip-mux-and-mouse',
|
||||
// Pages its own transcript on PageUp/PageDown (measured: PageUp brought the welcome header
|
||||
// back, PageDown returned to the composer). Over the hollow alt-screen buffer the wheel and a
|
||||
// swipe become those keys instead of doing nothing.
|
||||
transcriptPageKeys: true,
|
||||
// Closing a jcode tab must not take the shared daemon (note 1) down with it: it is a
|
||||
// descendant of whichever pane started it, in a process group of its own (setsid).
|
||||
// Measured before this flag: deleting that tab left the next tab's client looping on
|
||||
// `Connection lost - retrying` with its prompt queued.
|
||||
sharedDaemon: true,
|
||||
echo: { policy: 'buffer', anchor: { kind: 'cursor' } },
|
||||
// At rest the composer row is `<n>> ` (the prompt number, then `>`); while a turn runs the
|
||||
// same row reads `<n>…` and a status row appears directly above it, a braille spinner (the
|
||||
// gemini/omp frame set, ~12 fps) then the phase and the elapsed time:
|
||||
// `⠋ sending… 0s`, `⠦ thinking… 2s · https`, `⠼ waiting for response… 3s · https`,
|
||||
// `⠸ running bash · <detail> · 12s · Alt+B bg`, or, while streaming, the time first
|
||||
// (`⠼ 12s · 45.1 tps · ↑16k ↓20`). Past a minute the time reads `1m 3s`, past an hour
|
||||
// `1h 2m`. A dropped connection swaps the spinner for `↻ network disconnected, waiting to
|
||||
// retry · … · 5s`, still mid-turn. At rest that row is empty or a dim tip, never a spinner.
|
||||
// The spinner repaints the pane at least every 100 ms for the whole turn (recorded tmux
|
||||
// client stream), so the pane is never quiet mid-turn, and at rest it sends nothing at all
|
||||
// (12 s recorded: zero bytes), so the idle check settles.
|
||||
// The glyph is the bare `>`: the turn-ending repaint redraws only the cells that changed,
|
||||
// which on that row is the `…` turning into `>` (recorded: `\x1b[43;2H...> `), never the
|
||||
// prompt number. It only ARMS the idle check. It also leaves the submit verifier inert,
|
||||
// which is what jcode wants (no early-Enter defect): every composer row starts with its
|
||||
// number, so no screen row starts with `>` the way the verifier requires.
|
||||
workDetect: {
|
||||
promptGlyph: '>',
|
||||
workingLine: String.raw`(?:^|\n) *(?:[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] [^\n]*\d[smh]\b|↻ network disconnected)`,
|
||||
},
|
||||
// jcode reads `./.agents/skills` (and `./.jcode/skills`, `./.claude/skills`), but its tools
|
||||
// run in the shared daemon, whose CODEMAN_* identity belongs to whichever session started
|
||||
// it (note 1). An injected skill would act as THAT session, so it is not injected.
|
||||
agentSkillInjection: false,
|
||||
customModelInjection: { kind: 'unsupported' },
|
||||
},
|
||||
overlays: {
|
||||
// No credStore: Docker seeding is CRED_STORES in docker-hosts.ts, which has no `.jcode` row.
|
||||
// `~/.jcode` holds the OAuth tokens beside 160 MB of builds, sessions and SQLite caches, and
|
||||
// API keys live in `~/.config/jcode`; sign in inside the container instead.
|
||||
},
|
||||
};
|
||||
|
||||
/** The full stock catalog, in the order the run menu shows by default. */
|
||||
export const STOCK_CLIS: CliEntry[] = [
|
||||
CLAUDE,
|
||||
@@ -2052,4 +2188,5 @@ export const STOCK_CLIS: CliEntry[] = [
|
||||
DEEPSEEK,
|
||||
OMP,
|
||||
COPILOT,
|
||||
JCODE,
|
||||
];
|
||||
@@ -640,6 +640,19 @@ export interface CliCapabilities {
|
||||
* losing lines, so it resizes as before), and for remote and docker sessions.
|
||||
*/
|
||||
keyboardHold?: 'inline';
|
||||
/**
|
||||
* The CLI's TUI is a CLIENT of a per-user daemon that its first client starts and every
|
||||
* later session of the CLI shares (jcode). The daemon detaches into its own session and
|
||||
* process group, but stays a descendant of that first pane by parent pid, so a session
|
||||
* kill signals only the descendants still in the pane's process group
|
||||
* (`descendantsInPaneGroup()`, proc-tree.ts). Without it, closing the tab that happened
|
||||
* to start the daemon took it down and cut every other session of the CLI off.
|
||||
*
|
||||
* The flip side is that processes the agent started through that daemon (its tools run
|
||||
* there, not in the pane) outlive the tab; stopping them is the CLI's own business
|
||||
* (`jcode server stop`). Absent = every descendant is killed, as for every other CLI.
|
||||
*/
|
||||
sharedDaemon?: boolean;
|
||||
keyboardAccessory: 'agent' | 'shell';
|
||||
/** Multi-user: this CLI is a raw shell, so its commands need the privileged gate. */
|
||||
privilegedCommandGate: boolean;
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
PiConfig,
|
||||
GrokConfig,
|
||||
CopilotConfig,
|
||||
JcodeConfig,
|
||||
DeepSeekConfig,
|
||||
OmpConfig,
|
||||
SessionRemote,
|
||||
@@ -127,6 +128,7 @@ export interface CreateSessionOptions {
|
||||
piConfig?: PiConfig;
|
||||
grokConfig?: GrokConfig;
|
||||
copilotConfig?: CopilotConfig;
|
||||
jcodeConfig?: JcodeConfig;
|
||||
deepSeekConfig?: DeepSeekConfig;
|
||||
ompConfig?: OmpConfig;
|
||||
/** When restoring after reboot, resume a previous Claude conversation by its session ID */
|
||||
@@ -179,6 +181,7 @@ export interface RespawnPaneOptions {
|
||||
piConfig?: PiConfig;
|
||||
grokConfig?: GrokConfig;
|
||||
copilotConfig?: CopilotConfig;
|
||||
jcodeConfig?: JcodeConfig;
|
||||
deepSeekConfig?: DeepSeekConfig;
|
||||
ompConfig?: OmpConfig;
|
||||
/** Resume a previous Claude conversation when respawning */
|
||||
|
||||
@@ -85,3 +85,36 @@ export function collectDescendants(
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The descendants a session kill may signal when the pane's CLI runs a SHARED daemon
|
||||
* (`capabilities.sharedDaemon`): only those still in the pane's own process group.
|
||||
*
|
||||
* Such a CLI's first client starts a per-user daemon that every other session of that
|
||||
* CLI talks to, and the daemon detaches itself into its own session (setsid), which also
|
||||
* makes it the leader of a new process group. It stays a descendant of that first pane
|
||||
* by parent pid, though, so the plain descendant kill took it down with the pane and cut
|
||||
* every other session off (measured with jcode: deleting the tab that happened to start
|
||||
* the daemon left the next tab's client reconnecting forever). A process that left the
|
||||
* pane's group did so on purpose, and its whole subtree inherits the new group, so the
|
||||
* group is what draws the line.
|
||||
*
|
||||
* A pid whose group could not be read is KEPT in the kill list: it most likely exited
|
||||
* between the walk and the lookup, and the kill path already checks liveness before it
|
||||
* signals. Callers that could not read the groups at all keep the full list, which is
|
||||
* the behaviour before this existed.
|
||||
*
|
||||
* @param descendants the walk's result for the pane pid
|
||||
* @param groupOf pgid per pid, as read once by the caller
|
||||
* @param paneGroup the pane process's own pgid
|
||||
*/
|
||||
export function descendantsInPaneGroup(
|
||||
descendants: readonly number[],
|
||||
groupOf: ReadonlyMap<number, number>,
|
||||
paneGroup: number
|
||||
): number[] {
|
||||
return descendants.filter((pid) => {
|
||||
const group = groupOf.get(pid);
|
||||
return group === undefined || group === paneGroup;
|
||||
});
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import type {
|
||||
GeminiConfig,
|
||||
GrokConfig,
|
||||
CopilotConfig,
|
||||
JcodeConfig,
|
||||
OmpConfig,
|
||||
OpenCodeConfig,
|
||||
PiConfig,
|
||||
@@ -53,6 +54,7 @@ export interface SpawnBridgeOptions {
|
||||
piConfig?: PiConfig;
|
||||
grokConfig?: GrokConfig;
|
||||
copilotConfig?: CopilotConfig;
|
||||
jcodeConfig?: JcodeConfig;
|
||||
deepSeekConfig?: DeepSeekConfig;
|
||||
ompConfig?: OmpConfig;
|
||||
resumeSessionId?: string;
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
type PiConfig,
|
||||
type GrokConfig,
|
||||
type CopilotConfig,
|
||||
type JcodeConfig,
|
||||
type DeepSeekConfig,
|
||||
type OmpConfig,
|
||||
type SessionRemote,
|
||||
@@ -865,6 +866,8 @@ export class Session extends EventEmitter {
|
||||
private _grokConfig: GrokConfig | undefined;
|
||||
// GitHub Copilot configuration (only for mode === 'copilot')
|
||||
private _copilotConfig: CopilotConfig | undefined;
|
||||
// jcode configuration (only for mode === 'jcode')
|
||||
private _jcodeConfig: JcodeConfig | undefined;
|
||||
|
||||
// DeepSeek Harness configuration (only for mode === 'deepseek')
|
||||
private _deepSeekConfig: DeepSeekConfig | undefined;
|
||||
@@ -1004,6 +1007,8 @@ export class Session extends EventEmitter {
|
||||
/** Grok configuration (only for mode === 'grok') */
|
||||
grokConfig?: GrokConfig;
|
||||
copilotConfig?: CopilotConfig;
|
||||
/** jcode configuration (only for mode === 'jcode') */
|
||||
jcodeConfig?: JcodeConfig;
|
||||
/** DeepSeek Harness configuration (only for mode === 'deepseek') */
|
||||
deepSeekConfig?: DeepSeekConfig;
|
||||
/** OMP configuration (only for mode === 'omp') */
|
||||
@@ -1198,6 +1203,11 @@ export class Session extends EventEmitter {
|
||||
this._copilotConfig = config.copilotConfig;
|
||||
}
|
||||
|
||||
// Apply jcode configuration
|
||||
if (config.jcodeConfig) {
|
||||
this._jcodeConfig = config.jcodeConfig;
|
||||
}
|
||||
|
||||
// Apply env overrides (exported at spawn, not persisted to disk).
|
||||
// Legacy migration: pre-0.7.2 carried effort as the CLAUDE_CODE_EFFORT_LEVEL env var,
|
||||
// which hard-locks /effort switching. Extract it into _effort (--settings soft default)
|
||||
@@ -2527,6 +2537,7 @@ export class Session extends EventEmitter {
|
||||
piConfig: this._piConfig,
|
||||
grokConfig: this._grokConfig,
|
||||
copilotConfig: this._copilotConfig,
|
||||
jcodeConfig: this._jcodeConfig,
|
||||
deepSeekConfig: this._deepSeekConfig,
|
||||
ompConfig: this._ompConfig,
|
||||
resumeSessionId: this._resumeSessionId,
|
||||
@@ -2908,6 +2919,7 @@ export class Session extends EventEmitter {
|
||||
piConfig: this._piConfig,
|
||||
grokConfig: this._grokConfig,
|
||||
copilotConfig: this._copilotConfig,
|
||||
jcodeConfig: this._jcodeConfig,
|
||||
deepSeekConfig: this._deepSeekConfig,
|
||||
// OMP resolution/pinning does NOT happen here. This object is built
|
||||
// EAGERLY — including on every boot-recovery reattach, before anyone
|
||||
@@ -3426,6 +3438,7 @@ export class Session extends EventEmitter {
|
||||
piConfig: this._piConfig,
|
||||
grokConfig: this._grokConfig,
|
||||
copilotConfig: this._copilotConfig,
|
||||
jcodeConfig: this._jcodeConfig,
|
||||
deepSeekConfig: this._deepSeekConfig,
|
||||
ompConfig: this._ompConfig,
|
||||
resumeSessionId: this._resumeSessionId,
|
||||
@@ -4009,6 +4022,7 @@ export class Session extends EventEmitter {
|
||||
piConfig: this._piConfig,
|
||||
grokConfig: this._grokConfig,
|
||||
copilotConfig: this._copilotConfig,
|
||||
jcodeConfig: this._jcodeConfig,
|
||||
deepSeekConfig: this._deepSeekConfig,
|
||||
ompConfig: this._ompConfig,
|
||||
};
|
||||
|
||||
+50
-4
@@ -22,7 +22,7 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { collectDescendants } from './proc-tree.js';
|
||||
import { collectDescendants, descendantsInPaneGroup } from './proc-tree.js';
|
||||
import {
|
||||
foreignViewSessionName,
|
||||
buildForeignDockerViewKillCommand,
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
type PiConfig,
|
||||
type GrokConfig,
|
||||
type CopilotConfig,
|
||||
type JcodeConfig,
|
||||
type DeepSeekConfig,
|
||||
type OmpConfig,
|
||||
type SessionRemote,
|
||||
@@ -1213,6 +1214,7 @@ export function buildSpawnCommand(options: {
|
||||
piConfig?: PiConfig;
|
||||
grokConfig?: GrokConfig;
|
||||
copilotConfig?: CopilotConfig;
|
||||
jcodeConfig?: JcodeConfig;
|
||||
deepSeekConfig?: DeepSeekConfig;
|
||||
ompConfig?: OmpConfig;
|
||||
resumeSessionId?: string;
|
||||
@@ -2563,6 +2565,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
piConfig,
|
||||
grokConfig,
|
||||
copilotConfig,
|
||||
jcodeConfig,
|
||||
deepSeekConfig,
|
||||
ompConfig,
|
||||
resumeSessionId,
|
||||
@@ -2662,6 +2665,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
piConfig,
|
||||
grokConfig,
|
||||
copilotConfig,
|
||||
jcodeConfig,
|
||||
deepSeekConfig,
|
||||
ompConfig,
|
||||
resumeSessionId,
|
||||
@@ -2931,6 +2935,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
piConfig,
|
||||
grokConfig,
|
||||
copilotConfig,
|
||||
jcodeConfig,
|
||||
deepSeekConfig,
|
||||
ompConfig,
|
||||
resumeSessionId,
|
||||
@@ -2982,6 +2987,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
piConfig,
|
||||
grokConfig,
|
||||
copilotConfig,
|
||||
jcodeConfig,
|
||||
deepSeekConfig,
|
||||
ompConfig,
|
||||
resumeSessionId,
|
||||
@@ -3160,6 +3166,45 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow a pane's descendants to the ones a kill may signal, for a CLI that runs a
|
||||
* shared daemon (`capabilities.sharedDaemon`): those still in the pane's process group
|
||||
* (see `descendantsInPaneGroup()`). One bounded `ps` for the pane and its descendants,
|
||||
* as an argv (no shell). Any other CLI, or a lookup that failed, gets the list back
|
||||
* unchanged, which is the kill as it always was.
|
||||
*/
|
||||
private async killableDescendants(mode: SessionMode, panePid: number, descendants: number[]): Promise<number[]> {
|
||||
if (descendants.length === 0 || !getCli(mode)?.capabilities.sharedDaemon) return descendants;
|
||||
try {
|
||||
const { stdout } = await new Promise<{ stdout: string }>((resolve, reject) => {
|
||||
execFile(
|
||||
'ps',
|
||||
['-o', 'pid=,pgid=', '-p', [panePid, ...descendants].join(',')],
|
||||
{ timeout: EXEC_TIMEOUT_MS },
|
||||
// `ps -p` exits non-zero when some pid is already gone; what it printed still counts.
|
||||
(err, out) => (out ? resolve({ stdout: out }) : reject(err ?? new Error('ps printed nothing')))
|
||||
);
|
||||
});
|
||||
const groupOf = new Map<number, number>();
|
||||
for (const line of stdout.split('\n')) {
|
||||
const [pid, pgid] = line.trim().split(/\s+/).map(Number);
|
||||
if (Number.isInteger(pid) && Number.isInteger(pgid)) groupOf.set(pid, pgid);
|
||||
}
|
||||
const paneGroup = groupOf.get(panePid);
|
||||
if (paneGroup === undefined) return descendants;
|
||||
const killable = descendantsInPaneGroup(descendants, groupOf, paneGroup);
|
||||
if (killable.length < descendants.length) {
|
||||
console.log(
|
||||
`[TmuxManager] Sparing ${descendants.length - killable.length} process(es) outside pane ${panePid}'s group (the CLI's shared daemon)`
|
||||
);
|
||||
}
|
||||
return killable;
|
||||
} catch (err) {
|
||||
console.warn(`[TmuxManager] Could not read process groups for pane ${panePid}; killing every descendant:`, err);
|
||||
return descendants;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a process is still alive. Signal decisions only: an unreaped zombie
|
||||
// counts here, so its process group still gets the SIGKILL below. The WAITS use
|
||||
// waitForProcessesExit(), which counts a zombie as exited.
|
||||
@@ -3308,8 +3353,9 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
const allPids: number[] = [currentPid];
|
||||
|
||||
// Strategy 1: Kill all child processes recursively
|
||||
let childPids = await this.getChildPidsFresh(currentPid);
|
||||
// Strategy 1: Kill all child processes recursively (but never a shared daemon the
|
||||
// pane's CLI started for every session of that CLI: see killableDescendants)
|
||||
let childPids = await this.killableDescendants(session.mode, currentPid, await this.getChildPidsFresh(currentPid));
|
||||
if (childPids.length > 0) {
|
||||
console.log(`[TmuxManager] Found ${childPids.length} child processes to kill`);
|
||||
allPids.push(...childPids);
|
||||
@@ -3328,7 +3374,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
// catch anything spawned since the first one.
|
||||
await waitForProcessesExit(childPids, { timeoutMs: TMUX_KILL_WAIT_MS });
|
||||
|
||||
childPids = await this.getChildPidsFresh(currentPid);
|
||||
childPids = await this.killableDescendants(session.mode, currentPid, await this.getChildPidsFresh(currentPid));
|
||||
for (const childPid of childPids) {
|
||||
if (this.isProcessAlive(childPid)) {
|
||||
try {
|
||||
|
||||
@@ -1062,6 +1062,7 @@ const MODE_ITEMS: ReadonlyArray<{ id: TuiRunMode; label: string; detail: string
|
||||
{ id: 'pi', label: 'pi', detail: 'pi.dev' },
|
||||
{ id: 'grok', label: 'grok', detail: 'xAI Grok Build' },
|
||||
{ id: 'deepseek', label: 'deepseek', detail: 'DeepSeek Harness (dsh)' },
|
||||
{ id: 'jcode', label: 'jcode', detail: 'jcode' },
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -155,7 +155,7 @@ export type TuiAnswerResult =
|
||||
|
||||
export interface TuiQuickStartOptions {
|
||||
caseName: string;
|
||||
mode?: 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek';
|
||||
mode?: 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'jcode';
|
||||
sessionName?: string;
|
||||
/** The tab this spawn came from, for the lineage lines (cosmetic, dropped if unresolvable). */
|
||||
parentSessionId?: string;
|
||||
|
||||
+48
-4
@@ -8,7 +8,7 @@
|
||||
* - SessionConfig — creation-time config (id, workingDir, createdAt)
|
||||
* - SessionOutput — captured stdout/stderr/exitCode
|
||||
* - SessionStatus — 'idle' | 'busy' | 'stopped' | 'error'
|
||||
* - SessionMode — 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'omp' | 'copilot' (which CLI backend)
|
||||
* - SessionMode — 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'omp' | 'copilot' | 'jcode' (which CLI backend)
|
||||
* - ClaudeMode — CLI permission mode ('dangerously-skip-permissions' | 'auto' | 'normal' | 'allowedTools')
|
||||
* - SessionColor — visual differentiation color
|
||||
* - OpenCodeConfig — OpenCode-specific settings (model, autoAllowTools, continueSession)
|
||||
@@ -19,6 +19,7 @@
|
||||
* - GrokConfig — Grok Build CLI (xAI `grok`) settings (model, alwaysApprove, resume/continue)
|
||||
* - CopilotConfig — GitHub Copilot CLI settings (model, allowAll, resume/continue)
|
||||
* - DeepSeekConfig — DeepSeek Harness (`dsh`) settings (profile, permissionMode, resume, status bridge)
|
||||
* - JcodeConfig: jcode settings (model, resume)
|
||||
*
|
||||
* Cross-domain relationships:
|
||||
* - SessionState.respawnConfig embeds RespawnConfig (respawn domain)
|
||||
@@ -60,7 +61,8 @@ export type SessionMode =
|
||||
| 'grok'
|
||||
| 'deepseek'
|
||||
| 'omp'
|
||||
| 'copilot';
|
||||
| 'copilot'
|
||||
| 'jcode';
|
||||
|
||||
/**
|
||||
* Who owns a session's name. `placeholder`: Codeman's own `w<n>-<case>` (or no
|
||||
@@ -82,7 +84,18 @@ export interface SessionWriteOptions {
|
||||
|
||||
export type RemoteCommandMode = Extract<
|
||||
SessionMode,
|
||||
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'omp' | 'copilot'
|
||||
| 'shell'
|
||||
| 'claude'
|
||||
| 'opencode'
|
||||
| 'codex'
|
||||
| 'gemini'
|
||||
| 'antigravity'
|
||||
| 'pi'
|
||||
| 'grok'
|
||||
| 'deepseek'
|
||||
| 'omp'
|
||||
| 'copilot'
|
||||
| 'jcode'
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -217,7 +230,18 @@ export interface RemoteSessionInfo {
|
||||
/** Which CLI backends a Docker case can run (same set as remote). */
|
||||
export type DockerCommandMode = Extract<
|
||||
SessionMode,
|
||||
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'omp' | 'copilot'
|
||||
| 'shell'
|
||||
| 'claude'
|
||||
| 'opencode'
|
||||
| 'codex'
|
||||
| 'gemini'
|
||||
| 'antigravity'
|
||||
| 'pi'
|
||||
| 'grok'
|
||||
| 'deepseek'
|
||||
| 'omp'
|
||||
| 'copilot'
|
||||
| 'jcode'
|
||||
>;
|
||||
|
||||
/** Container engine. Docker and Podman differ in the uid/userns + host-gateway alias. */
|
||||
@@ -631,6 +655,24 @@ export interface CopilotConfig {
|
||||
resumeSessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* jcode (1jehuang/jcode) session configuration.
|
||||
*
|
||||
* jcode has no approval prompts and no bypass flag, so there is nothing
|
||||
* privileged to configure; the provider and its login come from jcode's own
|
||||
* `~/.jcode/config.toml`. Verified against jcode 0.98.1.
|
||||
*/
|
||||
export interface JcodeConfig {
|
||||
/** Model ID passed via --model (e.g. "claude-opus-4-8", "gpt-5.5"). */
|
||||
model?: string;
|
||||
/**
|
||||
* Resume a jcode session (--resume): its id (`session_fox_<ms>_<hex>`) or its
|
||||
* short name (`fox`), as ONE plain word starting with a letter or digit, since
|
||||
* --resume's value is optional and a leading `-` would make it its own flag.
|
||||
*/
|
||||
resumeSessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DeepSeek Harness (`dsh`) session configuration.
|
||||
*
|
||||
@@ -1108,6 +1150,8 @@ export interface SessionState {
|
||||
deepSeekConfig?: DeepSeekConfig;
|
||||
/** OMP-specific configuration (only for mode === 'omp') */
|
||||
ompConfig?: OmpConfig;
|
||||
/** jcode configuration (only for mode === 'jcode') */
|
||||
jcodeConfig?: JcodeConfig;
|
||||
/** Claude conversation session ID to resume after reboot (set by restore script) */
|
||||
resumeSessionId?: string;
|
||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 后端',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user