Skip to content

Commit 68bd95b

Browse files
authored
Fix app lint failures and terminal copy regression (#342)
* fix(app): satisfy current lint rules * fix(app): preserve terminal selection copy * fix(app): protect terminal right-click copy flow * fix(api): disable tmux right-click menu * fix(api): suppress tmux right-click drag menu * fix(app): address coderabbit review findings * fix(app): resolve remaining coderabbit findings * fix(app): address coderabbit follow-up review * fix(app): ignore finalized ssh startup failures * fix(app): document terminal attach and bound browser e2e curl
1 parent 9140d58 commit 68bd95b

55 files changed

Lines changed: 6704 additions & 4618 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/api/src/services/terminal-sessions.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,174 @@ const writePtyInput = (pty: PtyBridge | null, data: string): void => {
765765

766766
const shellQuote = (value: string): string => `'${value.replace(/'/gu, "'\\''")}'`
767767

768+
// CHANGE: Predicate for when tmux should forward right-click pane events.
769+
// WHY: Mouse-aware apps and copy/view mode still need pane mouse events, while tmux menus must stay disabled.
770+
// QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
771+
// REF: PR #342 tmux right-click handling.
772+
// SOURCE: n/a
773+
// FORMAT THEOREM: mouse-aware-or-copy-mode => predicate evaluates truthy in tmux.
774+
// PURITY: CORE
775+
// EFFECT: none
776+
// INVARIANT: The predicate contains only tmux format language and no shell interpolation.
777+
// COMPLEXITY: O(1) time/O(1) space.
778+
/**
779+
* Tmux format predicate used by right-click pane bindings.
780+
*
781+
* @returns A tmux format expression, not a shell command.
782+
* @pure true
783+
* @effect none
784+
* @invariant Expression is constant and contains no user-controlled input.
785+
* @precondition tmux understands mouse_any_flag and pane mode format variables.
786+
* @postcondition The value is safe to embed after shellQuote.
787+
* @complexity O(1) time/O(1) space.
788+
* @throws Never
789+
*/
790+
const tmuxRightClickForwardPredicate =
791+
"#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}"
792+
// CHANGE: Pane right-click bindings that are overridden at tmux startup.
793+
// WHY: These cover down/drag/up/end and Meta-modified events that previously reached display-menu.
794+
// QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
795+
// REF: PR #342 tmux right-click handling.
796+
// SOURCE: n/a
797+
// FORMAT THEOREM: every binding in the array is mapped to renderTmuxPaneRightClickBinding.
798+
// PURITY: CORE
799+
// EFFECT: none
800+
// INVARIANT: Each entry is a static tmux root-table mouse binding name.
801+
// COMPLEXITY: O(1) time/O(1) space.
802+
/**
803+
* Tmux pane right-click binding names that should conditionally forward mouse events.
804+
*
805+
* @pure true
806+
* @effect none
807+
* @invariant The array contains only static tmux binding identifiers.
808+
* @precondition tmux root key table supports these binding names.
809+
* @postcondition Consumers can map each entry to a shell-safe bind-key command.
810+
* @complexity O(1) time/O(1) space.
811+
* @throws Never
812+
*/
813+
const tmuxRightClickPaneBindings: ReadonlyArray<string> = [
814+
"MouseDown3Pane",
815+
"MouseDrag3Pane",
816+
"MouseDragEnd3Pane",
817+
"MouseUp3Pane",
818+
"M-MouseDown3Pane",
819+
"M-MouseDrag3Pane",
820+
"M-MouseDragEnd3Pane",
821+
"M-MouseUp3Pane"
822+
]
823+
// CHANGE: Non-pane right-click bindings that are suppressed at tmux startup.
824+
// WHY: Status and border right-clicks are the tmux menu entry points that cannot be forwarded to pane apps.
825+
// QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
826+
// REF: PR #342 tmux right-click handling.
827+
// SOURCE: n/a
828+
// FORMAT THEOREM: every binding in the array is mapped to renderTmuxRightClickSuppressBinding.
829+
// PURITY: CORE
830+
// EFFECT: none
831+
// INVARIANT: Each entry is a static tmux root-table mouse binding name.
832+
// COMPLEXITY: O(1) time/O(1) space.
833+
/**
834+
* Tmux status/border right-click binding names that should be unbound.
835+
*
836+
* @pure true
837+
* @effect none
838+
* @invariant The array contains only static tmux binding identifiers.
839+
* @precondition tmux root key table supports these binding names.
840+
* @postcondition Consumers can map each entry to a shell-safe unbind-key command.
841+
* @complexity O(1) time/O(1) space.
842+
* @throws Never
843+
*/
844+
const tmuxRightClickSuppressBindings: ReadonlyArray<string> = [
845+
"MouseDown3Status",
846+
"MouseDown3StatusLeft",
847+
"MouseDown3StatusRight",
848+
"MouseDown3Border",
849+
"M-MouseDown3Status",
850+
"M-MouseDown3StatusLeft",
851+
"M-MouseDown3StatusRight",
852+
"M-MouseDown3Border"
853+
]
854+
855+
// CHANGE: Render one tmux bind-key command for a right-click pane event.
856+
// WHY: Pane events must reach mouse-aware programs without allowing tmux display-menu.
857+
// QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
858+
// REF: PR #342 tmux right-click handling.
859+
// SOURCE: n/a
860+
// FORMAT THEOREM: static binding => shellQuote(protected fragments) in result.
861+
// PURITY: CORE
862+
// EFFECT: none
863+
// INVARIANT: Dynamic shell fragments are emitted through shellQuote.
864+
// COMPLEXITY: O(1) time/O(1) space.
865+
/**
866+
* Builds a tmux root-table command for a pane right-click binding.
867+
*
868+
* @param binding - Static tmux mouse binding name.
869+
* @returns Shell command that binds the event to conditional pane forwarding.
870+
* @pure true
871+
* @effect none
872+
* @invariant Shell-interpreted tmux format/action fragments are quoted.
873+
* @precondition binding is one of tmuxRightClickPaneBindings.
874+
* @postcondition The command exits successfully even when tmux rejects a binding.
875+
* @complexity O(1) time/O(1) space.
876+
* @throws Never
877+
*/
878+
const renderTmuxPaneRightClickBinding = (binding: string): string =>
879+
`tmux bind-key -T root ${binding} if-shell -F -t = ${shellQuote(tmuxRightClickForwardPredicate)} ${
880+
shellQuote("select-pane -t = ; send-keys -M")
881+
} >/dev/null 2>&1 || true`
882+
883+
// CHANGE: Render one tmux unbind-key command for a suppressed right-click event.
884+
// WHY: Non-pane right-click targets are tmux UI affordances and should not open display-menu.
885+
// QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
886+
// REF: PR #342 tmux right-click handling.
887+
// SOURCE: n/a
888+
// FORMAT THEOREM: static binding => deterministic unbind command.
889+
// PURITY: CORE
890+
// EFFECT: none
891+
// INVARIANT: Result contains no user-controlled input.
892+
// COMPLEXITY: O(1) time/O(1) space.
893+
/**
894+
* Builds a tmux root-table command that suppresses a non-pane right-click binding.
895+
*
896+
* @param binding - Static tmux mouse binding name.
897+
* @returns Shell command that unbinds the event and tolerates unsupported bindings.
898+
* @pure true
899+
* @effect none
900+
* @invariant The returned command contains only static text plus binding.
901+
* @precondition binding is one of tmuxRightClickSuppressBindings.
902+
* @postcondition The command exits successfully even when the binding is absent.
903+
* @complexity O(1) time/O(1) space.
904+
* @throws Never
905+
*/
906+
const renderTmuxRightClickSuppressBinding = (binding: string): string =>
907+
`tmux unbind-key -T root ${binding} >/dev/null 2>&1 || true`
908+
909+
// CHANGE: Aggregate all tmux right-click startup commands.
910+
// WHY: Terminal session startup needs one ordered command list for pane forwarding and UI suppression.
911+
// QUOTE(TZ): PR #342 preserves right-click copy while tmux mouse tracking is active.
912+
// REF: PR #342 tmux right-click handling.
913+
// SOURCE: n/a
914+
// FORMAT THEOREM: result length = paneBindings length + suppressBindings length.
915+
// PURITY: CORE
916+
// EFFECT: none
917+
// INVARIANT: Pane commands precede suppress commands.
918+
// COMPLEXITY: O(n) time/O(n) space where n is the total binding count.
919+
/**
920+
* Renders the complete tmux right-click binding setup command list.
921+
*
922+
* @returns Readonly array of shell commands for tmux startup.
923+
* @pure true
924+
* @effect none
925+
* @invariant Pane forwarding commands are emitted before suppressing status/border commands.
926+
* @precondition Binding arrays contain static tmux binding identifiers.
927+
* @postcondition The result contains one command per configured binding.
928+
* @complexity O(n) time/O(n) space where n is total binding count.
929+
* @throws Never
930+
*/
931+
const renderTmuxRightClickBindingCommands = (): ReadonlyArray<string> => [
932+
...tmuxRightClickPaneBindings.map(renderTmuxPaneRightClickBinding),
933+
...tmuxRightClickSuppressBindings.map(renderTmuxRightClickSuppressBinding)
934+
]
935+
768936
const writeBufferToProjectContainer = (
769937
containerName: string,
770938
containerPath: string,
@@ -982,6 +1150,7 @@ export const renderTmuxAttachCommand = (
9821150
`tmux set-option -t ${shellQuote(args.tmuxName)} status off >/dev/null 2>&1 || true`,
9831151
`tmux set-option -t ${shellQuote(args.tmuxName)} history-limit 50000 >/dev/null 2>&1 || true`,
9841152
`tmux set-option -t ${shellQuote(args.tmuxName)} mouse on >/dev/null 2>&1 || true`,
1153+
...renderTmuxRightClickBindingCommands(),
9851154
`exec tmux attach-session -t ${shellQuote(args.tmuxName)}`
9861155
].join("; ")
9871156
return `bash --noprofile --norc -lc ${shellQuote(script)}`

packages/api/tests/terminal-sessions.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,28 @@ describe("terminal sessions service", () => {
254254
expect(command).toContain("status off")
255255
expect(command).toContain("history-limit 50000")
256256
expect(command).toContain("mouse on")
257+
expect(command).toContain("bind-key -T root MouseDown3Pane")
258+
expect(command).toContain("bind-key -T root MouseDrag3Pane")
259+
expect(command).toContain("bind-key -T root MouseDragEnd3Pane")
260+
expect(command).toContain("bind-key -T root MouseUp3Pane")
261+
expect(command).toContain("bind-key -T root M-MouseDown3Pane")
262+
expect(command).toContain("bind-key -T root M-MouseDrag3Pane")
263+
expect(command).toContain("bind-key -T root M-MouseDragEnd3Pane")
264+
expect(command).toContain("bind-key -T root M-MouseUp3Pane")
265+
expect(command).toContain("#{||:#{mouse_any_flag}")
266+
expect(command).toContain("#{pane_in_mode}")
267+
expect(command).toContain("#{pane_mode}")
268+
expect(command).toContain("select-pane -t = ; send-keys -M")
269+
expect(command).toContain("send-keys -M")
270+
expect(command).toContain("unbind-key -T root MouseDown3Status")
271+
expect(command).toContain("unbind-key -T root MouseDown3StatusLeft")
272+
expect(command).toContain("unbind-key -T root MouseDown3StatusRight")
273+
expect(command).toContain("unbind-key -T root MouseDown3Border")
274+
expect(command).toContain("unbind-key -T root M-MouseDown3Status")
275+
expect(command).toContain("unbind-key -T root M-MouseDown3StatusLeft")
276+
expect(command).toContain("unbind-key -T root M-MouseDown3StatusRight")
277+
expect(command).toContain("unbind-key -T root M-MouseDown3Border")
278+
expect(command).not.toContain("display-menu")
257279
expect(command).toContain("tmux attach-session -t")
258280
expect(command).toContain("docker-git-session-1")
259281
expect(command).toContain("/home/dev/project with spaces")
@@ -264,6 +286,7 @@ describe("terminal sessions service", () => {
264286
const statusOffIndex = command.indexOf("status off")
265287
const sessionHistoryLimitIndex = command.lastIndexOf("history-limit 50000")
266288
const mouseOnIndex = command.indexOf("mouse on")
289+
const rightClickBindingIndex = command.indexOf("MouseDown3Pane")
267290
const attachSessionIndex = command.indexOf("tmux attach-session -t")
268291

269292
expect(startServerIndex).toBeGreaterThanOrEqual(0)
@@ -272,13 +295,15 @@ describe("terminal sessions service", () => {
272295
expect(statusOffIndex).toBeGreaterThanOrEqual(0)
273296
expect(sessionHistoryLimitIndex).toBeGreaterThanOrEqual(0)
274297
expect(mouseOnIndex).toBeGreaterThanOrEqual(0)
298+
expect(rightClickBindingIndex).toBeGreaterThan(mouseOnIndex)
275299
expect(attachSessionIndex).toBeGreaterThanOrEqual(0)
276300
expect(startServerIndex).toBeLessThan(globalHistoryLimitIndex)
277301
expect(globalHistoryLimitIndex).toBeLessThan(newSessionIndex)
278302
expect(newSessionIndex).toBeLessThan(statusOffIndex)
279303
expect(statusOffIndex).toBeLessThan(sessionHistoryLimitIndex)
280304
expect(sessionHistoryLimitIndex).toBeLessThan(mouseOnIndex)
281-
expect(mouseOnIndex).toBeLessThan(attachSessionIndex)
305+
expect(mouseOnIndex).toBeLessThan(rightClickBindingIndex)
306+
expect(rightClickBindingIndex).toBeLessThan(attachSessionIndex)
282307
})
283308

284309
it("fails before creating a durable session when tmux is unavailable", async () => {

0 commit comments

Comments
 (0)