-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpatch.py
More file actions
executable file
·180 lines (150 loc) · 6.65 KB
/
Copy pathpatch.py
File metadata and controls
executable file
·180 lines (150 loc) · 6.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env python3
"""
Patch Kilo Code VS Code extension keyboard behavior to match Claude Code.
This is the standalone script. For the VS Code extension version with
auto-detection, commands, and status checking, see:
https://github.com/zeyutang/kilo-code-kb-patch
Claude Code behavior (reference):
- useCtrlEnterToSend=true: Enter=newline, Cmd+Enter=send
- When chat input is empty + permission visible: chat input is hidden (display:none),
focus shifts to permission buttons → bare Enter approves, Escape rejects
- When chat input has content + permission visible: chat input stays visible,
bare Enter→newline, bare Escape→dismiss popups, NOT approve/reject
- Permission handler is on container div (bubbling), so chat input events
never reach it (siblings, not parent-child)
This patch applies to Kilo Code:
1. Chat input: Enter=newline, Cmd+Enter=send (always, like useCtrlEnterToSend=true)
2. Permission prompt behavior depends on chat textarea content:
- Textarea EMPTY + focused: bare Enter approves, Escape rejects
(matches Claude Code where empty input is hidden + focus on permission)
- Textarea HAS CONTENT + focused: bare Enter/Space/Escape go to chat
(newline/typing/dismiss), NOT to permission handler
Cmd+Enter CAN approve, Cmd+Escape CAN reject
- Textarea NOT focused: bare Enter approves, Escape rejects (unchanged)
3. KiloClaw chat: same Enter/Cmd+Enter swap
Key difference: Claude Code hides the input when empty+permission visible.
Kilo Code doesn't, so we check textarea content to emulate the same behavior.
Re-run after extension updates (auto-finds latest version).
"""
import glob
import os
import sys
EXT_DIR = os.path.expanduser("~/.vscode/extensions")
def find_latest_ext():
dirs = sorted(glob.glob(os.path.join(EXT_DIR, "kilocode.kilo-code-*")))
if not dirs:
print("ERROR: No kilocode.kilo-code-* extension found in ~/.vscode/extensions/")
sys.exit(1)
return dirs[-1]
PATCHES = {
"webview.js": [
(
"Fm(je)&&!je.shiftKey&&(je.preventDefault(),Ce())",
"Fm(je)&&je.metaKey&&(je.preventDefault(),Ce())",
"Chat input: Enter→newline, Cmd+Enter→send",
),
(
'if(je.key==="Escape"&&ge()){je.preventDefault(),je.stopPropagation(),t.abort();return}',
'if(je.key==="Escape"&&ge()&&(je.shiftKey||!je.target?.value)){je.preventDefault(),je.stopPropagation(),t.abort();return}',
"Chat Escape: bare Escape aborts when textarea empty; "
"Shift+Escape always aborts",
),
(
"G?!1:S(j)",
'z.target?.value?(z.key==="Enter"&&!z.metaKey||z.key===" "||z.key==="Escape"&&!z.shiftKey&&!z.ctrlKey):!1',
"Permission L(): when textarea has content, skip bare Enter/Space/Escape; "
"works regardless of focus",
),
(
'P=z=>{if(z.key==="Escape"){N(z,"reject");return}}',
'P=z=>{if(z.key==="Escape"&&(z.shiftKey||!z.target?.value)){N(z,"reject");return}}',
"Permission P: bare Escape rejects only when textarea empty; "
"Shift+Escape always rejects",
),
(
'if(M(z)){N(z,"once");return}}};',
'if(M(z)||z.key===" "&&!z.metaKey&&!z.ctrlKey&&!z.target?.value||z.key==="Enter"&&z.metaKey){N(z,"once");return}if(z.key==="Escape"&&z.shiftKey){N(z,"reject");return}}};',
"Permission O: Cmd+Enter approves always; Space approves when empty; "
"Shift+Escape rejects always",
),
(
'ee.key!=="Escape"||!t.submitting()&&t.status()==="idle"||ee.defaultPrevented||(ee.preventDefault(),t.abort())',
'ee.key!=="Escape"||!t.submitting()&&t.status()==="idle"||ee.defaultPrevented||!ee.shiftKey&&ee.target?.value||(ee.preventDefault(),t.abort())',
"Document Escape: bare Escape does not abort; Shift+Escape aborts",
),
],
"kiloclaw.js": [
(
'LA(Q)&&!Q.shiftKey?(Q.preventDefault(),y()):Q.key==="Escape"&&w()',
'LA(Q)&&Q.metaKey?(Q.preventDefault(),y()):Q.key==="Escape"&&w()',
"KiloClaw edit: Enter→newline, Cmd+Enter→save",
),
(
"LA(D)&&!D.shiftKey&&(D.preventDefault(),v())",
"LA(D)&&D.metaKey&&(D.preventDefault(),v())",
"KiloClaw chat: Enter→newline, Cmd+Enter→send",
),
],
}
def apply_patches(path, patches):
with open(path, "r") as f:
content = f.read()
original = content
changes = []
for old, new, desc in patches:
if old in content:
content = content.replace(old, new)
changes.append(desc)
else:
changes.append(f"SKIP: {desc} (pattern not found)")
if content != original:
with open(path, "w") as f:
f.write(content)
print(f" Patched {os.path.basename(path)}")
else:
print(f" WARNING: No changes made to {os.path.basename(path)}")
for c in changes:
print(f" - {c}")
def restore_patches(path, patches):
with open(path, "r") as f:
content = f.read()
original = content
changes = []
for old, new, desc in patches:
if new in content:
content = content.replace(new, old)
changes.append(f"Reverted: {desc}")
else:
changes.append(f"SKIP: {desc} (patched pattern not found)")
if content != original:
with open(path, "w") as f:
f.write(content)
print(f" Restored {os.path.basename(path)}")
else:
print(f" No patches to restore in {os.path.basename(path)}")
for c in changes:
print(f" - {c}")
def main():
mode = sys.argv[1] if len(sys.argv) > 1 else "patch"
if mode not in ("patch", "restore"):
print(f"Usage: {sys.argv[0]} [patch|restore]")
print(" patch - Apply patches (default)")
print(" restore - Revert all patches to original")
sys.exit(1)
ext_path = find_latest_ext()
version = os.path.basename(ext_path).split("-")[-1]
action = "Patching" if mode == "patch" else "Restoring"
print(f"{action} extension: {os.path.basename(ext_path)} (v{version})")
dist_dir = os.path.join(ext_path, "dist")
handler = apply_patches if mode == "patch" else restore_patches
for name, patches in PATCHES.items():
fpath = os.path.join(dist_dir, name)
if os.path.exists(fpath):
handler(fpath, patches)
else:
print(f" SKIP: {name} not found in dist/")
print()
print("Done! Reload the VS Code window for changes to take effect:")
print(" Cmd+Shift+P → 'Developer: Reload Window'")
if __name__ == "__main__":
main()