From 33d2de52d8ec0c2c7501267603e82a68b3083efc Mon Sep 17 00:00:00 2001 From: notch Date: Wed, 7 Oct 2026 21:31:24 +0900 Subject: [PATCH 1/2] feat(vscode): add the extension icon Three nodes of different kinds (script, file, a person's step) on one loop, in the viewer's voice: colourless marks, amber only on the loop. Contrast against the background: loop 8.73, marks 10.96. One arrowhead only; two read as a refresh symbol. media/icon.svg is the source; scripts/icon.js renders it to icon.png (256px) with headless Chrome or Edge. The package.json in this commit also carries the l10n keys of the next commit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015bPmcKMG3u2dhXdAKAM4sK --- vscode/.vscodeignore | 1 + vscode/icon.png | Bin 0 -> 8452 bytes vscode/media/icon.svg | 10 ++++++++++ vscode/package.json | 30 ++++++++++++++++-------------- vscode/scripts/icon.js | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 vscode/icon.png create mode 100644 vscode/media/icon.svg create mode 100644 vscode/scripts/icon.js diff --git a/vscode/.vscodeignore b/vscode/.vscodeignore index 10d1f3b..d7d447f 100644 --- a/vscode/.vscodeignore +++ b/vscode/.vscodeignore @@ -1,3 +1,4 @@ scripts/** **/*.vsix **/__pycache__/** +media/icon.svg diff --git a/vscode/icon.png b/vscode/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a431e849abe52655787239813e3f81c07ccf0d8 GIT binary patch literal 8452 zcmZ{~Wmr_-7dCum=pH%+28IR!K|nz|RY0Y?yOmO61{h%I5|sui1*AbbB_%}Z?(Xh+ z=J$WE_ser#&!@Ameb&C$I(wh9);{;WBVK8!kPtEu0sugwrmFY`06>@~2*Ah1T#Vm- zvjqSMpr$CN?VG+kN8qnL){J+!kkR)j$?K6w3Sgnu!In*JP03vuA=?!`9&E@@NK+gz zH}B{ANBX8jyLjx4(M&^49o3F$@t9#Xm@Um2hkT072tPi6bH_TR{ve(4tJ+K3`geBq z7q_ihngg&D`+-$-qg~?$3>su(vqmbMvYLyR*r&;`5F+8{8x02~ogs>6P&)mjmdFtsk^-4R+ zr~l#roNH%CI4N6(?{}ImMPyOg)&}S0PxaYv4pn|OT9R4v7l7iEk|aa#Xg^m;-$_B_ z8_X>Z#hmD+UTJAn5C~Hw0lj@5Lm{xEjx4aesw_XgSOsfc7EZe)s`4AIY*?o-n}HZQ zA!b|?7YgHd=CpWGylSLtrsD;?wp6B?+&kxwdK{2XU$8N$1Bc*4Ev#sVK%52*U8$cw zEx9<(c1VvA#yHG_dxyMZg*ns&s~nozS&{a^>g<8*Zb@ZFzPvsP3Wl`+tZ~k((Yk|Z zw@8!76MqEKm_2O}wOAnPWT>E}wIXJ(m_#G1tQ^M2(RFP%Zks|Puj<)%+$bQR9<{Tx z^P4;eg@qtH&F?L^&_CJ;flx43`V$@welIP(ry>`Ef~UMEcCDwXlOwa@>5}s99}O5` zCha>4u`LXw4c1J;`pU}hTd0xoU#x2Eg47JHD+-b;4Zu3Z_&PYU8nGudNe0)HHy}1g zIfy)4=k=jl329lieo2?9+ln-jXIB{UuK0RBZ8~teUMXT(h9Sr6u*{afM!Y2e^p}vny5PauiOCdGY_Yy2F&6Zf zY~IQ4`E321%uPNZ)VIFIilmF>52&a9!vRe8qCLd~-G%X?VeE3Lwm)YYnc62ffR~&O zm}kMYZK*j?$-rVINvsBtkd;LD$2WFwdcw2tFbnfe+V3-|JSs>n#W@=t85`GP5Bl*fqz%?ZVVv)hSo&c^Ou|pKDy#KamNhm zbXu@Z%m@E}Eto<94FHu-Bua#};mV^>*X0I41Q-Ek{|#@Ufi3aExktDT_ItzpTnG7Pl#f*$G-}N~W3Da@nL^^JO0%+}DkSHcAFpmFWycfg$ zqz`#TnQ<8x^u#@j* zWxH&h_s;6CtM+T$`$|ig>2)?!9J-n6Y%nl>G8;e<(CeuWb&;)auycvful^q3htLl8 zT8XfD->$nA9-g1`#&6~VsLGtXlDeB~u<Re+v$+lZ z9hfuCq=O^PaOMRTTN{<08e`{kje=3Z)!LN%MsJV~w14|{5*D4Hu4Y1zm2ex2u74^^ zE;$vTAFqjItrYZ=&^Th*N?m0S?i>9R?lhvV^&t@+osW*r`ZQ*J>C-+43;;Pah=TRl zs+Gc$RyVaA-H(zq&>bM9{kCx6+IV~wUToja&>n!@PIu&O<;YO z_^w77RW^Hfl_<}j@sAcJ=6Y`^b9?Yr4!_p%gSjhV=Iw}(zSaOJOKrUpEOl+ma~g&oxV3|?o>6>kG=h+bsBD;@ zp^5cuO&VUPe{%2Bt0a%*<9f0*Kv7IB3(5{=7aTzGyoT|oFPVbkvHD8Jdwt?XBgmdWLP@?C?XmsQ)h^-WW!JEyz6fRi^5K}W*a2nRRI%m}P(976q?T|4 zHziNslG(x~F2|Uc3nbLqr|wj;pNd_k#g&!D`0JcH&%u6C_cOROsT_Z3SDE#_!!V+>#dRju4T2(2ZvB! z0e}#}%%8sfYqU%;PzHVUNHA^xDT**oBDa4jRlGf@v6Q|o^|twz$D68^0=u6J^08m8 zEeVsV50-j-aw4=t8$;Dk9#~?%(7tV^e4V_gWC2_TEC`dC z?D1k{yG0rFAeQWMu7s*pBFe8L+4|E**G~;e@lUB`-01B^b=@Puvd$C316s=W>?6Ml zDq3zfEzdRRn|*&g-Ug?49vt56Q(vJJ(=@xu z(66F3HUHYfh1hY68Vw5V-o%l0dhAAjnTgW^(%;<#gUH=B&JK&Yl27(k-8WQRiaosu zUa^^a2XeHr}g4!S5+slJwuQauH1WBa?a@k{S#$i|l4jJBwZYUXE5LcA=P zkI6}-TcbtBsDO@o2aQdbCwxE45_=CTlJE@pcnu}-!h^FQFkTBg(B*y^JNx9k z(_m}=tLNgEiX~%Qd^gzlUhg<;WBS@3f36d9bax)!Tj3K|;&2A|Q|9m-b$ksEMp69DFLw{nW zUwhV#M%|c7MEw4Apd&^abI$72(#f`3VV)kJ7JS^i#YH?E%y7Gn{kxbxU60;LcOQ)Wtr?Y2; zxpmH6%u+|^4l#Hn90L!W!cj;fAP|S=&3o#l5T&v}eNqY;of;S4WtKwWgZ(&<4Xv(8 z5AMG%V>6xpmu;S>K1Gr*WQUPEVE)vljxW&dA^6QdnPF}{HVXJ{=|Zkuv+qtWZ^@ia zzGvTM^nf(dZlvL)B(Vw94v*+*FBip`+!W-`TqX$5VusbpX2 zR*vULOH)Gc8s)DYTzqOkDPYpxO@nZB^7=cTm->k0L8fA9y-s(k*V?2v!mP`0hUeSL z2}Q%g(*bdA`Y*-%2MT3KLWHown4r-6v_VbE0L&V2G|>2N2wlEi@)7g?)-VNarr-s- zi*|3s#cs-_LE|M!e@bRn!hZgqO9_fI>P>-P-Sj9>^n54u5&&MilF)q36_H(wdf$*Jb~h?n1D#*s=~kDM$3~c}5w>ZK zg8wvY@iuy#^AiG{s#@>2-Mn1*P%S#|F_7a_$s5r-(GZaN>-rkc+|S zMCNpkc%YqTj8Dlk6F}GykoWZT=Z!lOjye)Ix34VOuMtVbT;B|lU%ZmXtR-%Kr z&|GCfkyF3;`>|kSJmG4(>|34Q%%%i?`8OM#)CLaw# zrmbt1JquA;;J^F3@!m=HtLUF((Ha2+&3}He5#rIJ@awtEsI1)O36>fsSq2<>fCdAv z;0)sKLM!v2Jid_tjvzW!?hu6b?R9?isgE@FCeys-&8jaJ&9ic|DT?`&j9}>}eP1Nl z7-?y6=dbiW%E>~(*_96uNuZb^f8RT5J~=6TH&J3F7?VZoTaG*+z3n1xk82IUE#0+# z$(S5NpZiwG)t)nPz{H49$IEgSpHmU|sCydtV$)G<@uE?Dj8=w(xO<2;l;vvRWql!a znRAKu{M)Lea%Wc=-CVBGICC{?Fe% z{ZepL*Mgu7OHQIgUv|M$vnwkHbc8Y!kB(1K{5(E6kcrE(%`o$^r&-{5t8cYTquolg zq?<&1p6c((cggG@=j(yDfrSr1)~n{UE2mi12P8lD%$4-J;5QjEy3BB^rJDYmjZ6~~ z3X}e9UU)oUF-Th+{Oje{_VTT7%k@l$4O zlwc<6XoagI`E&Td0Kc`hH7mOaC376(nb+C3va+s(W%kJ$D@pHz=?HgvuUeHa0KSe} z*Fumy>xT}3N;w=Q{mRoBIuUQCj*;2ju5cFDUkv9@$45tzw*kt;psL3Lcgzr_^5qTX zOgQLGqE@-jL93i7EL=LwvTDtI%Ez_qq6N)Jpxp2cx1U>7M<)(GE*-U+uNH z^lzWIXRN?{0PY&x|8q(xU-Bb?U9b&{?2dt5Ii1Brj+299rQ>z3@cUz`-Pt2@1_lOJ z49$hqyM4foZRs{oeKNIE4W@~z#q+VXv9nswuX5j91ZjW#tN`o4L!>Sr$U?J0(qQV2 zQ2!&67ZqRBds1JHq||GusdeR7*{-ZctZ?%@p+CJ+rYD0u1=x7*NvXi(ggIda8fiJF zS5_9c$cX1YZF{6r680aD01GVHsQ*D~aO;hngvGUK?zeC`>fP^O0!zqmiUhbSMcDCxSpIY@fs)rP7vN+png|=@#1Aa-Mza`mI!SPxw zg+^^UzY^Ja%{9G0S#p;kC-b*8Ka9@-WmzFz5wAdy!T&jq86DL&J{eR(721g7vH1MC zd1PS$W~KSx@6``8dBxAZy!;#$LDkgOREg8 z01bo)_rMXej7QWyKk6h)F z@jWFh5of|U+%1$2{JiRKZ`!NgAj-C_UL`gUiI6*~>pk4fZ#a3y{T{TrFxv|9w z$loRTy6s6`t_cRlDxUWL7F^yM)6B_Nihj1?N4fife(2$|3dn9Z>$88FM2N>|g$GPC zO;-xH$3Zvos-`wwt?TuL3Li>|D>NC8Vfw(RwjSPXL#7#zi5IAYCvq@NY`NX6gxI0D z(0=6x|NhCZxIlLqf@@G*Eo$5gkE9?5e|?B#zdZ=Lw2kB7 z;1Y5;{`k#BSgDI8)qD<7TdRA%PIU8!OyKTz9vvdti-6Sto}8%yAl~~!>>b>uyKzQt zKFKKG$)+s@Cdq4qwvAp~{W2b7z;>26iWX3=AOmG`hm*5&TfbjmWQOEVynxaK4T6FyP|ic_ z4uN~T78C4~`o>w@2L3OS^RXr>+plyOXYO6TK7qet2&}CG4$w0(V(>*e3C~Sd(!gt; zhnb=TeI-iS$s&a>Dwa!Ujtq_x;xpayZbZ3(Ap$P!|58KBIBXzmRnf(Y@jS~Oi7L%n z>bQFmrO#^kda@_j#|N(!4>3&W?29(*?eEw1-i^tldk;sC>6!jwFJ^vp`T!pE3h<qokhx|PYGxrSJ)5>=RFPPM-;y_Gx6r?prAaCx@H%RPNHn@QAtfr zEq9rr*_}Khe{1r7bLJ<@|B`Vx+4xFO7MgfQW}4=jA1kX?bkZa>ac=2rti5=%3ulqd zkJQH8ODbqf z{#=;O6`zpuor!1tsP46yUflJyA=BM1H&KksWX(Ig4x79{xE&SHyvYn6;lCe}BKhHK z5%A=`vW55zmjG;fxzHd=HlhAoLHEProEPt`K;7EUaQ7yD4>5IN$D?;0IID&_}4XT&~T(Ys06^%06{ zj*%3krAeF@3H4*?*4 zo{Hw4ZO>#{qw@(%A}6S`UC^INri{;`0y-UC8#Y#dd--27r^E!tPP)3aTvUCqan3H% zm?@1%g)Rgc0t}3^6OrL=%+YerrIMyYy5cL^HaA8*etZ^g{+`X>yT}CnoE*=q;t#8W zy3bt|3&sW_XR!JFR+emR+)C56Pkr*T;jX;L203Kr2PbcreKJ9AIJ4*06@RB41oe5o z&LuT2MDlMx9H0VTw?%EvGZG5zut}<}&QHA#O@=?oLF)PJdCgvF>R;bleee0P+8Ai! zi7~BqFQWVzV3lo)W%<>?^=xTLPjjh8otkw7!$jI(IJY`G52!M=4Fc(UF!EvcZdW*; zQQ>PiI2*5-gjo5Jxdi+x7W(Y$zT5`ZU4Ic#R8UGC*Az$yM}H%PU2_8;a&|8`efCwJ zlP};UX1I34&7L7bgFJ@Q^rFzQSLx`Y)=!(>Yq`v?0c`RZuSHs#0=~K6V2L3rSmKI} z>G-S+@2<CGH@T_C5}RmgZW;AeL<{9vs5ub>7uu&mL2O7yPW zQ-j4nUp6|ZBcNxy{KK9BhWEkxk9s9Qx)HynBnpn`tg=uvBK18yofet&%43EJW60iY zP(2TZ*Q2cJS?88q+ngGyQup<-dptj75SkcC6aPyd6u%gD_S14-UvfRVD!1%e;0upV zkA@<8Cxel)Ii1kXeAD<}N}z0Zr2u|9Nq(%AFk6d(cw&jl>KS@*3JmMB`>?cgh`nag ztB$#+|J&wSFj%(7m-Wyiq(VVNSX4&9_Fts`^#0*@T0tHG!QRJ~?Swz&wrwzB~%C8sg6ok74OSN7Ldd)-3$w`8r;9 z4r~6feiuB9z=#_ePR;)k#NTguhokv#s4^_oP#%xVw`6Gj%9uJww`R91acM}(FhYrO zuO`tX z_CDs=kmSryo)jppt^KMmKLalZh3R78aLd$8)0&hq5@2=+V(#(d#~GD*`Jc;=5XG|i zYWS5%o5y21ZqSz&c)$imBekkAd||+~@>VUe!%+z$U)`y9UGda-y|mO9mlyez=ud7s z(}vWSmt_@jHC}wEa2H{#LE>6*Q8C{=K{6X5Foe+19w`0@(OSzSZFdmnctFm7=K>fA1bd~W zI#RFE%F$gMz|*RkJ<2Kl)f(qN?$SGKR6&psD|LS{_yIiT?7-|CBSf>sE;9oeQ_i(u zwdYq1FTb$MWvXdK{GwZZ>K$a%lX`K`|Ms`ctm}mb422=3_CHdG*}liV5mu=jZ^t)~ z2kmfAP4*b<{!Xbj;8DZgIPd*t73XN6RL?=*GqpilzJ3R4HM + + + + + + + + diff --git a/vscode/package.json b/vscode/package.json index 2c06f65..d3de91b 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1,7 +1,7 @@ { "name": "loopfinder", - "displayName": "loopfinder", - "description": "Find the feedback loops in your workspace: trace your scripts with writes blocked, add the human/AI steps, and see every loop, including the ones you never declared.", + "displayName": "%displayName%", + "description": "%description%", "version": "0.1.0", "publisher": "motimotinotch", "license": "MIT", @@ -15,10 +15,6 @@ "engines": { "vscode": "^1.85.0" }, - "categories": [ - "Visualization", - "Other" - ], "keywords": [ "feedback-loop", "data-flow", @@ -46,24 +42,24 @@ "commands": [ { "command": "loopfinder.show", - "title": "Show the loops", + "title": "%command.show%", "category": "loopfinder" }, { "command": "loopfinder.build", - "title": "Build the graph (runs the traced scripts)", + "title": "%command.build%", "category": "loopfinder", "enablement": "isWorkspaceTrusted" }, { "command": "loopfinder.init", - "title": "Set up this workspace", + "title": "%command.init%", "category": "loopfinder", "enablement": "isWorkspaceTrusted" }, { "command": "loopfinder.copyPrompt", - "title": "Copy the request for your AI agent", + "title": "%command.copyPrompt%", "category": "loopfinder" } ], @@ -73,18 +69,18 @@ "loopfinder.allowNetwork": { "type": "boolean", "default": false, - "markdownDescription": "Let traced scripts reach the network. It is always recorded; blocked by default, so the script sees an error." + "markdownDescription": "%config.allowNetwork%" }, "loopfinder.allowSubprocess": { "type": "boolean", "default": false, - "markdownDescription": "Let traced scripts start other programs. Blocked by default." + "markdownDescription": "%config.allowSubprocess%" }, "loopfinder.layoutBudget": { "type": "number", "default": 15, "minimum": 0, - "markdownDescription": "Seconds spent searching each flow's layout during a build." + "markdownDescription": "%config.layoutBudget%" } } } @@ -92,5 +88,11 @@ "scripts": { "prepare-core": "node scripts/prepare.js", "package": "node scripts/prepare.js && npx @vscode/vsce package --no-dependencies" - } + }, + "icon": "icon.png", + "categories": [ + "Visualization", + "Other" + ], + "l10n": "./l10n" } diff --git a/vscode/scripts/icon.js b/vscode/scripts/icon.js new file mode 100644 index 0000000..04066c1 --- /dev/null +++ b/vscode/scripts/icon.js @@ -0,0 +1,33 @@ +// Renders media/icon.svg to icon.png (256px, transparent corners) with headless Chrome or Edge. +// Run by hand when the SVG changes; icon.png is committed. +'use strict'; +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const { pathToFileURL } = require('url'); + +const SIZE = 256; +const ext = path.join(__dirname, '..'); +const browsers = [ + process.env.CHROME, + 'C:/Program Files/Google/Chrome/Application/chrome.exe', + 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/usr/bin/google-chrome', + '/usr/bin/chromium', +].filter(Boolean); +const browser = browsers.find(b => fs.existsSync(b)); +if (!browser) { console.error('no Chrome/Edge found; set CHROME='); process.exit(1); } + +const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'loopfinder-icon-')); +const svg = fs.readFileSync(path.join(ext, 'media', 'icon.svg'), 'utf8').replace(/\s*/, ''); +const page = path.join(tmp, 'icon.html'); +fs.writeFileSync(page, `${svg}`); +const out = path.join(ext, 'icon.png'); +const r = spawnSync(browser, ['--headless', '--disable-gpu', '--hide-scrollbars', '--default-background-color=00000000', + `--window-size=${SIZE},${SIZE}`, `--screenshot=${out}`, `--user-data-dir=${path.join(tmp, 'profile')}`, pathToFileURL(page).href], + { encoding: 'utf8', timeout: 60000 }); +fs.rmSync(tmp, { recursive: true, force: true }); +if (!fs.existsSync(out)) { console.error(r.stderr); process.exit(1); } +console.log(`wrote ${path.relative(process.cwd(), out)}`); From 735dd6df877b1a016a6e920cba667f4f67557c39 Mon Sep 17 00:00:00 2001 From: notch Date: Wed, 7 Oct 2026 21:31:24 +0900 Subject: [PATCH 2/2] fix(vscode): Japanese UI, editor language, graphlib notice From the release check before publishing the extension: - Messages go through vscode.l10n (l10n/bundle.l10n.ja.json); command titles and setting descriptions through package.nls(.ja).json. - The viewer starts in the editor's display language (a language picked in the viewer still wins, then the config's lang). - vscode/README.ja.md, linked from the English README by absolute URL (vsce rewrites relative links against the repository root). - dagre.min.js bundles @dagrejs/graphlib 2.2.4: add LICENSE-graphlib (from the npm tarball) to the npm files, the extension, the viewer's footer and both READMEs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015bPmcKMG3u2dhXdAKAM4sK --- LICENSE-graphlib | 19 ++++++++++++++++ README.ja.md | 2 +- README.md | 2 +- package.json | 1 + public/app.js | 3 ++- public/index.html | 1 + vscode/README.ja.md | 37 +++++++++++++++++++++++++++++++ vscode/README.md | 6 ++++- vscode/extension.js | 39 ++++++++++++++++++--------------- vscode/l10n/bundle.l10n.ja.json | 17 ++++++++++++++ vscode/media/host.js | 6 ++++- vscode/package.nls.ja.json | 11 ++++++++++ vscode/package.nls.json | 11 ++++++++++ vscode/scripts/prepare.js | 2 +- 14 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 LICENSE-graphlib create mode 100644 vscode/README.ja.md create mode 100644 vscode/l10n/bundle.l10n.ja.json create mode 100644 vscode/package.nls.ja.json create mode 100644 vscode/package.nls.json diff --git a/LICENSE-graphlib b/LICENSE-graphlib new file mode 100644 index 0000000..7d7dd94 --- /dev/null +++ b/LICENSE-graphlib @@ -0,0 +1,19 @@ +Copyright (c) 2012-2014 Chris Pettitt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.ja.md b/README.ja.md index 10c6e46..5bf105f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -160,7 +160,7 @@ module.exports = ({ config, graph }) => ({ - [Neoloopy](https://forum.obsidian.md/t/neoloopy-think-in-feedback-loops-a-systems-thinking-tool-in-your-vault/115448) は、手で描いた因果ループ図から強化ループ・均衡ループを見つけます。 - strace や [monkeyfs](https://pypi.org/project/monkeyfs/) はファイルの入出力を横取りします。loopfinder の Python のフックも、同じモンキーパッチの手法です。 -レイアウトは [dagre](https://github.com/dagrejs/dagre)(MIT、`LICENSE-dagre`)。アイコンは [Lucide](https://lucide.dev) の公式 SVG を `scripts/gen-icons.js` で改変せずに取り込んでいます(ISC。`moon` と `info` は Feather 由来で MIT。`LICENSE-lucide`)。 +レイアウトは [dagre](https://github.com/dagrejs/dagre)(MIT、`LICENSE-dagre`)と、それに同梱されている [graphlib](https://github.com/dagrejs/graphlib)(MIT、`LICENSE-graphlib`)。アイコンは [Lucide](https://lucide.dev) の公式 SVG を `scripts/gen-icons.js` で改変せずに取り込んでいます(ISC。`moon` と `info` は Feather 由来で MIT。`LICENSE-lucide`)。 ## 支援について diff --git a/README.md b/README.md index d266655..85e6427 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ module.exports = ({ config, graph }) => ({ - strace and [monkeyfs](https://pypi.org/project/monkeyfs/) intercept file I/O; loopfinder's Python hook uses the same monkey-patching approach. -Layout by [dagre](https://github.com/dagrejs/dagre) (MIT, see `LICENSE-dagre`). Icons from [Lucide](https://lucide.dev), vendored unmodified by `scripts/gen-icons.js` (ISC; `moon` and `info` derive from Feather, MIT; see `LICENSE-lucide`). +Layout by [dagre](https://github.com/dagrejs/dagre) (MIT, see `LICENSE-dagre`), which bundles [graphlib](https://github.com/dagrejs/graphlib) (MIT, see `LICENSE-graphlib`). Icons from [Lucide](https://lucide.dev), vendored unmodified by `scripts/gen-icons.js` (ISC; `moon` and `info` derive from Feather, MIT; see `LICENSE-lucide`). ## Support diff --git a/package.json b/package.json index 8db20cc..fa2c949 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "public/", "LICENSE", "LICENSE-dagre", + "LICENSE-graphlib", "LICENSE-lucide", "README.md", "README.ja.md" diff --git a/public/app.js b/public/app.js index 2cbe3a9..bbfb9ee 100644 --- a/public/app.js +++ b/public/app.js @@ -349,7 +349,8 @@ function fillIcons() { async function main() { fillIcons(); graph = host ? await host.load() : await (await fetch('flow.json', { cache: 'no-store' })).json(); - pickLang(graph.lang); + // Inside VS Code the editor's display language comes before the config's; a choice made here wins over both. + pickLang(host?.lang?.() || graph.lang); const select = document.getElementById('flow'); fillFlows(select); // #flow= opens that flow diff --git a/public/index.html b/public/index.html index 753a50f..807fce1 100644 --- a/public/index.html +++ b/public/index.html @@ -58,6 +58,7 @@

·
Ko-fi
dagre (MIT) + · graphlib (MIT) · Lucide (ISC) diff --git a/vscode/README.ja.md b/vscode/README.ja.md new file mode 100644 index 0000000..eb6ce06 --- /dev/null +++ b/vscode/README.ja.md @@ -0,0 +1,37 @@ +# loopfinder for VS Code + +[English](README.md) + +**ワークスペースのフィードバックループを見つける。意図せず作っていたものも含めて。** + +[loopfinder](https://github.com/MotimotiNotch/loopfinder) の CLI と画面を、エディタの中で使えるようにしたものです。 +Node を入れる必要はありません。エディタに入っている実行環境で動きます(Python は `.py` のスクリプトを記録するときだけ要ります)。 + +## 使い方 + +コマンドパレットから: + +1. **loopfinder: このワークスペースを準備** — `loopfinder/`(config.json・flows.json・AGENTS.md)を作ります。 +2. 使っている AI エージェント(Claude Code・Codex・Cursor・Copilot など)に頼みます: + *"Read loopfinder/AGENTS.md and survey this workspace."* + (**loopfinder: AI エージェントへの依頼文をコピー** でクリップボードに入ります。)エージェントが `loopfinder/flows.json` を書きます。 +3. **loopfinder: 図をビルド** — 書き込みを止めた状態でスクリプトを動かして記録し、図を組み立てます。 +4. **loopfinder: 輪を表示** — 画面を開きます。ビルドするたびに自動で描き直します(コマンドパレットからでも、 + 端末で `loopfinder build` を実行しても)。右の一覧のパスを押すと、そのファイルが開きます。 + +画面の言語は、最初はエディタの表示言語に合わせます。画面の上の言語ボタンで切り替えると、そちらが優先されます。 + +## 安全について + +ビルドは**スクリプトを実際に動かします**。書き込みは記録して捨て、通信とほかのプログラムの起動は +`loopfinder.allowNetwork` / `loopfinder.allowSubprocess` を有効にしない限り止めます。記録係であって +サンドボックスではないので、動かしたくないコードは記録しないでください。 + +[信頼](https://code.visualstudio.com/docs/editor/workspace-trust)していないワークスペースでは、準備とビルドは +使えません。ビルド済みの図を見ることはできます。 + +`.loopfinder/`(ビルドの出力)にはファイルのパスが並びます。バージョン管理には入れないでください。 + +## 支援 + +[Ko-fi](https://ko-fi.com/motimotinotch) · MIT License · dagre と graphlib(MIT)、Lucide のアイコン(ISC)を同梱 diff --git a/vscode/README.md b/vscode/README.md index 20de74f..08abec8 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -1,5 +1,7 @@ # loopfinder for VS Code +[日本語](https://github.com/MotimotiNotch/loopfinder/blob/main/vscode/README.ja.md) + **Find the feedback loops in your workspace — including the ones you never meant to build.** The [loopfinder](https://github.com/MotimotiNotch/loopfinder) CLI and viewer, inside the editor. No Node install @@ -17,6 +19,8 @@ From the Command Palette: 4. **loopfinder: Show the loops** — opens the viewer. It redraws by itself after every build, from the palette or from `loopfinder build` in a terminal. Click a path in the side pane to open the file. +The viewer starts in the editor's display language. Picking a language with the button at the top of the viewer wins over it. + ## Safety Building **really runs your scripts**. Writes are recorded and dropped; network and subprocesses are blocked @@ -30,4 +34,4 @@ build are off; you can still view a graph that is already built. ## Support -[Ko-fi](https://ko-fi.com/motimotinotch) · MIT License · bundles dagre (MIT) and Lucide icons (ISC) +[Ko-fi](https://ko-fi.com/motimotinotch) · MIT License · bundles dagre and graphlib (MIT) and Lucide icons (ISC) diff --git a/vscode/extension.js b/vscode/extension.js index 09d26ff..31ad8d3 100644 --- a/vscode/extension.js +++ b/vscode/extension.js @@ -17,6 +17,8 @@ const PUBLIC = path.join(CORE, 'public'); const { loadConfig } = require(path.join(CORE, 'src', 'config')); const PROMPT = 'Read loopfinder/AGENTS.md and survey this workspace.'; +// Messages go through vscode.l10n (l10n/bundle.l10n.ja.json); the menus through package.nls.*.json. +const t = (message, ...args) => vscode.l10n.t(message, ...args); const configOf = folder => path.join(folder, 'loopfinder', 'config.json'); let output; @@ -27,16 +29,16 @@ async function pickFolder(needConfig) { const folders = (vscode.workspace.workspaceFolders || []).filter(f => f.uri.scheme === 'file'); const candidates = needConfig ? folders.filter(f => fs.existsSync(configOf(f.uri.fsPath))) : folders; if (!candidates.length) { - if (!folders.length) vscode.window.showWarningMessage('loopfinder: open a folder first.'); + if (!folders.length) vscode.window.showWarningMessage(t('loopfinder: open a folder first.')); else { - const init = 'Set up'; - const choice = await vscode.window.showWarningMessage('loopfinder: no loopfinder/config.json in this workspace.', ...(vscode.workspace.isTrusted ? [init] : [])); + const init = t('Set up'); + const choice = await vscode.window.showWarningMessage(t('loopfinder: no loopfinder/config.json in this workspace.'), ...(vscode.workspace.isTrusted ? [init] : [])); if (choice === init) vscode.commands.executeCommand('loopfinder.init'); } return null; } if (candidates.length === 1) return candidates[0].uri.fsPath; - const pick = await vscode.window.showQuickPick(candidates.map(f => ({ label: f.name, description: f.uri.fsPath })), { placeHolder: 'Which folder?' }); + const pick = await vscode.window.showQuickPick(candidates.map(f => ({ label: f.name, description: f.uri.fsPath })), { placeHolder: t('Which folder?') }); return pick ? pick.description : null; } @@ -57,7 +59,7 @@ function runCli(folder, args, token) { // `enablement` only greys out the menus; a command run from code still arrives here. function trusted() { if (vscode.workspace.isTrusted) return true; - vscode.window.showWarningMessage('loopfinder: trust this workspace first. Building runs its scripts.'); + vscode.window.showWarningMessage(t('loopfinder: trust this workspace first. Building runs its scripts.')); return false; } @@ -66,16 +68,16 @@ async function cmdInit() { const folder = await pickFolder(false); if (!folder) return; const code = await runCli(folder, ['init']); - if (code !== 0) { showFailure('init'); return; } - const copy = 'Copy the request'; + if (code !== 0) { showFailure(t('loopfinder: init failed.')); return; } + const copy = t('Copy the request'); const choice = await vscode.window.showInformationMessage( - `loopfinder: set up. Next, ask your AI agent: "${PROMPT}" It writes loopfinder/flows.json; then build.`, copy); + t('loopfinder: set up. Next, ask your AI agent: "{0}" It writes loopfinder/flows.json; then build.', PROMPT), copy); if (choice === copy) cmdCopyPrompt(); } function cmdCopyPrompt() { vscode.env.clipboard.writeText(PROMPT); - vscode.window.setStatusBarMessage('loopfinder: copied the request for your AI agent', 4000); + vscode.window.setStatusBarMessage(t('loopfinder: copied the request for your AI agent'), 4000); } async function cmdBuild() { @@ -87,16 +89,16 @@ async function cmdBuild() { if (settings.get('allowNetwork')) args.push('--allow-network'); if (settings.get('allowSubprocess')) args.push('--allow-subprocess'); const code = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: 'loopfinder: tracing the scripts and building the graph', cancellable: true }, + { location: vscode.ProgressLocation.Notification, title: t('loopfinder: tracing the scripts and building the graph'), cancellable: true }, (_, token) => runCli(folder, args, token)); - if (code !== 0) { showFailure('build'); return; } + if (code !== 0) { showFailure(t('loopfinder: build failed.')); return; } // An open viewer picks the new graph up through its watcher. if (!viewer || viewer.folder !== folder) show(folder); } -async function showFailure(what) { - const open = 'Show output'; - if (await vscode.window.showErrorMessage(`loopfinder: ${what} failed.`, open) === open) output.show(); +async function showFailure(message) { + const open = t('Show output'); + if (await vscode.window.showErrorMessage(message, open) === open) output.show(); } async function cmdShow() { @@ -111,8 +113,8 @@ function readGraph(config) { async function show(folder) { const config = loadConfig(configOf(folder)); if (!fs.existsSync(config.output)) { - const build = 'Build'; - const choice = await vscode.window.showInformationMessage('loopfinder: no graph yet. Build it first.', ...(vscode.workspace.isTrusted ? [build] : [])); + const build = t('Build'); + const choice = await vscode.window.showInformationMessage(t('loopfinder: no graph yet. Build it first.'), ...(vscode.workspace.isTrusted ? [build] : [])); if (choice === build) cmdBuild(); return; } @@ -128,7 +130,8 @@ async function show(folder) { const send = () => { const graph = readGraph(config); // A half-written file fails to parse; the watcher fires again when the write finishes. - if (graph) panel.webview.postMessage({ type: 'graph', graph }); + // The editor's display language is the viewer's default language (a choice made in the viewer wins). + if (graph) panel.webview.postMessage({ type: 'graph', graph, lang: vscode.env.language }); }; panel.webview.onDidReceiveMessage(m => { if (m?.type === 'ready') send(); @@ -158,7 +161,7 @@ function openPath(config, p) { try { stat = fs.statSync(file); } catch { /* not there */ } if (stat?.isFile()) vscode.commands.executeCommand('vscode.open', vscode.Uri.file(file), { preview: true, viewColumn: vscode.ViewColumn.Beside }); else if (stat?.isDirectory()) vscode.commands.executeCommand('revealInExplorer', vscode.Uri.file(file)); - else vscode.window.showInformationMessage(`loopfinder: not a single file: ${p}`); + else vscode.window.showInformationMessage(t('loopfinder: not a single file: {0}', p)); } // The viewer's own index.html, with its files served through the webview and a CSP that allows only them. diff --git a/vscode/l10n/bundle.l10n.ja.json b/vscode/l10n/bundle.l10n.ja.json new file mode 100644 index 0000000..9b6ff92 --- /dev/null +++ b/vscode/l10n/bundle.l10n.ja.json @@ -0,0 +1,17 @@ +{ + "loopfinder: open a folder first.": "loopfinder: 先にフォルダを開いてください。", + "Set up": "準備する", + "loopfinder: no loopfinder/config.json in this workspace.": "loopfinder: このワークスペースには loopfinder/config.json がありません。", + "Which folder?": "どのフォルダで動かしますか?", + "loopfinder: trust this workspace first. Building runs its scripts.": "loopfinder: 先にこのワークスペースを信頼してください。ビルドは中のスクリプトを実際に動かします。", + "loopfinder: init failed.": "loopfinder: 準備に失敗しました。", + "Copy the request": "依頼文をコピー", + "loopfinder: set up. Next, ask your AI agent: \"{0}\" It writes loopfinder/flows.json; then build.": "loopfinder: 準備しました。次に AI エージェントに「{0}」と頼んでください。エージェントが loopfinder/flows.json を書いたら、ビルドします。", + "loopfinder: copied the request for your AI agent": "loopfinder: AI エージェントへの依頼文をコピーしました", + "loopfinder: tracing the scripts and building the graph": "loopfinder: スクリプトを記録して図を組み立てています", + "loopfinder: build failed.": "loopfinder: ビルドに失敗しました。", + "Show output": "出力を見る", + "Build": "ビルド", + "loopfinder: no graph yet. Build it first.": "loopfinder: まだ図がありません。先にビルドしてください。", + "loopfinder: not a single file: {0}": "loopfinder: 1つのファイルではありません: {0}" +} diff --git a/vscode/media/host.js b/vscode/media/host.js index 0392c56..bf8b01c 100644 --- a/vscode/media/host.js +++ b/vscode/media/host.js @@ -1,14 +1,17 @@ // The bridge between the viewer (public/app.js) and the extension. Loaded before app.js, which looks for // window.loopfinderHost: the graph comes by message instead of fetch('flow.json'), a rebuilt graph comes -// the same way, a path click opens the file in the editor, and "auto" theme follows the editor's theme. +// the same way, a path click opens the file in the editor, "auto" theme follows the editor's theme, and the +// editor's display language is the default language. (() => { const vscode = acquireVsCodeApi(); let waiting = null; // load() before the first graph arrived let onGraph = null; // later graphs (after a build) + let lang = null; // the editor's display language, e.g. "ja" or "en-us" window.addEventListener('message', e => { const m = e.data; if (!m || m.type !== 'graph') return; + if (typeof m.lang === 'string') lang = m.lang.split('-')[0]; if (waiting) { const resolve = waiting; waiting = null; resolve(m.graph); } else if (onGraph) onGraph(m.graph); }); @@ -27,6 +30,7 @@ }, onGraph(fn) { onGraph = fn; }, open(path) { vscode.postMessage({ type: 'open', path }); }, + lang: () => lang, theme, onTheme(fn) { new MutationObserver(fn).observe(document.body, { attributes: true, attributeFilter: ['class'] }); }, }; diff --git a/vscode/package.nls.ja.json b/vscode/package.nls.ja.json new file mode 100644 index 0000000..151c223 --- /dev/null +++ b/vscode/package.nls.ja.json @@ -0,0 +1,11 @@ +{ + "displayName": "loopfinder", + "description": "ワークスペースのフィードバックループを見つける。書き込みを止めてスクリプトを記録し、人と AI の段を足して、宣言していないものも含めてすべての輪を数える。", + "command.show": "輪を表示", + "command.build": "図をビルド(記録するスクリプトを実際に動かす)", + "command.init": "このワークスペースを準備", + "command.copyPrompt": "AI エージェントへの依頼文をコピー", + "config.allowNetwork": "記録するスクリプトにネットワークへの接続を許す。接続は常に記録される。既定では止めるので、スクリプトにはエラーが返る。", + "config.allowSubprocess": "記録するスクリプトにほかのプログラムの起動を許す。既定では止める。", + "config.layoutBudget": "ビルドのとき、流れごとに並び順を探す秒数。" +} diff --git a/vscode/package.nls.json b/vscode/package.nls.json new file mode 100644 index 0000000..2c0669f --- /dev/null +++ b/vscode/package.nls.json @@ -0,0 +1,11 @@ +{ + "displayName": "loopfinder", + "description": "Find the feedback loops in your workspace: trace your scripts with writes blocked, add the human/AI steps, and see every loop, including the ones you never declared.", + "command.show": "Show the loops", + "command.build": "Build the graph (runs the traced scripts)", + "command.init": "Set up this workspace", + "command.copyPrompt": "Copy the request for your AI agent", + "config.allowNetwork": "Let traced scripts reach the network. It is always recorded; blocked by default, so the script sees an error.", + "config.allowSubprocess": "Let traced scripts start other programs. Blocked by default.", + "config.layoutBudget": "Seconds spent searching each flow's layout during a build." +} diff --git a/vscode/scripts/prepare.js b/vscode/scripts/prepare.js index d94c1a2..f5c9762 100644 --- a/vscode/scripts/prepare.js +++ b/vscode/scripts/prepare.js @@ -12,7 +12,7 @@ fs.rmSync(core, { recursive: true, force: true }); for (const name of ['bin', 'src', 'public', 'trace', 'package.json']) { fs.cpSync(path.join(repo, name), path.join(core, name), { recursive: true, filter: src => !src.includes('__pycache__') }); } -for (const name of ['LICENSE', 'LICENSE-dagre', 'LICENSE-lucide']) { +for (const name of ['LICENSE', 'LICENSE-dagre', 'LICENSE-graphlib', 'LICENSE-lucide']) { fs.copyFileSync(path.join(repo, name), path.join(ext, name)); }