From 0ec5bd62c0beceee4801ac26e23ae08a7cc4bee3 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Mon, 29 Jun 2026 19:38:00 +0800 Subject: [PATCH] chore(kimi-code): upgrade pi-tui to 0.78.1 and adapt native helpers Bump @earendil-works/pi-tui from ^0.74.0 to ^0.78.1. pi-tui 0.75.5 replaced its koffi-based Windows VT input with a bundled native helper, and 0.76.0 added a darwin native helper for Terminal.app Shift+Enter. - SEA build: teach native-deps to collect pi-tui's per-target .node files, drop the koffi registry, and add a native-file-only collect mode so only package.json + the target .node ship (28 -> 2 files). - Redirect pi-tui's absolute-path native require() into the native-asset cache through the Module._load hook, and extend the native smoke test to actually load the helper. - npm package: ship pi-tui's native/ directory so macOS Terminal.app Shift+Enter and Windows Shift+Tab keep working for npm installs. --- .../darwin-arm64/darwin-modifiers.node | Bin 0 -> 50200 bytes .../darwin-x64/darwin-modifiers.node | Bin 0 -> 12776 bytes .../native/darwin/src/darwin-modifiers.c | 70 ++++++++++++++++++ .../win32-arm64/win32-console-mode.node | Bin 0 -> 3072 bytes .../win32-x64/win32-console-mode.node | Bin 0 -> 3072 bytes .../native/win32/src/win32-console-mode.c | 53 +++++++++++++ apps/kimi-code/package.json | 4 +- apps/kimi-code/scripts/native/assets.mjs | 16 ++-- .../kimi-code/scripts/native/check-bundle.mjs | 2 +- apps/kimi-code/scripts/native/native-deps.mjs | 39 +++++----- apps/kimi-code/src/native/module-hook.ts | 34 ++++++--- apps/kimi-code/src/native/smoke.ts | 35 ++++++++- .../test/scripts/native/native-deps.test.ts | 34 ++++++--- pnpm-lock.yaml | 39 ++++------ 14 files changed, 249 insertions(+), 77 deletions(-) create mode 100755 apps/kimi-code/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node create mode 100755 apps/kimi-code/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node create mode 100644 apps/kimi-code/native/darwin/src/darwin-modifiers.c create mode 100644 apps/kimi-code/native/win32/prebuilds/win32-arm64/win32-console-mode.node create mode 100644 apps/kimi-code/native/win32/prebuilds/win32-x64/win32-console-mode.node create mode 100644 apps/kimi-code/native/win32/src/win32-console-mode.c diff --git a/apps/kimi-code/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node b/apps/kimi-code/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node new file mode 100755 index 0000000000000000000000000000000000000000..4614842699ca81944f15dff61a999878f841be28 GIT binary patch literal 50200 zcmeI5U2I%e5y$85`YW~Lrr05F6BOg3$_c@Cp%q4ImFqZm95rc@_0pK9llAVkeertN zxpz0Io1(6iwj!wl-YR@l6|jAXh^;s(bts?;a8(h3M1Y1IenPzvrxs?-Ul8)TN5 z`{Aw~S%?RuBK}93oilUh&YAh0d++X({f8S@{#iw&5swO%TUqv&57*HJNwS~KgKN^@!ix^Yx-W9AJTYNlPr`&xq ztQ*-DKGO5^Y)h?sUv}?zhLx}G~e^F{X6vq)|t-nI0O#eImbm2773;=RIic|HeNB$rcD=>#8*PR-pTcFGuP z-evY$5o@Hm(~1lkhm+Qz-Q1qEj2%{Fc)*O>O?|$<`60uy&1AxEZd($;d1c5xtt|dN zM7y|cjhfT8)f_i_Mcgf$wl&2@*pjP=?qZR8PfxiVHhH%2mGK!CS-;Ah8l{2%a1?^t z#rgf(*!4Yuv4tbSv4vMCdo8GqUk}naGaUV_-jJ=Q%+oodlk>jsr0aW@eT8z$zZswp zOX~C+dVs=z^JA_zWch4Iqb;{*-@cQwGr`*N0O#S?*z{Cj>!lf@d*3S8^IY=iHjb6` z%fXuQoUE~4IMU8FZ5x~4-!{fIW~YMcQsEWt;zugTz7k$5<$BA;7LID!d9L>w@5}e! zXoaTdMP}Dg-l_k-JDvK6xz@k?wPjr2Ao~{Uec$!NuB%ej8>&gb=!uUDT4ZN6~E+Nj+!_|57} zs9x)OL=RGUmgCI#PfU+ZOw4Wi{e`(>RWoxNwN>xs_)Pq@-<>!+xAOyDTkWp#4Ig&$ zeyQ!}@2>t<=y=nFleh88iO_FX=Pb$H_K!k%t(FUwaSdOkT!>#2wQB>p5T9G^ci86F zL#T$CZKSpDUM~O99nwSXjlxgaKRvrn&-48{x{1$2WUE&3-escj!jVoszYadf?PDLQ z_Ax$J=X^Q$$-NxECZOjjuJFDshg;?DPR%-y||y}IMqV=WT@aZFvlP*^9A0AE`- z&-0Dm{2I@{%=1KK*i=!=h@=eFmrg`e{K&da+eoPdKjbkrY$f?iIyFM3y*n8*`%J^y zYZ1aU6=o(+2-&knd!bPW82Z9NU{r#*IjV>;bbcMbYHYP$Us! zok&^9I3(100@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@ANBZsVR3v3^gjKODVPC|I zi+>IKV{vfXEwc|Svx97xu*mkZ zMLA2cubx)yXGL5~$UC5xc-*t_sNHlL%Cv=<*=I7mVau>>BSuO&al@8*NY0~Z-1uYF z6t~;JE}uy)`?&u=H3hh{fWsoe>pk9OtBGE|S|6Y)&;Bg=XO zGG@@^PhL1U^1<%s>!1A1`InA%9NVz_;Pm~Iubo=;mjh*6-*{kV;;}O?^d!%}(>GP} z+Qo~5fjvE?g-6dl`p6S+{PL-;V_SawzrKF7Q2&F;@4ocRT@~lQ@~bPa>aUNS&7VGX z_Cm?@{!{c+$H0HPzxl}@zB)NwHF3Q4*%wj8~?(zN$ D%;!fI literal 0 HcmV?d00001 diff --git a/apps/kimi-code/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node b/apps/kimi-code/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node new file mode 100755 index 0000000000000000000000000000000000000000..0c82b391329a43f4d241183f915217b51b8380f5 GIT binary patch literal 12776 zcmeHOZ)jUp6u;S8ZQa(SW0TEoEDWZbNV9B;Q$Lt&%l3lFoTU3By4}4bFYRM#UibcV z77>aygGUHdurLHc{M0W(aZ@)GNw-O>A9Mr7;=oVpDs_SkCL6N&JMT}IWb}*QlzZU3 zbI*C_o_p@^+?N;HlW*_bxceYufu)SG4saJZ*3MWNl4vb>HozF?y@?~ej;Z+)YF3%2 zBLwrAJUBO%*UWpqt?|al8FXxkD@kv2hwinFjWnv98>W>t7EG=2{=lY)R=R@ux~H-Z zFWv*5!HviJmExeH!`2$l0SET&3jrAOQgto&<(D%|T`l!_{;l!06WAcz70lN?wK+D7 zoLi++YAMgvQbBX{=6ZbY#XIf=AiXI+&+ZGV>lrZTd*Z!uzWc@f8MnHAJr|L5PKa?N zoKkb%r!|yy(RhA1nnd$`BK#Y5%4-hn=H+`qhP|;Ls73el^7qVnQRc?*KvpaAK~rBe z-Wo3~#c#}mj*oXF9S_d)vMKw{_vg0;^NGANFwz_2IX4?Fv`-1lc`CX8g~Xnu|7>mZ zocw)y`WyicBH;}keG_9Njtefdb_b+4#ks!|3avvZKYlxOw*35oj}jZcIQQ`CPI`XAU}q1(hY@La z+4eZ@g}^>7$Dkifv7DURKSXI_ERq>EOl2UFQnR|O4@dUu@_=$m(@z+Y-I}iK)8)Z_ zHD~k`e0$`Oq8qAKG9vLN4{sq_XJ^AYjHR&c0E<|L9x19>Vt2`&T|N0>Xzs*EAt>Jo z56`WhPWb*CIw;w{#1F?0#d|Z-=GE4r2QqVR8oIeMaMeb;(G|zbooYb{Gzv&tF)EZ5%qoS zbb^i%evqu>2k-2M^bRTN9Irb%tUg(qyWdICFt&i*Uf8Tr)eO*X9l4^G3f~*pZkFewscv z`ls1BWp0%!n0+Cq4GhSoJd>W8F*lG}GIgy;sbQ^_zS7cB*^b$V-Z%Pj1?=bP2Eq9T_V!EQ2w3~M@{8|x`w zP@}}5V)XqX21)F{`)S-r^x3u?#aIO<0=|LnWo?10N80IJ2@(hh1Ox&C0fB%(Kp-Fx z5C{ka1Ofs9fq+0jARrJB2nYlO0s;Ynz<(Bj5dLLAwBD0jJn34+RUjY`5C{ka1Ofs9 zfq+0jARrJB2nYlO0s?{m0|MQfScdkoGRCO|Ue2B1x&9Np&yw{#d+8(_i=o*y?=;)< zEc(yU96v_>I_r+{-TM+lO3BP&~! zFNpj8?+MmHhw9A5kUi<@Zo?nO)Cr553;N3~`tcT>dIsrt3A8-*MN^-6qb +#include +#include +#include +#include + +#define NAPI_AUTO_LENGTH ((size_t)-1) + +typedef void* napi_env; +typedef void* napi_value; +typedef void* napi_callback_info; +typedef napi_value (*napi_callback)(napi_env, napi_callback_info); +typedef int (*napi_create_function_fn)(napi_env, const char*, size_t, napi_callback, void*, napi_value*); +typedef int (*napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value); +typedef int (*napi_get_boolean_fn)(napi_env, bool, napi_value*); +typedef int (*napi_get_cb_info_fn)(napi_env, napi_callback_info, size_t*, napi_value*, napi_value*, void**); +typedef int (*napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, size_t, size_t*); + +static void* node_symbol(const char* name) { + return dlsym(RTLD_DEFAULT, name); +} + +static CGEventFlags modifier_mask_for_name(const char* name) { + if (strcmp(name, "shift") == 0) return kCGEventFlagMaskShift; + if (strcmp(name, "command") == 0) return kCGEventFlagMaskCommand; + if (strcmp(name, "control") == 0) return kCGEventFlagMaskControl; + if (strcmp(name, "option") == 0) return kCGEventFlagMaskAlternate; + return 0; +} + +static napi_value is_modifier_pressed(napi_env env, napi_callback_info info) { + napi_get_cb_info_fn napi_get_cb_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info"); + napi_get_value_string_utf8_fn napi_get_value_string_utf8 = (napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8"); + napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean"); + + bool pressed = false; + if (napi_get_cb_info && napi_get_value_string_utf8) { + size_t argc = 1; + napi_value args[1] = {0}; + if (napi_get_cb_info(env, info, &argc, args, 0, 0) == 0 && argc >= 1 && args[0]) { + char name[16] = {0}; + size_t copied = 0; + if (napi_get_value_string_utf8(env, args[0], name, sizeof(name), &copied) == 0) { + CGEventFlags mask = modifier_mask_for_name(name); + if (mask != 0) { + CGEventFlags flags = CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState); + pressed = (flags & mask) != 0; + } + } + } + } + + napi_value result = 0; + if (napi_get_boolean) napi_get_boolean(env, pressed, &result); + return result; +} + +__attribute__((visibility("default"))) napi_value napi_register_module_v1(napi_env env, napi_value exports) { + napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function"); + napi_set_named_property_fn napi_set_named_property = (napi_set_named_property_fn)node_symbol("napi_set_named_property"); + + napi_value fn = 0; + if (napi_create_function && + napi_set_named_property && + napi_create_function(env, "isModifierPressed", NAPI_AUTO_LENGTH, is_modifier_pressed, 0, &fn) == 0) { + napi_set_named_property(env, exports, "isModifierPressed", fn); + } + + return exports; +} diff --git a/apps/kimi-code/native/win32/prebuilds/win32-arm64/win32-console-mode.node b/apps/kimi-code/native/win32/prebuilds/win32-arm64/win32-console-mode.node new file mode 100644 index 0000000000000000000000000000000000000000..42b2c77ccadb03f7ad745fda39583b2e6711d10f GIT binary patch literal 3072 zcmeHIU1%It6#ni^o1_T|tzE22(MeoE#4a^IJ_(&zgB4p1+2BKc*v#%+lhNJTb#}Ja z1Z2UN(t>0}TJ*(MeCRF+5hJ7pCAA8^6x4@2`LOW^^58@5whar?^_)99c2lV$_CX<+ z+&%Yv=V!iq?#;P}4$ngeu!Ux`2`rK1VXEK1{`rQ?eGi=P!{W9}{Y&EDrT)nor)U(s z{IqA~jEv>FdEZD&!z;Ol;~GyrpEPoLTPAvYyW_3YpWOS?SL;WQpN!_4jT5J?D*eRC zD|eWq04pk?k@Dj2^A;QD}s8L@3GaYF!pA zcun>W<#bc3LU3`Ed5T=aph)~#50Pbqcu>q*KkzUIM`JH=Pqc269~!{!HvMmxXUVeH`m&mI^?zp1DHy9w&h#ES*{HM%BQ_X1ex@gD1?5BJAat{yJ@x!Q@OSQ zo6G-v1?tJqD>r)3Xzs_iUBE)u4zp3$Q8h&QE{J_AO&#a1lKy@NKCTH&DxYm;V^v4M zc2i(@O*hZg^jKAhU4zQrYc|+61q=_PJV93_79+_v2oa0`@2pYIW+xgz`Ma-y#UCC- zu&T!%sp&hb99!!&nNx71hxjDFIJ>N;R=Ak;y$_3wM1}T7uEYI z_q7PSAcFfyXa2O0#)yW+7)|n@*9C%|#K#)Yf}d$GTZo5-^4m3iSM{{EZ)F)6`B66; zX|k{CcUKuN6JDb=^ID&Kl6Z_(>dA|W8*{18p~76cL|8-K8}Eg3@i!N`wsPIV{hb#0 zRPnxmAd0!2FV~NA)#ps!a}d>n>+x^L_&}J!ZpkApd8{SPmW;mRa{soo**|ov;H0Ld zpGxQRS!uZ&@Qf!dU#4Cvxf$QdE1S#yMe=v8oU~K)RurV?zYghI>8yOg@%)mNos?eA zajon#ZlT0Yj7HSYdujb8%{cYdcqd!<-Qt%ExMBd@?xMLy`Y$vu(_Ek)*iA9OYmPfK zxF?f$i$r*jdXIQhP|Q3z?G%0Kr3f!&W$MUd9KwU*xtFTG8qbCMG{PR7aJVn|L5GZ1u0dMjw~g2kW83LG_L6M*4Kv1P zG;e$uxOSgxIwz!{D7L#lWoGzxsr*tnO^ zjM}y*i$yv)M)rqCK+Y8R#Mb03$g^V;&y5`%8f?F98@X<3cc21vuRp>Eg8x6>#Y5VG R9l)zNa|aS{ruaWp;CE*fda3{b literal 0 HcmV?d00001 diff --git a/apps/kimi-code/native/win32/prebuilds/win32-x64/win32-console-mode.node b/apps/kimi-code/native/win32/prebuilds/win32-x64/win32-console-mode.node new file mode 100644 index 0000000000000000000000000000000000000000..2c6d86d8725cb88e742a168f0fd9db2e200e4766 GIT binary patch literal 3072 zcmeHIQD_`h6urCK#&qNErmzc93Qp>L1ZkIbH;RHtstJA7t%fA1h`62XjLFFC>@quB zV+4sV5tCshh_w9```-`!sQn7GVhd>vh#J%ng?isadnBG@Sa3 zqt%qEX4`dFnKl%sWh{5L8_C0mjNq4V1aXo(9~{R>Vsy!f4DAALoB#R> zh@}_QUa{n={m+W_6-9VgKi={oxdbjv=^No)L%@h=U-RAjL}v=7cizu^nIT$Sbq72N ze3m)$!!AE}0bu8~*y3s{?)@e@sqf)$ZlD5uh7gF~UG9vk!uv{im;80%G3vSML&7`D zPC>NKDV4KpYau5~9bjS#6Dxq2@V>^bAMmbk@~)lKDcHi>+-$P@DYoWvgR)rs(LKlq zJ|_6UL+$gvSQ=G_%AM4m$W3%|9bSnQO0eRR{{?7g%U)R(<<8E!-1l>)39gnp9x_Do zuKFBk7MNzhGc4Qn(giE{A$6)V=ox_-g0}9+A!cTeqwmzSb}+GlpB4uGcL?vv0zyj-k0m<)xNgb + +#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT +#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200 +#endif + +#define NAPI_AUTO_LENGTH ((unsigned long long)-1) + +typedef void* napi_env; +typedef void* napi_value; +typedef void* napi_callback_info; +typedef napi_value (__cdecl *napi_callback)(napi_env, napi_callback_info); +typedef int (__cdecl *napi_create_function_fn)(napi_env, const char*, unsigned long long, napi_callback, void*, napi_value*); +typedef int (__cdecl *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value); +typedef int (__cdecl *napi_get_boolean_fn)(napi_env, int, napi_value*); + +static void* node_symbol(const char* name) { + HMODULE module = GetModuleHandleA(0); + void* proc = module ? (void*)GetProcAddress(module, name) : 0; + if (proc) return proc; + + module = GetModuleHandleA("node.dll"); + return module ? (void*)GetProcAddress(module, name) : 0; +} + +static napi_value __cdecl enable_virtual_terminal_input(napi_env env, napi_callback_info info) { + (void)info; + + HANDLE handle = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode = 0; + int enabled = handle != INVALID_HANDLE_VALUE && + GetConsoleMode(handle, &mode) && + SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_INPUT); + + napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean"); + napi_value result = 0; + if (napi_get_boolean) napi_get_boolean(env, enabled, &result); + return result; +} + +__declspec(dllexport) napi_value __cdecl napi_register_module_v1(napi_env env, napi_value exports) { + napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function"); + napi_set_named_property_fn napi_set_named_property = (napi_set_named_property_fn)node_symbol("napi_set_named_property"); + + napi_value fn = 0; + if (napi_create_function && + napi_set_named_property && + napi_create_function(env, "enableVirtualTerminalInput", NAPI_AUTO_LENGTH, enable_virtual_terminal_input, 0, &fn) == 0) { + napi_set_named_property(env, exports, "enableVirtualTerminalInput", fn); + } + + return exports; +} diff --git a/apps/kimi-code/package.json b/apps/kimi-code/package.json index 3917f6586..4b05ab8c0 100644 --- a/apps/kimi-code/package.json +++ b/apps/kimi-code/package.json @@ -28,6 +28,7 @@ "files": [ "dist", "dist-web", + "native", "scripts/postinstall.mjs", "scripts/postinstall", "README.md" @@ -75,11 +76,10 @@ }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.9", - "koffi": "^2.16.0", "node-pty": "^1.1.0" }, "devDependencies": { - "@earendil-works/pi-tui": "^0.74.0", + "@earendil-works/pi-tui": "^0.78.1", "@moonshot-ai/acp-adapter": "workspace:^", "@moonshot-ai/kimi-code-oauth": "workspace:^", "@moonshot-ai/kimi-code-sdk": "workspace:^", diff --git a/apps/kimi-code/scripts/native/assets.mjs b/apps/kimi-code/scripts/native/assets.mjs index 7b0560b69..859262449 100644 --- a/apps/kimi-code/scripts/native/assets.mjs +++ b/apps/kimi-code/scripts/native/assets.mjs @@ -17,9 +17,7 @@ export const NATIVE_TARGETS = Object.freeze( SUPPORTED_TARGETS.map((t) => { const deps = resolveTargetDeps(t); const clipboardTarget = deps.find((d) => d.id === 'clipboard-target')?.resolvedName; - const koffiNativeFile = deps.find((d) => d.id === 'koffi')?.nativeFileRelatives?.[0]; - const koffiTriplet = koffiNativeFile?.match(/koffi\/([^/]+)\/koffi\.node$/)?.[1] ?? null; - return [t, { clipboardPackage: clipboardTarget, koffiTriplet }]; + return [t, { clipboardPackage: clipboardTarget }]; }), ), ); @@ -161,16 +159,19 @@ async function collectPackageFiles({ packageName, packageRoot, includeNativeFiles, + includeEntryJs = true, nativeFileRelatives = [], }) { const packageJsonPath = join(packageRoot, 'package.json'); const packageJson = await readJson(packageJsonPath); const selected = new Set([packageJsonPath]); - const entry = resolvePackageEntry(packageRoot, packageJson); - if (entry !== null) { - selected.add(entry); - await addRuntimeDependencyFiles(packageRoot, entry, selected); + if (includeEntryJs) { + const entry = resolvePackageEntry(packageRoot, packageJson); + if (entry !== null) { + selected.add(entry); + await addRuntimeDependencyFiles(packageRoot, entry, selected); + } } for (const nativeFileRelative of nativeFileRelatives) { @@ -250,6 +251,7 @@ export async function collectNativeAssets({ appRoot, target }) { packageName: dep.resolvedName, packageRoot, includeNativeFiles: dep.collect === 'native-files', + includeEntryJs: dep.collect !== 'native-file-only', nativeFileRelatives: dep.nativeFileRelatives, }); const result = await packageManifestEntries({ diff --git a/apps/kimi-code/scripts/native/check-bundle.mjs b/apps/kimi-code/scripts/native/check-bundle.mjs index 1521d6716..39fd70b45 100644 --- a/apps/kimi-code/scripts/native/check-bundle.mjs +++ b/apps/kimi-code/scripts/native/check-bundle.mjs @@ -23,7 +23,7 @@ const optionalRuntimeRequires = new Set([ 'utf-8-validate', ]); const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']); -const handledNativeRuntimeRequires = new Set(['koffi']); +const handledNativeRuntimeRequires = new Set(); function isAllowedSpecifier(specifier) { if (builtins.has(specifier) || specifier.startsWith('node:')) return true; diff --git a/apps/kimi-code/scripts/native/native-deps.mjs b/apps/kimi-code/scripts/native/native-deps.mjs index f195a3cb1..f7a28c139 100644 --- a/apps/kimi-code/scripts/native/native-deps.mjs +++ b/apps/kimi-code/scripts/native/native-deps.mjs @@ -27,13 +27,16 @@ const clipboardSubpackageByTarget = Object.freeze({ 'win32-x64': '@mariozechner/clipboard-win32-x64-msvc', }); -const koffiTripletByTarget = Object.freeze({ - 'darwin-arm64': 'darwin_arm64', - 'darwin-x64': 'darwin_x64', - 'linux-arm64': 'linux_arm64', - 'linux-x64': 'linux_x64', - 'win32-arm64': 'win32_arm64', - 'win32-x64': 'win32_x64', +// pi-tui ships platform-specific native helpers (no Linux build): +// - darwin: Shift-modifier detection for Terminal.app Shift+Enter +// - win32: enable ENABLE_VIRTUAL_TERMINAL_INPUT so Shift+Tab is distinguishable +const piTuiNativeFileByTarget = Object.freeze({ + 'darwin-arm64': ['native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node'], + 'darwin-x64': ['native/darwin/prebuilds/darwin-x64/darwin-modifiers.node'], + 'linux-arm64': [], + 'linux-x64': [], + 'win32-arm64': ['native/win32/prebuilds/win32-arm64/win32-console-mode.node'], + 'win32-x64': ['native/win32/prebuilds/win32-x64/win32-console-mode.node'], }); export function isSupportedTarget(target) { @@ -45,13 +48,15 @@ export function isSupportedTarget(target) { * @property {string} id — stable internal id used for parent refs * @property {(target: string) => string} name * — npm package name (may depend on target) - * @property {'js-only'|'native-files'|'js-and-native-file'|'virtual'} collect + * @property {'js-only'|'native-files'|'js-and-native-file'|'native-file-only'|'virtual'} collect * @property {string|null} parent * — id of another registered dep this nests under (for pnpm), * or null for top-level (resolvable from app root) * @property {(target: string) => string[]} [nativeFileRelatives] * — explicit list of .node files relative to package root - * (used by 'js-and-native-file'; native-files mode auto-scans *.node) + * (used by 'js-and-native-file' and 'native-file-only'; + * native-files mode auto-scans *.node). 'native-file-only' collects + * package.json + these .node files but skips the package entry JS. */ /** @type {readonly NativeDepDescriptor[]} */ @@ -71,17 +76,13 @@ export const nativeDeps = Object.freeze([ { id: 'pi-tui', name: () => '@earendil-works/pi-tui', - // pi-tui is bundled into main.cjs at build time — we don't collect it as - // a native dep, only register it so koffi can declare it as parent. - collect: 'virtual', + // pi-tui's JS is bundled into main.cjs, so only the platform-specific + // native helper (.node under native/) ships alongside the binary — its + // dist/ JS is intentionally NOT collected (it stays in the bundle). This + // keeps the SEA native-asset payload small. Linux has no native helper. + collect: 'native-file-only', parent: null, - }, - { - id: 'koffi', - name: () => 'koffi', - collect: 'js-and-native-file', - parent: 'pi-tui', - nativeFileRelatives: (target) => [`build/koffi/${koffiTripletByTarget[target]}/koffi.node`], + nativeFileRelatives: (target) => piTuiNativeFileByTarget[target] ?? [], }, ]); diff --git a/apps/kimi-code/src/native/module-hook.ts b/apps/kimi-code/src/native/module-hook.ts index bbef5d4cb..db459bbe9 100644 --- a/apps/kimi-code/src/native/module-hook.ts +++ b/apps/kimi-code/src/native/module-hook.ts @@ -1,6 +1,8 @@ +import { existsSync } from 'node:fs'; import { createRequire } from 'node:module'; +import { join } from 'node:path'; -import { loadNativePackage } from './native-require'; +import { getNativePackageRoot } from './native-assets'; type ModuleLoad = (request: string, parent: unknown, isMain: boolean) => unknown; @@ -10,7 +12,16 @@ interface ModuleWithLoad { const nodeRequire = createRequire(import.meta.url); let installed = false; -let loadingNativePackage = false; + +// pi-tui loads its platform-specific native helpers via an absolute-path +// require() computed from import.meta.url / process.execPath +// (see pi-tui dist/terminal.js and dist/native-modifiers.js). In a SEA binary +// those .node files live in the native-asset cache, so redirect any absolute +// require of a pi-tui native helper to the cached copy. +// +// Path shape: native//prebuilds//.node — note the +// two path segments after "prebuilds", so ".+" (not "[^/]+") is required. +const PI_TUI_NATIVE_PATTERN = /native[\\/](?:win32|darwin)[\\/]prebuilds[\\/].+\.node$/; export function installNativeModuleHook(): void { if (installed) return; @@ -26,13 +37,18 @@ export function installNativeModuleHook(): void { parent: unknown, isMain: boolean, ): unknown { - if (request === 'koffi' && !loadingNativePackage) { - loadingNativePackage = true; - try { - const pkg = loadNativePackage('koffi'); - if (pkg !== null) return pkg; - } finally { - loadingNativePackage = false; + if ( + typeof request === 'string' && + PI_TUI_NATIVE_PATTERN.test(request) && + !existsSync(request) + ) { + const pkgRoot = getNativePackageRoot('@earendil-works/pi-tui'); + if (pkgRoot !== null) { + const match = request.match(PI_TUI_NATIVE_PATTERN); + if (match !== null) { + const redirected = join(pkgRoot, match[0]); + return originalLoad.call(this, redirected, parent, isMain); + } } } return originalLoad.call(this, request, parent, isMain); diff --git a/apps/kimi-code/src/native/smoke.ts b/apps/kimi-code/src/native/smoke.ts index 56d39253f..de968194f 100644 --- a/apps/kimi-code/src/native/smoke.ts +++ b/apps/kimi-code/src/native/smoke.ts @@ -1,6 +1,38 @@ +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; + import { getEmbeddedNativeAssetManifest, getNativePackageRoot } from './native-assets'; -const smokePackages = ['@mariozechner/clipboard', 'koffi']; +const smokePackages = ['@mariozechner/clipboard', '@earendil-works/pi-tui']; + +// Verify pi-tui's native helper can actually be loaded through the module hook. +// pi-tui computes native helper paths from process.execPath and require()s them; +// those paths do not exist next to the SEA binary, so this only succeeds when +// installNativeModuleHook() redirects the require into the native-asset cache. +function smokePiTuiNativeLoad(): void { + const platform = process.platform; + const arch = process.arch; + let rel: string | undefined; + if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) { + rel = join('native', 'darwin', 'prebuilds', `darwin-${arch}`, 'darwin-modifiers.node'); + } else if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) { + rel = join('native', 'win32', 'prebuilds', `win32-${arch}`, 'win32-console-mode.node'); + } + if (rel === undefined) return; // Linux: no native helper, nothing to load. + + const req = createRequire(import.meta.url); + const bogusPath = join(dirname(process.execPath), rel); + const helper = req(bogusPath) as { + isModifierPressed?: unknown; + enableVirtualTerminalInput?: unknown; + }; + const ok = + typeof helper.isModifierPressed === 'function' || + typeof helper.enableVirtualTerminalInput === 'function'; + if (!ok) { + throw new Error(`pi-tui native helper loaded but exports are unexpected: ${rel}`); + } +} export function runNativeAssetSmokeIfRequested(): boolean { if (process.env['KIMI_CODE_NATIVE_ASSET_SMOKE'] !== '1') return false; @@ -16,6 +48,7 @@ export function runNativeAssetSmokeIfRequested(): boolean { throw new Error(`Native package is not available: ${packageName}`); } } + smokePiTuiNativeLoad(); process.stdout.write(`Native asset smoke passed: ${manifest.target}\n`); process.exit(0); } catch (error) { diff --git a/apps/kimi-code/test/scripts/native/native-deps.test.ts b/apps/kimi-code/test/scripts/native/native-deps.test.ts index c96b642e6..21bc1183d 100644 --- a/apps/kimi-code/test/scripts/native/native-deps.test.ts +++ b/apps/kimi-code/test/scripts/native/native-deps.test.ts @@ -41,7 +41,7 @@ describe('resolveTargetDeps', () => { const names = deps.map((d) => d.resolvedName); expect(names).toContain('@mariozechner/clipboard'); expect(names).toContain('@mariozechner/clipboard-darwin-arm64'); - expect(names).toContain('koffi'); + expect(names).toContain('@earendil-works/pi-tui'); }); it('picks the right clipboard subpackage per target', () => { @@ -56,13 +56,23 @@ describe('resolveTargetDeps', () => { ).toContain('@mariozechner/clipboard-win32-arm64-msvc'); }); - it('encodes koffi native file path with target triplet', () => { - const linuxKoffi = resolveTargetDeps('linux-arm64').find((d) => d.resolvedName === 'koffi'); - expect(linuxKoffi?.nativeFileRelatives).toEqual(['build/koffi/linux_arm64/koffi.node']); - const macKoffi = resolveTargetDeps('darwin-x64').find((d) => d.resolvedName === 'koffi'); - expect(macKoffi?.nativeFileRelatives).toEqual(['build/koffi/darwin_x64/koffi.node']); - const winArmKoffi = resolveTargetDeps('win32-arm64').find((d) => d.resolvedName === 'koffi'); - expect(winArmKoffi?.nativeFileRelatives).toEqual(['build/koffi/win32_arm64/koffi.node']); + it('encodes pi-tui native file path per target', () => { + const linuxPiTui = resolveTargetDeps('linux-arm64').find( + (d) => d.resolvedName === '@earendil-works/pi-tui', + ); + expect(linuxPiTui?.nativeFileRelatives).toEqual([]); + const macPiTui = resolveTargetDeps('darwin-x64').find( + (d) => d.resolvedName === '@earendil-works/pi-tui', + ); + expect(macPiTui?.nativeFileRelatives).toEqual([ + 'native/darwin/prebuilds/darwin-x64/darwin-modifiers.node', + ]); + const winArmPiTui = resolveTargetDeps('win32-arm64').find( + (d) => d.resolvedName === '@earendil-works/pi-tui', + ); + expect(winArmPiTui?.nativeFileRelatives).toEqual([ + 'native/win32/prebuilds/win32-arm64/win32-console-mode.node', + ]); }); it('throws on unsupported target', () => { @@ -82,9 +92,9 @@ describe('nativeDeps registry shape', () => { expect(target?.parent).toBe('clipboard-host'); }); - it('has koffi (collect=js-and-native-file, parent=pi-tui)', () => { - const koffi = nativeDeps.find((d) => d.id === 'koffi'); - expect(koffi?.collect).toBe('js-and-native-file'); - expect(koffi?.parent).toBe('pi-tui'); + it('has pi-tui (collect=native-file-only, no parent)', () => { + const piTui = nativeDeps.find((d) => d.id === 'pi-tui'); + expect(piTui?.collect).toBe('native-file-only'); + expect(piTui?.parent).toBe(null); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ddb67e8cc..0762021c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,8 +73,8 @@ importers: apps/kimi-code: devDependencies: '@earendil-works/pi-tui': - specifier: ^0.74.0 - version: 0.74.0 + specifier: ^0.78.1 + version: 0.78.1 '@moonshot-ai/acp-adapter': specifier: workspace:^ version: link:../../packages/acp-adapter @@ -142,9 +142,6 @@ importers: '@mariozechner/clipboard': specifier: ^0.3.9 version: 0.3.9 - koffi: - specifier: ^2.16.0 - version: 2.16.0 node-pty: specifier: ^1.1.0 version: 1.1.0 @@ -984,9 +981,9 @@ packages: search-insights: optional: true - '@earendil-works/pi-tui@0.74.0': - resolution: {integrity: sha512-1aIfXZp7D/z+1VlZX8BZcs6pgO8rjmil7kwyhctNDsWvce3Yfl8GVgu4eq+I0Mjhr8Cj+ipBiv9CLIzdoyCOIQ==} - engines: {node: '>=20.0.0'} + '@earendil-works/pi-tui@0.78.1': + resolution: {integrity: sha512-07GVQo/38a0yvIPlWDr3RJn1B8gk3ZuIX9h2oIQ+Biyu3JN0KppWmgWHfaWRydQgse5JtC++KDw5MWaIRnV0mw==} + engines: {node: '>=22.19.0'} '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -2699,9 +2696,6 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/mime-types@2.1.4': - resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -3932,6 +3926,10 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4383,9 +4381,6 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - koffi@2.16.0: - resolution: {integrity: sha512-h/2NJueOKWd0YYycEOWDspomizgNfuOKf/V7ZE2fytvuRtHoY9Tb+y4x6GJ6pFqaVndWn9dLK+sCI14eWtu5rA==} - layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -6867,15 +6862,10 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@earendil-works/pi-tui@0.74.0': + '@earendil-works/pi-tui@0.78.1': dependencies: - '@types/mime-types': 2.1.4 - chalk: 5.6.2 - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 marked: 15.0.12 - mime-types: 3.0.2 - optionalDependencies: - koffi: 2.16.0 '@emnapi/core@1.10.0': dependencies: @@ -8189,8 +8179,6 @@ snapshots: '@types/mdurl@2.0.0': {} - '@types/mime-types@2.1.4': {} - '@types/ms@2.1.0': {} '@types/node@12.20.55': {} @@ -9621,6 +9609,8 @@ snapshots: get-east-asian-width@1.5.0: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -10101,9 +10091,6 @@ snapshots: kind-of@6.0.3: {} - koffi@2.16.0: - optional: true - layout-base@1.0.2: {} layout-base@2.0.1: {}