@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +4 -0
  3. package/README.zh-CN.md +4 -0
  4. package/docs/README.md +9 -0
  5. package/docs/README.zh-CN.md +9 -0
  6. package/docs/commands.md +53 -0
  7. package/docs/commands.zh-CN.md +48 -0
  8. package/docs/dsh.md +137 -0
  9. package/docs/dsh.zh-CN.md +139 -0
  10. package/docs/mcp.md +9 -0
  11. package/docs/mcp.zh-CN.md +23 -0
  12. package/docs/native.md +295 -0
  13. package/docs/native.zh-CN.md +253 -0
  14. package/docs/web-handoff.zh-CN.md +53 -0
  15. package/lib/cli/app.mjs +25 -6
  16. package/lib/cli/commands/doctor.mjs +17 -2
  17. package/lib/cli/commands/dsh.mjs +79 -0
  18. package/lib/cli/commands/hook.mjs +241 -90
  19. package/lib/cli/commands/lock.mjs +34 -19
  20. package/lib/cli/commands/mcp.mjs +28 -0
  21. package/lib/cli/commands/native.mjs +130 -0
  22. package/lib/cli/commands/run.mjs +66 -21
  23. package/lib/cli/commands/task.mjs +6 -3
  24. package/lib/cli-args.mjs +4 -0
  25. package/lib/coordination-state.mjs +15 -1
  26. package/lib/core/coordination/event-retention.mjs +14 -2
  27. package/lib/core/coordination/gc-plan.mjs +3 -13
  28. package/lib/core/coordination/locks.mjs +45 -0
  29. package/lib/core/coordination/results.mjs +79 -0
  30. package/lib/core/peers/bindings.mjs +9 -3
  31. package/lib/diagnostics/codex.mjs +121 -0
  32. package/lib/integrations/codex-interactions.mjs +114 -0
  33. package/lib/integrations/dsh-collaboration.mjs +170 -0
  34. package/lib/integrations/dsh-cordis.d.ts +19 -0
  35. package/lib/integrations/dsh-cordis.mjs +177 -0
  36. package/lib/integrations/dsh.bundle.yml +7 -0
  37. package/lib/integrations/dsh.mjs +287 -0
  38. package/lib/integrations/native/claude.mjs +451 -0
  39. package/lib/integrations/native/codex.mjs +242 -0
  40. package/lib/integrations/native/dsh-acp.mjs +343 -0
  41. package/lib/integrations/native/index.mjs +19 -0
  42. package/lib/integrations/native/interactions.mjs +53 -0
  43. package/lib/integrations/native/jsonrpc.mjs +185 -0
  44. package/lib/integrations/peers/identity.mjs +36 -10
  45. package/lib/integrations/peers/processes.mjs +22 -8
  46. package/lib/integrations/providers.mjs +6 -2
  47. package/lib/mcp/scope.mjs +104 -0
  48. package/lib/mcp/stdio.mjs +80 -0
  49. package/lib/mcp/tools.mjs +197 -0
  50. package/lib/process/identity.mjs +3 -1
  51. package/lib/release/release-notes.mjs +9 -0
  52. package/lib/runtime/native/client.mjs +38 -0
  53. package/lib/runtime/native/service.mjs +511 -0
  54. package/lib/runtime/native/store.mjs +121 -0
  55. package/lib/ui/help.mjs +5 -2
  56. package/lib/web/adoption-state.mjs +55 -0
  57. package/lib/web/auto-attach.mjs +18 -2
  58. package/lib/web/codex-app-server.mjs +557 -0
  59. package/lib/web/codex-sessions.mjs +287 -0
  60. package/lib/web/control-lease.mjs +204 -0
  61. package/lib/web/http-routes.mjs +197 -15
  62. package/lib/web/http.mjs +5 -4
  63. package/lib/web/native-sessions.mjs +286 -0
  64. package/lib/web/project-contexts.mjs +2 -1
  65. package/lib/web/pty-sessions.mjs +60 -33
  66. package/lib/web/runtime-main.mjs +98 -6
  67. package/lib/web/session-serialize.mjs +0 -0
  68. package/lib/web/task-results.mjs +76 -0
  69. package/lib/web/tmux-sessions.mjs +92 -11
  70. package/lib/web/tmux-stream.mjs +2 -2
  71. package/lib/web/ui-codex.mjs +550 -0
  72. package/lib/web/ui-command-palette.mjs +119 -0
  73. package/lib/web/ui-handoff.mjs +102 -0
  74. package/lib/web/ui-history.mjs +118 -0
  75. package/lib/web/ui-i18n.mjs +841 -0
  76. package/lib/web/ui-interactions.mjs +78 -0
  77. package/lib/web/ui-login.mjs +101 -0
  78. package/lib/web/ui-native.mjs +165 -0
  79. package/lib/web/ui-preferences.mjs +278 -0
  80. package/lib/web/ui-review.mjs +132 -0
  81. package/lib/web/ui-template.mjs +1106 -576
  82. package/lib/web/ui-terminal-find.mjs +98 -0
  83. package/lib/web/ui-workbench.mjs +94 -0
  84. package/lib/web/ui-workspace.mjs +222 -0
  85. package/package.json +11 -3
  86. package/scripts/check-free-identifiers.mjs +67 -8
  87. package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
  88. package/scripts/dsh-cordis-acceptance.mjs +200 -0
  89. package/scripts/dsh-installed-acceptance.mjs +247 -0
  90. package/scripts/dsh-official-acceptance.mjs +180 -0
  91. package/scripts/dsh-web-startup-acceptance.mjs +87 -0
  92. package/scripts/github-release.mjs +3 -1
  93. package/scripts/native-live-acceptance.mjs +369 -0
  94. package/scripts/regression.mjs +243 -89
  95. package/scripts/web-handoff-installed-acceptance.mjs +98 -0
  96. package/scripts/web-native-interaction-acceptance.mjs +204 -0
  97. 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
- try { fs.appendFileSync(session.inFile, data); } catch {}
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
- attachTmuxSession({
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 (default Stop = detach-without-kill) are recovered.
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 = tmuxListSessionNames().filter((name) => isProjectManagedTmuxSession(
1258
+ names = adoptionSessionNames().filter((name) => isProjectManagedTmuxSession(
1185
1259
  projectCtx,
1186
1260
  name,
1187
- tmuxSessionEnvironmentValue(name, 'HCC_ROOT')
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 = tmuxSessionEnvironmentValue(name, 'HCC_ROOT');
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 = tmuxPaneInfo(`${name}:0.0`); } catch { continue; }
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 (!tmuxManagedSessionNameMatches(projectCtx, name, peerId, hccRoot)) return false;
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,
@@ -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) {