@logicseek/hello-cc 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/README.md +4 -0
- package/README.zh-CN.md +4 -0
- package/docs/README.md +9 -0
- package/docs/README.zh-CN.md +9 -0
- package/docs/commands.md +53 -0
- package/docs/commands.zh-CN.md +48 -0
- package/docs/dsh.md +137 -0
- package/docs/dsh.zh-CN.md +139 -0
- package/docs/mcp.md +9 -0
- package/docs/mcp.zh-CN.md +23 -0
- package/docs/native.md +295 -0
- package/docs/native.zh-CN.md +253 -0
- package/docs/web-handoff.zh-CN.md +53 -0
- package/lib/cli/app.mjs +25 -6
- package/lib/cli/commands/doctor.mjs +17 -2
- package/lib/cli/commands/dsh.mjs +79 -0
- package/lib/cli/commands/hook.mjs +241 -90
- package/lib/cli/commands/lock.mjs +34 -19
- package/lib/cli/commands/mcp.mjs +28 -0
- package/lib/cli/commands/native.mjs +130 -0
- package/lib/cli/commands/run.mjs +66 -21
- package/lib/cli/commands/task.mjs +6 -3
- package/lib/cli-args.mjs +4 -0
- package/lib/coordination-state.mjs +15 -1
- package/lib/core/coordination/event-retention.mjs +14 -2
- package/lib/core/coordination/gc-plan.mjs +3 -13
- package/lib/core/coordination/locks.mjs +45 -0
- package/lib/core/coordination/results.mjs +79 -0
- package/lib/core/peers/bindings.mjs +9 -3
- package/lib/diagnostics/codex.mjs +121 -0
- package/lib/integrations/codex-interactions.mjs +114 -0
- package/lib/integrations/dsh-collaboration.mjs +170 -0
- package/lib/integrations/dsh-cordis.d.ts +19 -0
- package/lib/integrations/dsh-cordis.mjs +177 -0
- package/lib/integrations/dsh.bundle.yml +7 -0
- package/lib/integrations/dsh.mjs +287 -0
- package/lib/integrations/native/claude.mjs +451 -0
- package/lib/integrations/native/codex.mjs +242 -0
- package/lib/integrations/native/dsh-acp.mjs +343 -0
- package/lib/integrations/native/index.mjs +19 -0
- package/lib/integrations/native/interactions.mjs +53 -0
- package/lib/integrations/native/jsonrpc.mjs +185 -0
- package/lib/integrations/peers/identity.mjs +36 -10
- package/lib/integrations/peers/processes.mjs +22 -8
- package/lib/integrations/providers.mjs +6 -2
- package/lib/mcp/scope.mjs +104 -0
- package/lib/mcp/stdio.mjs +80 -0
- package/lib/mcp/tools.mjs +197 -0
- package/lib/process/identity.mjs +3 -1
- package/lib/release/release-notes.mjs +9 -0
- package/lib/runtime/native/client.mjs +38 -0
- package/lib/runtime/native/service.mjs +511 -0
- package/lib/runtime/native/store.mjs +121 -0
- package/lib/ui/help.mjs +5 -2
- package/lib/web/adoption-state.mjs +55 -0
- package/lib/web/auto-attach.mjs +18 -2
- package/lib/web/codex-app-server.mjs +557 -0
- package/lib/web/codex-sessions.mjs +287 -0
- package/lib/web/control-lease.mjs +204 -0
- package/lib/web/http-routes.mjs +197 -15
- package/lib/web/http.mjs +5 -4
- package/lib/web/native-sessions.mjs +286 -0
- package/lib/web/project-contexts.mjs +2 -1
- package/lib/web/pty-sessions.mjs +60 -33
- package/lib/web/runtime-main.mjs +98 -6
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/task-results.mjs +76 -0
- package/lib/web/tmux-sessions.mjs +92 -11
- package/lib/web/tmux-stream.mjs +2 -2
- package/lib/web/ui-codex.mjs +550 -0
- package/lib/web/ui-command-palette.mjs +119 -0
- package/lib/web/ui-handoff.mjs +102 -0
- package/lib/web/ui-history.mjs +118 -0
- package/lib/web/ui-i18n.mjs +841 -0
- package/lib/web/ui-interactions.mjs +78 -0
- package/lib/web/ui-login.mjs +101 -0
- package/lib/web/ui-native.mjs +165 -0
- package/lib/web/ui-preferences.mjs +278 -0
- package/lib/web/ui-review.mjs +132 -0
- package/lib/web/ui-template.mjs +1106 -576
- package/lib/web/ui-terminal-find.mjs +98 -0
- package/lib/web/ui-workbench.mjs +94 -0
- package/lib/web/ui-workspace.mjs +222 -0
- package/package.json +11 -3
- package/scripts/check-free-identifiers.mjs +67 -8
- package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
- package/scripts/dsh-cordis-acceptance.mjs +200 -0
- package/scripts/dsh-installed-acceptance.mjs +247 -0
- package/scripts/dsh-official-acceptance.mjs +180 -0
- package/scripts/dsh-web-startup-acceptance.mjs +87 -0
- package/scripts/github-release.mjs +3 -1
- package/scripts/native-live-acceptance.mjs +369 -0
- package/scripts/regression.mjs +243 -89
- package/scripts/web-handoff-installed-acceptance.mjs +98 -0
- package/scripts/web-native-interaction-acceptance.mjs +204 -0
- package/scripts/web-native-model-acceptance.mjs +138 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { selectCurrentTask } from '../core/coordination/automation.mjs';
|
|
3
|
+
import { listTaskResults, recordTaskResult, summarizeTaskResults } from '../core/coordination/results.mjs';
|
|
4
|
+
import { CliError } from '../shared/errors.mjs';
|
|
5
|
+
|
|
6
|
+
export function createTaskResults({ connectWebProject, resolveSessionPeerId, now, addEvent }) {
|
|
7
|
+
const turnScopes = new WeakMap();
|
|
8
|
+
function currentTask(db, peer) {
|
|
9
|
+
return selectCurrentTask(db.prepare(`SELECT id,title,status,owner,priority FROM tasks
|
|
10
|
+
WHERE owner = ? AND status NOT IN ('done','abandoned')`).all(peer), peer);
|
|
11
|
+
}
|
|
12
|
+
function readSessionResults(session) {
|
|
13
|
+
const db = connectWebProject(session.ctx);
|
|
14
|
+
try {
|
|
15
|
+
const peer = resolveSessionPeerId(db, session);
|
|
16
|
+
const task = currentTask(db, peer);
|
|
17
|
+
const results = listTaskResults(db, { peer });
|
|
18
|
+
return { results, current_task: task, summary: summarizeTaskResults(results.filter(r => r.task_id === task?.id)) };
|
|
19
|
+
} finally { db.close(); }
|
|
20
|
+
}
|
|
21
|
+
function writeSessionResult(session, input) {
|
|
22
|
+
const db = connectWebProject(session.ctx);
|
|
23
|
+
try {
|
|
24
|
+
const peer = resolveSessionPeerId(db, session);
|
|
25
|
+
const state = session.adapter?.snapshot() || session.nativeSnapshot?.() || {};
|
|
26
|
+
const result = recordTaskResult(db, { peer, taskId: input.taskId, kind: 'verification',
|
|
27
|
+
source: 'user', stage: input.stage, status: input.status, title: input.title,
|
|
28
|
+
details: input.details, evidence: input.evidence, executorId: state.executorId,
|
|
29
|
+
threadId: state.threadId || state.sessionId, turnId: state.turnId }, { now, addEvent });
|
|
30
|
+
return { result };
|
|
31
|
+
} finally { db.close(); }
|
|
32
|
+
}
|
|
33
|
+
function observeCodexResults(session, state) {
|
|
34
|
+
if (!session.binding?.provider_session_id || state.executorId !== session.adapter?.snapshot().executorId) return;
|
|
35
|
+
const thread = state.threads?.find(t => t.id === session.binding.provider_session_id);
|
|
36
|
+
if (!thread?.turns?.length) return;
|
|
37
|
+
const db = connectWebProject(session.ctx);
|
|
38
|
+
try {
|
|
39
|
+
const peer = resolveSessionPeerId(db, session);
|
|
40
|
+
let scopes = turnScopes.get(session);
|
|
41
|
+
if (!scopes) { scopes = new Map(); turnScopes.set(session, scopes); }
|
|
42
|
+
for (const event of state.events || []) {
|
|
43
|
+
if (event.method === 'turn/started' && event.params?.threadId === thread.id && event.params.turn?.id && !scopes.has(event.params.turn.id)) {
|
|
44
|
+
scopes.set(event.params.turn.id, currentTask(db, peer)?.id || null);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const turn of thread.turns) {
|
|
48
|
+
const taskId = scopes.get(turn.id);
|
|
49
|
+
if (!taskId) continue;
|
|
50
|
+
const common = { peer, taskId, executorId: state.executorId, threadId: thread.id,
|
|
51
|
+
turnId: turn.id, source: 'executor', stage: 'local' };
|
|
52
|
+
for (const item of turn.items || []) {
|
|
53
|
+
if (item.type !== 'commandExecution' || !['completed', 'failed', 'declined'].includes(item.status)) continue;
|
|
54
|
+
const exitCode = item.exitCode;
|
|
55
|
+
const status = exitCode === 0 && item.status === 'completed' ? 'passed'
|
|
56
|
+
: item.status === 'failed' || item.status === 'declined' || (Number.isSafeInteger(exitCode) && exitCode !== 0) ? 'failed' : 'pending';
|
|
57
|
+
recordTaskResult(db, { ...common, itemId: item.id, kind: 'command', status, exitCode,
|
|
58
|
+
resultKey: `${state.executorId}:${turn.id}:${item.id}:command`,
|
|
59
|
+
title: String(item.command || 'Command execution').slice(0, 300),
|
|
60
|
+
command: String(item.command || '').slice(0, 64000),
|
|
61
|
+
details: String(item.aggregatedOutput || '').slice(0, 64000), evidence: [] }, { now, addEvent });
|
|
62
|
+
}
|
|
63
|
+
if (turn.diff && ['completed', 'failed', 'interrupted'].includes(turn.status)) {
|
|
64
|
+
const diff = String(turn.diff).slice(0, 64000);
|
|
65
|
+
recordTaskResult(db, { ...common, kind: 'diff', status: 'pending', title: 'Turn changes awaiting review', diff,
|
|
66
|
+
resultKey: `${state.executorId}:${turn.id}:diff:${createHash('sha256').update(diff).digest('hex')}` }, { now, addEvent });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
while (scopes.size > 100) scopes.delete(scopes.keys().next().value);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// A delayed executor result must not attach to a task after ownership changed.
|
|
72
|
+
if (!(error instanceof CliError) || !['TASK_OWNER_MISMATCH', 'NOT_FOUND'].includes(error.code)) throw error;
|
|
73
|
+
} finally { db.close(); }
|
|
74
|
+
}
|
|
75
|
+
return { readSessionResults, writeSessionResult, observeCodexResults };
|
|
76
|
+
}
|
|
@@ -13,11 +13,13 @@ import { tx } from '../db/schema.mjs';
|
|
|
13
13
|
import { required } from '../cli-args.mjs';
|
|
14
14
|
import { readClockGraceUntil } from '../shared/clock-grace.mjs';
|
|
15
15
|
import { peerEvidenceAllowsReap } from '../core/peers/evidence.mjs';
|
|
16
|
+
import { isDshHookBinding } from '../core/peers/bindings.mjs';
|
|
16
17
|
import { conditionalTmuxKill, conditionalTmuxRename, prepareTmuxRestartBinding, rollbackTmuxRestartBinding, validateTmuxDestructiveEvidence } from '../core/peers/tmux-safety.mjs';
|
|
17
18
|
import { LAUNCH_FINGERPRINT_ENV, PROVIDER_STATE_ENV, childSessionEnv, isRelaunchableProviderSession, isolatedEnvCommandArgs, launchFingerprint, providerRestartReason } from '../core/sessions/launch.mjs';
|
|
18
19
|
import { defaultSessionCommand, inferPeerKind } from '../integrations/providers.mjs';
|
|
19
20
|
import { contextForProject } from '../runtime/paths.mjs';
|
|
20
21
|
import { detectBranch } from '../project-context.mjs';
|
|
22
|
+
import { adoptionPaused } from './adoption-state.mjs';
|
|
21
23
|
import { ensureTmuxAvailable, runTmux, tmuxCapturePane, tmuxEnvironmentArgs, tmuxHasSession, tmuxKillSession, tmuxLaunchFingerprint, tmuxListSessionNames, tmuxManagedSessionName, tmuxManagedSessionNameMatches, tmuxPaneInfo, tmuxProviderState, tmuxSendLiteral, tmuxSessionEnvironmentValue, tmuxSessionHasClients } from '../tmux.mjs';
|
|
22
24
|
|
|
23
25
|
export function createTmuxSessions(deps) {
|
|
@@ -41,6 +43,26 @@ export function createTmuxSessions(deps) {
|
|
|
41
43
|
findProviderSessionBinding, providerOwnerEvidenceFromDb,
|
|
42
44
|
shellCommand, detectBranch
|
|
43
45
|
} = deps;
|
|
46
|
+
const adoptionPaneInfo = deps.tmuxPaneInfo || tmuxPaneInfo;
|
|
47
|
+
const adoptionSessionNames = deps.tmuxListSessionNames || tmuxListSessionNames;
|
|
48
|
+
const adoptionSessionEnvironment = deps.tmuxSessionEnvironmentValue || tmuxSessionEnvironmentValue;
|
|
49
|
+
const adoptionSessionMatches = deps.tmuxManagedSessionNameMatches || tmuxManagedSessionNameMatches;
|
|
50
|
+
|
|
51
|
+
function isAdoptionPaused(projectCtx, info, peer, existingDb = null) {
|
|
52
|
+
const db = existingDb || connectWebProject(projectCtx);
|
|
53
|
+
try {
|
|
54
|
+
return adoptionPaused(db, {
|
|
55
|
+
peer,
|
|
56
|
+
pane: info.pane,
|
|
57
|
+
pid: info.pid,
|
|
58
|
+
process_identity: liveProcessIdentity(info.pid),
|
|
59
|
+
tmux_session_created: tmuxSessionCreationToken(info.pane),
|
|
60
|
+
tmux_session_id: tmuxSessionId(info.pane)
|
|
61
|
+
});
|
|
62
|
+
} finally {
|
|
63
|
+
if (!existingDb) db.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
44
66
|
|
|
45
67
|
// ── Serialize + broadcast helpers ─────────────────────────────────────────
|
|
46
68
|
function detachTmuxSession(session, status = 'detached') {
|
|
@@ -493,14 +515,50 @@ export function createTmuxSessions(deps) {
|
|
|
493
515
|
return false;
|
|
494
516
|
}
|
|
495
517
|
|
|
518
|
+
function assertDshTerminalAttachAllowed(db, id, kind, provider) {
|
|
519
|
+
const binding = db?.prepare('SELECT * FROM peer_bindings WHERE peer = ?').get(id);
|
|
520
|
+
if (kind !== 'dsh' && provider !== 'dsh' && !isDshHookBinding(binding)) return;
|
|
521
|
+
throw new CliError('DSH_SESSION_CONTROL_UNSUPPORTED',
|
|
522
|
+
'dsh sessions use their native Web UI. Use hcc dsh web; the shared service pane cannot be attached as a session terminal.',
|
|
523
|
+
{ peer: id, provider: 'dsh' });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function assertStructuredTerminalAttachAllowed(db, id, proposedBinding = null) {
|
|
527
|
+
const owner = db.prepare('SELECT * FROM peer_bindings WHERE peer = ?').get(id);
|
|
528
|
+
const providerOwner = proposedBinding && findProviderSessionBinding(db, { ...proposedBinding, peer: id });
|
|
529
|
+
if ([owner, providerOwner].some(binding => ['native', 'app-server'].includes(binding?.transport))) {
|
|
530
|
+
throw new CliError('STRUCTURED_SESSION_NO_TERMINAL',
|
|
531
|
+
'Open the structured worker; a service process or saved thread cannot be replaced with a terminal', { peer: id });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
496
535
|
function attachTmuxSession(input) {
|
|
497
536
|
const pctx = input.projectCtx || ctx;
|
|
498
537
|
const id = input.id;
|
|
499
538
|
if (!id) throw new CliError('BAD_REQUEST', 'id required');
|
|
500
539
|
const actorPeer = requestActorPeer(input, id);
|
|
501
540
|
const auditSource = requestSource(input, input.autoAttach ? 'runtime' : 'web');
|
|
541
|
+
// Logical dsh sessions share a service process. Reject before pane
|
|
542
|
+
// takeover, even when an explicit shell kind or force flag is supplied.
|
|
543
|
+
const preflightDb = connectWebProject(pctx);
|
|
544
|
+
try {
|
|
545
|
+
assertStructuredTerminalAttachAllowed(preflightDb, id, input.binding);
|
|
546
|
+
assertDshTerminalAttachAllowed(preflightDb, id, input.kind, input.binding?.provider);
|
|
547
|
+
} finally {
|
|
548
|
+
preflightDb.close();
|
|
549
|
+
}
|
|
502
550
|
const info = tmuxPaneInfo(input.pane || null);
|
|
503
551
|
if (info.dead) throw new CliError('TMUX_PANE_DEAD', `tmux pane is not running: ${info.pane}`);
|
|
552
|
+
const kind = inferPeerKind(id, input.kind || null, info.command);
|
|
553
|
+
assertDshTerminalAttachAllowed(null, id, kind, input.binding?.provider);
|
|
554
|
+
for (const existing of sessions.values()) {
|
|
555
|
+
if (existing.pane === info.pane ||
|
|
556
|
+
(existing.root === pctx.root && [existing.id, existing.peerId].includes(id)) ||
|
|
557
|
+
providerSessionBindingMatches(existing.binding, input.binding)) input.authorizeMutation?.(existing);
|
|
558
|
+
}
|
|
559
|
+
if ((input.autoAttach || auditSource === 'runtime') && isAdoptionPaused(pctx, info, id)) {
|
|
560
|
+
throw new CliError('WEB_ADOPTION_PAUSED', 'Web terminal adoption is paused; explicitly attach to resume', { peer: id, pane: info.pane });
|
|
561
|
+
}
|
|
504
562
|
|
|
505
563
|
for (const existing of [...sessions.values()]) {
|
|
506
564
|
if (existing.type === 'tmux' && existing.pane === info.pane && existing.id !== id) {
|
|
@@ -523,7 +581,6 @@ export function createTmuxSessions(deps) {
|
|
|
523
581
|
}
|
|
524
582
|
}
|
|
525
583
|
|
|
526
|
-
const kind = inferPeerKind(id, input.kind || null, info.command);
|
|
527
584
|
const role = input.role || 'peer';
|
|
528
585
|
const cwd = path.resolve(input.cwd || info.cwd || pctx.root);
|
|
529
586
|
const command = input.command || `tmux ${info.pane} (${info.command})`;
|
|
@@ -557,6 +614,12 @@ export function createTmuxSessions(deps) {
|
|
|
557
614
|
command,
|
|
558
615
|
cwd,
|
|
559
616
|
pid: info.pid,
|
|
617
|
+
adoptionIdentity: {
|
|
618
|
+
pid: info.pid,
|
|
619
|
+
process_identity: attachedEvidence.process_identity,
|
|
620
|
+
tmux_session_created: attachedEvidence.session_created,
|
|
621
|
+
tmux_session_id: attachedEvidence.session_id
|
|
622
|
+
},
|
|
560
623
|
type: 'tmux',
|
|
561
624
|
pane: info.pane,
|
|
562
625
|
status: 'running',
|
|
@@ -631,6 +694,9 @@ export function createTmuxSessions(deps) {
|
|
|
631
694
|
}
|
|
632
695
|
}
|
|
633
696
|
tx(db, () => {
|
|
697
|
+
// A hook may have registered the same peer after the early check.
|
|
698
|
+
assertStructuredTerminalAttachAllowed(db, id, nextBinding);
|
|
699
|
+
assertDshTerminalAttachAllowed(db, id, kind, nextBinding.provider);
|
|
634
700
|
if (rebindOldBindingSubject) {
|
|
635
701
|
const currentOldBinding = db.prepare('SELECT * FROM peer_bindings WHERE peer = ?').get(rebindOldPeer);
|
|
636
702
|
if (JSON.stringify(currentOldBinding || null) !== JSON.stringify(rebindOldBindingSubject)) {
|
|
@@ -735,7 +801,7 @@ export function createTmuxSessions(deps) {
|
|
|
735
801
|
|
|
736
802
|
function writeSessionInput(session, data) {
|
|
737
803
|
if (session.type === 'external') {
|
|
738
|
-
|
|
804
|
+
fs.appendFileSync(session.inFile, data);
|
|
739
805
|
} else if (session.type === 'tmux') {
|
|
740
806
|
tmuxSendLiteral(session.pane, data);
|
|
741
807
|
scheduleTmuxInputRefresh(session);
|
|
@@ -778,10 +844,16 @@ export function createTmuxSessions(deps) {
|
|
|
778
844
|
}
|
|
779
845
|
|
|
780
846
|
function startTmuxManagedSession(input) {
|
|
847
|
+
for (const existing of sessions.values()) {
|
|
848
|
+
if (providerSessionBindingMatches(existing.binding, input.binding)) input.authorizeMutation?.(existing);
|
|
849
|
+
}
|
|
781
850
|
ensureTmuxAvailable({ autoInstall: false });
|
|
782
851
|
const pctx = input.projectCtx || ctx;
|
|
783
852
|
const kind = input.kind || 'shell';
|
|
784
853
|
const id = input.id || nextProjectSessionId(pctx, kind);
|
|
854
|
+
const structuredDb = connectWebProject(pctx);
|
|
855
|
+
try { assertStructuredTerminalAttachAllowed(structuredDb, id, input.binding); }
|
|
856
|
+
finally { structuredDb.close(); }
|
|
785
857
|
const actorPeer = requestActorPeer(input, id);
|
|
786
858
|
const auditSource = requestSource(input, 'web');
|
|
787
859
|
const role = input.role || 'peer';
|
|
@@ -1141,7 +1213,9 @@ export function createTmuxSessions(deps) {
|
|
|
1141
1213
|
continue;
|
|
1142
1214
|
}
|
|
1143
1215
|
try {
|
|
1144
|
-
|
|
1216
|
+
const info = adoptionPaneInfo(row.runtime_target);
|
|
1217
|
+
if (isAdoptionPaused(projectCtx, info, row.id)) continue;
|
|
1218
|
+
(deps.attachTmuxSession || attachTmuxSession)({
|
|
1145
1219
|
id: row.id,
|
|
1146
1220
|
kind: row.kind,
|
|
1147
1221
|
role: row.role || 'peer',
|
|
@@ -1168,7 +1242,7 @@ export function createTmuxSessions(deps) {
|
|
|
1168
1242
|
}
|
|
1169
1243
|
// The fast path above only re-adopts bindings whose runtime_target is still
|
|
1170
1244
|
// set. Also sweep by session name so sessions left alive with a cleared
|
|
1171
|
-
// runtime_target
|
|
1245
|
+
// runtime_target are recovered unless an explicit Web detach paused them.
|
|
1172
1246
|
reAdoptOrphanManagedTmuxSessions(projectCtx);
|
|
1173
1247
|
}
|
|
1174
1248
|
|
|
@@ -1181,10 +1255,10 @@ export function createTmuxSessions(deps) {
|
|
|
1181
1255
|
const live = new Set();
|
|
1182
1256
|
let names;
|
|
1183
1257
|
try {
|
|
1184
|
-
names =
|
|
1258
|
+
names = adoptionSessionNames().filter((name) => isProjectManagedTmuxSession(
|
|
1185
1259
|
projectCtx,
|
|
1186
1260
|
name,
|
|
1187
|
-
|
|
1261
|
+
adoptionSessionEnvironment(name, 'HCC_ROOT')
|
|
1188
1262
|
));
|
|
1189
1263
|
} catch {
|
|
1190
1264
|
return live;
|
|
@@ -1200,7 +1274,7 @@ export function createTmuxSessions(deps) {
|
|
|
1200
1274
|
for (const row of db.prepare(`
|
|
1201
1275
|
SELECT p.id, p.status, p.pid, p.pid_start_token, p.pid_command_hash,
|
|
1202
1276
|
b.provider, b.provider_session_id, b.provider_session_name,
|
|
1203
|
-
b.resume_mode, b.resume_arg, b.command, b.runtime_target
|
|
1277
|
+
b.resume_mode, b.resume_arg, b.command, b.runtime_target, b.transport
|
|
1204
1278
|
FROM peers p
|
|
1205
1279
|
LEFT JOIN peer_bindings b ON b.peer = p.id
|
|
1206
1280
|
`).all()) {
|
|
@@ -1221,15 +1295,16 @@ export function createTmuxSessions(deps) {
|
|
|
1221
1295
|
|
|
1222
1296
|
for (const name of names) {
|
|
1223
1297
|
// Ownership: the session's HCC_ROOT env must match this project root.
|
|
1224
|
-
const hccRoot =
|
|
1298
|
+
const hccRoot = adoptionSessionEnvironment(name, 'HCC_ROOT');
|
|
1225
1299
|
if (rootEvidence(projectCtx.root, hccRoot).state !== 'match') continue;
|
|
1226
1300
|
|
|
1227
1301
|
let info;
|
|
1228
|
-
try { info =
|
|
1302
|
+
try { info = adoptionPaneInfo(`${name}:0.0`); } catch { continue; }
|
|
1229
1303
|
if (!info || info.dead) continue;
|
|
1230
1304
|
|
|
1231
1305
|
const candidates = [...bindingByPeer.entries()].filter(([peerId, binding]) => {
|
|
1232
|
-
if (
|
|
1306
|
+
if (['native', 'app-server'].includes(binding.transport)) return false;
|
|
1307
|
+
if (!adoptionSessionMatches(projectCtx, name, peerId, hccRoot)) return false;
|
|
1233
1308
|
return observePeerEvidence(projectCtx, binding, {
|
|
1234
1309
|
transport: 'tmux',
|
|
1235
1310
|
runtime_target: info.pane
|
|
@@ -1239,6 +1314,12 @@ export function createTmuxSessions(deps) {
|
|
|
1239
1314
|
const [[peerId, b]] = candidates;
|
|
1240
1315
|
live.add(peerId);
|
|
1241
1316
|
if (tracked.has(peerId)) continue;
|
|
1317
|
+
try {
|
|
1318
|
+
if (isAdoptionPaused(projectCtx, info, peerId)) continue;
|
|
1319
|
+
} catch {
|
|
1320
|
+
// Missing pause state never authorizes automatic terminal adoption.
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1242
1323
|
const binding = b ? {
|
|
1243
1324
|
provider: b.provider,
|
|
1244
1325
|
provider_session_id: b.provider_session_id,
|
|
@@ -1252,7 +1333,7 @@ export function createTmuxSessions(deps) {
|
|
|
1252
1333
|
} : null;
|
|
1253
1334
|
|
|
1254
1335
|
try {
|
|
1255
|
-
attachTmuxSession({
|
|
1336
|
+
(deps.attachTmuxSession || attachTmuxSession)({
|
|
1256
1337
|
id: peerId,
|
|
1257
1338
|
pane: info.pane,
|
|
1258
1339
|
cwd: info.cwd || projectCtx.root,
|
package/lib/web/tmux-stream.mjs
CHANGED
|
@@ -19,8 +19,8 @@ export function createTmuxStream({
|
|
|
19
19
|
}) {
|
|
20
20
|
function cursorEscape(payload) {
|
|
21
21
|
if (!payload) return '';
|
|
22
|
-
return '[' + (payload.row + 1) + ';' + (payload.col + 1) + 'H' +
|
|
23
|
-
(payload.visible ? '[?25h' : '[?25l');
|
|
22
|
+
return '\x1b[' + (payload.row + 1) + ';' + (payload.col + 1) + 'H' +
|
|
23
|
+
(payload.visible ? '\x1b[?25h' : '\x1b[?25l');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function tmuxSnapshot(session) {
|