@gajae-code/coding-agent 0.12.11 → 0.12.12

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 (112) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli/notify-cli.d.ts +42 -1
  3. package/dist/types/cli/update-cli.d.ts +9 -3
  4. package/dist/types/commands/notify.d.ts +6 -0
  5. package/dist/types/config/file-lock.d.ts +3 -0
  6. package/dist/types/coordinator/contract.d.ts +1 -1
  7. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  8. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  9. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  10. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  11. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  12. package/dist/types/main.d.ts +6 -2
  13. package/dist/types/modes/components/session-selector.d.ts +1 -1
  14. package/dist/types/modes/utils/host-status.d.ts +23 -0
  15. package/dist/types/runtime-mcp/types.d.ts +2 -0
  16. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  17. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  18. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  20. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  21. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  22. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  23. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  24. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  25. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  26. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  27. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  28. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  29. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +24 -7
  31. package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
  32. package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
  33. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  34. package/dist/types/sdk/host/host.d.ts +52 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  36. package/dist/types/sdk/session-activation.d.ts +41 -0
  37. package/dist/types/sdk/session.d.ts +18 -0
  38. package/dist/types/sdk/startup-capability.d.ts +8 -1
  39. package/dist/types/session/agent-session.d.ts +7 -0
  40. package/dist/types/session/artifacts.d.ts +2 -2
  41. package/dist/types/session/session-manager.d.ts +23 -5
  42. package/dist/types/tools/index.d.ts +8 -0
  43. package/dist/types/utils/npm-registry.d.ts +71 -0
  44. package/examples/extensions/README.md +38 -4
  45. package/examples/extensions/ooo-bridge.ts +15 -0
  46. package/package.json +8 -8
  47. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  48. package/src/async/job-manager.ts +9 -2
  49. package/src/cli/notify-cli.ts +226 -2
  50. package/src/cli/update-cli.ts +90 -23
  51. package/src/commands/notify.ts +27 -3
  52. package/src/commands/sdk.ts +1 -0
  53. package/src/config/file-lock-gc.ts +18 -0
  54. package/src/config/file-lock.ts +61 -16
  55. package/src/config/model-profiles.ts +31 -15
  56. package/src/coordinator/contract.ts +1 -0
  57. package/src/coordinator-mcp/question-state.ts +2 -0
  58. package/src/coordinator-mcp/server.ts +358 -7
  59. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  60. package/src/extensibility/extensions/index.ts +1 -0
  61. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  62. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  63. package/src/extensibility/extensions/runner.ts +15 -2
  64. package/src/extensibility/extensions/types.ts +2 -0
  65. package/src/gjc-runtime/launch-tmux.ts +53 -1
  66. package/src/internal-urls/docs-index.generated.ts +11 -10
  67. package/src/main.ts +203 -67
  68. package/src/modes/DESIGN.md +8 -5
  69. package/src/modes/components/footer.ts +10 -18
  70. package/src/modes/components/session-selector.ts +26 -3
  71. package/src/modes/components/status-line/gh.ts +23 -2
  72. package/src/modes/controllers/event-controller.ts +3 -0
  73. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  74. package/src/modes/controllers/input-controller.ts +11 -0
  75. package/src/modes/controllers/selector-controller.ts +8 -2
  76. package/src/modes/interactive-mode.ts +7 -1
  77. package/src/modes/utils/host-status.ts +38 -0
  78. package/src/runtime-mcp/types.ts +2 -0
  79. package/src/sdk/broker/lifecycle.ts +60 -3
  80. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  81. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  82. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  83. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  84. package/src/sdk/bus/control-plane-frames.ts +51 -0
  85. package/src/sdk/bus/conversation-store.ts +29 -1
  86. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  87. package/src/sdk/bus/index.ts +70 -0
  88. package/src/sdk/bus/notification-service.ts +28 -1
  89. package/src/sdk/bus/rich-render.ts +3 -2
  90. package/src/sdk/bus/slack-daemon.ts +99 -4
  91. package/src/sdk/bus/slack-live-provider.ts +32 -2
  92. package/src/sdk/bus/slack-provider.ts +11 -0
  93. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  94. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  95. package/src/sdk/bus/telegram-daemon-contract.ts +24 -7
  96. package/src/sdk/bus/telegram-daemon.ts +1961 -497
  97. package/src/sdk/bus/telegram-reference.ts +42 -6
  98. package/src/sdk/bus/topic-registry.ts +674 -181
  99. package/src/sdk/host/host.ts +180 -1
  100. package/src/sdk/lifecycle-session.ts +8 -2
  101. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  102. package/src/sdk/session-activation.ts +127 -0
  103. package/src/sdk/session.ts +139 -28
  104. package/src/sdk/startup-capability.ts +10 -1
  105. package/src/session/agent-session.ts +220 -9
  106. package/src/session/artifacts.ts +56 -13
  107. package/src/session/internal/managed-session-storage.ts +206 -7
  108. package/src/session/session-manager.ts +148 -38
  109. package/src/setup/provider-presets.json +1 -1
  110. package/src/task/index.ts +92 -44
  111. package/src/tools/index.ts +8 -0
  112. package/src/utils/npm-registry.ts +660 -0
@@ -0,0 +1,235 @@
1
+ import type { MCPServerConnection, MCPToolCallResult } from "../../runtime-mcp";
2
+ import { callTool, connectToServer, disconnectServer } from "../../runtime-mcp";
3
+ import { createExactPrefixCommandBridge } from "./prefix-command-bridge";
4
+ import type { ExtensionContext, InputEvent, InputEventResult } from "./types";
5
+
6
+ const OUROBOROS_CLI_ENV = "OUROBOROS_CLI";
7
+ const INTERVIEW_COMMAND = "ooo interview";
8
+ const INTERVIEW_TOOL = "ouroboros_interview";
9
+ const INTERVIEW_SESSION_PATTERN = /^interview_[A-Za-z0-9_-]+$/;
10
+
11
+ interface OuroborosOooBridgeOptions {
12
+ connect?: typeof connectToServer;
13
+ callTool?: typeof callTool;
14
+ disconnect?: typeof disconnectServer;
15
+ }
16
+
17
+ interface InterviewState {
18
+ sessionId: string;
19
+ }
20
+
21
+ interface OuroborosOooBridgeHandler {
22
+ (event: InputEvent, ctx: ExtensionContext): Promise<InputEventResult>;
23
+ reset(): Promise<void>;
24
+ }
25
+
26
+ function resolveOuroborosCommand(): string {
27
+ return process.env[OUROBOROS_CLI_ENV]?.trim() || "ouroboros";
28
+ }
29
+
30
+ function interviewArgument(text: string): string | undefined {
31
+ if (text === INTERVIEW_COMMAND) return "";
32
+ if (text.startsWith(`${INTERVIEW_COMMAND} `) || text.startsWith(`${INTERVIEW_COMMAND}\t`)) {
33
+ return text.slice(INTERVIEW_COMMAND.length).trim();
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ function isOooCommand(text: string): boolean {
39
+ return text === "ooo" || text.startsWith("ooo ") || text.startsWith("ooo\t");
40
+ }
41
+
42
+ function isBuiltInControlInput(text: string): boolean {
43
+ return text === "." || text === "c" || text.startsWith("/");
44
+ }
45
+
46
+ function resetsInterviewState(text: string): boolean {
47
+ return /^\/(?:clear|drop|exit|new|quit)(?:\s|$)/.test(text);
48
+ }
49
+
50
+ function resultText(result: MCPToolCallResult): string {
51
+ return result.content
52
+ .filter(content => content.type === "text")
53
+ .map(content => content.text)
54
+ .join("\n\n")
55
+ .trim();
56
+ }
57
+
58
+ function resultMeta(result: MCPToolCallResult): Record<string, unknown> {
59
+ return result._meta ?? {};
60
+ }
61
+
62
+ function resultSessionId(result: MCPToolCallResult, text: string): string | undefined {
63
+ const metadataSessionId = resultMeta(result).session_id;
64
+ if (typeof metadataSessionId === "string" && INTERVIEW_SESSION_PATTERN.test(metadataSessionId)) {
65
+ return metadataSessionId;
66
+ }
67
+ const textSessionId = /\bSession(?: ID)?:\s*(interview_[A-Za-z0-9_-]+)/.exec(text)?.[1];
68
+ return textSessionId && INTERVIEW_SESSION_PATTERN.test(textSessionId) ? textSessionId : undefined;
69
+ }
70
+
71
+ function resultCompleted(result: MCPToolCallResult): boolean {
72
+ const meta = resultMeta(result);
73
+ return meta.completed === true || meta.phase === "complete";
74
+ }
75
+
76
+ export function createOuroborosOooBridge(options: OuroborosOooBridgeOptions = {}): OuroborosOooBridgeHandler {
77
+ const connect = options.connect ?? connectToServer;
78
+ const invoke = options.callTool ?? callTool;
79
+ const disconnect = options.disconnect ?? disconnectServer;
80
+ let interview: InterviewState | undefined;
81
+ let interviewCaptureActive = false;
82
+ let activeConnection: MCPServerConnection | undefined;
83
+ let pendingConnection: Promise<MCPServerConnection> | undefined;
84
+ let activeOperationAbort: AbortController | undefined;
85
+ let lifecycleGeneration = 0;
86
+ let operationTail: Promise<void> = Promise.resolve();
87
+
88
+ const commandBridge = createExactPrefixCommandBridge({
89
+ prefix: "ooo",
90
+ command: resolveOuroborosCommand(),
91
+ args: ["dispatch", "--runtime", "gjc"],
92
+ });
93
+
94
+ function assertCurrent(generation: number, signal: AbortSignal | undefined): void {
95
+ if (generation !== lifecycleGeneration || signal?.aborted) {
96
+ throw signal?.reason instanceof Error ? signal.reason : new Error("Ouroboros interview operation cancelled");
97
+ }
98
+ }
99
+
100
+ async function disconnectSafely(connection: MCPServerConnection | undefined): Promise<void> {
101
+ if (!connection) return;
102
+ try {
103
+ await disconnect(connection);
104
+ } catch {
105
+ // State is already fenced. A dead transport must not keep ordinary input captured.
106
+ }
107
+ }
108
+
109
+ async function resetInterview(): Promise<void> {
110
+ lifecycleGeneration++;
111
+ interviewCaptureActive = false;
112
+ const operationAbort = activeOperationAbort;
113
+ activeOperationAbort = undefined;
114
+ operationAbort?.abort(new Error("Ouroboros interview reset"));
115
+ const connectionToClose = activeConnection;
116
+ interview = undefined;
117
+ activeConnection = undefined;
118
+ pendingConnection = undefined;
119
+ await disconnectSafely(connectionToClose);
120
+ }
121
+
122
+ async function connection(ctx: ExtensionContext, generation: number): Promise<MCPServerConnection> {
123
+ assertCurrent(generation, ctx.signal);
124
+ if (activeConnection) return activeConnection;
125
+ const pending =
126
+ pendingConnection ??
127
+ connect(
128
+ "ouroboros-ooo-bridge",
129
+ {
130
+ type: "stdio",
131
+ command: resolveOuroborosCommand(),
132
+ args: ["mcp", "serve", "--runtime", "gjc"],
133
+ cwd: ctx.cwd,
134
+ },
135
+ { signal: ctx.signal },
136
+ );
137
+ pendingConnection = pending;
138
+ try {
139
+ const connected = await pending;
140
+ try {
141
+ assertCurrent(generation, ctx.signal);
142
+ } catch (error) {
143
+ await disconnectSafely(connected);
144
+ throw error;
145
+ }
146
+ activeConnection = connected;
147
+ return connected;
148
+ } finally {
149
+ if (pendingConnection === pending) pendingConnection = undefined;
150
+ }
151
+ }
152
+
153
+ async function runInterview(text: string, ctx: ExtensionContext): Promise<InputEventResult> {
154
+ const operationAbort = new AbortController();
155
+ activeOperationAbort = operationAbort;
156
+ const operationSignal = ctx.signal ? AbortSignal.any([ctx.signal, operationAbort.signal]) : operationAbort.signal;
157
+ const operationContext: ExtensionContext = { ...ctx, signal: operationSignal };
158
+ const generation = lifecycleGeneration;
159
+ const abortHandler = () => {
160
+ void resetInterview();
161
+ };
162
+ ctx.signal?.addEventListener("abort", abortHandler, { once: true });
163
+ try {
164
+ const interviewConnection = await connection(operationContext, generation);
165
+ const commandArgument = interviewArgument(text);
166
+ const args: Record<string, unknown> = { cwd: ctx.cwd };
167
+ if (interview) {
168
+ args.session_id = interview.sessionId;
169
+ const answer = commandArgument === undefined ? text.trim() : commandArgument;
170
+ if (answer) args.answer = answer;
171
+ } else {
172
+ args.initial_context = commandArgument ?? "";
173
+ }
174
+
175
+ const result = await invoke(interviewConnection, INTERVIEW_TOOL, args, { signal: operationSignal });
176
+ assertCurrent(generation, operationSignal);
177
+ const output = resultText(result);
178
+ if (result.isError) throw new Error(output || "Ouroboros interview failed");
179
+
180
+ const sessionId = resultSessionId(result, output);
181
+ if (!resultCompleted(result)) {
182
+ if (!sessionId) throw new Error("Ouroboros interview response did not include a session ID");
183
+ interview = { sessionId };
184
+ } else {
185
+ await resetInterview();
186
+ }
187
+ return output ? { handled: true, text: output } : { handled: true };
188
+ } catch (error) {
189
+ await resetInterview();
190
+ if (!ctx.signal?.aborted) {
191
+ const message = error instanceof Error ? error.message : String(error);
192
+ ctx.ui?.notify(message, "error");
193
+ }
194
+ return { handled: true };
195
+ } finally {
196
+ ctx.signal?.removeEventListener("abort", abortHandler);
197
+ if (activeOperationAbort === operationAbort) activeOperationAbort = undefined;
198
+ }
199
+ }
200
+
201
+ function enqueueInterview(
202
+ text: string,
203
+ ctx: ExtensionContext,
204
+ explicitInterview: boolean,
205
+ ): Promise<InputEventResult> {
206
+ const submissionGeneration = lifecycleGeneration;
207
+ const operation = operationTail.then(async () => {
208
+ if (submissionGeneration !== lifecycleGeneration) return { handled: true };
209
+ if (!explicitInterview && !interviewCaptureActive && !interview) return { handled: true };
210
+ return runInterview(text, ctx);
211
+ });
212
+ operationTail = operation.then(
213
+ () => undefined,
214
+ () => undefined,
215
+ );
216
+ return operation;
217
+ }
218
+
219
+ const handler = async (event: InputEvent, ctx: ExtensionContext): Promise<InputEventResult> => {
220
+ if (event.source !== undefined && event.source !== "interactive") return {};
221
+ if (isBuiltInControlInput(event.text)) {
222
+ if (resetsInterviewState(event.text)) await resetInterview();
223
+ return {};
224
+ }
225
+ const argument = interviewArgument(event.text);
226
+ const explicitInterview = argument !== undefined;
227
+ if (explicitInterview) interviewCaptureActive = true;
228
+ if (explicitInterview || ((interviewCaptureActive || interview) && !isOooCommand(event.text))) {
229
+ return enqueueInterview(event.text, ctx, explicitInterview);
230
+ }
231
+ return commandBridge(event, ctx);
232
+ };
233
+
234
+ return Object.assign(handler, { reset: resetInterview });
235
+ }
@@ -86,7 +86,10 @@ export function createExactPrefixCommandBridge(options: ExactPrefixCommandBridge
86
86
  process.env[recursionEnv] = nextRecursionDepth(recursionEnv);
87
87
  try {
88
88
  const result = await dispatch(options.command, [...args, event.text], ctx, { timeout });
89
- if (result.code === 0) return { handled: true };
89
+ if (result.code === 0) {
90
+ const output = result.stdout.trim() || result.stderr.trim();
91
+ return output ? { handled: true, text: output } : { handled: true };
92
+ }
90
93
  if (result.code === continueExitCode) return {};
91
94
 
92
95
  const output =
@@ -118,11 +121,3 @@ export function createExactPrefixCommandBridge(options: ExactPrefixCommandBridge
118
121
  }
119
122
  };
120
123
  }
121
-
122
- export function createOuroborosOooBridge() {
123
- return createExactPrefixCommandBridge({
124
- prefix: "ooo",
125
- command: "ouroboros",
126
- args: ["dispatch"],
127
- });
128
- }
@@ -79,6 +79,16 @@ export function testSetExtensionHandlerTimeoutMs(timeoutMs: number): void {
79
79
  const EXTENSION_HANDLER_TIMEOUT = Symbol("extensionHandlerTimeout");
80
80
 
81
81
  const MAX_PENDING_CREDENTIAL_DISABLED = 32;
82
+ function createHandlerContext(ctx: ExtensionContext, signal: AbortSignal): ExtensionContext {
83
+ const descriptors = Object.getOwnPropertyDescriptors(ctx);
84
+ descriptors.signal = {
85
+ configurable: true,
86
+ enumerable: true,
87
+ writable: true,
88
+ value: signal,
89
+ };
90
+ return Object.defineProperties({}, descriptors) as ExtensionContext;
91
+ }
82
92
 
83
93
  /**
84
94
  * Events handled by the generic emit() method.
@@ -688,12 +698,14 @@ export class ExtensionRunner {
688
698
  ext: Extension,
689
699
  timeoutMs: number,
690
700
  ): Promise<TResult | undefined> {
691
- let timeout: ReturnType<typeof setTimeout> | undefined;
701
+ let timeout: NodeJS.Timeout | undefined;
702
+ const abortController = new AbortController();
703
+ const handlerContext = createHandlerContext(ctx, abortController.signal);
692
704
  try {
693
705
  const timeoutPromise = new Promise<typeof EXTENSION_HANDLER_TIMEOUT>(resolve => {
694
706
  timeout = setTimeout(() => resolve(EXTENSION_HANDLER_TIMEOUT), timeoutMs);
695
707
  });
696
- const handlerResult = await Promise.race([Promise.resolve(handler(event, ctx)), timeoutPromise]);
708
+ const handlerResult = await Promise.race([Promise.resolve(handler(event, handlerContext)), timeoutPromise]);
697
709
  if (timeout !== undefined) {
698
710
  clearTimeout(timeout);
699
711
  timeout = undefined;
@@ -701,6 +713,7 @@ export class ExtensionRunner {
701
713
 
702
714
  if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
703
715
  const error = `handler timed out after ${timeoutMs}ms`;
716
+ abortController.abort(new Error(error));
704
717
  logger.warn("Extension handler timed out", {
705
718
  extensionPath: ext.path,
706
719
  event: event.type,
@@ -352,6 +352,8 @@ export interface ExtensionContext {
352
352
  hasUI: boolean;
353
353
  /** Current working directory */
354
354
  cwd: string;
355
+ /** Aborted when the runner stops waiting for this handler, including handler timeout. */
356
+ signal?: AbortSignal;
355
357
  /** Session manager (read-only) */
356
358
  sessionManager: ReadonlySessionManager;
357
359
  /** Session classification supplied by the SDK for extension policy decisions. */
@@ -652,6 +652,56 @@ function renameTmuxWindow(
652
652
  spawnSync(tmuxCommand, buildTmuxRenameWindowArgs(title, target), options);
653
653
  }
654
654
 
655
+ interface TmuxWindowIdentity {
656
+ paneId: string;
657
+ windowId: string;
658
+ windowIndex: string;
659
+ }
660
+
661
+ function parseTmuxWindowIdentity(value: string): TmuxWindowIdentity | null {
662
+ const [paneId, windowId, windowIndex, extra] = value.trim().split("\t");
663
+ if (
664
+ extra !== undefined ||
665
+ paneId === undefined ||
666
+ windowId === undefined ||
667
+ windowIndex === undefined ||
668
+ !/^%\d+$/.test(paneId) ||
669
+ !/^@\d+$/.test(windowId) ||
670
+ !/^\d+$/.test(windowIndex)
671
+ )
672
+ return null;
673
+ return { paneId, windowId, windowIndex };
674
+ }
675
+
676
+ function quoteTmuxCommandArgument(value: string): string {
677
+ return `'${value.replaceAll("'", "'\\''")}'`;
678
+ }
679
+
680
+ function renameExistingTmuxWindow(
681
+ tmuxCommand: string,
682
+ paneId: string,
683
+ title: string,
684
+ spawnSync: TmuxSpawnSync,
685
+ options: TmuxSpawnOptions,
686
+ ): void {
687
+ if (!/^%\d+$/.test(paneId)) return;
688
+ const observed = spawnSync(
689
+ tmuxCommand,
690
+ ["display-message", "-p", "-t", paneId, "#{pane_id}\t#{window_id}\t#{window_index}"],
691
+ options,
692
+ );
693
+ if (observed.exitCode !== 0) return;
694
+ const identity = parseTmuxWindowIdentity(observed.stdout ?? "");
695
+ if (!identity || identity.paneId !== paneId) return;
696
+
697
+ // `if-shell -F` evaluates the pane binding and inserts the rename into the
698
+ // same tmux command queue. The nested command targets the immutable window
699
+ // id, so an active-window switch or index reuse cannot redirect the rename.
700
+ const predicate = `#{&&:#{==:#{pane_id},${identity.paneId}},#{&&:#{==:#{window_id},${identity.windowId}},#{==:#{window_index},${identity.windowIndex}}}}`;
701
+ const command = `rename-window -t ${identity.windowId} -- ${quoteTmuxCommandArgument(title)}`;
702
+ spawnSync(tmuxCommand, ["if-shell", "-t", identity.paneId, "-F", predicate, command], options);
703
+ }
704
+
655
705
  function renameExistingTmuxWindowIfNeeded(context: TmuxLaunchContext): void {
656
706
  const env = context.env ?? process.env;
657
707
  if (!env.TMUX || env[GJC_TMUX_LAUNCHED_ENV] === "1") return;
@@ -669,11 +719,13 @@ function renameExistingTmuxWindowIfNeeded(context: TmuxLaunchContext): void {
669
719
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
670
720
  if (!tmuxAvailable) return;
671
721
 
722
+ const paneId = env.TMUX_PANE?.trim();
723
+ if (!paneId) return;
672
724
  const cwd = context.cwd ?? process.cwd();
673
725
  const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
674
726
  const title = buildGjcTmuxWindowTitle(context.project ?? cwd, branch);
675
727
  const spawnSync = context.spawnSync ?? defaultSpawnSync;
676
- renameTmuxWindow(tmuxCommand, title, spawnSync, {
728
+ renameExistingTmuxWindow(tmuxCommand, paneId, title, spawnSync, {
677
729
  cwd,
678
730
  env,
679
731
  stdin: "pipe",