Compare commits

...

538 commits
3.2.8 ... main

Author SHA1 Message Date
murashit
8dc4d82c26 chore(release): 5.2.1 2026-07-22 14:42:14 +09:00
murashit
811a4b7158 fix(threads): publish fork activity with list replacement 2026-07-22 13:23:18 +09:00
murashit
05ea87643a refactor(threads): coordinate fork publication atomically 2026-07-22 12:31:16 +09:00
murashit
bb673b2a75 refactor(chat): replace rollback RPC with marker fork 2026-07-22 11:10:22 +09:00
murashit
01a20dcf70 feat(chat): honor app-server direct input capability 2026-07-22 09:47:18 +09:00
murashit
73be2beea7 chore(app-server): update bindings for Codex CLI 0.145.0 2026-07-22 09:09:31 +09:00
murashit
14adac592d chore(deps): apply npm audit dependency updates 2026-07-21 21:07:10 +09:00
murashit
f4616cf945 refactor: consolidate thin module ownership 2026-07-21 20:59:09 +09:00
murashit
67289765c6 refactor(selection-rewrite): consolidate transport output 2026-07-21 20:59:09 +09:00
murashit
ab0a9047f4 refactor(diff): replace custom LCS with jsdiff 2026-07-21 20:58:23 +09:00
murashit
488344a74d refactor(markdown): derive code ranges from mdast 2026-07-21 20:57:59 +09:00
murashit
6bdd9e5972 refactor(archive): consolidate export ownership 2026-07-21 20:57:49 +09:00
murashit
5b6fce1a4d fix(archive): normalize markdown links with mdast 2026-07-21 19:40:52 +09:00
murashit
05f9740e8e chore(release): 5.2.0 2026-07-21 17:05:36 +09:00
murashit
6b2a20cf0d perf(chat): resume before shared resource hydration 2026-07-21 16:58:21 +09:00
murashit
d0965d19b1 feat(context): replace metadata envelopes with thread links 2026-07-21 16:39:27 +09:00
murashit
381e2a5467 chore(release): 5.1.2 2026-07-21 14:40:40 +09:00
murashit
03305a47ef fix(context): hide normalized resume manifests 2026-07-21 14:35:33 +09:00
murashit
d74747fbf0 fix(threads): serialize rename saves 2026-07-21 14:33:04 +09:00
murashit
f707da0274 fix(threads): keep errors out of navigation rows 2026-07-21 14:25:44 +09:00
murashit
2fb7d29841 fix(threads): disable unavailable auto-name actions 2026-07-21 14:25:44 +09:00
murashit
8a798a5bca fix(ui): disable operations with unmet preconditions 2026-07-21 14:25:44 +09:00
murashit
689368fe8d docs(design): prefer coherent UI over explanatory copy 2026-07-21 13:19:03 +09:00
murashit
5cd106973c refactor(threads): make auto-name exclusive and cancellable 2026-07-21 10:43:01 +09:00
murashit
f3d987ee03 refactor(settings): serialize dynamic refreshes 2026-07-21 10:26:57 +09:00
murashit
bbe311c4de refactor(chat): flatten session runtime indirection 2026-07-21 10:09:47 +09:00
murashit
4f3ebcaa31 docs: remove defensive reference caveats 2026-07-21 08:29:05 +09:00
murashit
96b88c1715 test: remove obsolete specification guards 2026-07-21 07:11:27 +09:00
murashit
193b680f06 refactor(runtime): remove obsolete lifetime and query abstractions 2026-07-20 23:47:49 +09:00
murashit
2d9f98f434 refactor(query): consolidate runtime-owned resources 2026-07-20 23:35:21 +09:00
murashit
ffcc814ba5 refactor(runtime): simplify disposable runtime boundaries 2026-07-20 23:33:43 +09:00
murashit
1f5aa50862 chore(release): 5.1.1 2026-07-20 21:31:36 +09:00
murashit
a2c896d0ae docs(review): require proportional evidence for findings 2026-07-20 19:32:45 +09:00
murashit
205893cc1f fix(thread-picker): keep one active picker per runtime 2026-07-20 19:07:33 +09:00
murashit
b00f6292c4 refactor: remove test-only source exports 2026-07-20 18:22:21 +09:00
murashit
8d952fcfbf chore: ignore local pnpm store 2026-07-20 18:00:02 +09:00
murashit
54dcbd8759 refactor(query): require complete runtime-owned context 2026-07-20 18:00:02 +09:00
murashit
381646ebfb refactor(settings): bind dynamic data to one runtime lease 2026-07-20 18:00:02 +09:00
murashit
aaa04507e0 refactor(chat): make reconnect reset one scoped transition 2026-07-20 17:58:03 +09:00
murashit
a92f3b031d refactor(chat): construct thread lifecycle without late binding 2026-07-20 17:52:33 +09:00
murashit
567c744624 refactor(protocol): isolate released legacy reference decoding
Decode released /refer envelopes and file references only in a dedicated protocol adapter.
Keep the compatibility layer removable without changing the current message path.
2026-07-20 17:27:57 +09:00
murashit
11c233b8fe refactor(runtime): replace executable-bound runtime atomically
Own executable- and vault-bound resources in one disposable runtime.
On executable changes, detach stable view shells, dispose all bound work,
and publish a fully attached replacement runtime.
2026-07-20 17:27:57 +09:00
murashit
aadcf179bc refactor(runtime): make vault the fixed workspace root
Remove protocol thread cwd from panel state and operations.
Require the immutable vault root wherever path presentation needs a workspace base.
2026-07-20 17:27:56 +09:00
murashit
52c350093a fix(query): align resource refresh semantics 2026-07-20 09:56:39 +09:00
murashit
a35c9f4263 feat(threads): share paginated thread history and search 2026-07-20 09:09:41 +09:00
murashit
dc467695f4 refactor(chat): replace thread stream presenter facade 2026-07-19 06:31:34 +09:00
murashit
1ef344b02a test(chat): split view lifecycle coverage for parallel execution 2026-07-19 06:22:46 +09:00
murashit
f93570848f refactor(chat): derive Threads activity from panel state 2026-07-19 06:17:10 +09:00
murashit
cf0548a0d0 refactor(app-server): make TanStack Query authoritative 2026-07-19 05:40:36 +09:00
murashit
ea665af455 chore(lint): re-enable Obsidian setting definition rule 2026-07-18 20:14:21 +09:00
murashit
7ec4acf2b1 test(chat): cover auto-review audit history 2026-07-18 19:33:10 +09:00
murashit
44128caf2c test(chat): protect stale reducer transitions 2026-07-18 19:33:10 +09:00
murashit
e7efd773b3 test(protocol): protect app-server boundary contracts 2026-07-18 19:28:21 +09:00
murashit
9c71b0eb88 test: expand mutation coverage at protocol boundaries 2026-07-18 19:11:18 +09:00
murashit
d3b426d500 test(runtime): cover approval policy commit state 2026-07-18 19:05:58 +09:00
murashit
d114c3e473 test(chat): protect context manifest placement 2026-07-18 19:05:58 +09:00
murashit
eecf73dc77 fix(chat): preserve accepted MCP elicitation URLs 2026-07-18 19:05:58 +09:00
murashit
358cdd7b7c test(chat): harden context serialization boundaries 2026-07-18 18:18:36 +09:00
murashit
5cd898fabe test(chat): harden stream behavior contracts 2026-07-18 18:18:28 +09:00
murashit
8351156589 test: harden vault and runtime boundaries 2026-07-18 18:18:24 +09:00
murashit
ef5d1d803d feat(chat): show live subagent activity summaries 2026-07-18 18:15:41 +09:00
murashit
760421613d test: add exploratory domain mutation testing 2026-07-18 17:43:34 +09:00
murashit
46ecf3addd test(chat): cover pending request UI boundaries 2026-07-18 14:50:45 +09:00
murashit
93c4ba50a1 test(settings): cover declarative API parity 2026-07-18 14:50:42 +09:00
murashit
d568f9abb1 test(app-server): cover RPC boundary failures 2026-07-18 14:48:22 +09:00
murashit
d1337a6c85 test(ui): cover turn diff and toolbar boundaries 2026-07-18 14:35:05 +09:00
murashit
eb6d4a3c2d test(app-server): cover client and transport lifecycles 2026-07-18 14:35:00 +09:00
murashit
4820fd5821 test(chat): cover pending requests and agent states 2026-07-18 14:34:14 +09:00
murashit
f588719440 test: add source coverage reporting 2026-07-18 14:17:42 +09:00
murashit
6524409313 chore(release): 5.1.0 2026-07-18 12:00:49 +09:00
murashit
d8f4f8a899 refactor(input): distinguish Vault file references from mentions 2026-07-18 11:40:43 +09:00
murashit
9b2585b2bd refactor(context): model Obsidian excerpts as references 2026-07-18 11:03:48 +09:00
murashit
4581ee4c5f feat(context): bound and persist explicit turn context 2026-07-18 10:47:10 +09:00
murashit
56a7662b2e docs: recast README around user workflow 2026-07-18 09:16:06 +09:00
murashit
93d0021391 docs: correct app-server compatibility key 2026-07-18 09:01:09 +09:00
murashit
dfeeb0e4b2 refactor(threads): bound lifecycle event arbitration 2026-07-18 08:23:49 +09:00
murashit
ffb11490ce fix(composer): make attachment saves explicit editing state 2026-07-18 08:23:49 +09:00
murashit
51994c6777 refactor(metadata): publish app-server resources independently 2026-07-18 08:19:02 +09:00
murashit
5e2007c899 refactor(chat): remove redundant auto-title tracking 2026-07-18 07:50:36 +09:00
murashit
4ddb17a27b refactor(chat): narrow lifecycle coordination guarantees 2026-07-18 07:27:01 +09:00
murashit
48bd261f64 refactor(settings): centralize dynamic data ownership 2026-07-18 07:26:21 +09:00
murashit
58f8805328 docs(design): keep architectural guidance conceptual 2026-07-18 07:25:19 +09:00
murashit
6800bcc339 refactor(runtime): simplify thread settings serialization 2026-07-18 07:23:29 +09:00
murashit
562513099f refactor(async): unify scoped operation coordination 2026-07-18 07:23:29 +09:00
murashit
e056fee378 fix(settings): preserve ordered mutation facts 2026-07-18 07:23:29 +09:00
murashit
2de242819e fix(composer): preserve attachment insertion targets 2026-07-18 07:23:29 +09:00
murashit
9ad8ade044 fix(chat): serialize runtime settings commits 2026-07-18 07:23:29 +09:00
murashit
1eb074b361 refactor(chat): model scoped async operations 2026-07-18 07:23:29 +09:00
murashit
716d6a2db5 fix(chat): unsubscribe when leaving running subagents 2026-07-18 07:23:29 +09:00
murashit
4b0218169c fix(chat): centralize active panel operation policy 2026-07-17 20:28:13 +09:00
murashit
ec3bcd0312 feat(settings): support declarative settings search 2026-07-17 11:27:53 +09:00
murashit
7dfc023360 chore(release): 5.0.4 2026-07-17 10:44:59 +09:00
murashit
900d5cd53c chore(release): 5.0.3 2026-07-17 10:26:25 +09:00
murashit
01f5de79b7 chore(release): 5.0.2 2026-07-17 10:08:14 +09:00
murashit
a7fbb2bddc ci: scope compatibility checks to relevant changes 2026-07-17 09:18:38 +09:00
murashit
eff82d1166 chore(deps): update development dependencies 2026-07-17 09:07:45 +09:00
murashit
3dd4c8b9a3 ci(deps): keep TypeScript updates on version 6 2026-07-17 09:07:45 +09:00
murashit
60882f68ea
Merge pull request #29 from murashit/dependabot/npm_and_yarn/preact-10.29.7 2026-07-17 08:39:09 +09:00
murashit
dfff322541 refactor(test): trim development script coverage 2026-07-17 08:34:34 +09:00
murashit
40153a1da5 refactor(test): simplify plugin composition contracts 2026-07-17 08:10:31 +09:00
murashit
ed942d1c9b refactor(test): focus UI tests on observable behavior 2026-07-17 08:10:31 +09:00
murashit
86fc06998b refactor(test): consolidate command and protocol contracts 2026-07-17 08:10:31 +09:00
murashit
c37832d0a3 refactor(test): align tests with owning modules 2026-07-17 08:10:31 +09:00
murashit
760bdf3016 fix(lifecycle): preserve identity across context changes 2026-07-17 06:40:06 +09:00
murashit
d525d97f9e refactor(app-server): bind query caches to resource leases 2026-07-17 06:40:01 +09:00
murashit
bf8c87046b refactor(chat): remove partial state cloning 2026-07-17 06:19:57 +09:00
murashit
edf2b70902 fix(composer): unify suggestion state construction 2026-07-17 06:19:57 +09:00
murashit
cb6e89b1ae fix(threads): scope mutations to their operation context 2026-07-17 06:19:57 +09:00
murashit
f7efd475a4 fix(chat): preserve reconnectable state across disconnects 2026-07-17 06:15:59 +09:00
dependabot[bot]
48401b6201
chore(deps): bump preact from 10.29.4 to 10.29.7
Bumps [preact](https://github.com/preactjs/preact) from 10.29.4 to 10.29.7.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.29.4...10.29.7)

---
updated-dependencies:
- dependency-name: preact
  dependency-version: 10.29.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 14:26:26 +00:00
murashit
e4e69ca060 refactor(test): remove incidental implementation contracts 2026-07-16 22:56:00 +09:00
murashit
a97f80e25e refactor(chat): replace signal mirror with selectors 2026-07-16 22:56:00 +09:00
murashit
359bf1c21e refactor(app-server): inject structured turn runner 2026-07-16 22:56:00 +09:00
murashit
cb172edfb7 refactor(app-server): own context leases in resource store 2026-07-16 22:55:56 +09:00
murashit
d202849313 refactor(chat): model panel thread phase explicitly 2026-07-16 19:22:14 +09:00
murashit
a099e1fffc refactor(chat): split current and connected app-server gateways 2026-07-16 19:22:02 +09:00
murashit
f7e5319c3e refactor(chat): simplify turn and rename transitions 2026-07-16 19:22:02 +09:00
murashit
98a17574e2 refactor(chat): remove obsolete thread-list loaded state 2026-07-16 18:52:14 +09:00
murashit
779d12f401 chore(release): 5.0.1 2026-07-15 18:36:29 +09:00
murashit
a7317fd518 docs: clarify when documentation should change 2026-07-15 18:13:45 +09:00
murashit
5e3eaf9b9f refactor(ui): consolidate shared CSS primitives 2026-07-15 18:12:53 +09:00
murashit
4faa986e17 fix: address CodeQL string escaping and regex findings 2026-07-15 16:07:10 +09:00
murashit
805e7bfdf4 test: simplify grit policy fixtures 2026-07-15 15:13:23 +09:00
murashit
8ae6aab1fb test: focus architecture checks on durable invariants 2026-07-15 14:37:16 +09:00
murashit
bbd981f255 refactor: simplify feature state lifecycles 2026-07-15 14:37:16 +09:00
murashit
c23c453597 refactor(chat): remove forwarding-only interfaces 2026-07-15 14:37:16 +09:00
murashit
7d6b81d367 refactor(app-server): split shared metadata queries 2026-07-15 14:37:16 +09:00
murashit
d010e2ac47 docs: streamline agent guidance 2026-07-14 22:23:59 +09:00
murashit
04341f7258 docs: record context and web import boundaries 2026-07-14 22:17:45 +09:00
murashit
e7ad2ca4c4 fix(web): make context imports safe and cancellable 2026-07-14 22:17:45 +09:00
murashit
2e97525d4f build: verify app-server binding provenance 2026-07-14 22:17:45 +09:00
murashit
ca1a71eaac fix: make app-server context changes atomic 2026-07-14 22:17:34 +09:00
murashit
9624f19a95
Merge pull request #27 from murashit/dependabot/npm_and_yarn/preact-10.29.4 2026-07-14 17:34:40 +09:00
murashit
635353cb99
Merge pull request #26 from murashit/dependabot/npm_and_yarn/preact/signals-2.9.3 2026-07-14 17:34:17 +09:00
murashit
10da7f273e
Merge pull request #25 from murashit/dependabot/npm_and_yarn/dev-dependencies-781ad7fc56 2026-07-14 17:34:05 +09:00
dependabot[bot]
09e2eb05ce
chore(deps): bump the dev-dependencies group across 1 directory with 3 updates
Bumps the dev-dependencies group with 3 updates in the / directory: [moment](https://github.com/moment/moment), [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `moment` from 2.29.4 to 2.30.1
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.4...2.30.1)

Updates `typescript-eslint` from 8.62.1 to 8.63.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.63.0/packages/typescript-eslint)

Updates `vitest` from 4.1.9 to 4.1.10
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest)

---
updated-dependencies:
- dependency-name: moment
  dependency-version: 2.30.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-14 08:28:16 +00:00
dependabot[bot]
788d5cc949
chore(deps): bump preact from 10.29.3 to 10.29.4
Bumps [preact](https://github.com/preactjs/preact) from 10.29.3 to 10.29.4.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.29.3...10.29.4)

---
updated-dependencies:
- dependency-name: preact
  dependency-version: 10.29.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-14 08:27:59 +00:00
dependabot[bot]
f8efaf1806
chore(deps): bump @preact/signals from 2.9.2 to 2.9.3
Bumps [@preact/signals](https://github.com/preactjs/signals/tree/HEAD/packages/preact) from 2.9.2 to 2.9.3.
- [Release notes](https://github.com/preactjs/signals/releases)
- [Changelog](https://github.com/preactjs/signals/blob/main/packages/preact/CHANGELOG.md)
- [Commits](https://github.com/preactjs/signals/commits/@preact/signals@2.9.3/packages/preact)

---
updated-dependencies:
- dependency-name: "@preact/signals"
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-14 08:27:51 +00:00
murashit
41ad68a419 test: avoid cumulative commitlint timeout 2026-07-14 17:18:05 +09:00
murashit
88694b2a6a docs: streamline repository guidance 2026-07-14 16:42:56 +09:00
murashit
8fc051ebd4 chore(release): adopt Conventional Commits and generated release notes 2026-07-14 16:08:25 +09:00
murashit
84b920d0da Bump version to 5.0.0 2026-07-14 11:48:35 +09:00
murashit
00402e0d56 Show web submissions while fetching context 2026-07-14 11:34:42 +09:00
murashit
2c254339cf Show web attachments in message context 2026-07-14 10:45:32 +09:00
murashit
10c69cec3f Replace persistent /clip with transient /web context 2026-07-14 10:18:03 +09:00
murashit
34aca42dc0 Prevent automatic titles from overwriting manual names 2026-07-13 21:26:24 +09:00
murashit
3db509a98a Fix hook trust state transitions 2026-07-13 21:19:31 +09:00
murashit
08cd3f624e Bump version to 4.7.1 2026-07-13 17:12:27 +09:00
murashit
bd74d47ec1 Align side chats with Codex CLI lifecycle 2026-07-13 14:34:19 +09:00
murashit
4d138a3f9b Remove deprecated app-server compatibility consumers 2026-07-13 13:52:22 +09:00
murashit
57cab74ecf Preserve chat shell identity across toolbar changes 2026-07-13 13:38:42 +09:00
murashit
c053888fcd Serialize settings mutations across tab lifecycle 2026-07-13 13:37:35 +09:00
murashit
b92b6d5fd3 Own restored panel state and discard unrestorable side chats 2026-07-13 13:36:31 +09:00
murashit
b30b913c04 Make the chat session runtime the per-panel composition owner 2026-07-12 20:33:19 +09:00
murashit
9d2b750d77 Move selection rewrite behind a feature-owned transport 2026-07-12 20:33:19 +09:00
murashit
8cd3bafaff Keep thread query snapshots raw and publish catalog projections 2026-07-12 20:33:19 +09:00
murashit
ce427a9a6c Move thread workflows behind feature-owned app-server ports 2026-07-12 18:21:05 +09:00
murashit
acd6c418d8 Reset thread catalog lifecycle facts with runtime state 2026-07-12 18:17:58 +09:00
murashit
859ccb2ac1 Tighten app-server boundary contracts 2026-07-12 09:09:25 +09:00
murashit
2364664c2e Make thread cleanup and workspace recovery resilient 2026-07-12 09:09:24 +09:00
murashit
eb56414cd7 Prevent stale shared app-server snapshots 2026-07-12 09:09:24 +09:00
murashit
26adfa4d46 Prevent concurrent chat operations from overwriting active work 2026-07-12 08:52:20 +09:00
murashit
a1ad965dcc Bump version to 4.7.0 2026-07-12 00:20:53 +09:00
murashit
160c344008 Model subagent threads as read-only panels 2026-07-12 00:18:32 +09:00
murashit
697b748aaf Block persistent thread mutations from side chats 2026-07-11 23:47:16 +09:00
murashit
d174f3273c Add ephemeral side chats in new Codex panels 2026-07-11 21:50:05 +09:00
murashit
bcb9759a91 Resolve audited lifecycle, protocol, Vault, and async-boundary debt 2026-07-11 18:28:44 +09:00
murashit
c039e8be7a Bump version to 4.6.1 2026-07-10 21:18:27 +09:00
murashit
45f1a47848 Address Obsidian review feedback 2026-07-10 21:16:46 +09:00
murashit
17835a8b7d Bump version to 4.6.0 2026-07-10 21:05:09 +09:00
murashit
e1e3786531 Clarify composer context reference suggestions 2026-07-10 20:46:54 +09:00
murashit
0c587f72cd Resolve relative daily-note references in the composer 2026-07-10 20:45:40 +09:00
murashit
dec80ff6cf Document intentional native element creation 2026-07-10 16:48:35 +09:00
murashit
3845da80e0
Merge pull request #22 from murashit/dependabot/npm_and_yarn/dev-dependencies-80f8c9e043
Bump the dev-dependencies group across 1 directory with 6 updates
2026-07-10 16:35:25 +09:00
murashit
305a257ace Keep settings lint aligned with Obsidian 1.12 2026-07-10 16:35:03 +09:00
dependabot[bot]
ad50c56090
Bump the dev-dependencies group across 1 directory with 6 updates
---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: eslint-plugin-obsidianmd
  dependency-version: 0.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: knip
  dependency-version: 6.24.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 07:20:35 +00:00
murashit
78a8432bed
Merge pull request #23 from murashit/dependabot/npm_and_yarn/tanstack/query-core-5.101.2
Bump @tanstack/query-core from 5.101.1 to 5.101.2
2026-07-10 16:18:08 +09:00
murashit
e2f5b88f2e
Merge pull request #24 from murashit/dependabot/npm_and_yarn/preact-10.29.3
Bump preact from 10.29.2 to 10.29.3
2026-07-10 16:17:56 +09:00
murashit
d21922f814
Merge pull request #21 from murashit/dependabot/github_actions/actions/attest-4.1.1
Bump actions/attest from 4.1.0 to 4.1.1
2026-07-10 16:16:35 +09:00
dependabot[bot]
1ff3aa8c14
Bump @tanstack/query-core from 5.101.1 to 5.101.2
Bumps [@tanstack/query-core](https://github.com/TanStack/query/tree/HEAD/packages/query-core) from 5.101.1 to 5.101.2.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/query-core/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/query-core@5.101.2/packages/query-core)

---
updated-dependencies:
- dependency-name: "@tanstack/query-core"
  dependency-version: 5.101.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 07:15:43 +00:00
dependabot[bot]
919f9f14b3
Bump preact from 10.29.2 to 10.29.3
Bumps [preact](https://github.com/preactjs/preact) from 10.29.2 to 10.29.3.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.29.2...10.29.3)

---
updated-dependencies:
- dependency-name: preact
  dependency-version: 10.29.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 07:15:37 +00:00
murashit
58bbaf6a5f Bump version to 4.5.2 2026-07-10 16:12:37 +09:00
murashit
164c307bd1 Read Defuddle Markdown output from the content field 2026-07-10 16:07:28 +09:00
murashit
5f22d21dc7 Remove direct query cache access and unused readers 2026-07-10 16:07:28 +09:00
murashit
d9024ba983 Recompute collapsible content overflow after resize 2026-07-10 16:07:28 +09:00
murashit
0cb39fb6dc Clean up textarea measurement mirrors across plugin reloads 2026-07-10 16:07:28 +09:00
murashit
ee0cfa9891 Load thread history incrementally and complete it for search 2026-07-10 16:07:28 +09:00
murashit
2a0fb96ce6 Clean temporary test workspaces and build before checks 2026-07-10 16:07:28 +09:00
murashit
7f09418f7c Align Node 26 tooling and enforce recorded compatibility checks 2026-07-10 16:07:28 +09:00
murashit
5e362add5b Stage generated app-server bindings before replacement 2026-07-10 16:07:28 +09:00
murashit
b6f6d04147 Bound shared query and catalog context retention 2026-07-10 16:07:28 +09:00
murashit
c0e4fc590c Share vault reference providers across chat panels 2026-07-10 16:07:28 +09:00
murashit
e9ca2b0448 Reuse stream indexes across delta updates 2026-07-10 16:07:28 +09:00
murashit
c375f110a2 Use model queries as the sole model metadata source 2026-07-10 16:07:28 +09:00
murashit
dccd63b75b Move connection orchestration into connection actions 2026-07-10 16:07:28 +09:00
murashit
03a026512e Reject unsupported MCP elicitation forms 2026-07-10 16:07:28 +09:00
murashit
9616fe588d Paginate model and MCP server inventories 2026-07-10 16:07:28 +09:00
murashit
d52841240a Validate inbound JSON-RPC messages and contain handler failures 2026-07-10 16:07:28 +09:00
murashit
d417157162 Namespace pending request controls by chat panel 2026-07-10 16:07:28 +09:00
murashit
75bf1a0fb8 Cancel asynchronous UI work after owner disposal 2026-07-10 16:07:28 +09:00
murashit
d28895b214 Scope app-server diagnostics to their owning panel and thread 2026-07-10 16:07:28 +09:00
murashit
2ca7dc42d2 Preserve app-server metadata snapshots atomically 2026-07-10 16:07:28 +09:00
murashit
ca62b40929 Stabilize app-server connection lifecycle boundaries 2026-07-10 16:07:28 +09:00
murashit
5a2dff36c5 Update app-server bindings for Codex 0.144.0
Regenerate experimental app-server TypeScript bindings with Codex CLI 0.144.0 and bump the recorded tested CLI version. Add coverage for hook prompt turn-item mapping so the canonical turn-item display path is covered without requiring live Obsidian smoke validation.
2026-07-10 02:34:18 +09:00
murashit
37051838a8 Flatten runtime slash command details 2026-07-10 02:09:41 +09:00
murashit
b3a770d1b1 Structure runtime slash command details 2026-07-10 02:02:50 +09:00
dependabot[bot]
34e85b9588
Bump actions/attest from 4.1.0 to 4.1.1
Bumps [actions/attest](https://github.com/actions/attest) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/actions/attest/releases)
- [Changelog](https://github.com/actions/attest/blob/main/RELEASE.md)
- [Commits](59d89421af...a1948c3f04)

---
updated-dependencies:
- dependency-name: actions/attest
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-09 13:25:14 +00:00
murashit
e44f95e368 Bump version to 4.5.1 2026-07-09 17:05:53 +09:00
murashit
a297859ec1 Update composer placeholder copy 2026-07-09 09:38:54 +09:00
murashit
085af148e6 Rename dialogue fork action classes 2026-07-09 08:07:16 +09:00
murashit
2debad388b Rename turn transcript summaries 2026-07-09 08:06:57 +09:00
murashit
682a23d712 Clarify steering activity heading 2026-07-09 07:51:43 +09:00
murashit
d544791b3a Align user-facing vocabulary 2026-07-09 07:32:41 +09:00
murashit
35af82b5ed Tighten shared CSS vocabulary 2026-07-09 07:14:06 +09:00
murashit
5c840a277f Tighten remaining internal vocabulary 2026-07-09 07:14:06 +09:00
murashit
4202748cc6 Align thread stream CSS classes 2026-07-09 06:55:51 +09:00
murashit
36ed7fceb3 Align internal thread stream vocabulary 2026-07-09 06:52:13 +09:00
murashit
d81044c17b Move chat app-server actions to owned workflows 2026-07-08 21:17:38 +09:00
murashit
3f2c4fd970 Bump version to 4.5.0 2026-07-08 14:37:17 +09:00
murashit
3060834fda Fix view connection timer tests 2026-07-08 13:50:18 +09:00
murashit
2353f725d1 Open subagent threads from agent summaries 2026-07-08 13:31:54 +09:00
murashit
0ce5ceedd5 Show reasoning effort descriptions in composer suggestions 2026-07-08 11:49:40 +09:00
murashit
19d35f7e33 Update app-server support for Codex CLI 0.143.0 2026-07-08 11:44:19 +09:00
murashit
e83dc59521 Polish toolbar compact action label 2026-07-08 11:06:10 +09:00
murashit
48c0b80f39 Polish slash command help wording 2026-07-08 10:39:42 +09:00
murashit
df9585499c Revert active note mention alias 2026-07-08 06:49:57 +09:00
murashit
d6fc86bb2e Clean up composer controller test subscriptions 2026-07-08 06:43:45 +09:00
murashit
84367e795b Fix composer suggestion equality 2026-07-08 06:36:29 +09:00
murashit
3b42c1dbd4 Bump version to 4.4.1 2026-07-07 22:52:23 +09:00
murashit
a330f181b5 Restore active note mention completion alias 2026-07-07 22:52:02 +09:00
murashit
22be754296 Move protocol boundary filename exception to Biome includes 2026-07-07 22:44:30 +09:00
murashit
a2fd30850b Lighten Grit policy tests 2026-07-07 22:39:06 +09:00
murashit
dffca699f0 Show empty thread context as zero percent 2026-07-07 22:28:09 +09:00
murashit
42af40c299 Make app-server initialize profile explicit 2026-07-07 22:17:19 +09:00
murashit
a9e68e159d Align duplicated markdown and app-server helpers 2026-07-07 21:35:03 +09:00
murashit
1946ecffe0 Align tag behavior with Obsidian internals 2026-07-07 21:13:49 +09:00
murashit
08ccb61737 Bump version to 4.4.0 2026-07-07 20:43:14 +09:00
murashit
4f6732da5d Add Obsidian tag composer completions 2026-07-07 20:24:58 +09:00
murashit
eeece5e14d Bump version to 4.3.1 2026-07-07 09:41:59 +09:00
murashit
c610bc377e Align thread search ranking 2026-07-07 08:08:49 +09:00
murashit
3c684d239b Align vault save path behavior 2026-07-07 08:03:04 +09:00
murashit
95496f356f Polish settings tab copy 2026-07-06 16:47:12 +09:00
murashit
eeefedfc8f Refactor high-noise test setup 2026-07-06 14:38:19 +09:00
murashit
be63c6171d Model app-server metadata probe fallback 2026-07-06 14:28:10 +09:00
murashit
943ddd6b26 Model runtime setting precedence 2026-07-06 14:28:10 +09:00
murashit
947430abbc Model completed turn reconciliation 2026-07-06 14:28:10 +09:00
murashit
b622c9e427 Model chat turn lifecycle ordering 2026-07-06 14:28:10 +09:00
murashit
99257950d7 Model thread catalog event ordering 2026-07-06 14:18:32 +09:00
murashit
fe1ec64355 Model conversation runtime outcomes 2026-07-06 09:21:26 +09:00
murashit
4dd222ab2c Rename active note mention to active 2026-07-05 22:07:15 +09:00
murashit
c0a954d4ab Bump version to 4.3.0 2026-07-05 21:49:54 +09:00
murashit
b045f74a86 Fix Grit policy test isolation 2026-07-05 21:49:54 +09:00
murashit
1e3e34dd84 Add web clipping slash command 2026-07-05 21:33:57 +09:00
murashit
ee93b0c810 Bump version to 4.2.0 2026-07-05 13:49:46 +09:00
murashit
a3d3809e8b Rename active note context setting 2026-07-05 11:26:20 +09:00
murashit
982760f401 Add opt-in active note composer context (#19) 2026-07-05 11:16:59 +09:00
murashit
9f8fb2c5c8 Skip Biome plugins during fix 2026-07-04 20:12:03 +09:00
murashit
17b1dc8574 Clarify Grit policy test intent 2026-07-04 19:54:00 +09:00
murashit
1d90eaf602 Relax over-specified UI contracts 2026-07-04 19:37:35 +09:00
murashit
ba5e292487 Narrow chat session transport exports 2026-07-04 18:14:45 +09:00
murashit
504dae0886 Unify chat thread rename and assembly paths
Route generated title renames through ThreadOperations and inline one-use thread-bundle session helpers.
2026-07-04 17:43:23 +09:00
murashit
ceb85bd251 Collapse redundant design-review indirection
Inline low-value wrappers around app-server transports, runtime resolution, session lifecycle wiring, message-stream projection, and settings lifecycle state.
2026-07-04 17:43:19 +09:00
murashit
b4e4f6251e Separate chat runtime notification events 2026-07-03 17:13:48 +09:00
murashit
210a4632f2
Merge pull request #18 from murashit/dependabot/npm_and_yarn/tanstack/query-core-5.101.1 2026-07-03 10:06:10 +09:00
murashit
36f15b10ef
Merge pull request #17 from murashit/dependabot/npm_and_yarn/dev-dependencies-192533d169 2026-07-03 10:05:39 +09:00
dependabot[bot]
394a900023
Bump @tanstack/query-core from 5.101.0 to 5.101.1
Bumps [@tanstack/query-core](https://github.com/TanStack/query/tree/HEAD/packages/query-core) from 5.101.0 to 5.101.1.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/query-core/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/query-core@5.101.1/packages/query-core)

---
updated-dependencies:
- dependency-name: "@tanstack/query-core"
  dependency-version: 5.101.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 13:24:44 +00:00
dependabot[bot]
a35d0108d4
Bump the dev-dependencies group with 3 updates
Bumps the dev-dependencies group with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@types/node` from 25.9.3 to 26.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `knip` from 6.17.1 to 6.20.0
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.20.0/packages/knip)

Updates `typescript-eslint` from 8.61.1 to 8.62.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: knip
  dependency-version: 6.20.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 13:24:35 +00:00
murashit
ee3efb7c6c Bump version to 4.1.2 2026-07-02 17:18:45 +09:00
murashit
d2cb61e097 Add permission profile runtime selection 2026-07-02 15:13:17 +09:00
murashit
60dba267ba Update app-server types for Codex CLI 0.142.5 2026-07-02 12:23:51 +09:00
murashit
d5390d9752 Clarify controller-adjacent naming 2026-07-02 10:06:27 +09:00
murashit
8568b224dc Bump version to 4.1.1 2026-07-01 20:55:14 +09:00
murashit
65702b3bb0 Fix Windows cmd shim app-server launch 2026-07-01 20:51:01 +09:00
murashit
564c6b43f9 Refactor runtime resolution structure 2026-07-01 20:42:38 +09:00
murashit
070fd261fc Align permission runtime resolution 2026-07-01 20:11:22 +09:00
murashit
6575ad7bf3 Align status diagnostics sections 2026-07-01 19:07:21 +09:00
murashit
48ca57ebe8 Bump version to 4.1.0 2026-07-01 17:22:21 +09:00
murashit
12189e5ddd Add permissions slash command 2026-07-01 17:19:56 +09:00
murashit
b74e049360 Refine runtime status sections 2026-07-01 17:19:56 +09:00
murashit
636a890ed6 Separate approval runtime settings from permissions 2026-07-01 16:59:09 +09:00
murashit
f0651bc0a8 Bump version to 4.0.2 2026-07-01 16:02:08 +09:00
murashit
07c29add11 Show runtime permissions in status panel 2026-07-01 13:02:46 +09:00
murashit
9842ce38c4 Use panel-owned diagnostic probe ids 2026-07-01 07:54:10 +09:00
murashit
7be9a1de24 Move auto-review permission rows to app-server mapper 2026-07-01 07:46:43 +09:00
murashit
5a1d55de22 Rehome shared modules by ownership boundary 2026-07-01 06:43:14 +09:00
murashit
4ed285cedd Document fix-and-check edit loop 2026-06-30 23:24:58 +09:00
murashit
332f11ab7e Simplify settings dynamic section lifecycle 2026-06-30 23:17:05 +09:00
murashit
478e5ef563 Remove thin forwarding wrappers 2026-06-30 22:47:01 +09:00
murashit
158845c374 Clarify normal validation workflow 2026-06-30 22:03:44 +09:00
murashit
a82442a987 Extract MCP status domain types 2026-06-30 22:00:55 +09:00
murashit
8b25c32ce9 Simplify chat panel wiring indirection 2026-06-30 21:08:45 +09:00
murashit
82ef80ece4 Enforce app-server RPC boundary policy 2026-06-30 19:06:17 +09:00
murashit
783b375617 Tighten chat panel UI bridge placement 2026-06-30 12:49:34 +09:00
murashit
a7406997e5 Move chat diagnostics presentation helpers 2026-06-30 12:33:59 +09:00
murashit
4daeae0cab Move thread catalog into thread feature 2026-06-30 12:16:01 +09:00
murashit
0159606b6b Move archive export into thread workflows 2026-06-30 11:58:33 +09:00
murashit
f3c5a527d7 Group chat app-server transports 2026-06-30 11:21:04 +09:00
murashit
7fd1d8e254 Flatten settings app-server adapter 2026-06-30 11:21:04 +09:00
murashit
2b543f5904 Organize chat host wiring modules 2026-06-30 11:21:04 +09:00
murashit
2cb3844011 Enforce chat signal read-model boundaries 2026-06-30 11:20:56 +09:00
murashit
9447d53d57 Introduce chat panel read models 2026-06-30 11:20:56 +09:00
murashit
3d4454debe Bump version to 4.0.1 2026-06-29 17:10:02 +09:00
murashit
d749f4e031 Fix wikilink completion cursor restoration 2026-06-29 17:01:34 +09:00
murashit
e6a9b7d2b2 Refine design and development docs 2026-06-29 16:25:27 +09:00
murashit
b7f1176838 Refine README feature wording 2026-06-29 16:25:27 +09:00
murashit
f77fdc649f Add a screenshot to the README 2026-06-29 15:44:55 +09:00
murashit
9d20e03cbb Fix unsafe turn input text access 2026-06-28 21:33:30 +09:00
murashit
202328ffac Bump version to 4.0.0 2026-06-28 21:15:55 +09:00
murashit
bf26130fcc Prototype composer file attachments 2026-06-28 20:58:15 +09:00
murashit
25bda423cc Prototype Obsidian context references 2026-06-28 19:22:33 +09:00
murashit
7c86d89810 Resolve settings display and selection diff design issues 2026-06-28 17:48:03 +09:00
murashit
70bcd45e5b Tighten design and development docs 2026-06-28 16:42:50 +09:00
murashit
316806c069 Add chat app-server gateway wiring 2026-06-28 16:28:34 +09:00
murashit
1f95937dcf Preserve app-server metadata on probe failures 2026-06-28 15:59:48 +09:00
murashit
26a5267ce7 Calibrate Grit source policy strictness 2026-06-28 14:45:26 +09:00
murashit
207aefdf11 Make app-server protocol adapters generated-aware 2026-06-28 14:26:51 +09:00
murashit
526b7e981e Optimize tool inventory diagnostics 2026-06-28 12:55:34 +09:00
murashit
074a379b68 Address Obsidian review warnings 2026-06-28 12:53:28 +09:00
murashit
2aca116035 Remove stale app-server RPC compatibility shims 2026-06-28 12:45:23 +09:00
murashit
470c00a1f1 Move app-server method semantics into services 2026-06-28 12:15:49 +09:00
murashit
bf1e96d19e Scope thread catalog facts by app-server context 2026-06-28 11:43:55 +09:00
murashit
bd44303366 Bump version to 3.4.6 2026-06-28 09:31:42 +09:00
murashit
489f30d97c Refresh diagnostics before showing /tools 2026-06-28 09:25:12 +09:00
murashit
0b603354c4 Avoid app catalog diagnostics during capability refresh 2026-06-28 09:25:02 +09:00
murashit
34973dd7d7 Align settings controls with Obsidian components 2026-06-28 07:49:28 +09:00
murashit
e548d70356 Fix Windows npm shim app-server launch (#16) 2026-06-28 07:24:31 +09:00
murashit
193f84883e Organize Biome plugin policy configuration 2026-06-27 23:43:56 +09:00
murashit
d860c328be Simplify import boundary Grit references 2026-06-27 23:43:56 +09:00
murashit
2b04ed61e0 Move import type annotation lint to ESLint 2026-06-27 23:43:56 +09:00
murashit
b316481acd Organize Biome Grit policy configuration 2026-06-27 23:43:56 +09:00
murashit
f068607509 Separate settings app-server dynamic data 2026-06-27 23:43:56 +09:00
murashit
ca53d77fed Extract turn diff view feature 2026-06-27 23:43:56 +09:00
murashit
727f5393af Split shared thread capability folders 2026-06-27 23:43:56 +09:00
murashit
a14e3d19e7 Narrow chat host boundary dependencies 2026-06-27 23:43:56 +09:00
murashit
32fb1d629c Separate workspace panel live-state contracts 2026-06-27 23:43:56 +09:00
murashit
56b4c7d0b9 Rehome chat and shared boundary modules 2026-06-27 23:43:56 +09:00
murashit
19dc423f21 Constrain app-server root module placement 2026-06-27 23:43:56 +09:00
murashit
7b1b789f3b Move thread catalog ownership out of workspace 2026-06-27 23:43:56 +09:00
murashit
85739b4766 Tighten chat application boundary contracts 2026-06-27 23:43:56 +09:00
murashit
e515eb904e Move chat thread loading app-server access out of application 2026-06-27 23:43:56 +09:00
murashit
2f187d7e94 Move chat app-server client access out of application 2026-06-27 18:11:30 +09:00
murashit
0f428292f8 Move app-server infra details out of chat application 2026-06-27 16:37:11 +09:00
murashit
f9649cd870 Add explicit source boundary lint rules 2026-06-27 15:45:48 +09:00
murashit
e5f65736d6 Move DOM bridge lint exclusions to Biome includes 2026-06-27 15:22:27 +09:00
murashit
e39d4b90a4 Remove unjustified test-only exports 2026-06-27 14:55:34 +09:00
murashit
582bc0195d Inline low-value source helpers 2026-06-27 14:55:34 +09:00
murashit
7a9f179020 Add Knip cleanup to npm fix 2026-06-27 14:55:30 +09:00
murashit
0bca5b3477 Prune low-value regression tests 2026-06-27 14:55:26 +09:00
murashit
b52fc163dd Render settings tab through Preact shell 2026-06-27 12:44:16 +09:00
murashit
3471abb91e Enforce explicit DOM bridge boundaries 2026-06-27 12:44:16 +09:00
murashit
318ffb4532 Reorganize chat host session graph 2026-06-27 10:55:53 +09:00
murashit
b47a43e763 Group toolbar actions by UI region 2026-06-27 09:19:31 +09:00
murashit
5abc9cf2c3 Unify chat thread navigation actions 2026-06-27 08:51:10 +09:00
murashit
49c45c7166 Simplify chat goal surface contract 2026-06-27 08:38:50 +09:00
murashit
e91476c1b8 Reuse chat shell projections in tests 2026-06-27 08:22:31 +09:00
murashit
1382c3ff50 Consolidate thin helper modules 2026-06-26 21:41:07 +09:00
murashit
89131fd84c Align repository naming conventions 2026-06-26 21:15:38 +09:00
murashit
697f7a59d2 Tighten app-server helper and notification boundaries 2026-06-26 20:09:39 +09:00
murashit
6a85d4f6c6 Limit Vitest check workers 2026-06-26 19:06:11 +09:00
murashit
7f6089f728 Tighten DOM bridge lint policy 2026-06-26 18:05:32 +09:00
murashit
bcf7e10e6e Move chat server request protocol behind app-server boundary 2026-06-26 18:04:28 +09:00
murashit
638cecb991 Fix app-server thread request boundaries 2026-06-26 17:24:42 +09:00
murashit
c35a07bc9c Consolidate Grit policy test runs 2026-06-26 11:52:20 +09:00
murashit
6c9936a68c Minimize development script smoke tests 2026-06-26 11:46:14 +09:00
murashit
61bb05da0f Reduce script test process overhead 2026-06-26 11:44:24 +09:00
murashit
3759f76e90 Trim chat view connection integration tests 2026-06-26 11:36:53 +09:00
murashit
bf5a4cad47 Clarify Biome lint policy consistency 2026-06-26 11:18:30 +09:00
murashit
3f62afa94f Restrict TSX rendering source placement 2026-06-26 10:46:20 +09:00
murashit
5323337d84 Scope generated imports to app-server adapters 2026-06-26 10:46:15 +09:00
murashit
660544687e Refine lower-level import boundary policies 2026-06-26 10:46:15 +09:00
murashit
f71f40b61a Refine generated app-server import boundaries 2026-06-26 10:46:15 +09:00
murashit
d7bb49efbe Refine chat app-server protocol lint boundaries 2026-06-26 10:45:29 +09:00
murashit
4e107dadd3 Use noCommonJs for CommonJS import policy 2026-06-26 10:44:53 +09:00
murashit
1a90cf977d Split app-server protocol Grit boundaries 2026-06-26 10:44:53 +09:00
murashit
5ad3c18d34 Split generated app-server import shape rules 2026-06-26 10:44:53 +09:00
murashit
7c5da3d520 Scope Biome Grit plugins 2026-06-26 10:44:53 +09:00
murashit
dd538d9e44
Merge pull request #13 from murashit/dependabot/github_actions/actions/checkout-7.0.0 2026-06-25 22:55:54 +09:00
murashit
c8bc272ab7
Merge pull request #15 from murashit/dependabot/npm_and_yarn/preact/signals-2.9.2 2026-06-25 22:55:46 +09:00
murashit
9766c9edae
Merge pull request #14 from murashit/dependabot/npm_and_yarn/dev-dependencies-9471cb6c7e 2026-06-25 22:55:33 +09:00
dependabot[bot]
bc88c40201
Bump @preact/signals from 2.9.1 to 2.9.2
Bumps [@preact/signals](https://github.com/preactjs/signals/tree/HEAD/packages/preact) from 2.9.1 to 2.9.2.
- [Release notes](https://github.com/preactjs/signals/releases)
- [Changelog](https://github.com/preactjs/signals/blob/main/packages/preact/CHANGELOG.md)
- [Commits](https://github.com/preactjs/signals/commits/@preact/signals@2.9.2/packages/preact)

---
updated-dependencies:
- dependency-name: "@preact/signals"
  dependency-version: 2.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 13:24:52 +00:00
dependabot[bot]
147136ba52
Bump the dev-dependencies group with 4 updates
Bumps the dev-dependencies group with 4 updates: [eslint](https://github.com/eslint/eslint), [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip), [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `eslint` from 10.4.1 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0)

Updates `knip` from 6.16.1 to 6.17.1
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.17.1/packages/knip)

Updates `typescript-eslint` from 8.61.0 to 8.61.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: knip
  dependency-version: 6.17.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 13:24:45 +00:00
dependabot[bot]
195c891566
Bump actions/checkout from 6.0.3 to 7.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](df4cb1c069...9c091bb21b)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 13:23:55 +00:00
murashit
0a13754de7 Clarify accessibility handling for panel controls 2026-06-25 21:30:12 +09:00
murashit
b8a085b4bf Reduce test-suite bottlenecks and redundant assertions 2026-06-25 21:30:12 +09:00
murashit
dd280f74e7 Isolate app-server protocol boundaries 2026-06-25 21:29:40 +09:00
murashit
2888c0cff0 Tighten Biome GritQL lint policies 2026-06-25 21:29:40 +09:00
murashit
d05b083c12 Rename format script to Biome fix 2026-06-25 16:46:09 +09:00
murashit
fef24552e9 Simplify lint and format configuration 2026-06-25 15:04:18 +09:00
murashit
649dfba360 Render diffs with Preact components 2026-06-25 12:26:41 +09:00
murashit
a23ccfbd82 Move DOM boundary lint to Biome 2026-06-25 12:26:41 +09:00
murashit
199a757618 Replace chat mutation lint with readonly state 2026-06-25 12:26:35 +09:00
murashit
3c21ef6485 Enable stricter TypeScript compiler checks 2026-06-25 12:26:31 +09:00
murashit
30f5a79097 Align Biome commands 2026-06-25 09:27:11 +09:00
murashit
2de2e03409 Add app-server request responder boundary 2026-06-25 08:59:46 +09:00
murashit
6fb9e60dea Enable strict typed ESLint rules 2026-06-25 08:50:37 +09:00
murashit
498632cd7d Speed up GritQL policy tests 2026-06-24 23:27:40 +09:00
murashit
6ac54c494e Simplify check scripts and lint policy layout 2026-06-24 23:08:05 +09:00
murashit
f125a4cf17 Move CSS lint policy to Biome 2026-06-24 22:34:31 +09:00
murashit
6afe931d01 Consolidate ESLint source policy checks 2026-06-24 22:34:31 +09:00
murashit
2898e0f74a Move lint policies to Biome GritQL 2026-06-24 21:44:49 +09:00
murashit
fe41e82a64 Clarify ESLint config tests 2026-06-24 17:17:47 +09:00
murashit
f475fc4491 Speed up slow test suites 2026-06-24 17:11:31 +09:00
murashit
75937b332a Speed up local check preflight 2026-06-24 16:58:18 +09:00
murashit
ca26a53e1b Tighten Biome lint policy 2026-06-24 16:31:04 +09:00
murashit
79a2c7fcbf Eliminate dynamic CSS class exemptions 2026-06-24 15:59:19 +09:00
murashit
1a42558d68 Improve CSS usage dynamic prefix audit 2026-06-24 15:46:35 +09:00
murashit
8edb115e08 Align thread archive export with Obsidian vault paths 2026-06-24 15:25:52 +09:00
murashit
b393000132 Suppress workspace panel restoration console warnings 2026-06-24 15:12:26 +09:00
murashit
47c4ed4631 Add Biome assist import organization 2026-06-24 14:54:53 +09:00
murashit
ce99e76a2b Audit TypeScript lint migration to Biome 2026-06-24 14:54:53 +09:00
murashit
321323532b Introduce Biome formatting and linting 2026-06-24 14:54:53 +09:00
murashit
845194a65a Collapse thin helper modules 2026-06-24 12:26:17 +09:00
murashit
e57eb45933 Clarify repeated test assertions 2026-06-24 11:48:59 +09:00
murashit
b148ada1d0 Parameterize repeated test cases 2026-06-24 11:41:33 +09:00
murashit
beb7497b5c Bump version to 3.4.5 2026-06-24 10:55:40 +09:00
murashit
3ab8b82683 Smooth manual message stream scrolling 2026-06-24 10:49:41 +09:00
murashit
79d0d6ea55 Promote composer page scrolling defaults 2026-06-24 10:12:33 +09:00
murashit
f4f99af890 Refine copied debug details 2026-06-24 09:52:40 +09:00
murashit
c9d5ff46a5 Model layered runtime settings 2026-06-24 08:29:33 +09:00
murashit
15d9753789 Ban handwritten re-exports 2026-06-24 06:41:58 +09:00
murashit
439aca3eb0 Remove unnecessary re-exports 2026-06-24 06:33:19 +09:00
murashit
31c493ed63 Organize chat styles by visible surface 2026-06-24 05:50:00 +09:00
murashit
79a72586a6 Prune low-value chat and runtime tests 2026-06-24 05:50:00 +09:00
murashit
2992621354 Centralize chat domain derivations 2026-06-24 05:49:53 +09:00
murashit
28857413b3 Extract chat session graph responsibilities 2026-06-24 05:49:53 +09:00
murashit
8f14f89098 Clarify docs and skill lint guidance 2026-06-24 05:49:53 +09:00
murashit
412508534c Clarify runtime settings intent workflow 2026-06-24 05:49:53 +09:00
murashit
0d5f3e9a3f Centralize app-server lifecycle state handling 2026-06-24 05:49:53 +09:00
murashit
a2af9320e0 Bump version to 3.4.4 2026-06-23 17:06:43 +09:00
murashit
c2547fd026 Stabilize thread catalog lifecycle facts 2026-06-23 16:57:50 +09:00
murashit
936afbfc96 Clarify release version guidance 2026-06-23 13:58:04 +09:00
murashit
c5de0a7709 Bump version to 3.4.3 2026-06-23 12:58:04 +09:00
murashit
a24a56fabf Refresh thread recency after turn start 2026-06-23 12:46:14 +09:00
murashit
e74b118d04 Use Codex recency ordering for threads 2026-06-23 12:31:13 +09:00
murashit
d51b6338c0 Update Codex app-server bindings for 0.142.0 2026-06-23 12:19:51 +09:00
murashit
5b7c3fcda2 Remove low-value tests 2026-06-23 11:59:18 +09:00
murashit
5d0c83d05b Reduce chat test-only exports 2026-06-23 11:59:18 +09:00
murashit
683a703d85 Consolidate message stream rendering modules 2026-06-23 11:59:18 +09:00
murashit
4cb72e957d Move task progress mapping to app-server 2026-06-23 11:59:18 +09:00
murashit
60ae60c1e1 Consolidate pending request projections 2026-06-23 11:59:18 +09:00
murashit
f5c02b8340 Fold chat state cloning into store 2026-06-23 11:57:28 +09:00
murashit
128e777d9e Return focus to composer after pending request responses 2026-06-23 09:53:46 +09:00
murashit
c63bc1b9df Prioritize active thread in slash completions 2026-06-23 09:45:28 +09:00
murashit
4d9d9762ef Adjust thread title hover action spacing 2026-06-23 08:25:50 +09:00
murashit
46dd9135f0 Consolidate low-cohesion chat modules 2026-06-23 08:10:04 +09:00
murashit
b94c2ac804 Document panel-owned projections 2026-06-22 21:17:43 +09:00
murashit
612782ded9 Coarsen message stream diagnostics 2026-06-22 20:58:43 +09:00
murashit
7d36b552a0 Narrow chat runtime projections 2026-06-22 20:58:43 +09:00
murashit
b6629a75db Coarsen pending approval models 2026-06-22 20:58:43 +09:00
murashit
2ff5c89e2b Hide app-server query observer details 2026-06-22 20:58:43 +09:00
murashit
f474ff49c7 Extract chat state utilities 2026-06-22 20:20:51 +09:00
murashit
d38bdaf32c Classify chat state command selectors 2026-06-22 20:16:09 +09:00
murashit
24b38f8163 Narrow chat shell surface projections 2026-06-22 20:05:23 +09:00
murashit
5feae7757e Document chat signal projection policy 2026-06-22 19:04:55 +09:00
murashit
1aef614890 Derive message stream selectors with signals 2026-06-22 18:50:57 +09:00
murashit
4d4aae59db Use derived signals for chat composer state 2026-06-22 18:35:30 +09:00
murashit
d6214bbdc1 Move chat runtime labels into domain 2026-06-22 17:15:34 +09:00
murashit
98ec2aa6a3 Guard app-server client reuse by context 2026-06-22 16:37:42 +09:00
murashit
71feebcde0 Fix slash command connection gating 2026-06-22 15:54:00 +09:00
murashit
2d490c6fca Represent restored panel presence in threads view 2026-06-22 15:39:36 +09:00
murashit
1639c37ac3 Reconcile restored panel hydration through workspace panels 2026-06-22 15:39:28 +09:00
murashit
18327ff883 Separate restored placeholders from active chat state 2026-06-22 14:43:00 +09:00
murashit
711e300f1b Remove unused threads refresh scheduling 2026-06-22 13:57:53 +09:00
murashit
1b95c91034 Inline chat server request adapters 2026-06-22 13:57:53 +09:00
murashit
1e3e4c9f84 Simplify message stream flow rendering 2026-06-22 13:37:35 +09:00
murashit
1e8a2d4dcc Bump version to 3.4.2 2026-06-22 13:02:20 +09:00
murashit
9698146b11 Replace message stream virtualization with flow scrolling 2026-06-22 13:01:50 +09:00
murashit
255cb6d487 Clarify message virtualizer viewport reconciliation 2026-06-22 11:34:13 +09:00
murashit
efc16c4e98 Fix blank space after collapsible message content shrinks 2026-06-22 10:16:50 +09:00
murashit
bc4e47aa41 Delegate message virtualizer scroll policies to TanStack 2026-06-22 10:16:50 +09:00
murashit
e4a5be4828 Rework message stream scroll reconciliation 2026-06-22 07:59:08 +09:00
murashit
dc91a461d6 Fix work details stream spacing 2026-06-21 23:20:31 +09:00
murashit
6d00c0665d Bump version to 3.4.1 2026-06-21 21:39:43 +09:00
murashit
b7e024ada0 Tighten app-server lifecycle and settings boundaries 2026-06-21 21:27:18 +09:00
murashit
2de0dc8399 Move single-use helpers to owning boundaries 2026-06-21 20:12:00 +09:00
murashit
4582104d08 Share title and chat status invariants 2026-06-21 20:11:56 +09:00
murashit
f8fc93fbdd Consolidate pending request live state 2026-06-21 20:11:49 +09:00
murashit
20c56fa265 Share thread row core projection 2026-06-21 18:08:13 +09:00
murashit
3547898077 Normalize file change execution state 2026-06-21 18:02:25 +09:00
murashit
379ce709c4 Organize runtime override semantics 2026-06-21 17:59:18 +09:00
murashit
12479e3d20 Clarify service tier runtime intent 2026-06-21 17:51:12 +09:00
murashit
776b86dd79 Unify file path normalization 2026-06-21 17:43:57 +09:00
murashit
5eb8bc9933 Clarify thread title semantics 2026-06-21 17:37:22 +09:00
murashit
364be69754 Unify thread rename lifecycle 2026-06-21 17:30:11 +09:00
murashit
4b9575d6fb Track MCP elicitations in panel reuse guards 2026-06-21 16:52:48 +09:00
murashit
fb8f4eee91 Clarify thread catalog promotion semantics 2026-06-21 16:52:48 +09:00
murashit
d0f37a2cae Clarify runtime setting selection types 2026-06-21 16:52:48 +09:00
murashit
2e9d74167d Project pending requests into domain models 2026-06-21 16:52:48 +09:00
murashit
380587e2be Centralize observed query projections 2026-06-21 16:52:48 +09:00
murashit
80edc64443 Centralize thread title projections 2026-06-21 16:52:32 +09:00
murashit
dd9a0b42dc Centralize message stream execution semantics 2026-06-21 16:49:37 +09:00
murashit
91667051ff Centralize stale probe and pending request guards 2026-06-21 15:39:11 +09:00
murashit
3b48ab0b9b Move action invariants behind action APIs 2026-06-21 15:27:25 +09:00
murashit
dbc00031b0 Centralize connected client acquisition 2026-06-21 15:21:02 +09:00
murashit
fe7f63e669 Prune redundant internal helper exports 2026-06-21 15:09:10 +09:00
murashit
caf76c8ef8 Model rename generation lifecycles as state machines 2026-06-21 14:09:21 +09:00
murashit
1e42fe6ea2 Model selection rewrite lifecycle as state machine 2026-06-21 13:51:35 +09:00
murashit
2d6b52799a Model chat turn lifecycle as state machine 2026-06-21 13:45:04 +09:00
murashit
60026aa61d Clarify active thread identity sync boundaries 2026-06-21 08:53:23 +09:00
murashit
5763a2f3de Clarify thread catalog event boundaries 2026-06-21 08:53:15 +09:00
murashit
d38989a38a Refresh thread live state on identity changes 2026-06-21 06:54:57 +09:00
murashit
3d3e48af8b Move custom ESLint plugin to lint script 2026-06-21 06:38:32 +09:00
murashit
17d188ed88 Simplify development and release scripts 2026-06-21 06:38:32 +09:00
murashit
1f483c35fa Bump version to 3.4.0 2026-06-20 18:37:06 +09:00
murashit
059adee7be Streamline README usage guide 2026-06-20 18:30:30 +09:00
murashit
d45cb98c67 Align structured command rows 2026-06-20 17:38:34 +09:00
murashit
759adabb9c Add tool source diagnostics 2026-06-20 17:38:34 +09:00
murashit
58e24197b3 Bump version to 3.3.0 2026-06-20 13:01:46 +09:00
murashit
a2c359ea04 Refine settings descriptions 2026-06-20 12:55:04 +09:00
murashit
e87035ab04 Sync archived settings with thread catalog 2026-06-20 12:50:43 +09:00
murashit
08aad21265 Polish settings dynamic row labels 2026-06-20 12:06:26 +09:00
murashit
76c0a5a907 Group settings rows with Obsidian setting groups 2026-06-20 12:01:10 +09:00
murashit
3c7861fcd1 Limit settings dynamic section rerenders 2026-06-20 11:30:48 +09:00
murashit
d9d0a008a6 Refine settings helper rows 2026-06-20 11:24:24 +09:00
murashit
dad4f9cb5b Organize chat session and virtualizer internals 2026-06-20 09:34:54 +09:00
murashit
f118944787 Run API baseline in release preflight 2026-06-20 09:18:39 +09:00
murashit
d9f65c3b6c Add streaming markdown renderer safety tests 2026-06-20 09:18:12 +09:00
murashit
1914e986ed Align release preflight with CI checks 2026-06-20 09:00:08 +09:00
murashit
b14969cbfb Simplify validation scripts 2026-06-20 09:00:08 +09:00
murashit
880ef3f35f Reduce low-value validation overhead 2026-06-20 09:00:08 +09:00
murashit
e5afddd14f Document plugin version policy 2026-06-19 20:35:42 +09:00
murashit
0cd731621c Clarify Obsidian API baseline policy 2026-06-19 20:13:05 +09:00
murashit
36a1d6baa1 Allow TanStack Virtual patch updates 2026-06-19 20:00:11 +09:00
murashit
13c558311c Separate chat request protocol adapters 2026-06-19 16:45:31 +09:00
murashit
56802a2021 Support MCP elicitation pending requests 2026-06-19 14:31:28 +09:00
murashit
e7ac2a4b7b Bump version to 3.2.9 2026-06-19 11:27:35 +09:00
murashit
6f0fb20bdb Show sleep history as wait status 2026-06-19 10:46:39 +09:00
murashit
357a6d9f9f Update app-server bindings for Codex CLI 0.141.0 2026-06-19 10:33:07 +09:00
murashit
82e5c4bd5c
Merge pull request #11 from murashit/dependabot/npm_and_yarn/dev-dependencies-f5606e4f2c
Bump the dev-dependencies group across 1 directory with 4 updates
2026-06-19 10:29:09 +09:00
dependabot[bot]
e6e6e90d33
Bump the dev-dependencies group across 1 directory with 4 updates
Bumps the dev-dependencies group with 4 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [prettier](https://github.com/prettier/prettier), [stylelint](https://github.com/stylelint/stylelint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@types/node` from 25.9.1 to 25.9.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `prettier` from 3.8.3 to 3.8.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.8.4)

Updates `stylelint` from 17.12.0 to 17.13.0
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/17.12.0...17.13.0)

Updates `typescript-eslint` from 8.60.1 to 8.61.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: stylelint
  dependency-version: 17.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 01:26:31 +00:00
murashit
612288f0e0 Update audited dependency lockfile 2026-06-19 10:23:21 +09:00
murashit
c367afc473 Replace custom parallel runner with concurrently 2026-06-19 10:21:38 +09:00
murashit
d87d56b955 Document release note scope policy 2026-06-19 09:57:49 +09:00
murashit
076f831975 Cache local TypeScript lint 2026-06-19 09:43:52 +09:00
murashit
1065a71c25 Document design boundaries 2026-06-19 09:09:26 +09:00
murashit
43e8d7fc20 Align chat naming conventions 2026-06-18 21:21:10 +09:00
murashit
0062590cba Refactor chat inbound handler 2026-06-18 20:53:32 +09:00
murashit
84184cbf6d Refactor stream markdown renderer 2026-06-18 20:36:11 +09:00
919 changed files with 78557 additions and 47624 deletions

View file

@ -5,32 +5,24 @@ description: Use when updating Codex Panel for a new Codex CLI or app-server API
# Codex Panel App-Server Update
Use this skill when Codex Panel needs to follow Codex CLI or experimental `codex app-server` API changes.
## Ground Rules
- Treat `src/generated/app-server/` as generated output. Do not hand-edit generated bindings.
- Use `docs/design.md` for app-server boundary and source-of-truth decisions before adding compatibility layers or product behavior.
- Prefer current Codex CLI behavior over broad backward compatibility unless a concrete user need requires compatibility code.
- Separate required compatibility fixes from optional cleanup or product improvements.
- Do not rely on local Git hooks. Run required generation and verification commands explicitly.
## Procedure
1. Read the README Compatibility and Development sections, `package.json`, and app-server-related source around requests, notifications, threads, approvals, Plan mode, hooks, and model listing.
2. Compare the README Compatibility table's `codex.testedCliVersion` with the target `codex --version`.
3. Check official Codex CLI or app-server release information when the target version is newer or behavior is uncertain.
4. Regenerate bindings with `npm run generate:app-server-types`.
5. Review generated diffs for protocol changes that affect runtime behavior. If mechanical normalization is needed, update `scripts/normalize-generated-types.mjs` and regenerate instead of patching generated files by hand.
6. Implement only the compatibility changes needed for the target Codex CLI version.
7. Update the README Compatibility table's `codex.testedCliVersion` only after validating against that version.
8. Identify optional follow-ups separately from required compatibility work:
- API changes that allow removing compatibility shims, fallback code, or local workarounds.
- New routes, notifications, fields, or lifecycle signals that could simplify existing implementation.
- New app-server capabilities that could enable Codex Panel product improvements.
9. Report optional follow-ups as proposals only, grouped separately from implemented required fixes unless the user explicitly asks to implement them.
1. Read the README Compatibility section and the relevant design and development guidance, then trace the affected app-server paths.
2. Confirm the recorded, installed, and target Codex CLI versions. Check official release information when behavior is uncertain.
3. Regenerate bindings with `npm run generate:app-server-types`.
4. Review generated diffs for protocol changes that affect runtime behavior. If mechanical normalization is needed, update the normalization code in `scripts/generate-app-server-types.mjs` and regenerate instead of patching generated files by hand.
5. Implement only the required compatibility changes and record the target version after validating it.
6. Report optional cleanup or product follow-ups separately; do not implement them unless requested.
## Verification
- Run `npm run check` after regenerated bindings or compatibility changes.
- If the change must be reflected in Obsidian, run `npm run build` and verify the plugin reload path separately.
- Report the tested Codex CLI version, the generation command used, any app-server compatibility behavior that changed, and any optional simplification or feature proposals found.
- Use the live Obsidian workflow when runtime behavior needs verification.
- Report the tested Codex CLI version, changed compatibility behavior, and optional follow-ups.

View file

@ -5,14 +5,11 @@ description: Use when validating Codex Panel inside a live Obsidian app with the
# Codex Panel Obsidian Dev
Use this skill for live Obsidian checks after building Codex Panel or when investigating behavior that unit tests cannot observe.
## Ground Rules
- Work from the Codex Panel repository root.
- Treat `obsidian plugin:reload id=codex-panel`, `obsidian reload`, `obsidian restart`, `obsidian devtools`, `obsidian eval`, `obsidian dev:cdp`, and `obsidian dev:mobile` as state-changing or intrusive. Ask the user before running them.
- Prefer read-only inspection first: `plugin`, `commands`, `dev:errors`, `dev:dom`, `dev:css`, `dev:console`, `dev:screenshot`, `version`.
- If `dev:console` reports that the debugger is not attached, ask before running `obsidian dev:debug on`.
- Use `docs/development.md` for the build and generated-asset expectations before live Obsidian validation.
- Ask before altering or reloading the live Obsidian session, attaching a debugger, or running intrusive evaluation or input commands.
- Prefer read-only inspection before state-changing commands.
- Do not clear console or error buffers unless the user approves; clearing can destroy useful failure context.
## Workflow
@ -23,13 +20,13 @@ Use this skill for live Obsidian checks after building Codex Panel or when inves
npm run build
```
2. Before reloading the live plugin, ask the user whether it is OK to reload their current Obsidian session. If approved:
2. If approved, reload the plugin when the live session needs the new build:
```bash
obsidian plugin:reload id=codex-panel
```
3. Open the relevant Codex Panel surface when needed:
3. Open the relevant surface when needed:
```bash
obsidian command id=codex-panel:open-panel
@ -46,54 +43,12 @@ Use this skill for live Obsidian checks after building Codex Panel or when inves
obsidian dev:screenshot path=<scratch-path>/codex-panel.png
```
5. Use focused selectors for the behavior under test. Prefer stable Codex Panel classes such as `.codex-panel`, `.codex-panel__composer`, `.codex-panel-threads`, and `.codex-panel-chat-turn-diff`.
5. Use focused Codex Panel-owned selectors found in the relevant implementation or styles.
## Dynamic UI Investigations
When checking behavior that depends on browser layout, asynchronous rendering, or virtualized DOM state, measure the live UI rather than inferring from unit tests alone.
- Capture both the user-visible state and the underlying DOM metrics. Useful fields include `scrollTop`, `scrollHeight`, `clientHeight`, element bounding rects, rendered element counts, inline styles, and relevant computed styles.
- For virtualized or asynchronously rendered regions, compare framework state against DOM state where possible, such as virtualizer total size versus actual `scrollHeight`, rendered item count versus data count, or pre-event versus post-frame measurements.
- Prefer real input paths for input-sensitive bugs. Use `obsidian dev:cdp method=Input.dispatchMouseEvent ...` or focused keyboard events when a synthetic `dispatchEvent()` may skip browser/Electron behavior.
- Record measurements before the action, immediately after the action, after at least one animation frame, and after a short timeout when layout, markdown rendering, resize observers, or virtualizer settle loops may run later.
- If injecting temporary DOM probes with `obsidian eval`, remove them before finishing and re-check `dev:errors`. Avoid leaving long-running eval promises; use bounded `setTimeout`-based scripts that resolve.
- Treat console instrumentation as temporary. Remove debug logging before final validation, and search the touched files for probe markers or conflict markers before reporting.
## Common Checks
- Confirm the loaded version and enabled state:
```bash
obsidian plugin id=codex-panel
```
- List available Codex Panel commands:
```bash
obsidian commands filter=codex-panel
```
- Count rendered elements:
```bash
obsidian dev:dom selector=.codex-panel__composer total
```
- Inspect text, attributes, or computed styles:
```bash
obsidian dev:dom selector=.codex-panel__composer text
obsidian dev:dom selector=.codex-panel__composer attr=aria-label
obsidian dev:dom selector=.codex-panel__composer css=display
obsidian dev:css selector=.codex-panel__composer
```
- Capture a screenshot for visual review:
```bash
obsidian dev:screenshot path=<scratch-path>/codex-panel.png
```
When behavior depends on layout, asynchronous rendering, or virtualized DOM state, capture the visible state and relevant DOM metrics before and after the real input path, then again after rendering settles. Compare framework or data state with the DOM where possible. Remove temporary probes and instrumentation before final validation and re-check runtime errors.
## Reporting
Report the commands run, whether Obsidian was reloaded, key `dev:errors` or `dev:console` findings, and any screenshot path produced. If a command was skipped because it required user approval, state that plainly.
Report the observed behavior, runtime errors, intrusive actions taken, and evidence paths. State when an approval-gated check was skipped.

View file

@ -5,28 +5,23 @@ description: Use when preparing, checking, committing, tagging, pushing, or repa
# Codex Panel Release
Use this skill when delegating Codex Panel release work to an agent. `docs/release.md` is the public procedure and source of truth for the release command sequence. This skill adds agent-facing gates, review duties, and failure handling.
`docs/release.md` is the public procedure and source of truth for the release command sequence. This skill adds agent-facing gates, review duties, and failure handling.
## Ground Rules
- Read and follow `docs/release.md`; do not maintain a parallel release command sequence here.
- Do not assume local Git hooks exist or ran. Run required verification commands explicitly.
- Do not create GitHub Releases locally with `gh release create`; the tag-triggered GitHub Actions workflow owns release creation and asset attachment.
- Keep internal validation notes, procedural details, and implementation reasoning out of release notes.
- Release notes must be short, public-facing bullets under the single `## Changes` section required by `docs/release.md`.
- Treat generated release notes as a draft. Review the full release diff and keep one short, public-facing `## Changes` section without internal or procedural detail.
## Delegation Procedure
1. Read `docs/release.md`, `package.json`, `manifest.json`, `versions.json`, and existing `.github/release-notes/` files.
1. Read `docs/release.md` and inspect the current version metadata and recent release notes.
2. Identify the target release version and the commit range since the previous released tag.
3. Follow the preparation step in `docs/release.md`.
4. Draft `.github/release-notes/X.Y.Z.md` from the full diff since the previous released tag, not only the latest commit.
5. Before committing, ask the user to approve the release version, release-note bullets, and included commit range.
6. After approval, follow `docs/release.md` for the commit, preflight, tag, and push sequence.
7. After pushing, let GitHub Actions create or update the GitHub Release.
3. Inspect the full release diff, run preparation as documented, and edit the draft for completeness, accuracy, and user-facing wording.
4. Before committing, ask the user to approve the release version, reviewed release-note bullets, and included commit range.
5. After approval, follow `docs/release.md` through preflight, tag, and push. Let GitHub Actions create or update the GitHub Release.
## Failure Handling
- If a release script or preflight fails, fix the cause and rerun the explicit failed command.
- If the tag-triggered workflow fails before GitHub Release creation, use the repair procedure in `docs/release.md`.
- If a GitHub Release already exists or was partially created, inspect the state before taking action; do not assume local asset upload is the right recovery path.

View file

@ -4,6 +4,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci(deps)"
cooldown:
default-days: 7
@ -11,6 +13,9 @@ updates:
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(deps)"
prefix-development: "chore(deps)"
cooldown:
default-days: 7
open-pull-requests-limit: 5
@ -24,3 +29,6 @@ updates:
update-types:
- "version-update:semver-minor"
- "version-update:semver-major"
- dependency-name: "typescript"
update-types:
- "version-update:semver-major"

4
.github/release-notes/3.2.9.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Adds compatibility updates for Codex CLI 0.141.0 app-server changes.
- Shows Codex sleep history as wait status in the chat stream.

6
.github/release-notes/3.3.0.md vendored Normal file
View file

@ -0,0 +1,6 @@
## Changes
- Supports MCP elicitation requests in the chat stream.
- Refines Codex Panel settings with Obsidian-style grouped rows and shorter descriptions.
- Keeps archived thread settings in sync when threads are archived, restored, or deleted elsewhere.
- Improves chat message streaming and release validation reliability.

5
.github/release-notes/3.4.0.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Replaces `/mcp` with `/tools` for broader Codex capability diagnostics covering plugins, tool providers, MCP servers, and skills.
- Improves grouped slash-command and diagnostic output so structured command results are easier to scan.
- Streamlines the README usage guide around current panel, thread, composer, and diagnostics workflows.

5
.github/release-notes/3.4.1.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Improves panel state consistency when switching threads, reusing panels, and handling pending requests.
- Hardens turn, rename, title-generation, and selection-rewrite lifecycles so stale status updates are less likely to leak into the UI.
- Improves file-change and runtime-setting status handling across app-server reconnects and diagnostics.

5
.github/release-notes/3.4.2.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Improves chat scrolling so long threads stay anchored more reliably as messages stream in, expand, collapse, or change height.
- Fixes extra blank space that could remain after collapsing large message content.
- Tightens work-details spacing in the message stream.

7
.github/release-notes/3.4.3.md vendored Normal file
View file

@ -0,0 +1,7 @@
## Changes
- Adds support for Codex CLI 0.142.0 and its updated app-server protocol.
- Aligns thread lists and the thread picker with Codex's recency-based ordering, including refreshes after new turns start.
- Improves restored panel handling so reopened thread panels appear in Threads view and hydrate when focused or foregrounded.
- Improves slash-command thread suggestions for `/archive`, `/rename`, `/resume`, and `/refer`, and lets `/reconnect` and `/compact` run while disconnected.
- Returns focus to the composer after responding to pending approvals, user inputs, or MCP elicitations.

3
.github/release-notes/3.4.4.md vendored Normal file
View file

@ -0,0 +1,3 @@
## Changes
- Keep newly started or recently active threads visible and correctly ordered while Codex thread list snapshots catch up.

5
.github/release-notes/3.4.5.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Improve composer and message-stream scrolling behavior, including smoother manual scrolling.
- Refine copied diagnostic details for clearer troubleshooting output.
- Improve runtime settings state handling.

4
.github/release-notes/3.4.6.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Fixed Windows app-server launch when Codex is installed through npm and the configured executable is the `codex.cmd` shim (thanks @Lionel540, #16).
- Fixed capability diagnostics so Codex Panel no longer loads the full app catalog during connection or `/tools`, avoiding sustained Codex app-server CPU and energy use.

6
.github/release-notes/4.0.0.md vendored Normal file
View file

@ -0,0 +1,6 @@
## Changes
- Added `@active-note` and `@selection` composer completions for bringing the current Obsidian editor context into a Codex turn, including selected Markdown text as additional turn context.
- Added paste and drag-and-drop attachment support in the composer. Images are saved to the configured attachment folder as Obsidian embeds, while other files are inserted as wikilinks and sent to Codex as file mentions.
- Improved selection rewrite diffs and fixed settings dynamic-section display issues.
- Improved app-server diagnostics and cached metadata handling so transient probe failures and context changes do not wipe useful connection, tool, or thread information.

4
.github/release-notes/4.0.1.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Fixed wikilink completion so Tab keeps the cursor before the closing brackets for heading references, while Enter accepts the completed link.
- Refreshed the README wording and screenshot.

3
.github/release-notes/4.0.2.md vendored Normal file
View file

@ -0,0 +1,3 @@
## Changes
- Added runtime permission details to the status panel, including the active profile, sandbox and network access, writable roots, approval policy, and reviewer.

4
.github/release-notes/4.1.0.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Added a `/permissions` slash command that shows the current permissions and approval settings.
- Refined the status panel permissions display into Permissions and Approvals sections, with connection diagnostics labeled consistently.

4
.github/release-notes/4.1.1.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Fixed Windows connection failures when the configured Codex executable is an npm-installed command shim such as `codex.cmd`.
- Refined runtime permission and approval status resolution for more consistent diagnostics.

4
.github/release-notes/4.1.2.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Let `/permissions` set the permission profile used for subsequent Codex turns, with composer completions for available profiles.
- Updated app-server compatibility for Codex CLI 0.142.5.

4
.github/release-notes/4.2.0.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Added an opt-in **Reference active note on send** setting that references the current Obsidian note when sending from the composer without changing the prompt text.
- Renamed sent-message context metadata to `Context · n items`, with implicit active-note context shown as `Active note · path/to/note.md`.

4
.github/release-notes/4.3.0.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Added `/clip <url> [message]` to save readable web page clippings as Markdown notes and send wikilink references from the composer.
- Added web clipping settings for the saved note folder, filename template, and tags.

5
.github/release-notes/4.3.1.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Renamed active-note context to `@active` and updated related settings copy to refer to the active file consistently.
- Improved thread matching for composer thread commands and thread search so title and ID matches rank more predictably.
- Made saved vault paths for attachments, web clippings, and archived thread notes more consistent with their configured defaults.

4
.github/release-notes/4.4.0.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Add Obsidian tag completions in the composer after `#`.
- Open Obsidian Search when clicking rendered tags in Codex Panel messages.

5
.github/release-notes/4.4.1.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Match Obsidian's tag index and global search behavior for tag completions and rendered tag clicks.
- Show composer context as 0% before a thread starts instead of leaving it blank.
- Harden saved Markdown output for web clips and archived threads with safer YAML frontmatter handling.

6
.github/release-notes/4.5.0.md vendored Normal file
View file

@ -0,0 +1,6 @@
## Changes
- Add an action to open subagent threads directly from agent summary messages.
- Show reasoning effort descriptions in composer suggestions.
- Update app-server compatibility for Codex CLI 0.143.0.
- Polish toolbar and slash command wording, and fix composer suggestion matching.

3
.github/release-notes/4.5.1.md vendored Normal file
View file

@ -0,0 +1,3 @@
## Changes
- Polish chat panel wording around conversations, context, and composer guidance.

6
.github/release-notes/4.5.2.md vendored Normal file
View file

@ -0,0 +1,6 @@
## Changes
- Update app-server compatibility for Codex CLI 0.144.0 and improve reconnection, metadata, and MCP request handling.
- Load thread history incrementally in Threads View while retaining complete thread-picker search results.
- Fix web clips to save Defuddle's Markdown output and refresh collapsed-content controls after layout changes.
- Prevent stale asynchronous UI updates after panels, settings, or the plugin close.

4
.github/release-notes/4.6.0.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Add `@today`, `@tomorrow`, and `@yesterday` composer completions that resolve through the configured Daily Notes or Periodic Notes folder and date format.
- Improve `@active` and `@selection` suggestions with clearer note, range, and selected-text previews.

3
.github/release-notes/4.6.1.md vendored Normal file
View file

@ -0,0 +1,3 @@
## Changes
- Align DOM creation and daily-note date handling with Obsidian's bundled APIs for Community plugin review compatibility.

5
.github/release-notes/4.7.0.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Add `/btw` side chats that open an ephemeral conversation in a new panel without interrupting the source thread.
- Open subagent threads as read-only panels and hide conversation actions that do not apply to them.
- Improve reliability around Vault file destinations, web clipping, app-server connections, and asynchronous panel cleanup.

4
.github/release-notes/4.7.1.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Improve chat and side-chat stability during concurrent actions, panel restoration, and cleanup.
- Prevent stale app-server data and rapid settings changes from overwriting newer state.

5
.github/release-notes/5.0.0.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Replace `/clip` with `/web <url> [message]` for temporary web context without saving a note. For durable archives—especially rendered or signed-in pages—use Obsidian Web Clipper and reference the saved note.
- Improve hook controls so trusting a hook no longer enables it automatically, and only trusted unmanaged hooks can be enabled or disabled.
- Prevent an in-flight automatic thread title from overwriting a newer manual rename.

5
.github/release-notes/5.0.1.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- You can now cancel an in-progress `/web` import and keep the original prompt to edit or resend it.
- Changing the Codex executable in settings now reconnects open panels cleanly.
- File links containing backticks are now preserved when exporting archived threads.

5
.github/release-notes/5.0.2.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Persistent threads now reconnect cleanly after temporary Codex disconnects or app-server setting changes, while temporary side chats no longer resume incorrectly.
- Slash-command suggestions now immediately respect the restrictions for temporary side chats and subagent threads.
- Thread rename and archive results are now kept within the Codex context where the operation started, preventing stale updates after settings change.

3
.github/release-notes/5.0.3.md vendored Normal file
View file

@ -0,0 +1,3 @@
## Changes
- Release assets now carry individually verifiable provenance attestations.

3
.github/release-notes/5.0.4.md vendored Normal file
View file

@ -0,0 +1,3 @@
## Changes
- Restored availability of downloadable install assets while upstream attestation verification catches up with GitHub's API changes.

6
.github/release-notes/5.1.0.md vendored Normal file
View file

@ -0,0 +1,6 @@
## Changes
- Added search to Codex Panel settings.
- Large web, thread, selection, and vault references are now bounded predictably, report truncation when needed, and keep their context details after reopening a thread.
- Paste and drop attachments now preserve their intended insertion point while files are being saved, with clearer composer editing state.
- Improved reliability when switching threads, leaving running subagents, or changing runtime and app-server settings, preventing stale results and lost updates.

6
.github/release-notes/5.1.1.md vendored Normal file
View file

@ -0,0 +1,6 @@
## Changes
- Thread history now stays in sync across the chat toolbar, thread picker, and Threads view while loading older threads or refreshing.
- Thread search now uses fuzzy title matching across the complete active history instead of matching thread IDs.
- Running subagent summaries now show each agent's latest activity instead of only its initial status.
- Opening the thread picker again now closes the previous picker, and accepted MCP links are preserved in conversation history.

6
.github/release-notes/5.1.2.md vendored Normal file
View file

@ -0,0 +1,6 @@
## Changes
- Thread rename and auto-name controls now prevent conflicting edits while work is in progress, and cancelling auto-name returns to the existing draft.
- Actions with known unmet prerequisites are now disabled before execution, including unavailable auto-name and selection rewrite actions.
- Thread operation errors no longer appear as entries in navigation lists.
- Resumed conversations now hide Codex Panel's internal context metadata while preserving their context attachments.

4
.github/release-notes/5.2.0.md vendored Normal file
View file

@ -0,0 +1,4 @@
## Changes
- Thread references created with `/refer` now remain as readable, clickable links in conversation history and open the referenced thread in an available Codex Panel.
- Opening an existing thread in a new panel is now faster because resume no longer waits for background metadata and thread-list refreshes.

5
.github/release-notes/5.2.1.md vendored Normal file
View file

@ -0,0 +1,5 @@
## Changes
- Updated compatibility for Codex CLI 0.145.0.
- Subagent conversations that support replies can now be continued directly from their panel.
- Fixed malformed links in exported thread archives.

View file

@ -11,23 +11,76 @@ permissions:
jobs:
check:
name: npm run check:ci
name: npm run check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Detect compatibility changes
id: compatibility
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
run: |
if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- \
.github/workflows/check.yml \
README.md \
manifest.json \
package.json \
package-lock.json \
scripts/api-baseline.mjs \
scripts/app-server-compatibility.mjs \
scripts/generate-app-server-types.mjs \
src/app-server/connection/compatibility.json \
src/generated/app-server \
versions.json; then
echo "required=false" >> "$GITHUB_OUTPUT"
else
echo "required=true" >> "$GITHUB_OUTPUT"
fi
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
node-version: 26
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Install recorded Codex CLI
if: steps.compatibility.outputs.required == 'true'
run: npm install --global "@openai/codex@$(node scripts/app-server-compatibility.mjs --tested-cli-version)"
- name: Check API baselines
if: steps.compatibility.outputs.required == 'true'
run: npm run api:baseline
- name: Check generated app-server bindings
if: steps.compatibility.outputs.required == 'true'
run: npm run generate:app-server-types:check
- name: Check pull request commit messages
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: npm run commitlint -- --from "$BASE_SHA" --to "$HEAD_SHA" --verbose
- name: Check pushed commit messages
if: github.event_name == 'push'
env:
BASE_SHA: ${{ github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: npm run commitlint -- --from "$BASE_SHA" --to "$HEAD_SHA" --verbose
- name: Check release metadata
run: npm run release:check
- name: Check
run: npm run check:ci
run: npm run check

View file

@ -7,8 +7,6 @@ on:
permissions:
contents: write
id-token: write
attestations: write
jobs:
release:
@ -17,34 +15,44 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
node-version: 26
package-manager-cache: false
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Install recorded Codex CLI
run: npm install --global "@openai/codex@$(node scripts/app-server-compatibility.mjs --tested-cli-version)"
- name: Check API baselines
run: npm run api:baseline
- name: Check generated app-server bindings
run: npm run generate:app-server-types:check
- name: Check release version
run: npm run release:check
env:
RELEASE_VERSION: ${{ github.ref_name }}
- name: Run checks and build
run: npm run check:ci
- name: Check release commit messages
env:
TAG: ${{ github.ref_name }}
run: |
PREVIOUS_TAG="$(node -p 'Object.keys(require("./versions.json")).at(-2)')"
git rev-parse --verify "refs/tags/$PREVIOUS_TAG"
npm run commitlint -- --from "$PREVIOUS_TAG" --to "$TAG" --verbose
- name: Generate artifact attestations
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4
with:
subject-path: |
main.js
manifest.json
styles.css
- name: Run checks and build
run: npm run check
- name: Create GitHub release
env:

4
.gitignore vendored
View file

@ -1,4 +1,6 @@
node_modules/
.pnpm-store/
.stryker-tmp/
.jj/
.codex/
main.js
@ -7,4 +9,6 @@ data.json
*.log
.DS_Store
dist/
coverage/
reports/
tmp/

1
.node-version Normal file
View file

@ -0,0 +1 @@
26

View file

@ -1,8 +0,0 @@
node_modules/
.jj/
.codex/
main.js
styles.css
data.json
dist/
tmp/

View file

@ -1,4 +0,0 @@
{
"printWidth": 140,
"trailingComma": "all"
}

View file

@ -1,9 +1,22 @@
This repository contains the Codex Panel Obsidian plugin. Use the tracked docs as the source of truth: `README.md` for user-facing behavior, `docs/development.md` for development workflow, and `docs/release.md` for releases.
This repository contains the Codex Panel Obsidian plugin.
Jujutsu is the recommended local change-management workflow when available.
## Working Principles
## Working Rules
- Start from evidence. Reproduce defects before fixing them; for product changes, reconstruct the current workflow, user need, alternatives, and losses before deciding what should change. Treat theoretical failure modes and reviewer agreement as hypotheses, not evidence of meaningful user or operational harm.
- Use design documents as context, not as a substitute for current behavior and user expectations. Prefer coherent user-facing behavior and clear ownership, and remove needless abstraction or obsolete compatibility instead of preserving it by default.
- Treat implementation, tests, documentation, lint policy, and final history as one deliverable: inspect each for drift, but update only the durable contracts that changed. Documentation should record user-facing behavior, long-term design, or reusable workflow; do not add it merely to narrate an implementation change or duplicate tests and tooling.
- Parallelize only substantial concerns that can be implemented and tested independently with little shared-file contention. Keep small, tightly coupled, sequential, or coordination-heavy work together.
- Follow the documented ownership and source layout boundaries.
- Do not hand-edit generated app-server bindings.
- Keep internal validation notes and reasoning out of public release notes.
## What To Read
- Read `README.md` for user-facing behavior, requirements, commands, privacy, and compatibility.
- Read `docs/design.md` when changing responsibility boundaries, runtime ownership, app-server source-of-truth behavior, UI ownership, or testing philosophy.
- Read `docs/development.md` before implementation work, generated binding work, source layout decisions, validation, or compatibility baseline changes.
- Read `docs/release.md` for release preparation, release notes, preflight, tagging, pushing, and release repair.
- Use the repo-local skills in `.agents/skills/` when a task matches a more specific workflow.
## Changes And Validation
- Jujutsu is the recommended local change-management workflow when available. Make each final change a coherent review unit with an honest description.
- Before publishing, inspect and reorganize the graph when needed rather than preserving implementation chronology: normally fold corrective follow-ups into the concern they complete, split mixed changes, and keep a follow-up separate only when it remains meaningful on its own.
- Use Conventional Commits for new commits and follow `docs/development.md` for repository rules and validation. Re-run relevant validation after history edits and before handoff or publication.

167
README.md
View file

@ -1,179 +1,80 @@
# Codex Panel
Codex Panel brings your existing Codex setup into an Obsidian sidebar. It starts `codex app-server` locally, uses the current vault root as the Codex working directory, and leaves Codex in charge of models, reasoning effort, sandbox modes, approval policies, MCP servers, hooks, skills, providers, network access, and thread history.
Codex Panel brings Codex into an Obsidian sidebar. It keeps Codex threads beside your notes, helps you add vault context to prompts, and lets you handle approvals and file changes without switching windows.
The plugin is intentionally thin: it is an Obsidian surface for Codex, not a separate AI client with its own runtime policy. If your Codex CLI or Codex.app environment is already configured, Codex Panel uses that same runtime behavior in a persistent Obsidian pane, with note-aware support for links, threads, archives, and editor selections.
If the Codex CLI is already installed and authenticated, Codex Panel uses that local setup.
Use it when you want Codex beside your notes, not in a separate terminal or app window.
![Codex Panel](assets/screenshot.webp)
## Why use it
- Keep Codex next to the notes, specs, plans, and project context you are editing in Obsidian.
- Reuse Codex configuration instead of configuring models, sandboxing, approvals, MCP servers, hooks, skills, providers, or credentials again.
- Work with persistent Codex threads in Obsidian panes, including multiple panels side by side.
- Navigate threads from an Obsidian sidebar view, with live status for currently open threads.
- Use vault-aware composing: wikilink suggestions come from Obsidian files, sent wikilinks resolve to Codex file mentions when possible, and rendered vault file links open in Obsidian.
- Archive Codex threads as Markdown notes in your vault.
- Rewrite selected note text with Codex, then review a selection-scoped diff before applying the replacement.
- Keep Codex work next to the notes or projects it is about.
- Bring notes, selections, links, and attachments into prompts.
- Manage threads and respond to Codex requests inside Obsidian.
- Review file changes, rewrite selected note text, and archive useful threads as notes.
## How it works
Codex Panel runs `codex app-server` as a local child process and talks to it over stdio. Each open chat panel has its own app-server connection, active thread, pending requests, and composer draft. The vault root is passed to Codex as the working directory.
Codex Panel starts `codex app-server` locally. Each open panel keeps its own active thread and draft, with Codex working from the vault root.
Codex remains the runtime authority:
- Model and reasoning defaults come from Codex configuration.
- Sandbox mode, approval policy, approval reviewer, network policy, MCP servers, hooks, skills, provider settings, and thread history are owned by Codex.
- Codex Panel can display and adjust supported per-thread runtime controls, but it does not replace Codex configuration.
- Codex Panel stores panel preferences only. It does not store API keys or provider credentials.
That boundary is also the privacy and security boundary. Data sent from the panel is handled by the configured Codex CLI according to your Codex configuration.
Codex Panel stores panel preferences only, not API keys or provider credentials.
## Requirements
- Obsidian desktop app 1.12.0 or newer.
- Codex CLI installed, authenticated, and available as `codex`, or configured with an absolute executable path in Codex Panel settings.
- A vault where Codex is allowed to work according to your Codex CLI configuration.
- A local vault where Codex is allowed to work.
Codex Panel does not support Obsidian mobile.
## Installation
Install Codex Panel from Obsidian's Community plugins browser:
1. Open **Settings**.
2. Go to **Community plugins**.
3. Select **Browse**.
4. Search for **Codex Panel**.
5. Install and enable the plugin.
Install Codex Panel from Obsidian's Community plugins browser by searching for **Codex Panel**, then install and enable the plugin.
You can also open the plugin page directly: <https://community.obsidian.md/plugins/codex-panel>.
## Quick start
1. Open the command palette and run **Codex Panel: Open panel**, or select the ribbon icon.
2. If Obsidian cannot find `codex`, set **Settings -> Codex Panel -> Codex executable** to an absolute path such as `/opt/homebrew/bin/codex`.
3. Ask Codex to work on the vault or project from the composer.
4. Use **Codex Panel: Open thread...** or **Codex Panel: Open threads view** to return to recent non-archived Codex threads.
5. Select text in a Markdown note and run **Codex Panel: Rewrite selection** when you want a note-local rewrite with a reviewable diff.
1. Open the note or project you want to work on.
2. Run **Codex Panel: Open panel** from the command palette, or select the ribbon icon.
3. In the composer, reference what matters and describe the outcome you want. For example: `@active Summarize this note and suggest the next three actions.`
4. Follow the turn in the panel, respond to requests when needed, and review any file changes before continuing.
By default, `Enter` sends and `Shift+Enter` inserts a newline. You can switch sending to `Cmd/Ctrl+Enter` in Codex Panel settings.
If Obsidian cannot find `codex`, set **Settings -> Codex Panel -> Codex executable** to an absolute path such as `/opt/homebrew/bin/codex`.
## What you can do
## Working with Codex Panel
### Panels and threads
### Keep work separate or pick it up later
- Open one Codex panel in the right sidebar, or open multiple panels for multiple threads.
- Start a fresh thread in the current panel.
- Resume, rename, auto-name, fork, roll back, compact, and archive Codex threads.
- Use the left sidebar Threads view to browse non-archived threads and see which ones are open, running, or waiting for input.
- Forks open in a new right-sidebar pane so the source thread stays visible; fork-and-archive replaces the source panel with the forked thread.
A panel is an independent working surface with its own active thread and draft. Open multiple panels when different tasks should stay visible and separate. Regular Codex threads remain available after a panel closes; return to them from **Codex Panel: Open thread...**, panel history, or the Threads view.
### Compose and steer turns
For a one-off question alongside the current thread, use `/btw` to open a temporary side chat. To carry context between regular threads without merging their histories, use `/refer <thread> <message>`. The submitted message keeps a normal link to the referenced thread; clicking it opens the thread in an available Codex Panel, creating one when needed.
- Compose with completions for slash commands, enabled skills (`$skill-name`), recent threads, models, and supported reasoning efforts.
- Reference another non-archived thread without switching away from the current one.
- Follow a running turn as Codex streams assistant messages, reasoning, commands, tool calls, hooks, file changes, plan updates, and agent activity.
- Respond to Codex questions, command approvals, file approvals, MCP requests, and permission requests.
- Send steering messages while a turn is running, or interrupt when the composer is empty.
- Show the active Codex goal above the message stream, then edit, pause, resume, or clear it from the panel.
### Bring in the right context
### Runtime controls and diagnostics
The composer lets you point Codex to relevant material without pasting it into the prompt. Type wikilinks to reference vault files while keeping their names readable. Use `@active` for the active file or `@selection` for the current Markdown selection.
- Inspect context usage from the composer status row.
- Toggle Plan mode, fast mode, and approval auto-review for subsequent turns.
- Set the model and reasoning effort for subsequent turns when supported by Codex.
- Inspect usage limits, connection diagnostics, MCP servers, enabled skills, runtime debug details, and discovered hooks from the toolbar, settings, or slash commands.
When Obsidian Daily Notes or the daily section of Periodic Notes is enabled, `@today`, `@tomorrow`, and `@yesterday` resolve through its folder and date format. Paste or drop files to save them in the configured attachment folder and reference them from the same prompt. For material outside the vault, `/web <url> [message]` fetches readable page content and attaches it to the next turn without creating a note.
### Obsidian-native workflows
### Guide a running turn
- Use wikilink suggestions backed by Obsidian file search and recent notes.
- Send resolved wikilinks as Codex file mentions when the target exists.
- Open rendered Markdown links to vault files in Obsidian.
- Review Codex file changes in an Obsidian diff view with changed files and a copy-diff action.
- Archive threads as Markdown notes with configurable folder, filename template, and tags, archive without saving, then restore or permanently delete archived threads from settings.
- Rewrite selected note text using the current editor buffer as context, then review the replacement diff before applying it.
As a turn runs, the panel keeps the conversation together with requests that need your attention. You can answer questions, approve or reject actions, steer the turn with another message, or interrupt it. Plans, goals, tool activity, and agent work remain available when you need to inspect how the task is progressing.
Rollback restores the latest prompt in the Codex thread. It does not undo file edits in your vault.
### Review and keep the result
## Commands
When Codex changes files, open the Obsidian diff view to review the changes and copy the patch. For a focused edit, select Markdown text and run **Codex Panel: Rewrite selection** to request a replacement and review a selection-scoped diff before applying it.
### Obsidian commands
Archive a finished thread with or without saving it as a Markdown note. Saved notes use the configured folder, filename template, and tags; archived threads can later be restored or permanently deleted from settings.
The command palette entries are ordered by the workflow they support: opening panels, finding threads, managing the active chat, and editing note text.
| Command | What it does |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Codex Panel: Open panel** | Open or focus a Codex panel. |
| **Codex Panel: Open new panel** | Open another Codex panel. |
| **Codex Panel: Open thread...** | Search non-archived threads from a picker. Press `Enter` to open in the current panel, or `Cmd/Ctrl+Enter` to open in a new panel. |
| **Codex Panel: Open threads view** | Open the left sidebar thread list. |
| **Codex Panel: Start new chat** | Clear the current panel for a fresh Codex thread. |
| **Codex Panel: Rewrite selection** | Rewrite selected Markdown note text with Codex and review a selection-scoped diff before applying it. |
### Slash commands
Use `/help` in the composer to show the available slash commands. The list below follows the same groups shown in help.
#### Panel actions
| Command | What it does |
| ------------------------- | ------------------------------------------------------------- |
| `/clear` | Clear the current panel and start a fresh Codex thread. |
| `/resume [thread]` | Resume a recent Codex thread. |
| `/reconnect` | Reconnect to `codex app-server` and resume the active thread. |
| `/fork` | Fork the active Codex thread. |
| `/rollback` | Roll back the latest turn and restore its prompt. |
| `/compact` | Compact the current thread context. |
| `/archive <thread>` | Archive the selected Codex thread. |
| `/rename <thread> <name>` | Rename the selected Codex thread. |
#### Thread settings
| Command | What it does |
| ----------------------------- | -------------------------------------------------- |
| `/auto-review` | Toggle approval auto-review. |
| `/fast` | Toggle fast service tier for subsequent turns. |
| `/plan [message]` | Toggle Plan mode, optionally with a message. |
| `/goal` | Show the current thread goal. |
| `/goal set <objective>` | Create or update the current thread goal. |
| `/goal edit` | Load the current thread goal into the composer. |
| `/goal pause` | Pause the current thread goal. |
| `/goal resume` | Resume the current thread goal. |
| `/goal clear` | Clear the current thread goal. |
| `/model [model\|default]` | Show or set the model for subsequent turns. |
| `/reasoning [level\|default]` | Show or set reasoning effort for subsequent turns. |
#### Diagnostics
| Command | What it does |
| --------- | ------------------------------------------------ |
| `/status` | Show current thread, context, and usage limits. |
| `/doctor` | Show Codex CLI and Codex App Server diagnostics. |
| `/mcp` | Show MCP servers reported by Codex App Server. |
| `/help` | Show available slash commands. |
#### Composition
| Command | What it does |
| --------------------------- | ------------------------------------------------------------------ |
| `/refer <thread> <message>` | Send a message with recent turns from another non-archived thread. |
## Privacy and security
Codex Panel acts as a local Obsidian client for Codex App Server:
- It does not make network requests itself. It talks to the configured `codex app-server` process over stdio.
- User input, approvals, resolved file mentions, and thread/configuration operations are sent to Codex App Server.
- It stores only panel-specific preferences, not API keys or provider credentials.
- Network access, sandbox behavior, approval policy, and other runtime policy are controlled by your Codex CLI configuration.
Use `/help` for the current slash command list.
## Compatibility
| Key | Version | Policy |
| ------------------------ | --------- | ---------------------------------------------------------- |
| `obsidian.minAppVersion` | `1.12.0` | Track the latest patch for this Obsidian minor. |
| `codex.testedCliVersion` | `0.140.0` | Track app-server compatibility by Codex CLI minor version. |
| Key | Version | Policy |
| -------------------------------------- | --------- | --------------------------------------------------------------------------------------------------- |
| `manifest.minAppVersion` | `1.12.0` | Minimum Obsidian desktop version declared for plugin loading. |
| `obsidian` API types | `1.12.3` | TypeScript API package used for compile-time checks; kept in the same minor as `manifest` baseline. |
| `codexAppServer.testedCliVersion` | `0.145.0` | Exact CLI patch used to generate and verify bindings; compatibility is tracked by minor version. |
Codex Panel depends on the experimental `codex app-server` API.

BIN
assets/screenshot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

221
biome.jsonc Normal file
View file

@ -0,0 +1,221 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.1/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"plugins": [
// Project-wide source shape and placement.
{
"path": "./scripts/grit/source-shape/no-handwritten-reexports.grit",
"includes": ["**/*.{ts,tsx,mjs}"]
},
{
"path": "./scripts/grit/source-shape/no-responsibility-root-module-files.grit",
"includes": ["**/src/{app-server,domain}/*.{ts,tsx}", "**/src/features/{chat,threads}/*.{ts,tsx}"]
},
{
"path": "./scripts/grit/source-shape/no-misplaced-tsx.grit",
"includes": [
"**/src/**/*.tsx",
"!**/src/{settings,shared/ui}/**",
"!**/src/features/chat/{panel,ui}/**",
"!**/src/features/{selection-rewrite,threads-view,turn-diff}/*.dom.tsx",
"!**/src/shared/dom/*.dom.tsx",
"!**/src/shared/obsidian/*.obsidian.tsx"
]
},
{
"path": "./scripts/grit/source-shape/no-self-referential-initializer-callback.grit",
"includes": ["**/*.{ts,tsx,mjs}"]
},
{
"path": "./scripts/grit/source-shape/no-unsafe-iterator-value.grit",
"includes": ["**/*.{ts,tsx,mjs}"]
},
{
"path": "./scripts/grit/source-shape/no-uncontrolled-preact-form-state.grit",
"includes": ["**/src/**/*.tsx"]
},
// Cross-cutting source-layer import boundaries.
{
"path": "./scripts/grit/import-boundaries/no-domain-outer-layer-imports.grit",
"includes": ["**/src/domain/**/*.ts", "**/src/features/chat/domain/**/*.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-lower-level-feature-imports.grit",
"includes": ["**/src/app-server/**/*.ts", "**/src/shared/**/*.{ts,tsx}"]
},
// App-server and generated protocol boundaries.
{
"path": "./scripts/grit/import-boundaries/no-app-server-subfolder-root-imports.grit",
"includes": ["**/src/app-server/**/*.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-app-server-connection-boundary-imports.grit",
"includes": [
"**/src/app-server/protocol/**/*.ts",
"**/src/domain/**/*.ts",
"**/src/shared/**/*.{ts,tsx}",
"**/src/features/selection-rewrite/**/*.{ts,tsx}",
"!**/src/features/selection-rewrite/app-server-transport.ts"
]
},
{
"path": "./scripts/grit/import-boundaries/no-app-server-protocol-boundary-imports.grit",
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/app-server/**", "!**/src/features/chat/app-server/mappers/thread-stream/turn-items.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-chat-turn-item-non-turn-protocol-imports.grit",
"includes": ["**/src/features/chat/app-server/mappers/thread-stream/turn-items.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-generated-app-server-boundary-imports.grit",
"includes": [
"**/src/**/*.{ts,tsx}",
"!**/src/app-server/connection/**",
"!**/src/app-server/protocol/server-requests.ts",
"!**/src/app-server/protocol/thread.ts",
"!**/src/app-server/protocol/turn.ts"
]
},
{
"path": "./scripts/grit/runtime/no-app-server-direct-rpcs.grit",
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/app-server/**", "!**/src/features/chat/app-server/**"]
},
// Workspace and feature coordination boundaries.
{
"path": "./scripts/grit/import-boundaries/no-chat-workspace-boundary-imports.grit",
"includes": ["**/src/features/chat/**/*.{ts,tsx}"]
},
{
"path": "./scripts/grit/import-boundaries/no-feature-workspace-boundary-imports.grit",
"includes": ["**/src/features/**/*.{ts,tsx}", "!**/src/features/chat/**"]
},
{
"path": "./scripts/grit/import-boundaries/no-workspace-chat-internal-imports.grit",
"includes": ["**/src/workspace/**/*.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-thread-workflow-app-server-imports.grit",
"includes": ["**/src/features/threads/workflows/**/*.ts"]
},
// Chat feature layer boundaries.
{
"path": "./scripts/grit/import-boundaries/no-chat-application-outer-layer-imports.grit",
"includes": ["**/src/features/chat/application/**/*.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-chat-app-server-outer-layer-imports.grit",
"includes": ["**/src/features/chat/app-server/**/*.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-chat-host-rendering-layer-imports.grit",
"includes": ["**/src/features/chat/host/**/*.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-chat-panel-runtime-boundary-imports.grit",
"includes": ["**/src/features/chat/panel/**/*.{ts,tsx}"]
},
{
"path": "./scripts/grit/import-boundaries/no-chat-presentation-outer-layer-imports.grit",
"includes": ["**/src/features/chat/presentation/**/*.ts"]
},
{
"path": "./scripts/grit/import-boundaries/no-chat-ui-outer-layer-imports.grit",
"includes": ["**/src/features/chat/ui/**/*.{ts,tsx}"]
},
// Chat state and runtime ownership.
{
"path": "./scripts/grit/runtime/no-state-module-side-effects.grit",
"includes": ["**/src/features/chat/application/state/**/*.ts"]
},
// DOM bridge and Preact root boundaries.
{
"path": "./scripts/grit/dom/no-implicit-dom-bridges.grit",
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/**/*.{dom,obsidian,measure}.{ts,tsx}"]
},
{
"path": "./scripts/grit/dom/no-dom-events-imports.grit",
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/**/*.{dom,obsidian,measure}.{ts,tsx}"]
},
{
"path": "./scripts/grit/dom/no-preact-root-imports.grit",
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/**/*.{dom,obsidian}.{ts,tsx}"]
},
// CSS source policy.
{
"path": "./scripts/grit/no-restricted-css-policy.grit",
"includes": ["**/src/styles/**/*.css"]
}
],
"files": {
"includes": [
"*.config.mjs",
"*.config.ts",
"*.css",
"*.json",
"*.jsonc",
"*.mjs",
"scripts/**/*.{json,jsonc}",
"scripts/**/*.grit",
"scripts/**/*.mjs",
"src/**/*.{css,ts,tsx}",
"tests/**/*.{mjs,ts,tsx}",
"!src/generated"
]
},
"formatter": {
"indentStyle": "space",
"lineWidth": 140
},
"assist": {
"actions": {
"source": {
"noDuplicateClasses": "on",
"organizeImports": "on"
}
}
},
"linter": {
"domains": {
"react": "recommended",
"test": "recommended"
},
"rules": {
"preset": "recommended",
"complexity": {
"useLiteralKeys": "off"
},
"correctness": {
"useExhaustiveDependencies": {
"level": "error",
"options": {
"reportUnnecessaryDependencies": false
}
}
},
"nursery": {
"noExcessiveSelectorClasses": {
"level": "error",
"options": {
"maxClasses": 4
}
}
},
"style": {
"noCommonJs": "error"
},
"suspicious": {
"noImportCycles": "error"
}
}
}
}

9
commitlint.config.mjs Normal file
View file

@ -0,0 +1,9 @@
export default {
defaultIgnores: false,
extends: ["@commitlint/config-conventional"],
ignores: [(message) => /^Merge pull request #\d+ from \S+/.test(message)],
rules: {
"subject-case": [0],
"type-enum": [2, "always", ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "test"]],
},
};

101
docs/design.md Normal file
View file

@ -0,0 +1,101 @@
# Design
Codex Panel is an Obsidian surface for Codex. It exists to put Codex beside vault notes without becoming a separate AI client, runtime policy editor, terminal, search product, or writing suite.
This document records durable design direction. User-facing behavior belongs in `README.md`; daily workflow, source layout, generated files, and compatibility checks belong in `docs/development.md`.
## Product Boundary
Keep the panel thin. Codex Panel owns the Obsidian experience around Codex: panels, composing, vault-aware link handoff, approvals and user input, file-change review, archive export, panel preferences, diagnostics, and selection rewrite.
Codex owns runtime behavior and thread state: models, credentials, sandboxing, approval policy, MCP servers, hooks, providers, agent-initiated network access, thread history, archived state, goals, and runtime settings. The panel should read and update that state through `codex app-server`, not redefine it.
The panel may provide a narrow management surface when direct configuration of Codex-owned state is impractical and app-server still owns the semantics. Hook controls serve this role: help users recognize and manage hooks they added or changed without presenting the panel as a code-audit surface.
The panel may acquire prompt context through an explicit user action. `/web` fetches the requested page through Obsidian and attaches the extracted content as untrusted turn context; it is not a search surface or agent network policy.
Panel-acquired context remains reference material rather than current user authorization. Send its bounded payload as untrusted app-server context without adding Panel-owned metadata envelopes to Codex history. Apply physical byte and part limits at the app-server boundary; keep source-specific selection and truncation policy with the context producer. Keep read-only compatibility for envelopes written by older Panel versions isolated at history projection boundaries.
Vault file references are prompt handoff data, not durable thread metadata. Keep their display state local; ordinary wikilinks remain represented by the user-authored message text. Thread references persist as ordinary, human-readable `codex://threads/` Markdown links, while their bounded transcript payload remains ephemeral turn context.
Panel settings should store only panel-specific preferences. Do not mirror Codex configuration in Obsidian settings just to display or inspect it.
## Sources of Truth
`codex app-server` is the source of truth for Codex state. Panel-side caches exist to keep the UI stable across transient failures; failed reads or stale panels must not become authoritative empty state.
The app-server API is experimental. The project tracks the supported Codex CLI minor and favors a clean current flow over broad old-protocol compatibility.
Runtime controls should express visible user intent for the active thread rather than copy Codex configuration. Diagnostics should expose only actionable troubleshooting facts.
An app-server context is the pair of Codex executable and Vault root. Replacing it must invalidate context-bound connections and work before publishing the new context, keep events attributed to their source context, and discard old runtime metadata. Preserve last-known-good state only across transient failures within the same context.
Vault root is the Panel-owned workspace root for that context. Panel operations always use it as the thread working directory and do not project mutable protocol thread cwd values into panel state.
## Code Boundaries
Raw app-server protocol belongs at the app-server boundary. Boundary code should adapt protocol payloads into panel-owned domain models or small projections before those values reach features, workspace coordination, settings, or UI.
Application workflows should depend on feature-owned contracts rather than app-server clients, RPC details, connection checks, vault-path wiring, or raw protocol projections.
Turn stream conversion is the main exception: raw app-server stream payloads may be consumed at the conversion boundary because the event set is broad and changes with Codex. The converter should still reduce them into panel-owned display and diagnostic models before they reach chat state or UI.
Server request adapters should turn app-server requests into coarse panel models before they reach pending request state. The UI handles user-facing intent; app-server-specific decisions and response payloads stay boundary-owned.
Source modules should be organized by reason to change, not by the single Obsidian plugin entrypoint. Boundaries should stay close to the state, lifecycle, or external API they own.
Do not hide complexity behind forwarding layers. Add an abstraction only when it owns a lifecycle, boundary, state transition, or reusable domain capability.
## UI Ownership
Runtime UI composition is Preact-owned. Preact components should render the panel shell, toolbar, thread stream, composer, and request controls.
Obsidian and app-server boundaries stay outside Preact components. External lifecycles, app-server connections, editor/workspace APIs, and rendering bridges belong in boundary modules.
Chat-visible state should have one authoritative owner. Components should consume narrow projections of that state rather than mirror it into another reactive store.
TanStack Query is the single panel-side owner of cached app-server resources. Features may project authoritative event results into that state, but should not introduce parallel cache or synchronization mechanisms. Partial read models are reconciled at explicit lifecycle boundaries rather than kept globally and continuously consistent.
Thread lifecycle changes should be projected from completed operation facts into the shared read model. When one user action changes multiple visible projections, publish them coherently without delaying live operational state or coupling independent panels.
Reads with different completeness requirements have separate lifecycles. Complete operation-local reads must not replace bounded shared history, and transient activity should come from its owning live state rather than forcing history refreshes.
Imperative UI bridges are allowed only where the host platform requires them. They should remain narrow boundary adapters, not a second UI composition system inside Preact-owned surfaces.
## Interaction Principles
Multiple panels are separate Obsidian leaves. Treat each panel as its own Codex working surface with independent connection, thread, turn state, composer, and pending requests.
Long-running actions must preserve the user intent that started them. Panel-local results may affect a panel only while that intent still owns its target; Codex facts completed in the current app-server context remain shared truth even if the initiating panel has moved on.
Coordinate conflicting work at the narrowest shared semantic owner. Independent panels should not block one another, and stale reads or writes must not overwrite newer shared state. Coalesce work only when replacement is part of the operation's meaning.
Foreground reveal and focus are the narrow workspace-wide exception: the latest user intent wins without serializing unrelated panel or app-server work. Cleanup obligations created by a committed state transition must survive the UI action that initiated them.
Do not use explanatory copy as the default remedy for an unclear or incoherent interface. Before adding instructions, warnings, status text, validation messages, confirmations, or diagnostics, determine why the interface does not communicate the necessary behavior through its structure and state. Prefer improving information hierarchy, labels, defaults, available choices, affordances, validation timing, progress indication, and feedback. Add text only when irreducible information remains and it materially helps the user decide, act, or recover; do not expose implementation concepts merely because they explain the current behavior.
Subagent threads opened from agent activity remain persistent and restorable but stay outside ordinary thread history. Use the app-server direct-input capability when available, falling back to read-only subagent panels for older servers, while preserving parent and agent provenance for other specialized behavior.
Mode-derived restrictions for the active panel thread must remain consistent across actions and UI. Keep connection state, turn busy state, and operations targeting another listed thread in their owning workflows.
Thread history and other app-server resources should follow app-server semantics. Panel-side views are read models over app-server snapshots and lifecycle events; stale or partial refreshes must not overwrite newer state. Obsidian integrations such as archive note export are convenience views, not replacements for Codex history.
Routine thread lists should load a bounded recent set and paginate older threads on demand instead of eagerly fetching complete inventories.
Selection rewrite is intentionally scoped to a focused edit-and-review workflow. Avoid expanding it into a broader writing assistant without a separate design decision.
Server requests should become panel UI only when the user can naturally answer them in context. Unknown or unsupported requests should stay diagnostic instead of pretending to be normal conversation text.
Thread stream display should separate primary conversation from diagnostic detail and progress/status. Preserve stable item identity across history, streaming, and rendering updates.
Codex Panel UI should feel native inside Obsidian. Prefer Obsidian variables, standard classes, and side-panel patterns. Add custom visual treatment only when Codex-specific state would otherwise be hard to read.
## Testing Direction
Tests should protect user expectations, app-server/panel responsibility boundaries, and state-transition invariants.
Prefer tests for visible behavior and state-transition invariants across panels, threads, requests, streams, and display fallbacks.
Avoid tests that freeze incidental implementation details unless those details directly protect a user-visible invariant.
Panel tests may define how received structured values are displayed, retained, or normalized. They should not redefine Codex-owned runtime policy, model lists, sandbox behavior, approval policy, or thread history semantics.

View file

@ -1,57 +1,93 @@
# Development
Use this document for day-to-day implementation mechanics: commands, generated files, loaded artifacts, source layout, naming, validation, and common pitfalls. For product boundaries and design rationale, see `docs/design.md`.
## Commands
```sh
npm ci
npm run format
npm run fix
npm run check
npm run build
npm run test:coverage
npm run test:mutation
```
Run `npm run format` after edits and before `npm run check` so Prettier-only issues are fixed upfront. `npm run check` runs TypeScript type checking, unit tests, lint checks, Prettier check, CSS build verification, and a production esbuild bundle. The local `npm run check` path runs checks in parallel and uses local caches where available. Use `npm run check:ci` when you need to reproduce the sequential, non-cached CI validation path.
Use the Node.js version in `.node-version`.
`main.js`, `styles.css`, `data.json`, and `node_modules/` are ignored by Git. `main.js` and `styles.css` are still the files Obsidian loads, so run `npm run build` after source changes. CSS is authored in `src/styles/` and generated into the ignored root `styles.css` release asset. Use `npm run build:styles` when only regenerating CSS, or `npm run build:styles:check` to verify that the authored CSS can be rendered.
Use `npm run fix` for mechanical cleanup, review its diff, and run `npm run check` before handoff.
## Source Layout
Use `npm run test:coverage` to identify source modules and branches that lack exercised behavior. It reports every authored TypeScript source file, including files not imported by tests, while excluding generated app-server bindings. Open `coverage/index.html` to inspect line-level gaps. Coverage is diagnostic and has no pass/fail threshold; prioritize user-visible behavior and state-transition invariants rather than raising the aggregate percentage.
The source tree is organized by responsibility rather than by the original single Obsidian plugin entrypoint:
Use `npm run test:mutation` for an exploratory mutation test of domain-owned logic, app-server protocol normalization, thread-stream event mapping, and the chat root reducer. Review surviving mutants individually instead of treating the aggregate score as a quality gate: add tests for meaningful behavior gaps, simplify equivalent or redundant code, and leave mutants alone when neither change improves the durable contract. The run ignores expensive module-initialization mutants, is intentionally manual, and writes its ignored HTML report to `reports/mutation/mutation.html`.
- `src/main.ts` registers Obsidian views, commands, settings, and lifecycle hooks.
- `src/app-server/` owns the app-server boundary. Generated app-server payloads should stay behind this boundary; exported app-server services should expose panel-owned domain models or small local projection types unless an explicit boundary exception is documented.
- `src/features/chat/` owns the main Codex chat surface: Obsidian `ItemView` classes, panel orchestration, request/app-server controllers, composer behavior, display models, chat-only UI renderers, runtime state and status projection, approvals, user input, thread actions, and panel-specific state.
- `src/features/thread-picker/` owns the Obsidian thread picker modal and its thread search/opening behavior.
- `src/features/threads/` contains thread-related services shared by chat, the thread picker, and the Threads view, including archive export settings, thread operations, and title generation.
- `src/features/threads-view/` owns the dedicated Obsidian thread list view, including app-server thread list rendering and live open-panel snapshot aggregation.
- `src/features/selection-rewrite/` owns the Markdown editor selection rewrite command, popover, prompt/output handling, and ephemeral rewrite thread runner.
- `src/workspace/` owns Obsidian workspace coordination across Codex surfaces, including panel discovery/opening, open-panel snapshots, and thread surface broadcasts.
- `src/shared/` contains feature-neutral helpers, including reusable DOM pieces and unified diff display helpers shared by chat and selection rewrite.
- `src/settings/` contains Obsidian settings models, settings-tab rendering, and app-server-backed dynamic settings data.
- `src/domain/` contains panel-owned, generated-independent meaning models and pure derivations shared by app-server, features, workspace, and settings code. Domain code must not import app-server protocol, generated bindings, feature modules, UI, or Obsidian APIs; app-server protocol and service modules adapt generated payloads into domain models at the boundary.
- `src/styles/` contains the authored CSS modules and `order.json` concatenation order. `scripts/build-styles.mjs` concatenates them into the ignored root `styles.css`, which remains the Obsidian release asset.
## Commit Messages
Keep new code near the state or API it owns. A feature may import another feature only for a capability that feature owns; feature-neutral behavior belongs in `src/shared/`, `src/domain/`, or `src/app-server/`. Lower-level modules must not import `src/features/`, and generated app-server types should stay behind `src/app-server/`.
Use [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) for new commits:
Codex Panel's runtime UI is Preact-owned. Use imperative DOM writes only for explicit bridge modules, Obsidian-owned API boundaries, or rendering and measurement code that cannot be expressed cleanly as Preact components. Chat panel-visible state belongs in `ChatStateStore` and should flow through named reducer actions and the shell-state adapter. Revisit the project design notes before adding a parallel UI state or runtime path.
```text
feat(composer): add daily note context suggestions
fix: prevent manual titles from being overwritten
```
## App-Server Bindings
Use one of `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, or `test`. Scopes are optional. Keep the description concise and omit a trailing period; capitalization is unrestricted. Use `!` before the colon or a `BREAKING CHANGE:` footer for a disruptive change.
CI checks commits introduced by pull requests and direct pushes; GitHub-generated pull-request merge commits are exempt. To check a range locally, run `npm run commitlint -- --from <base> --to <head> --verbose`.
Use focused scripts while iterating, but run `npm run check` before handoff. CI and release preflight use the same check.
Keep rule suppressions local and include the Obsidian-specific reason when a native Obsidian UI pattern intentionally diverges from a generic browser rule.
Grit policy cases protect matcher semantics rather than diagnostic wording, source locations, or the exact shape of Biome configuration. Keep one valid and invalid source case for each policy, and change a case only when that policy's accepted or rejected source shape intentionally changes.
## Generated and Loaded Files
`main.js`, `styles.css`, `data.json`, and `node_modules/` are ignored by Git. `main.js` and `styles.css` are still the files Obsidian loads, so run `npm run build` before live Obsidian validation if you have not already run `npm run check` after the source change.
CSS is authored in `src/styles/` and generated into the ignored root `styles.css` release asset. Use `npm run build:styles` when only regenerating CSS; it also verifies the authored CSS order before writing `styles.css`.
The app-server TypeScript bindings in `src/generated/app-server/` are generated from the installed Codex CLI:
```sh
npm run generate:app-server-types
npm run generate:app-server-types:check
npm run check
```
The generation script uses `codex app-server generate-ts --experimental` because the panel depends on experimental app-server fields such as collaboration mode and generated v2 types.
Do not hand-edit the bindings. `src/app-server/connection/compatibility.json` records their CLI patch and generation arguments; the check command regenerates and compares them without replacing tracked files.
## Placement Rules
Follow the ownership boundaries in `docs/design.md` and the import and source-shape lint rules. Put behavior where its reason to change lives, and update the lint policy and its tests when intentionally changing a boundary.
Keep generated protocol types behind app-server adapters and expose panel-owned contracts or projections to features. Keep direct DOM work in explicit bridge, measurement, or rendering modules; use the established filename suffixes for those boundaries and reserve `.tsx` for rendering.
## CSS Rules
CSS should stay native to Obsidian. Prefer Obsidian variables and Codex Panel tokens for color, typography, spacing, and layout dimensions instead of hardcoded values.
Keep selectors shallow and scoped to Codex Panel classes. Add new authored CSS files to `src/styles/order.json`, and remove unused or test-only classes.
## Naming Conventions
Name modules by owned responsibility. Use lifecycle or boundary nouns only when the object owns that lifecycle or boundary, and passive-data names for values.
Prefer functions and factories. Reserve classes for mutable resource ownership, external class APIs, and `Error` types.
## Common Pitfalls
- Build before Obsidian validation. Obsidian loads ignored root assets, not the TypeScript or authored CSS sources directly.
- Preserve last-known-good app-server state on refresh failure. Do not turn disconnected reads into authoritative empty thread lists, settings snapshots, hook inventories, or diagnostics.
- Normalize optional and nullable app-server values before display. Users should not see raw `undefined`, `null`, protocol enum gaps, or fallback labels that imply Panel owns a Codex runtime setting.
- Do not use DOM order as thread history state. Thread stream DOM is a presentation surface, and delayed Markdown rendering can change heights after initial render.
## API Baselines
Use the local API baseline report when checking whether the development environment matches the supported API policy:
```sh
npm run api:baseline
npm run api:baseline:check
```
Obsidian API compatibility follows semver within the guaranteed minor. `manifest.json` and `versions.json` define the minimum supported Obsidian app minor with a `.0` patch version, while the `obsidian` npm dev dependency tracks the latest patch in that same minor using a `~` range. When the Obsidian API minor is raised, update `manifest.json`, `versions.json`, README requirements, and the `obsidian` dev dependency together.
Obsidian runtime compatibility is declared through `manifest.json` and `versions.json`. The `obsidian` npm package provides compile-time TypeScript API definitions; it is not runtime validation for an Obsidian app-version matrix. Because the project does not run app-version smoke tests, keep the API type package in the same minor as `manifest.minAppVersion` and use the latest patch in that minor for local type checking. `npm run api:baseline` exits non-zero when the local environment or recorded baselines drift. Raise `manifest.minAppVersion` only when intentionally adopting a newer Obsidian app/API minor.
Codex app-server compatibility is managed by Codex CLI minor version. README records the tested Codex CLI patch version, and the baseline check verifies that the local `codex --version` is in the same minor before app-server binding or compatibility work.
Codex app-server compatibility is managed by CLI minor version. `src/app-server/connection/compatibility.json` is the source of truth for the exact generation patch and app-server capabilities; README displays that patch.
Local compatibility work should run `npm run api:baseline` and `npm run generate:app-server-types:check`; CI runs the same verification with the recorded CLI.

View file

@ -4,13 +4,15 @@ GitHub Releases attach only `main.js`, `manifest.json`, and `styles.css` as Obsi
Release work is Jujutsu-first in a colocated Git repository; Git is still used to push the tag that triggers the GitHub Release workflow. If the checkout has not been initialized for Jujutsu yet, run `jj git init --colocate` and `jj bookmark track main --remote=origin` once.
Create a release by preparing the next version, editing the generated release notes, committing the release changes, then running the preflight before pushing the matching tag:
Plugin versions are for Obsidian distribution, not a library API compatibility contract. Use patch releases for workflow-preserving changes, minor releases for visible capabilities or raised runtime baselines, and major releases for disruptive workflow, settings, storage, or support-policy changes.
Create a release by preparing the next version, reviewing and editing the generated release notes, committing the release changes, then running the preflight before pushing the matching tag:
```sh
npm run release:prepare -- X.Y.Z
# Edit .github/release-notes/X.Y.Z.md.
# Review and edit .github/release-notes/X.Y.Z.md.
jj status
jj commit -m "Bump version to X.Y.Z"
jj commit -m "chore(release): X.Y.Z"
jj bookmark move main --to @-
npm run release:preflight
jj tag set X.Y.Z -r main
@ -18,6 +20,10 @@ jj git push --remote origin --bookmark main
git push origin X.Y.Z
```
`release:prepare` updates the version files and creates a `## Changes` release notes template. `release:preflight` verifies the local Jujutsu/Git state, release metadata, lockfile, and full build once after the release commit is on `main`.
`release:prepare` updates version files and drafts `## Changes` from selected Conventional Commits since the previous version. Use those commits to locate candidate changes, then compare the resulting user-visible behavior with the previous release before choosing the version. Review the draft against the full diff for complete, user-facing notes; consolidate and reorder entries as needed, and replace an empty placeholder.
The release workflow runs `npm ci`, `npm run release:check`, `npm run check:ci`, attaches the install assets, and generates GitHub artifact attestations for them. The release notes file is required and must contain a single `## Changes` section. If a tag-triggered release fails before creating the GitHub Release, fix the commit, move the local tag with `jj tag set --allow-move -r main X.Y.Z`, then update the remote tag with `git push --force origin X.Y.Z`.
Run `release:preflight` after the release commit is on `main`; it validates the commit range and release readiness.
Release notes should normally include only user-facing changes. Internal implementation changes, validation details, and release procedure notes should be omitted when minor; when they are important enough to mention, group them into at most one concise bullet.
The release notes file must contain a single `## Changes` section. The tag-triggered workflow validates and publishes the install assets. If it fails before creating the GitHub Release, fix the commit, move the local tag with `jj tag set --allow-move -r main X.Y.Z`, then update the remote tag with `git push --force origin X.Y.Z`.

View file

@ -1,636 +1,34 @@
import js from "@eslint/js";
import json from "@eslint/json";
import { defineConfig } from "eslint/config";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import obsidianmd from "eslint-plugin-obsidianmd";
import reactHooks from "eslint-plugin-react-hooks";
import ts from "typescript";
import { PlainTextParser } from "eslint-plugin-obsidianmd/dist/lib/plainTextParser.js";
import tseslint from "typescript-eslint";
const typeScriptFiles = ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"];
const nodeJavaScriptFiles = ["*.mjs", "scripts/**/*.mjs"];
const typeScriptConfigFiles = ["*.config.ts"];
const lintedTypeScriptFiles = [...typeScriptFiles, ...typeScriptConfigFiles];
const unsafeTypeScriptRules = {
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
};
const projectTypeScriptRules = {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
...unsafeTypeScriptRules,
"react-hooks/exhaustive-deps": "error",
"react-hooks/rules-of-hooks": "error",
};
const testTypeScriptRelaxations = {
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/require-await": "off",
};
const preactFormRestrictions = [
{
selector: "JSXAttribute[name.name=/^(defaultValue|defaultChecked)$/]",
message: "Keep Preact form state explicit with controlled value or checked props.",
},
];
const removedChatStateEscapeHatchRestrictions = [
{
selector: "Literal[value='state/patched']",
message: "Use a named ChatAction instead of reintroducing the generic state patch escape hatch.",
},
];
const chatSignalAdapterRestrictions = [
{
selector: "ImportDeclaration[source.value='@preact/signals']",
message: "Keep chat signals in panel/shell-state.tsx as a reducer-to-Preact notification adapter.",
},
];
const chatDomainLayerRestrictions = [
{
selector:
"ImportDeclaration[source.value=/^(?:\\.\\.\\/)+(?:app-server|application|host|panel|presentation|ui)(?:\\/|$)|^src\\/features\\/chat\\/(?:app-server|application|host|panel|presentation|ui)(?:\\/|$)/]",
message:
"Keep chat/domain as Panel-owned meaning models and pure derivations; app-server, application, host, panel, presentation, and UI layers may depend on domain, not the reverse.",
},
];
const uiRootImportRestrictions = [
{
selector: "ImportDeclaration[source.value=/shared\\/ui\\/ui-root$/]",
message: "Import the Preact root adapter only from explicit root bridge files.",
},
];
const generatedAppServerSourceImportPatterns = importBoundaryPatterns("generated/app-server", "src/generated/app-server", 6);
const generatedAppServerTestImportPatterns = importBoundaryPatterns("src/generated/app-server", "src/generated/app-server", 6);
const lowerLevelFeatureImportPatterns = importBoundaryPatterns("features", "src/features", 6);
const appServerConnectionImportPatterns = importBoundaryPatterns("app-server/connection", "src/app-server/connection", 6);
const appServerProtocolConnectionImportPatterns = [...appServerConnectionImportPatterns, "../connection", "../connection/**"];
const nonAppServerBannedAppServerProtocolModules = [
"catalog",
"diagnostics",
"file-change",
"initialization",
"request-input",
"runtime-config",
"runtime-metrics",
"runtime-policy",
"thread",
"thread-goal",
"thread-settings",
"turn",
"turn-history",
];
const nonAppServerBannedAppServerProtocolImportPatterns = nonAppServerBannedAppServerProtocolModules.flatMap((moduleName) =>
importBoundaryPatterns(`app-server/protocol/${moduleName}`, `src/app-server/protocol/${moduleName}`, 6),
);
const chatAppServerProtocolBoundaryBannedModules = nonAppServerBannedAppServerProtocolModules.filter((moduleName) => moduleName !== "turn");
const chatAppServerProtocolBoundaryBannedImportPatterns = chatAppServerProtocolBoundaryBannedModules.flatMap((moduleName) =>
importBoundaryPatterns(`app-server/protocol/${moduleName}`, `src/app-server/protocol/${moduleName}`, 6),
);
const generatedAppServerThreadImportRestrictions = [
{
selector:
"ImportDeclaration[source.value=/generated\\/app-server\\/v2\\/Thread$/] ImportSpecifier[imported.name='Thread'][local.name='Thread']",
message: "Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.",
},
];
const turnProtocolGeneratedImportRestrictions = [
{
selector: "ImportDeclaration[source.value=/generated\\/app-server\\//]:not([source.value=/generated\\/app-server\\/v2\\/ThreadItem$/])",
message: "Only generated ThreadItem is allowed in the turn protocol exception. Model other turn payload shapes locally.",
},
];
const chatAppServerProtocolBoundaryFiles = [
"src/features/chat/app-server/inbound/notification-plan.ts",
"src/features/chat/app-server/mappers/message-stream/turn-items.ts",
];
const unsafeIteratorRestrictions = [
{
selector: "MemberExpression[property.name='value'][object.type='CallExpression'][object.callee.property.name='next']",
message: "Avoid reading iterator.next().value directly; use for...of or inspect the typed IteratorResult first.",
},
];
const imperativeDomWriteMethods = new Set([
"addClass",
"addClasses",
"append",
"appendChild",
"after",
"before",
"createDiv",
"createEl",
"createSpan",
"empty",
"hide",
"insertAdjacentElement",
"insertAdjacentHTML",
"insertAdjacentText",
"insertBefore",
"prepend",
"removeClass",
"removeClasses",
"remove",
"removeChild",
"replaceChildren",
"replaceWith",
"setCssProps",
"setCssStyles",
"setText",
"show",
"setAttr",
"toggleClass",
]);
const imperativeDomEventMethods = new Set(["addEventListener", "removeEventListener"]);
const imperativeDomAssignmentProperties = new Set([
"checked",
"innerHTML",
"onblur",
"onchange",
"onclick",
"ondblclick",
"onfocus",
"oninput",
"onkeydown",
"onkeyup",
"onmousedown",
"onmousemove",
"onmouseup",
"onpointerdown",
"onpointerup",
"onscroll",
"onselect",
"outerHTML",
"textContent",
"value",
]);
const pureChatStateRestrictions = [
{
selector: "CallExpression[callee.object.name='Date'][callee.property.name='now']",
message: "Keep chat state transforms deterministic; generate IDs or timestamps at the controller/view boundary.",
},
{
selector: "NewExpression[callee.name='Date']",
message: "Keep chat state transforms deterministic; pass dates in from the controller/view boundary.",
},
{
selector: "CallExpression[callee.object.name='Math'][callee.property.name='random']",
message: "Keep chat state transforms deterministic; generate IDs at the controller/view boundary.",
},
{
selector: "NewExpression[callee.name=/^(AppServerClient|ConnectionManager|Notice)$/]",
message: "Keep app-server and Obsidian side effects out of chat state transforms.",
},
{
selector: "CallExpression[callee.property.name=/^(setTimeout|clearTimeout|requestAnimationFrame)$/]",
message: "Keep scheduling side effects out of chat state transforms.",
},
{
selector: "MemberExpression[object.name=/^(document|localStorage|sessionStorage)$/]",
message: "Keep browser globals out of chat state transforms.",
},
];
const chatExternalDomBridgeFiles = [
"src/features/chat/ui/message-stream/markdown-renderer.ts",
"src/features/chat/ui/message-stream/stream-markdown-renderer.ts",
"src/features/chat/ui/message-stream/virtualizer.ts",
];
const chatPreactDomBridgeFiles = [
"src/features/chat/ui/message-stream/text-content.tsx",
"src/features/chat/ui/message-stream/detail.tsx",
"src/features/chat/ui/message-stream/viewport.tsx",
"src/features/chat/ui/composer-dom.ts",
"src/features/chat/panel/shell.tsx",
"src/features/chat/ui/turn-diff/render.tsx",
];
const chatImperativeDomBridgeFiles = [...chatExternalDomBridgeFiles, ...chatPreactDomBridgeFiles];
const uiRootBridgeFiles = [
"src/features/chat/panel/shell.tsx",
"src/features/chat/ui/turn-diff/render.tsx",
"src/features/chat/ui/turn-diff/view.ts",
"src/features/selection-rewrite/popover.tsx",
"src/features/threads-view/renderer.tsx",
"src/settings/tab.tsx",
];
const nonChatImperativeDomBridgeFiles = [
"src/features/selection-rewrite/popover.tsx",
"src/features/thread-picker/modal.ts",
"src/features/threads-view/renderer.tsx",
"src/settings/tab.tsx",
"src/shared/diff/render.ts",
"src/shared/ui/components.tsx",
"src/shared/ui/textarea-autogrow.ts",
"src/shared/ui/textarea-caret.ts",
"src/shared/ui/ui-root.tsx",
];
const nonUiEventListenerFiles = ["src/shared/lifecycle/abortable.ts", "src/shared/ui/dom-events.ts"];
const appServerProjectionRpcMethodPattern = "^(resumeThread|threadTurnsList|forkThread|rollbackThread)$";
const appServerProjectionRpcRestrictions = [
{
selector: `CallExpression[callee.object.name='client'][callee.property.name=/${appServerProjectionRpcMethodPattern}/]`,
message:
"Keep app-server projection RPCs behind app-server facades; chat application code should consume Panel-owned snapshots or view models.",
},
];
const appServerClientProjectionTypeRestrictions = [
{
selector: `TSIndexedAccessType[objectType.typeName.name='AppServerClient'][indexType.literal.value=/${appServerProjectionRpcMethodPattern}/]`,
message:
"Do not expose app-server projection RPC signatures through AppServerClient indexed access types; define a Panel-owned projection type instead.",
},
];
const baseSourceSyntaxRestrictions = [
...removedChatStateEscapeHatchRestrictions,
...generatedAppServerThreadImportRestrictions,
...appServerClientProjectionTypeRestrictions,
...unsafeIteratorRestrictions,
...preactFormRestrictions,
];
const sourceSyntaxRestrictionsWithoutUiRoot = [...baseSourceSyntaxRestrictions, ...chatSignalAdapterRestrictions];
const sourceSyntaxRestrictions = [...sourceSyntaxRestrictionsWithoutUiRoot, ...uiRootImportRestrictions];
const chatSourceSyntaxRestrictions = sourceSyntaxRestrictions;
const chatDomBridgeSyntaxRestrictions = sourceSyntaxRestrictions;
const nonChatDomBridgeSyntaxRestrictions = sourceSyntaxRestrictions;
const pureChatStateSyntaxRestrictions = [...sourceSyntaxRestrictions, ...appServerProjectionRpcRestrictions, ...pureChatStateRestrictions];
const codexPanelEslintPlugin = {
rules: {
"no-self-referential-initializer-callback": {
meta: {
type: "problem",
docs: {
description: "Disallow callbacks in variable initializers from referencing the variable being initialized.",
},
messages: {
selfReference: "Avoid referencing '{{name}}' from a callback inside its own initializer; declare it first with an explicit type.",
},
schema: [],
},
create(context) {
return {
VariableDeclarator(node) {
if (node.id.type !== "Identifier" || !node.init) return;
if (node.init.type !== "NewExpression") return;
const reference = findInitializerCallbackReference(node.init, node.id.name);
if (reference) context.report({ node: reference, messageId: "selfReference", data: { name: node.id.name } });
},
};
},
},
"no-chat-state-direct-mutation": {
meta: {
type: "problem",
docs: {
description: "Disallow direct ChatState mutation in chat modules.",
},
messages: {
assign: "Route ChatState updates through ChatStateStore.dispatch().",
mutateCollection: "Clone ChatState collections and update them through ChatStateStore.dispatch().",
},
schema: [],
},
create(context) {
const mutatingCollectionMethods = new Set(["add", "clear", "delete", "push", "set"]);
const chatStateAliasVariables = new WeakSet();
let parserServices = null;
let checker = null;
const typeChecker = () => {
if (!parserServices) {
parserServices = parserServicesFromContext(context);
checker = parserServices.program.getTypeChecker();
}
return checker;
};
const variableForIdentifier = (node) => {
let scope = context.sourceCode.getScope(node);
while (scope) {
const variable = scope.variables.find((item) => item.name === node.name);
if (variable) return variable;
scope = scope.upper;
}
return null;
};
const markChatStateAlias = (node) => {
const variable = variableForIdentifier(node);
if (variable) chatStateAliasVariables.add(variable);
};
const isChatStateAlias = (node) => {
if (node?.type !== "Identifier") return false;
const variable = variableForIdentifier(node);
return Boolean(variable && chatStateAliasVariables.has(variable));
};
const typeAt = (node) => {
const services = parserServices ?? parserServicesFromContext(context);
if (!parserServices) {
parserServices = services;
checker = services.program.getTypeChecker();
}
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
return typeChecker().getTypeAtLocation(tsNode);
};
const isChatStateValue = (node) => typeIncludesChatState(typeAt(node), typeChecker());
const isChatStateAliasSource = (node) =>
(isChatStateTarget(node) || isChatStateValue(node)) && typeCanCarryChatStateMutation(typeAt(node));
const isChatStateTarget = (node) => {
if (isChatStateAlias(node)) return true;
if (!isMemberExpression(node)) return false;
if (isChatStateMember(node)) return true;
const root = rootMemberObject(node);
if (!root) return false;
if (isChatStateAlias(root)) return true;
return typeIncludesChatState(typeAt(root), typeChecker());
};
return {
VariableDeclarator(node) {
if (node.id.type !== "Identifier" || !node.init) return;
if (isChatStateAliasSource(node.init)) markChatStateAlias(node.id);
},
AssignmentExpression(node) {
if (isMemberExpression(node.left) && isChatStateTarget(node.left)) context.report({ node: node.left, messageId: "assign" });
},
CallExpression(node) {
if (!isMemberExpression(node.callee)) return;
const method = staticPropertyName(node.callee.property);
if (!method || !mutatingCollectionMethods.has(method)) return;
if (isChatStateTarget(node.callee.object)) context.report({ node: node.callee, messageId: "mutateCollection" });
},
};
},
},
"no-imperative-dom": {
meta: {
type: "problem",
docs: {
description: "Disallow imperative DOM writes and event wiring outside explicit bridge files.",
},
messages: {
event: "Keep imperative DOM event wiring in an explicit bridge module or Obsidian-owned UI boundary.",
write: "Keep imperative DOM writes in an explicit bridge module or Obsidian-owned UI boundary.",
},
schema: [
{
type: "object",
additionalProperties: false,
properties: {
allowEvents: { type: "boolean" },
allowWrites: { type: "boolean" },
},
},
],
},
create(context) {
const options = context.options[0] ?? {};
const allowEvents = options.allowEvents === true;
const allowWrites = options.allowWrites === true;
let parserServices = null;
let checker = null;
const typeChecker = () => {
if (!parserServices) {
parserServices = parserServicesFromContext(context);
checker = parserServices.program.getTypeChecker();
}
return checker;
};
const isDomTarget = (node) => {
const services = parserServices ?? parserServicesFromContext(context);
if (!parserServices) {
parserServices = services;
checker = services.program.getTypeChecker();
}
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
return typeIncludesDom(typeChecker().getTypeAtLocation(tsNode), typeChecker());
};
return {
AssignmentExpression(node) {
if (allowWrites || !isMemberExpression(node.left)) return;
const property = staticPropertyName(node.left.property);
if (!property || !imperativeDomAssignmentProperties.has(property)) return;
if (isDomTarget(node.left.object)) context.report({ node: node.left, messageId: "write" });
},
CallExpression(node) {
if (!isMemberExpression(node.callee)) return;
const method = staticPropertyName(node.callee.property);
if (!method) return;
if (!allowWrites && imperativeDomWriteMethods.has(method) && isDomTarget(node.callee.object)) {
context.report({ node: node.callee, messageId: "write" });
return;
}
if (!allowEvents && imperativeDomEventMethods.has(method) && isDomTarget(node.callee.object)) {
context.report({ node: node.callee, messageId: "event" });
}
},
};
},
},
},
};
function importBoundaryPatterns(relativeTarget, absoluteTarget, maxParentDepth) {
const targets = [absoluteTarget, ...Array.from({ length: maxParentDepth }, (_, index) => `${"../".repeat(index + 1)}${relativeTarget}`)];
return targets.flatMap((target) => [target, `${target}/**`]);
}
function restrictedSyntaxRule(restrictions) {
return {
"no-restricted-syntax": ["error", ...restrictions],
};
}
function isChatStateMember(node) {
if (!isMemberExpression(node)) return false;
let current = node;
while (isMemberExpression(current)) {
if (isIdentifier(current.object, "state")) return true;
if (isThisStateMember(current.object)) return true;
current = current.object;
}
return false;
}
function isThisStateMember(node) {
return isMemberExpression(node) && node.object?.type === "ThisExpression" && staticPropertyName(node.property) === "state";
}
function rootMemberObject(node) {
if (!isMemberExpression(node)) return null;
let current = node;
while (isMemberExpression(current)) current = current.object;
return current;
}
function isMemberExpression(node) {
return node?.type === "MemberExpression";
}
function isIdentifier(node, name) {
return node?.type === "Identifier" && node.name === name;
}
function staticPropertyName(node) {
return node?.type === "Identifier" ? node.name : node?.type === "Literal" && typeof node.value === "string" ? node.value : null;
}
function parserServicesFromContext(context) {
const services = context.sourceCode.parserServices;
if (!services?.program || !services.esTreeNodeToTSNodeMap) {
throw new Error("codex-panel/no-imperative-dom requires TypeScript parser services.");
}
return services;
}
function typeIncludesDom(type, checker, seen = new Set()) {
if (!type || seen.has(type.id)) return false;
seen.add(type.id);
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return false;
if (type.isUnionOrIntersection()) return type.types.some((item) => typeIncludesDom(item, checker, seen));
const typeName = checker.typeToString(type);
if (domTypeName(typeName)) return true;
const symbolName = type.getSymbol()?.getName() ?? type.aliasSymbol?.getName() ?? "";
if (domTypeName(symbolName)) return true;
const apparent = checker.getApparentType(type);
if (apparent !== type && typeIncludesDom(apparent, checker, seen)) return true;
const bases = typeof type.getBaseTypes === "function" ? (type.getBaseTypes() ?? []) : [];
return bases.some((base) => typeIncludesDom(base, checker, seen));
}
function typeIncludesChatState(type, checker, seen = new Set()) {
if (!type || seen.has(type.id)) return false;
seen.add(type.id);
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return false;
if (type.isUnionOrIntersection()) return type.types.some((item) => typeIncludesChatState(item, checker, seen));
const typeName = checker.typeToString(type);
if (chatStateTypeName(typeName)) return true;
const symbolName = type.getSymbol()?.getName() ?? type.aliasSymbol?.getName() ?? "";
if (chatStateTypeName(symbolName)) return true;
const apparent = checker.getApparentType(type);
if (apparent !== type && typeIncludesChatState(apparent, checker, seen)) return true;
const bases = typeof type.getBaseTypes === "function" ? (type.getBaseTypes() ?? []) : [];
return bases.some((base) => typeIncludesChatState(base, checker, seen));
}
function typeCanCarryChatStateMutation(type, seen = new Set()) {
if (!type || seen.has(type.id)) return false;
seen.add(type.id);
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return false;
if (type.isUnionOrIntersection()) return type.types.some((item) => typeCanCarryChatStateMutation(item, seen));
const primitiveLikeFlags =
ts.TypeFlags.StringLike |
ts.TypeFlags.NumberLike |
ts.TypeFlags.BooleanLike |
ts.TypeFlags.BigIntLike |
ts.TypeFlags.ESSymbolLike |
ts.TypeFlags.Null |
ts.TypeFlags.Undefined |
ts.TypeFlags.Void;
return (type.flags & primitiveLikeFlags) === 0;
}
function domTypeName(name) {
return /\b(?:AbortSignal|Document|Element|EventTarget|HTML[A-Za-z]*Element|HTMLElement|Node|SVG[A-Za-z]*Element|SVGElement|Window)\b/.test(
name,
);
}
function chatStateTypeName(name) {
return /\bChatState\b/.test(name);
}
function findInitializerCallbackReference(root, name) {
let reference = null;
const visit = (node, inCallback) => {
if (!node || reference) return;
if (Array.isArray(node)) {
for (const item of node) visit(item, inCallback);
return;
}
if (typeof node !== "object" || typeof node.type !== "string") return;
if (isFunctionNode(node)) {
if (functionShadowsName(node, name)) return;
visit(node.body, true);
return;
}
if (inCallback && node.type === "Identifier" && node.name === name) {
reference = node;
return;
}
for (const [key, value] of Object.entries(node)) {
if (key === "parent") continue;
if (node.type === "MemberExpression" && key === "property" && !node.computed) continue;
if (node.type === "Property" && key === "key" && !node.computed) continue;
visit(value, inCallback);
}
};
visit(root, false);
return reference;
}
function isFunctionNode(node) {
return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
}
function functionShadowsName(node, name) {
return node.params.some((param) => patternContainsName(param, name)) || patternContainsName(node.id, name);
}
function patternContainsName(node, name) {
if (!node) return false;
if (Array.isArray(node)) return node.some((item) => patternContainsName(item, name));
if (typeof node !== "object" || typeof node.type !== "string") return false;
if (node.type === "Identifier") return node.name === name;
return Object.entries(node).some(([key, value]) => key !== "parent" && patternContainsName(value, name));
}
const sourceTypeScriptFiles = ["src/**/*.{ts,tsx}"];
export default defineConfig([
{
ignores: ["main.js", "node_modules/**", "src/generated/**"],
},
js.configs.recommended,
{
basePath: "src",
plugins: {
obsidianmd,
},
},
...obsidianmd.configs.recommended.map(obsidianSourceRecommendedConfig).filter(Boolean),
...tseslint.configs.strictTypeChecked.map((config) => ({
...config,
files: lintedTypeScriptFiles,
})),
...tseslint.configs.stylisticTypeChecked.map((config) => ({
...config,
files: lintedTypeScriptFiles,
})),
...obsidianmd.configs.recommended.map((config) => ({
...config,
basePath: "src",
files: sourceTypeScriptFiles,
})),
{
files: lintedTypeScriptFiles,
plugins: {
"react-hooks": reactHooks,
},
files: sourceTypeScriptFiles,
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
projectService: true,
},
globals: {
AbortSignal: "readonly",
@ -646,256 +44,72 @@ export default defineConfig([
},
},
rules: {
...projectTypeScriptRules,
// Biome import restrictions do not cover `import("...").T`; keep that type syntax out before source-boundary lint runs.
"@typescript-eslint/consistent-type-imports": [
"error",
{
disallowTypeAnnotations: true,
fixStyle: "separate-type-imports",
prefer: "type-imports",
},
],
"@typescript-eslint/require-await": "off",
},
},
{
files: nodeJavaScriptFiles,
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
URL: "readonly",
console: "readonly",
process: "readonly",
},
},
},
{
files: ["tests/**/*.{ts,tsx}"],
rules: {
...testTypeScriptRelaxations,
...restrictedSyntaxRule(generatedAppServerThreadImportRestrictions),
},
},
{
files: ["src/**/*.ts", "src/**/*.tsx"],
files: ["manifest.json"],
plugins: {
"codex-panel": codexPanelEslintPlugin,
json,
obsidianmd,
},
language: "json/json",
rules: {
"codex-panel/no-self-referential-initializer-callback": "error",
"obsidianmd/ui/sentence-case": [
"error",
{
acronyms: ["MCP"],
brands: ["Codex", "Codex Panel", "Obsidian"],
},
],
"obsidianmd/validate-manifest": "error",
},
},
{
files: ["src/**/*.{ts,tsx}"],
ignores: ["src/features/chat/**/*.{ts,tsx}", ...nonChatImperativeDomBridgeFiles, ...nonUiEventListenerFiles],
files: ["LICENSE"],
plugins: {
obsidianmd,
},
languageOptions: {
parser: PlainTextParser,
},
rules: {
...restrictedSyntaxRule(sourceSyntaxRestrictions),
"codex-panel/no-imperative-dom": "error",
"obsidianmd/validate-license": "error",
},
},
{
files: ["src/features/chat/**/*.{ts,tsx}"],
ignores: ["src/features/chat/panel/shell-state.tsx", ...chatImperativeDomBridgeFiles],
rules: {
...restrictedSyntaxRule(chatSourceSyntaxRestrictions),
"codex-panel/no-imperative-dom": "error",
"codex-panel/no-chat-state-direct-mutation": "error",
},
},
{
files: ["src/features/chat/application/**/*.{ts,tsx}"],
rules: {
...restrictedSyntaxRule([...chatSourceSyntaxRestrictions, ...appServerProjectionRpcRestrictions]),
"codex-panel/no-imperative-dom": "error",
"codex-panel/no-chat-state-direct-mutation": "error",
},
},
{
files: ["src/features/chat/panel/shell-state.tsx"],
rules: {
...restrictedSyntaxRule(baseSourceSyntaxRestrictions),
"codex-panel/no-imperative-dom": "error",
"codex-panel/no-chat-state-direct-mutation": "error",
},
},
{
files: chatImperativeDomBridgeFiles,
rules: {
...restrictedSyntaxRule(chatDomBridgeSyntaxRestrictions),
"codex-panel/no-chat-state-direct-mutation": "error",
},
},
{
files: nonChatImperativeDomBridgeFiles,
rules: restrictedSyntaxRule(nonChatDomBridgeSyntaxRestrictions),
},
{
files: nonUiEventListenerFiles,
rules: {
...restrictedSyntaxRule(sourceSyntaxRestrictions),
"codex-panel/no-imperative-dom": ["error", { allowEvents: true }],
},
},
{
files: uiRootBridgeFiles,
rules: restrictedSyntaxRule(sourceSyntaxRestrictionsWithoutUiRoot),
},
{
files: ["src/features/chat/application/state/**/*.{ts,tsx}"],
rules: {
...restrictedSyntaxRule(pureChatStateSyntaxRestrictions),
"codex-panel/no-imperative-dom": "error",
"codex-panel/no-chat-state-direct-mutation": "error",
},
},
{
files: ["src/features/chat/domain/**/*.{ts,tsx}"],
rules: restrictedSyntaxRule([...sourceSyntaxRestrictions, ...chatDomainLayerRestrictions]),
},
{
files: ["src/app-server/**/*.{ts,tsx}", "src/domain/**/*.{ts,tsx}", "src/shared/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: appServerProtocolConnectionImportPatterns,
message:
"Keep app-server protocol modules as connection-independent adapters. Put protocol-local projections in protocol modules and let connection/client consume them.",
},
{
group: lowerLevelFeatureImportPatterns,
message: "Lower-level modules must not import feature modules. Move shared behavior to shared, domain, or app-server.",
},
],
},
],
},
},
{
files: ["src/app-server/**/*.{ts,tsx}"],
ignores: ["src/app-server/connection/**/*.{ts,tsx}", "src/app-server/protocol/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: generatedAppServerSourceImportPatterns,
message:
"Keep generated app-server bindings in the raw connection layer or explicit protocol adapters. App-server services, queries, catalogs, and thread helpers should expose domain models or local projections.",
},
],
},
],
},
},
{
files: ["src/app-server/protocol/**/*.{ts,tsx}"],
ignores: ["src/app-server/protocol/turn.ts"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: appServerProtocolConnectionImportPatterns,
message:
"Keep app-server protocol modules as connection-independent adapters. Put protocol-local projections in protocol modules and let connection/client consume them.",
},
{
group: lowerLevelFeatureImportPatterns,
message: "Lower-level modules must not import feature modules. Move shared behavior to shared, domain, or app-server.",
},
{
group: generatedAppServerSourceImportPatterns,
message:
"Keep generated app-server bindings out of protocol modules except the explicit turn protocol exception. Model app-server payloads with protocol-local projections.",
},
],
},
],
},
},
{
files: ["src/app-server/protocol/turn.ts"],
rules: {
...restrictedSyntaxRule(turnProtocolGeneratedImportRestrictions),
"no-restricted-imports": [
"error",
{
patterns: [
{
group: appServerProtocolConnectionImportPatterns,
message:
"Keep app-server protocol modules as connection-independent adapters. Put protocol-local projections in protocol modules and let connection/client consume them.",
},
],
},
],
},
},
{
files: ["src/**/*.{ts,tsx}"],
ignores: ["src/app-server/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: nonAppServerBannedAppServerProtocolImportPatterns,
message:
"Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.",
},
{
group: generatedAppServerSourceImportPatterns,
message: "Keep generated app-server types behind src/app-server; expose Panel-owned models to feature, UI, and reducer code.",
},
],
},
],
},
},
{
files: chatAppServerProtocolBoundaryFiles,
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: chatAppServerProtocolBoundaryBannedImportPatterns,
message:
"Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.",
},
{
group: generatedAppServerSourceImportPatterns,
message: "Keep generated app-server types behind src/app-server; expose Panel-owned models to feature, UI, and reducer code.",
},
],
},
],
},
},
{
files: ["tests/**/*.{ts,tsx}"],
ignores: ["tests/app-server/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: generatedAppServerTestImportPatterns,
message:
"Keep generated app-server types behind src/app-server and tests/app-server; feature tests should use Panel-owned models.",
},
],
},
],
},
},
eslintConfigPrettier,
]);
function obsidianSourceRecommendedConfig(config) {
const rules = Object.fromEntries(
Object.entries(config.rules ?? {})
.filter(([ruleName]) => {
return (
ruleName.startsWith("obsidianmd/") && ruleName !== "obsidianmd/validate-manifest" && ruleName !== "obsidianmd/validate-license"
);
})
.map(([ruleName, ruleConfig]) => {
if (ruleName !== "obsidianmd/ui/sentence-case") return [ruleName, ruleConfig];
return [
ruleName,
[
"error",
{
enforceCamelCaseLower: true,
acronyms: ["MCP"],
brands: ["Codex", "Codex Panel", "Obsidian"],
},
],
];
}),
);
if (Object.keys(rules).length === 0) return null;
const obsidianConfig = {
basePath: "src",
rules,
};
if (config.files) obsidianConfig.files = config.files;
if (config.ignores) obsidianConfig.ignores = config.ignores;
return obsidianConfig;
}

View file

@ -2,5 +2,5 @@
"entry": ["src/main.ts", "scripts/**/*.mjs", "*.config.{ts,mjs}", "tests/**/*.test.{ts,tsx}"],
"project": ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}", "scripts/**/*.mjs", "*.config.{ts,mjs}"],
"ignore": ["src/generated/**"],
"ignoreBinaries": ["codex"]
"ignoreBinaries": ["codex", "taskkill"]
}

View file

@ -1,7 +1,7 @@
{
"id": "codex-panel",
"name": "Codex Panel",
"version": "3.2.8",
"version": "5.2.1",
"minAppVersion": "1.12.0",
"description": "Codex in your sidebar.",
"author": "murashit",

4985
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "codex-panel",
"version": "3.2.8",
"version": "5.2.1",
"description": "Codex in your Obsidian sidebar.",
"main": "main.js",
"author": "murashit",
@ -13,57 +13,65 @@
"url": "https://github.com/murashit/codex-panel/issues"
},
"homepage": "https://github.com/murashit/codex-panel#readme",
"engines": {
"node": ">=26"
},
"scripts": {
"api:baseline": "node scripts/api-baseline.mjs",
"api:baseline:check": "node scripts/api-baseline.mjs --check",
"build": "node esbuild.config.mjs",
"build:styles": "node scripts/build-styles.mjs",
"build:styles:check": "node scripts/build-styles.mjs --check",
"check": "node scripts/check.mjs",
"check:ci": "node scripts/check.mjs --ci",
"format": "node scripts/format.mjs",
"format:check": "node scripts/format.mjs --check --cache",
"format:check:ci": "node scripts/format.mjs --check",
"commitlint": "commitlint",
"check": "concurrently --group --pad-prefix 'node:check:*' && node --run build",
"check:biome": "biome check --diagnostic-level=warn --error-on-warnings",
"check:css-usage": "node scripts/check-css-usage.mjs",
"check:eslint": "eslint src manifest.json LICENSE --max-warnings=0",
"check:test": "node --run test",
"check:typecheck": "node --run typecheck",
"check:unused": "knip --no-progress",
"fix": "biome check --fix --skip=plugin --diagnostic-level=warn --error-on-warnings && knip --fix --no-progress",
"generate:app-server-types": "node scripts/generate-app-server-types.mjs",
"lint": "node scripts/run-parallel.mjs lint:ts lint:css lint:css-usage lint:deps lint:unused",
"lint:ci": "node scripts/run-parallel.mjs lint:ts:ci lint:css lint:css-usage lint:deps lint:unused",
"lint:css": "stylelint \"src/**/*.css\" --max-warnings=0",
"lint:css-usage": "node scripts/lint/check-css-usage.mjs --fail-on-candidates",
"lint:deps": "node scripts/lint/check-import-cycles.mjs",
"lint:ts": "eslint src tests scripts \"*.config.ts\" \"*.config.mjs\" --max-warnings=0",
"lint:ts:ci": "eslint src tests scripts \"*.config.ts\" \"*.config.mjs\" --max-warnings=0",
"lint:unused": "knip --no-progress",
"generate:app-server-types:check": "node scripts/generate-app-server-types.mjs --check",
"release:check": "node scripts/release/check.mjs",
"release:notes": "node scripts/release/notes.mjs",
"release:preflight": "node scripts/release/preflight.mjs",
"release:prepare": "node scripts/release/prepare.mjs",
"test": "vitest run --cache",
"test:ci": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit --incremental --tsBuildInfoFile node_modules/.cache/typescript/tsconfig.tsbuildinfo",
"typecheck:ci": "tsc -p tsconfig.json --noEmit"
"test": "vitest run --pool threads --maxWorkers 6",
"test:coverage": "vitest run --pool threads --maxWorkers 6 --coverage",
"test:mutation": "stryker run",
"typecheck": "tsc -p tsconfig.json --noEmit --incremental --tsBuildInfoFile node_modules/.cache/typescript/tsconfig.tsbuildinfo"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^25.6.0",
"@biomejs/biome": "^2.5.3",
"@commitlint/cli": "^21.2.1",
"@commitlint/config-conventional": "^21.2.0",
"@stryker-mutator/core": "^9.6.1",
"@stryker-mutator/vitest-runner": "^9.6.1",
"@types/mdast": "^4.0.4",
"@types/node": "^26.1.1",
"@vitest/coverage-v8": "^4.1.10",
"concurrently": "^9.2.4",
"conventional-changelog-conventionalcommits": "^10.2.1",
"conventional-commits-parser": "^7.1.0",
"esbuild": "^0.28.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint": "^10.6.0",
"eslint-plugin-obsidianmd": "^0.4.1",
"jsdom": "^29.1.1",
"knip": "^6.16.1",
"knip": "^6.25.0",
"moment": "^2.30.1",
"obsidian": "~1.12.3",
"prettier": "^3.8.3",
"stylelint": "^17.12.0",
"stylelint-config-standard": "^40.0.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.1",
"vitest": "^4.1.8"
"typescript-eslint": "^8.64.0",
"vitest": "^4.1.10"
},
"dependencies": {
"@preact/signals": "^2.9.1",
"@tanstack/query-core": "^5.101.0",
"@tanstack/virtual-core": "3.17.0",
"@tanstack/query-core": "^5.101.2",
"defuddle": "^0.19.1",
"diff": "^9.0.0",
"mdast-util-from-markdown": "^2.0.3",
"mdast-util-to-markdown": "^2.1.2",
"micromark": "^4.0.2",
"preact": "^10.29.2"
"obsidian-daily-notes-interface": "^0.9.5",
"preact": "^10.29.7",
"unist-util-visit": "^5.1.0"
}
}

View file

@ -1,17 +1,28 @@
import { readFile } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import { readJson } from "./utils.mjs";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const args = new Set(process.argv.slice(2));
const shouldCheck = args.has("--check");
const asJson = args.has("--json");
const validArgs = new Set(["--json"]);
function fail(message) {
failures.push(message);
}
if (isMain()) {
const args = new Set(process.argv.slice(2));
const asJson = args.has("--json");
for (const arg of args) {
if (!validArgs.has(arg)) {
console.error("Usage: node scripts/api-baseline.mjs [--json]");
process.exit(1);
}
}
function warn(message) {
warnings.push(message);
const report = await createApiBaselineReport();
if (asJson) {
console.log(JSON.stringify(report, null, 2));
} else {
printReport(report);
}
if (report.failures.length > 0) process.exit(1);
}
function parseSemver(value) {
@ -39,12 +50,139 @@ function readCodexVersion() {
return parseSemver(`${result.stdout}\n${result.stderr}`)?.version ?? null;
}
export async function createApiBaselineReport(options = {}) {
const failures = [];
const fail = (message) => {
failures.push(message);
};
const inputs = await readBaselineInputs(options.cwd ?? process.cwd());
const readmeBaselines = readCompatibilityBaselines(inputs.readme);
const codexReadmeVersion = readmeBaselines.codexTestedCliVersion;
const codexRecordedVersion = inputs.appServerCompatibilityJson.codexAppServer?.testedCliVersion ?? null;
const codexLocalVersion = options.readCodexVersion ? await options.readCodexVersion() : readCodexVersion();
const codexReadmeSemver = parseSemver(codexReadmeVersion);
const codexRecordedSemver = parseSemver(codexRecordedVersion);
const codexLocalSemver = parseSemver(codexLocalVersion);
const obsidianMinVersion = inputs.manifestJson.minAppVersion;
const obsidianReadmeApiTypesVersion = readmeBaselines.obsidianApiTypesVersion;
const obsidianReadmeMinVersion = readmeBaselines.obsidianMinAppVersion;
const obsidianVersionEntry = inputs.versionsJson[inputs.packageJson.version] ?? null;
const obsidianSpec = inputs.packageJson.devDependencies?.obsidian ?? null;
const obsidianLockVersion = inputs.packageLockJson.packages?.["node_modules/obsidian"]?.version ?? null;
const obsidianSpecSemver = parseSemver(obsidianSpec);
const obsidianLockSemver = parseSemver(obsidianLockVersion);
const obsidianMinSemver = parseSemver(obsidianMinVersion);
const generationArguments = inputs.appServerCompatibilityJson.codexAppServer?.typeGeneration?.arguments ?? null;
const expectedGenerationArguments = ["app-server", "generate-ts", "--experimental"];
const appServerGenerationArgumentsDeclared =
Array.isArray(generationArguments) && generationArguments.every((argument) => typeof argument === "string");
const appServerGenerationArgumentsSupported =
appServerGenerationArgumentsDeclared && arraysEqual(generationArguments, expectedGenerationArguments);
const initializeCapabilities = inputs.appServerCompatibilityJson.codexAppServer?.initialize?.capabilities ?? {};
const initializeExperimentalApi = initializeCapabilities.experimentalApi === true;
const initializeRequestAttestationDisabled = initializeCapabilities.requestAttestation === false;
if (!codexReadmeSemver) {
fail("README.md Compatibility table must define `codexAppServer.testedCliVersion` as X.Y.Z.");
}
if (!codexRecordedSemver || codexRecordedSemver.version !== codexRecordedVersion) {
fail("src/app-server/connection/compatibility.json must declare codexAppServer.testedCliVersion as X.Y.Z.");
}
if (!codexLocalSemver) {
fail("local codex --version could not be read.");
}
if (codexReadmeVersion && codexRecordedVersion && codexReadmeVersion !== codexRecordedVersion) {
fail(`README Codex CLI ${codexReadmeVersion} does not match recorded tested CLI ${codexRecordedVersion}.`);
}
if (codexLocalSemver && codexRecordedSemver && codexLocalSemver.version !== codexRecordedSemver.version) {
fail(`local Codex CLI ${codexLocalSemver.version} does not match recorded tested CLI ${codexRecordedSemver.version}.`);
}
if (!appServerGenerationArgumentsDeclared) {
fail("src/app-server/connection/compatibility.json must declare codexAppServer.typeGeneration.arguments as a string array.");
} else if (!appServerGenerationArgumentsSupported) {
fail(`app-server type generation arguments must be ${expectedGenerationArguments.join(" ")}.`);
}
if (!initializeExperimentalApi) {
fail("src/app-server/connection/compatibility.json must declare codexAppServer.initialize.capabilities.experimentalApi: true.");
}
if (!initializeRequestAttestationDisabled) {
fail("src/app-server/connection/compatibility.json must declare codexAppServer.initialize.capabilities.requestAttestation: false.");
}
if (!obsidianMinSemver) fail("manifest.json minAppVersion must be X.Y.Z.");
if (obsidianMinSemver && obsidianMinSemver.patch !== 0) {
fail(`manifest.json minAppVersion should be the minor baseline patch 0, got ${obsidianMinVersion}.`);
}
if (!parseSemver(obsidianReadmeMinVersion)) {
fail("README.md Compatibility table must define `manifest.minAppVersion` as X.Y.Z.");
} else if (obsidianReadmeMinVersion !== obsidianMinVersion) {
fail(`README Obsidian baseline ${displayValue(obsidianReadmeMinVersion)} does not match manifest ${obsidianMinVersion}.`);
}
if (obsidianVersionEntry !== obsidianMinVersion) {
fail(`versions.json must map ${inputs.packageJson.version} to manifest minAppVersion ${obsidianMinVersion}.`);
}
if (!obsidianSpecSemver) fail("package.json devDependency obsidian must include an X.Y.Z version.");
if (!obsidianLockSemver) fail("package-lock.json must lock node_modules/obsidian to X.Y.Z.");
if (packageRangeKind(obsidianSpec) !== "patch") {
fail(`package.json devDependency obsidian should use a patch-only '~' range, got ${displayValue(obsidianSpec)}.`);
}
if (obsidianMinSemver && obsidianSpecSemver && minorKey(obsidianSpecSemver) !== minorKey(obsidianMinSemver)) {
fail(`obsidian devDependency minor ${minorKey(obsidianSpecSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
}
if (obsidianMinSemver && obsidianLockSemver && minorKey(obsidianLockSemver) !== minorKey(obsidianMinSemver)) {
fail(`locked obsidian minor ${minorKey(obsidianLockSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
}
if (!parseSemver(obsidianReadmeApiTypesVersion)) {
fail("README.md Compatibility table must define `obsidian` API types as X.Y.Z.");
} else if (obsidianReadmeApiTypesVersion !== obsidianLockVersion) {
fail(
`README Obsidian API types ${displayValue(obsidianReadmeApiTypesVersion)} does not match package-lock obsidian ${displayValue(obsidianLockVersion)}.`,
);
}
return {
codex: {
policy: "compatibility is managed by minor; generated bindings are proven against an exact CLI patch",
recordedTestedCliVersion: codexRecordedVersion,
readmeTestedCliVersion: codexReadmeVersion,
readmeTestedMinor: minorKey(codexReadmeSemver),
localCliVersion: codexLocalVersion,
localCliMinor: minorKey(codexLocalSemver),
readmeMatchesRecordedVersion: codexReadmeVersion && codexRecordedVersion ? codexReadmeVersion === codexRecordedVersion : false,
localCliMatchesRecordedVersion:
codexLocalSemver && codexRecordedSemver ? codexLocalSemver.version === codexRecordedSemver.version : null,
generationArguments,
appServerGenerationArgumentsSupported,
initializeExperimentalApi,
initializeRequestAttestationDisabled,
},
obsidian: {
policy: "manifest declares the minimum app version; obsidian npm provides compile-time API types in the same minor",
minAppVersion: obsidianMinVersion,
minAppVersionMinor: minorKey(obsidianMinSemver),
readmeMinAppVersion: obsidianReadmeMinVersion,
versionsJsonCurrentMinAppVersion: obsidianVersionEntry,
readmeApiTypesVersion: obsidianReadmeApiTypesVersion,
packageDependency: obsidianSpec,
packageDependencyRange: packageRangeKind(obsidianSpec),
packageDependencyMinor: minorKey(obsidianSpecSemver),
lockedPackageVersion: obsidianLockVersion,
lockedPackageMinor: minorKey(obsidianLockSemver),
},
failures,
};
}
function readCompatibilityBaselines(readme) {
const section = markdownSection(readme, "Compatibility");
const table = readMarkdownTableValues(section);
return {
codexTestedCliVersion: table.get("codex.testedCliVersion") ?? null,
obsidianMinAppVersion: table.get("obsidian.minAppVersion") ?? null,
codexTestedCliVersion: table.get("codexAppServer.testedCliVersion") ?? null,
obsidianApiTypesVersion: table.get("obsidian") ?? null,
obsidianMinAppVersion: table.get("manifest.minAppVersion") ?? null,
};
}
@ -64,7 +202,7 @@ function readMarkdownTableValues(markdown) {
const header = splitMarkdownTableRow(lines[index]);
if (!header) continue;
const separator = splitMarkdownTableRow(lines[index + 1]);
if (!separator || !separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
if (!separator?.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
const keyColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "key");
const versionColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "version");
@ -109,118 +247,49 @@ function displayValue(value) {
return value ?? "(missing)";
}
const failures = [];
const warnings = [];
const packageJson = await readJson("package.json");
const packageLockJson = await readJson("package-lock.json");
const manifestJson = await readJson("manifest.json");
const versionsJson = await readJson("versions.json");
const readme = await readFile("README.md", "utf8");
const clientSource = await readFile("src/app-server/connection/client.ts", "utf8");
const appServerGenerateSource = await readFile("scripts/generate-app-server-types.mjs", "utf8");
const readmeBaselines = readCompatibilityBaselines(readme);
const codexReadmeVersion = readmeBaselines.codexTestedCliVersion;
const codexLocalVersion = readCodexVersion();
const codexReadmeSemver = parseSemver(codexReadmeVersion);
const codexLocalSemver = parseSemver(codexLocalVersion);
const obsidianMinVersion = manifestJson.minAppVersion;
const obsidianReadmeMinVersion = readmeBaselines.obsidianMinAppVersion;
const obsidianVersionEntry = versionsJson[packageJson.version] ?? null;
const obsidianSpec = packageJson.devDependencies?.obsidian ?? null;
const obsidianLockVersion = packageLockJson.packages?.["node_modules/obsidian"]?.version ?? null;
const obsidianSpecSemver = parseSemver(obsidianSpec);
const obsidianLockSemver = parseSemver(obsidianLockVersion);
const obsidianMinSemver = parseSemver(obsidianMinVersion);
const appServerGenerationExperimental =
appServerGenerateSource.includes("app-server") &&
appServerGenerateSource.includes("generate-ts") &&
appServerGenerateSource.includes("--experimental");
const initializeExperimentalApi = /experimentalApi:\s*true/.test(clientSource);
const initializeRequestAttestationDisabled = /requestAttestation:\s*false/.test(clientSource);
if (!codexReadmeSemver) {
fail("README.md Compatibility table must define `codex.testedCliVersion` as X.Y.Z.");
}
if (!codexLocalSemver) {
const message = "local codex --version could not be read.";
if (shouldCheck) fail(message);
else warn(message);
}
if (codexReadmeSemver && codexLocalSemver && minorKey(codexReadmeSemver) !== minorKey(codexLocalSemver)) {
fail(`local Codex CLI minor ${minorKey(codexLocalSemver)} does not match compatibility table minor ${minorKey(codexReadmeSemver)}.`);
}
if (!appServerGenerationExperimental) fail("generate:app-server-types must use codex app-server generate-ts --experimental.");
if (!initializeExperimentalApi) fail("app-server initialize must declare experimentalApi: true.");
if (!initializeRequestAttestationDisabled) fail("app-server initialize must declare requestAttestation: false.");
if (!obsidianMinSemver) fail("manifest.json minAppVersion must be X.Y.Z.");
if (obsidianMinSemver && obsidianMinSemver.patch !== 0) {
fail(`manifest.json minAppVersion should be the minor baseline patch 0, got ${obsidianMinVersion}.`);
}
if (!parseSemver(obsidianReadmeMinVersion)) {
fail("README.md Compatibility table must define `obsidian.minAppVersion` as X.Y.Z.");
} else if (obsidianReadmeMinVersion !== obsidianMinVersion) {
fail(`README Obsidian baseline ${displayValue(obsidianReadmeMinVersion)} does not match manifest ${obsidianMinVersion}.`);
}
if (obsidianVersionEntry !== obsidianMinVersion) {
fail(`versions.json must map ${packageJson.version} to manifest minAppVersion ${obsidianMinVersion}.`);
}
if (!obsidianSpecSemver) fail("package.json devDependency obsidian must include an X.Y.Z version.");
if (!obsidianLockSemver) fail("package-lock.json must lock node_modules/obsidian to X.Y.Z.");
if (packageRangeKind(obsidianSpec) !== "patch") {
fail(`package.json devDependency obsidian should use a patch-only '~' range, got ${displayValue(obsidianSpec)}.`);
}
if (obsidianMinSemver && obsidianSpecSemver && minorKey(obsidianSpecSemver) !== minorKey(obsidianMinSemver)) {
fail(`obsidian devDependency minor ${minorKey(obsidianSpecSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
}
if (obsidianMinSemver && obsidianLockSemver && minorKey(obsidianLockSemver) !== minorKey(obsidianMinSemver)) {
fail(`locked obsidian minor ${minorKey(obsidianLockSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
function arraysEqual(left, right) {
return left.length === right.length && left.every((value, index) => value === right[index]);
}
const report = {
codex: {
policy: "managed by minor version",
readmeTestedCliVersion: codexReadmeVersion,
readmeTestedMinor: minorKey(codexReadmeSemver),
localCliVersion: codexLocalVersion,
localCliMinor: minorKey(codexLocalSemver),
localCliMatchesTestedMinor: codexReadmeSemver && codexLocalSemver ? minorKey(codexReadmeSemver) === minorKey(codexLocalSemver) : null,
appServerGenerationExperimental,
initializeExperimentalApi,
initializeRequestAttestationDisabled,
},
obsidian: {
policy: "track latest patch within the guaranteed minor; raise minAppVersion when the minor changes",
minAppVersion: obsidianMinVersion,
minAppVersionMinor: minorKey(obsidianMinSemver),
readmeMinAppVersion: obsidianReadmeMinVersion,
versionsJsonCurrentMinAppVersion: obsidianVersionEntry,
packageDependency: obsidianSpec,
packageDependencyRange: packageRangeKind(obsidianSpec),
packageDependencyMinor: minorKey(obsidianSpecSemver),
lockedPackageVersion: obsidianLockVersion,
lockedPackageMinor: minorKey(obsidianLockSemver),
},
warnings,
failures,
};
async function readBaselineInputs(cwd) {
const [packageJson, packageLockJson, manifestJson, versionsJson, readme, appServerCompatibilityJson] = await Promise.all([
readJson(cwd, "package.json"),
readJson(cwd, "package-lock.json"),
readJson(cwd, "manifest.json"),
readJson(cwd, "versions.json"),
readFile(path.join(cwd, "README.md"), "utf8"),
readJson(cwd, "src/app-server/connection/compatibility.json"),
]);
if (asJson) {
console.log(JSON.stringify(report, null, 2));
} else {
return {
appServerCompatibilityJson,
manifestJson,
packageJson,
packageLockJson,
readme,
versionsJson,
};
}
async function readJson(cwd, file) {
return JSON.parse(await readFile(path.join(cwd, file), "utf8"));
}
function isMain() {
return process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
}
function printReport(report) {
console.log("API baseline");
console.log("");
console.log("Codex app-server");
console.log(` policy: ${report.codex.policy}`);
console.log(` compatibility table CLI: ${displayValue(report.codex.readmeTestedCliVersion)}`);
console.log(` recorded tested CLI: ${displayValue(report.codex.recordedTestedCliVersion)}`);
console.log(` compatibility table minor: ${displayValue(report.codex.readmeTestedMinor)}`);
console.log(` local codex CLI: ${displayValue(report.codex.localCliVersion)}`);
console.log(` local codex minor: ${displayValue(report.codex.localCliMinor)}`);
console.log(` generate-ts --experimental: ${report.codex.appServerGenerationExperimental ? "yes" : "no"}`);
console.log(` generation arguments: ${report.codex.generationArguments?.join(" ") ?? "(missing)"}`);
console.log(` initialize experimentalApi: ${report.codex.initializeExperimentalApi ? "yes" : "no"}`);
console.log(` initialize requestAttestation disabled: ${report.codex.initializeRequestAttestationDisabled ? "yes" : "no"}`);
console.log("");
@ -229,19 +298,13 @@ if (asJson) {
console.log(` manifest minAppVersion: ${displayValue(report.obsidian.minAppVersion)}`);
console.log(` compatibility table minAppVersion: ${displayValue(report.obsidian.readmeMinAppVersion)}`);
console.log(` versions.json current minAppVersion: ${displayValue(report.obsidian.versionsJsonCurrentMinAppVersion)}`);
console.log(` compatibility table API types: ${displayValue(report.obsidian.readmeApiTypesVersion)}`);
console.log(` package obsidian: ${displayValue(report.obsidian.packageDependency)}`);
console.log(` package range: ${report.obsidian.packageDependencyRange}`);
console.log(` package-lock obsidian: ${displayValue(report.obsidian.lockedPackageVersion)}`);
if (warnings.length > 0) {
console.log("");
console.log("Warnings");
for (const message of warnings) console.log(` - ${message}`);
}
if (failures.length > 0) {
if (report.failures.length > 0) {
console.log("");
console.log("Failures");
for (const message of failures) console.log(` - ${message}`);
for (const message of report.failures) console.log(` - ${message}`);
}
}
if (shouldCheck && failures.length > 0) process.exit(1);

View file

@ -0,0 +1,49 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const compatibilityRelativePath = "src/app-server/connection/compatibility.json";
const exactSemverPattern = /^\d+\.\d+\.\d+$/;
if (isMain()) {
const args = process.argv.slice(2);
if (args.length !== 1 || args[0] !== "--tested-cli-version") {
console.error("Usage: node scripts/app-server-compatibility.mjs --tested-cli-version");
process.exit(1);
}
try {
const policy = await readAppServerGenerationPolicy(process.cwd());
console.log(policy.testedCliVersion);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
export async function readAppServerGenerationPolicy(cwd) {
const compatibilityPath = path.join(cwd, compatibilityRelativePath);
const compatibility = JSON.parse(await readFile(compatibilityPath, "utf8"));
const testedCliVersion = compatibility.codexAppServer?.testedCliVersion;
const generationArguments = compatibility.codexAppServer?.typeGeneration?.arguments;
if (typeof testedCliVersion !== "string" || !exactSemverPattern.test(testedCliVersion)) {
throw new Error(`${compatibilityRelativePath} must declare codexAppServer.testedCliVersion as X.Y.Z.`);
}
if (!isStringArray(generationArguments) || generationArguments.length === 0) {
throw new Error(`${compatibilityRelativePath} must declare codexAppServer.typeGeneration.arguments as a non-empty string array.`);
}
if (generationArguments.includes("--out")) {
throw new Error(`${compatibilityRelativePath} must not include --out in codexAppServer.typeGeneration.arguments.`);
}
return { generationArguments, testedCliVersion };
}
function isStringArray(value) {
return Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0);
}
function isMain() {
return process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
}

View file

@ -5,35 +5,23 @@ import { fileURLToPath } from "node:url";
const sourceDir = path.join("src", "styles");
const orderPath = path.join(sourceDir, "order.json");
const outputPath = "styles.css";
const checkMode = process.argv.includes("--check");
const validArgs = new Set(["--check"]);
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
for (const arg of process.argv.slice(2)) {
if (!validArgs.has(arg)) {
console.error("Usage: node scripts/build-styles.mjs [--check]");
process.exit(1);
}
}
if (checkMode) {
await checkStyles();
} else {
await buildStyles();
if (process.argv.length > 2) {
console.error("Usage: node scripts/build-styles.mjs");
process.exit(1);
}
await buildStyles();
}
export async function buildStyles() {
await checkStyleOrder();
await mkdir(path.dirname(outputPath), { recursive: true });
await writeFile(outputPath, await renderStyles());
}
async function checkStyles() {
await checkStyleOrder();
await renderStyles();
}
export async function renderStyles() {
const { version } = JSON.parse(await readFile("package.json", "utf8"));
const parts = [];
const sourceFiles = await readStyleOrder();
@ -42,7 +30,7 @@ export async function renderStyles() {
parts.push(content.trimEnd());
}
return `${parts.join("\n\n")}\n`;
return `/* Codex Panel v${version} */\n\n${parts.join("\n\n")}\n`;
}
async function readStyleOrder() {

View file

@ -3,15 +3,9 @@ import path from "node:path";
const root = process.cwd();
const stylesDir = path.join(root, "src", "styles");
const args = new Set(process.argv.slice(2));
const failOnCandidates = args.has("--fail-on-candidates");
const validArgs = new Set(["--fail-on-candidates"]);
for (const arg of args) {
if (!validArgs.has(arg)) {
console.error("Usage: node scripts/lint/check-css-usage.mjs [--fail-on-candidates]");
process.exit(1);
}
if (process.argv.length > 2) {
console.error("Usage: node scripts/check-css-usage.mjs");
process.exit(1);
}
const cssFiles = await orderedCssFiles();
@ -21,28 +15,20 @@ const sourceFiles = await filesInTree(path.join(root, "src"), new Set([".ts", ".
const testFiles = await filesInTree(path.join(root, "tests"), new Set([".ts", ".tsx"]));
const cssClasses = await collectCssClasses(cssFiles);
const cssCustomProperties = await collectCssCustomProperties(cssFiles);
const referencedCssCustomProperties = await collectReferencedCssCustomProperties(cssFiles);
const sourceTexts = await readTexts(sourceFiles);
const testTexts = await readTexts(testFiles);
const sourceText = sourceTexts.map((item) => item.text).join("\n");
const dynamicPrefixes = collectDynamicClassPrefixes(sourceText);
const exactSourceUsed = [];
const dynamicSourceUsed = [];
const testOnlyCandidates = [];
const candidates = [];
const unusedCustomProperties = [];
for (const [className, locations] of cssClasses) {
const sourceMatches = locationsInTexts(sourceTexts, className);
if (sourceMatches.length > 0) {
exactSourceUsed.push({ className, locations, sourceMatches });
continue;
}
const matchingPrefix = dynamicPrefixes.find((prefix) => className.startsWith(prefix));
if (matchingPrefix) {
dynamicSourceUsed.push({ className, locations, matchingPrefix });
continue;
}
if (sourceMatches.length > 0) continue;
const testMatches = locationsInTexts(testTexts, className);
if (testMatches.length > 0) {
@ -53,16 +39,15 @@ for (const [className, locations] of cssClasses) {
candidates.push({ className, locations });
}
printReport({
total: cssClasses.size,
exactSourceUsed,
dynamicSourceUsed,
testOnlyCandidates,
candidates,
dynamicPrefixes,
});
for (const [propertyName, locations] of cssCustomProperties) {
if (referencedCssCustomProperties.has(propertyName) || locationsInTexts(sourceTexts, propertyName).length > 0) continue;
unusedCustomProperties.push({ propertyName, locations });
}
if (failOnCandidates && candidates.length + testOnlyCandidates.length > 0) process.exit(1);
if (candidates.length + testOnlyCandidates.length + unusedCustomProperties.length + dynamicPrefixes.length > 0) {
printCandidates({ testOnlyCandidates, candidates, unusedCustomProperties, dynamicPrefixes });
process.exit(1);
}
async function orderedCssFiles() {
const orderPath = path.join(stylesDir, "order.json");
@ -76,12 +61,11 @@ async function orderedCssFiles() {
async function collectCssClasses(files) {
const result = new Map();
for (const file of files) {
const text = await readFile(file, "utf8");
const text = stripCssComments(await readFile(file, "utf8"));
const lines = text.split("\n");
for (const [index, line] of lines.entries()) {
const withoutComment = line.replace(/\/\*.*?\*\//g, "");
const classPattern = /(^|[^\\])\.([_a-zA-Z][\w-]*)/g;
for (const match of withoutComment.matchAll(classPattern)) {
for (const match of line.matchAll(classPattern)) {
const className = match[2];
if (!className.startsWith("codex-panel")) continue;
const locations = result.get(className) ?? [];
@ -93,6 +77,38 @@ async function collectCssClasses(files) {
return new Map([...result.entries()].sort(([left], [right]) => left.localeCompare(right)));
}
async function collectCssCustomProperties(files) {
const result = new Map();
for (const file of files) {
const text = stripCssComments(await readFile(file, "utf8"));
const lines = text.split("\n");
for (const [index, line] of lines.entries()) {
for (const match of line.matchAll(/(--codex-panel-[\w-]+)\s*:/g)) {
const propertyName = match[1];
const locations = result.get(propertyName) ?? [];
locations.push(`${relative(file)}:${index + 1}`);
result.set(propertyName, locations);
}
}
}
return new Map([...result.entries()].sort(([left], [right]) => left.localeCompare(right)));
}
async function collectReferencedCssCustomProperties(files) {
const result = new Set();
for (const file of files) {
const text = stripCssComments(await readFile(file, "utf8"));
for (const match of text.matchAll(/var\(\s*(--codex-panel-[\w-]+)/g)) {
result.add(match[1]);
}
}
return result;
}
function stripCssComments(text) {
return text.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, ""));
}
function collectDynamicClassPrefixes(text) {
const prefixes = new Set();
const templatePrefixPattern = /codex-panel(?:-[a-z]+)?__[a-z0-9-]+--\$\{/g;
@ -112,9 +128,12 @@ async function readTexts(files) {
function locationsInTexts(texts, needle) {
const locations = [];
const escapedNeedle = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`(^|[^\\w-])${escapedNeedle}(?![\\w-])`);
for (const { file, text } of texts) {
let offset = text.indexOf(needle);
if (offset === -1) continue;
const match = pattern.exec(text);
if (!match) continue;
const offset = match.index + match[1].length;
const line = text.slice(0, offset).split("\n").length;
locations.push(`${relative(file)}:${line}`);
}
@ -142,35 +161,41 @@ async function collectFiles(directory, extensions, excludedPrefixes, result) {
}
}
function printReport({ total, exactSourceUsed, dynamicSourceUsed, testOnlyCandidates, candidates, dynamicPrefixes }) {
const totalCandidates = candidates.length + testOnlyCandidates.length;
console.log("CSS usage audit");
console.log(` CSS classes: ${total}`);
console.log(` Source literals: ${exactSourceUsed.length}`);
console.log(` Source dynamic modifiers: ${dynamicSourceUsed.length}`);
console.log(` Test-only candidates: ${testOnlyCandidates.length}`);
console.log(` Unused candidates: ${totalCandidates}`);
function printCandidates({ testOnlyCandidates, candidates, unusedCustomProperties, dynamicPrefixes }) {
console.error("CSS usage check failed.");
if (dynamicPrefixes.length > 0) {
console.log("\nDynamic modifier prefixes:");
for (const prefix of dynamicPrefixes) console.log(` ${prefix}*`);
console.error("");
console.error("Dynamic CSS class prefixes are not allowed:");
for (const prefix of dynamicPrefixes) {
console.error(` ${prefix}`);
}
}
if (testOnlyCandidates.length > 0) {
console.log("\nTest-only unused CSS class candidates:");
console.error("");
console.error("Test-only unused CSS class candidates:");
for (const item of testOnlyCandidates) {
console.log(` ${item.className}`);
console.log(` css: ${item.locations.join(", ")}`);
console.log(` tests: ${item.testMatches.join(", ")}`);
console.error(` ${item.className}`);
console.error(` css: ${item.locations.join(", ")}`);
console.error(` tests: ${item.testMatches.join(", ")}`);
}
}
if (candidates.length > 0) {
console.log("\nUnused CSS class candidates:");
console.error("");
console.error("Unused CSS class candidates:");
for (const item of candidates) {
console.log(` ${item.className}`);
console.log(` css: ${item.locations.join(", ")}`);
console.error(` ${item.className}`);
console.error(` css: ${item.locations.join(", ")}`);
}
}
if (unusedCustomProperties.length > 0) {
console.error("");
console.error("Unused CSS custom property candidates:");
for (const item of unusedCustomProperties) {
console.error(` ${item.propertyName}`);
console.error(` css: ${item.locations.join(", ")}`);
}
}
}

View file

@ -1,42 +0,0 @@
import { spawnSync } from "node:child_process";
const args = new Set(process.argv.slice(2));
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
const checks = [
{ local: "typecheck", ci: "typecheck:ci", localPhase: "parallel" },
{ local: "test", ci: "test:ci", localPhase: "parallel" },
{ local: "lint", ci: "lint:ci", localPhase: "parallel" },
{ local: "format:check", ci: "format:check:ci", localPhase: "parallel" },
{ local: "build:styles:check", ci: "build:styles:check", localPhase: "parallel" },
{ local: "build", ci: "build", localPhase: "sequential" },
];
for (const arg of args) {
if (arg !== "--ci") {
console.error("Usage: node scripts/check.mjs [--ci]");
process.exit(1);
}
}
if (args.has("--ci")) {
for (const check of checks) {
if (check.ci === null) continue;
run(npmCommand, ["run", check.ci]);
}
} else {
const parallelScripts = checks.filter((check) => check.localPhase === "parallel").map((check) => check.local);
run("node", ["scripts/run-parallel.mjs", ...parallelScripts]);
for (const check of checks.filter((check) => check.localPhase === "sequential")) run(npmCommand, ["run", check.local]);
}
function run(command, args) {
const result = spawnSync(command, args, {
stdio: "inherit",
shell: false,
});
if (result.error) {
console.error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`);
process.exit(1);
}
if (result.status !== 0) process.exit(result.status ?? 1);
}

View file

@ -1,129 +0,0 @@
import { createHash } from "node:crypto";
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import prettier from "prettier";
const sourceExtensions = new Set([".ts", ".tsx", ".js", ".mjs", ".json", ".css", ".md"]);
const scriptExtensions = new Set([".js", ".mjs"]);
const rootExtensions = new Set([".ts", ".json", ".mjs", ".md", ".css"]);
const args = process.argv.slice(2);
const shouldCheck = args.includes("--check");
const shouldUseCache = args.includes("--cache");
const cacheLocation = argumentValue("--cache-location") ?? "node_modules/.cache/prettier/.prettier-cache";
const root = process.cwd();
const files = await formatFiles();
const cache = shouldCheck && shouldUseCache ? await readCache(cacheLocation) : new Map();
const nextCache = new Map(cache);
const unformattedFiles = [];
let hadError = false;
for (const file of files) {
try {
const info = await prettier.getFileInfo(file, { ignorePath: path.join(root, ".prettierignore") });
if (info.ignored || info.inferredParser === null) continue;
const input = await readFile(file, "utf8");
const config = (await prettier.resolveConfig(file)) ?? {};
const cacheKey = path.relative(root, file);
const cacheHash = hashFormatInput(input, config, info.inferredParser);
if (shouldCheck && cache.get(cacheKey) === cacheHash) continue;
const output = await prettier.format(input, { ...config, filepath: file });
if (output === input) {
nextCache.set(cacheKey, cacheHash);
continue;
}
if (shouldCheck) {
nextCache.delete(cacheKey);
unformattedFiles.push(cacheKey);
continue;
}
await writeFile(file, output);
console.log(path.relative(root, file));
} catch (error) {
hadError = true;
const message = error instanceof Error ? error.message : String(error);
console.error(`${path.relative(root, file)}: ${message}`);
}
}
if (shouldCheck && shouldUseCache) await writeCache(cacheLocation, nextCache);
if (unformattedFiles.length > 0) {
console.error("Unformatted files:");
for (const file of unformattedFiles) console.error(file);
process.exit(1);
}
if (hadError) process.exit(1);
function argumentValue(name) {
const index = args.indexOf(name);
if (index === -1) return null;
return args[index + 1] ?? null;
}
async function readCache(file) {
try {
const value = JSON.parse(await readFile(path.join(root, file), "utf8"));
return new Map(Object.entries(value.files ?? {}));
} catch {
return new Map();
}
}
async function writeCache(file, cacheEntries) {
const absoluteFile = path.join(root, file);
await mkdir(path.dirname(absoluteFile), { recursive: true });
const files = Object.fromEntries([...cacheEntries.entries()].sort(([left], [right]) => left.localeCompare(right)));
await writeFile(absoluteFile, `${JSON.stringify({ version: 1, files })}\n`);
}
function hashFormatInput(input, config, inferredParser) {
return createHash("sha256")
.update(prettier.version)
.update("\0")
.update(inferredParser ?? "")
.update("\0")
.update(JSON.stringify(config))
.update("\0")
.update(input)
.digest("hex");
}
async function formatFiles() {
const result = [
...(await filesInTree("src", sourceExtensions, { excludedPrefixes: ["src/generated"] })),
...(await filesInTree("tests", sourceExtensions)),
...(await filesInTree("scripts", scriptExtensions)),
...(await rootFiles(rootExtensions)),
];
return [...new Set(result)].sort((a, b) => path.relative(root, a).localeCompare(path.relative(root, b)));
}
async function rootFiles(extensions) {
const entries = await readdir(root, { withFileTypes: true });
return entries.filter((entry) => entry.isFile() && extensions.has(path.extname(entry.name))).map((entry) => path.join(root, entry.name));
}
async function filesInTree(relativeDirectory, extensions, options = {}) {
const directory = path.join(root, relativeDirectory);
const excludedPrefixes = options.excludedPrefixes?.map((prefix) => path.join(root, prefix)) ?? [];
const result = [];
await collectFiles(directory, extensions, excludedPrefixes, result);
return result;
}
async function collectFiles(directory, extensions, excludedPrefixes, result) {
if (excludedPrefixes.some((prefix) => directory === prefix || directory.startsWith(`${prefix}${path.sep}`))) return;
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const file = path.join(directory, entry.name);
if (entry.isDirectory()) {
await collectFiles(file, extensions, excludedPrefixes, result);
} else if (entry.isFile() && extensions.has(path.extname(entry.name))) {
result.push(file);
}
}
}

View file

@ -1,21 +1,105 @@
import { spawnSync } from "node:child_process";
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { readAppServerGenerationPolicy } from "./app-server-compatibility.mjs";
const generatedDir = path.resolve("src/generated/app-server");
const generatedRelativeDir = "src/generated/app-server";
const generatedHeader = "// GENERATED CODE! DO NOT MODIFY BY HAND!";
const normalizationNotice = "// This file was mechanically normalized after generation by scripts/generate-app-server-types.mjs.";
await cleanGeneratedTypes();
run("codex", ["app-server", "generate-ts", "--experimental", "--out", "src/generated/app-server"]);
await normalizeGeneratedTypes();
async function cleanGeneratedTypes() {
await rm(generatedDir, { recursive: true, force: true });
await mkdir(generatedDir, { recursive: true });
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
const args = process.argv.slice(2);
if (args.some((arg) => arg !== "--check") || args.length > 1) {
throw new Error("Usage: node scripts/generate-app-server-types.mjs [--check]");
}
await generateAppServerTypes({ check: args.includes("--check") });
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
async function normalizeGeneratedTypes() {
export async function generateAppServerTypes(options = {}) {
const cwd = options.cwd ?? process.cwd();
const generatedDir = path.resolve(cwd, generatedRelativeDir);
const runCommand = options.runCommand ?? run;
const readCodexVersion = options.readCodexVersion ?? readInstalledCodexVersion;
const policy = await readAppServerGenerationPolicy(cwd);
const installedCliVersion = await readCodexVersion(cwd);
if (installedCliVersion !== policy.testedCliVersion) {
throw new Error(
`Codex CLI ${policy.testedCliVersion} is required to generate app-server bindings; found ${installedCliVersion ?? "an unreadable version"}.`,
);
}
const generatedParent = path.dirname(generatedDir);
await mkdir(generatedParent, { recursive: true });
const stagedDir = await mkdtemp(path.join(generatedParent, ".app-server-"));
try {
const stagedRelativeDir = path.relative(cwd, stagedDir);
await runCommand("codex", [...policy.generationArguments, "--out", stagedRelativeDir], { cwd });
await normalizeGeneratedTypes(stagedDir);
if (options.check === true) {
const differences = await compareGeneratedTrees(generatedDir, stagedDir);
if (differences.length > 0) {
throw new Error(`generated app-server bindings are out of date:\n${differences.map((difference) => ` ${difference}`).join("\n")}`);
}
} else {
await replaceGeneratedTypes(generatedDir, stagedDir);
}
} finally {
await rm(stagedDir, { recursive: true, force: true });
}
}
async function compareGeneratedTrees(trackedDir, stagedDir) {
const [trackedFiles, stagedFiles] = await Promise.all([listFiles(trackedDir), listFiles(stagedDir)]);
const trackedSet = new Set(trackedFiles);
const stagedSet = new Set(stagedFiles);
const differences = [];
for (const file of stagedFiles) {
if (!trackedSet.has(file)) {
differences.push(`added: ${file}`);
continue;
}
const [trackedSource, stagedSource] = await Promise.all([readFile(path.join(trackedDir, file)), readFile(path.join(stagedDir, file))]);
if (!trackedSource.equals(stagedSource)) differences.push(`changed: ${file}`);
}
for (const file of trackedFiles) {
if (!stagedSet.has(file)) differences.push(`removed: ${file}`);
}
return differences;
}
async function replaceGeneratedTypes(generatedDir, stagedDir) {
const backupDir = `${generatedDir}.backup-${process.pid.toString()}-${Date.now().toString()}`;
let hasBackup = false;
try {
try {
await rename(generatedDir, backupDir);
hasBackup = true;
} catch (error) {
if (!isMissingPathError(error)) throw error;
}
await rename(stagedDir, generatedDir);
if (hasBackup) await rm(backupDir, { recursive: true, force: true });
} catch (error) {
if (hasBackup) {
await rm(generatedDir, { recursive: true, force: true });
await rename(backupDir, generatedDir);
}
throw error;
}
}
function isMissingPathError(error) {
return error instanceof Error && "code" in error && error.code === "ENOENT";
}
async function normalizeGeneratedTypes(generatedDir) {
const files = await listTypeScriptFiles(generatedDir);
await Promise.all(
@ -40,6 +124,24 @@ async function listTypeScriptFiles(dir) {
return files.flat();
}
async function listFiles(dir, relativeDir = "") {
let entries;
try {
entries = await readdir(path.join(dir, relativeDir), { withFileTypes: true });
} catch (error) {
if (isMissingPathError(error)) return [];
throw error;
}
const files = await Promise.all(
entries.map((entry) => {
const relativePath = path.join(relativeDir, entry.name);
if (entry.isDirectory()) return listFiles(dir, relativePath);
return entry.isFile() ? [relativePath.replaceAll(path.sep, "/")] : [];
}),
);
return files.flat().sort();
}
function normalizeSource(source) {
let normalized = source;
do {
@ -55,14 +157,25 @@ function addNormalizationNotice(source) {
return source.replace(generatedHeader, `${generatedHeader}\n${normalizationNotice}`);
}
function run(command, args) {
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd ?? process.cwd(),
stdio: "inherit",
shell: false,
});
if (result.error) {
console.error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`);
process.exit(1);
throw new Error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`);
}
if (result.status !== 0) process.exit(result.status ?? 1);
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} exited with status ${(result.status ?? 1).toString()}.`);
}
function readInstalledCodexVersion(cwd) {
const result = spawnSync("codex", ["--version"], {
cwd,
encoding: "utf8",
stdio: "pipe",
shell: false,
});
if (result.error || result.status !== 0) return null;
return `${result.stdout}\n${result.stderr}`.match(/\b\d+\.\d+\.\d+\b/)?.[0] ?? null;
}

View file

@ -0,0 +1,13 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: r"^[\"'].*shared/dom/events\.dom[\"']$" },
register_diagnostic(span=$stmt, message="Import DOM event listener helpers only from explicit .dom, .obsidian, or .measure bridge files.", severity="error")
}

View file

@ -0,0 +1,56 @@
language js
or {
`$target.createElement($...)` as $stmt,
`$target.createTextNode($...)` as $stmt,
`$target.createEl($...)` as $stmt,
`$target.createDiv($...)` as $stmt,
`$target.createSpan($...)` as $stmt,
`$target.empty()` as $stmt,
`$target.append($...)` as $stmt,
`$target.prepend($...)` as $stmt,
`$target.before($...)` as $stmt,
`$target.after($...)` as $stmt,
`$target.appendChild($...)` as $stmt,
`$target.insertBefore($...)` as $stmt,
`$target.replaceChildren($...)` as $stmt,
`$target.remove()` as $stmt,
`$target.insertAdjacentHTML($...)` as $stmt,
`$target.setAttribute($...)` as $stmt,
`$target.setAttr($...)` as $stmt,
`$target.addClass($...)` as $stmt,
`$target.removeClass($...)` as $stmt,
`$target.toggleClass($...)` as $stmt,
`$target.setText($...)` as $stmt,
`$target.classList.add($...)` as $stmt,
`$target.classList.remove($...)` as $stmt,
`$target.classList.toggle($...)` as $stmt,
`$target.addEventListener($...)` as $stmt,
`$target.removeEventListener($...)` as $stmt,
`$target.querySelector($...)` as $stmt,
`$target.querySelectorAll($...)` as $stmt,
`$target.closest($...)` as $stmt,
`$target.contains($...)` as $stmt,
`$target.getBoundingClientRect()` as $stmt,
`$target.focus($...)` as $stmt,
`$target.select()` as $stmt,
`$target.setSelectionRange($...)` as $stmt,
`$target.setCssProps($...)` as $stmt,
`$target.scrollHeight` as $stmt,
`$target.scrollWidth` as $stmt,
`$target.clientHeight` as $stmt,
`$target.clientWidth` as $stmt,
`$target.offsetTop` as $stmt,
`$target.offsetHeight` as $stmt,
`$target.selectionStart` as $stmt,
`$target.selectionEnd` as $stmt,
`$target.selectionDirection` as $stmt,
`$target.activeElement` as $stmt,
`$target.ownerDocument` as $stmt,
`$target.defaultView` as $stmt,
`$target.style.$property` as $stmt
} where {
not { $stmt <: `$target.addEventListener("abort", $...)` },
not { $stmt <: `$target.removeEventListener("abort", $...)` },
register_diagnostic(span=$stmt, message="Keep DOM reads, writes, measurements, hit-tests, focus, and event wiring in files named with a .dom, .obsidian, or .measure suffix.", severity="error")
}

View file

@ -0,0 +1,13 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: r"^[\"'].*shared/dom/preact-root\.dom[\"']$" },
register_diagnostic(span=$stmt, message="Import the Preact root adapter only from explicit root bridge files.", severity="error")
}

View file

@ -0,0 +1,29 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
private pattern app_server_protocol_connection_source() { r"^[\"'](?:\.\./)+connection(?:/.*)?[\"']$" }
private pattern root_app_server_connection_source() {
or {
r"^[\"'](?:\.\./)+app-server/connection(?:/.*)?[\"']$",
r"^[\"']src/app-server/connection(?:/.*)?[\"']$"
}
}
private pattern app_server_connection_boundary_source() {
or {
app_server_protocol_connection_source(),
root_app_server_connection_source()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: app_server_connection_boundary_source() },
register_diagnostic(span=$stmt, message="Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
private pattern app_server_protocol_source() { r"^[\"'](?:(?:\.\./)+app-server/protocol|src/app-server/protocol)/[^/\"']+(?:/.*)?[\"']$" }
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: app_server_protocol_source() },
register_diagnostic(span=$stmt, message="Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol at its app-server boundary; feature state and UI must use Panel-owned models.", severity="error")
}

View file

@ -0,0 +1,13 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: r"^[\"']\.\./[^/\"']+[\"']$" },
register_diagnostic(span=$stmt, message="App-server subfolders must not import sibling root modules; move the dependency into a responsibility subfolder.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:(?:\.\./)+(?:host|panel|presentation|ui)|src/features/chat/(?:host|panel|presentation|ui))(?:/.*)?[\"']$"
},
register_diagnostic(span=$stmt, message="Chat app-server adapters must not import chat host, panel, presentation, or UI layers.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:(?:\.\./)+(?:app-server|host|panel|presentation|ui)|(?:\.\./){2,}(?:selection-rewrite|thread-picker|threads|threads-view|turn-diff)|src/(?:app-server|features/(?:chat/(?:app-server|host|panel|presentation|ui)|selection-rewrite|thread-picker|threads|threads-view|turn-diff)))(?:/.*)?[\"']$"
},
register_diagnostic(span=$stmt, message="Chat application modules must not import app-server, sibling feature, host, panel, presentation, or UI layers; expose state and workflow contracts instead.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:(?:\.\./)+(?:presentation|ui)|src/features/chat/(?:presentation|ui))(?:/.*)?[\"']$"
},
register_diagnostic(span=$stmt, message="Chat host modules must not import chat presentation or UI layers; route render-facing projections through panel-owned adapters.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:(?:\.\./)+(?:app-server|host)|src/(?:app-server|features/chat/(?:app-server|host)))(?:/.*)?[\"']$"
},
register_diagnostic(span=$stmt, message="Chat panel modules must not import app-server adapters or chat host internals.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:(?:\.\./)+(?:application|app-server|host|panel|ui)|src/(?:app-server|features/chat/(?:application|app-server|host|panel|ui)))(?:/.*)?[\"']$"
},
register_diagnostic(span=$stmt, message="Chat presentation modules must stay pure view-model projection; keep application, app-server, host, panel, and UI dependencies outward.", severity="error")
}

View file

@ -0,0 +1,17 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
private pattern app_server_protocol_source() { r"^[\"'](?:(?:\.\./)+app-server/protocol|src/app-server/protocol)/[^/\"']+(?:/.*)?[\"']$" }
private pattern turn_protocol_source() { r"^[\"'](?:(?:\.\./)+app-server/protocol/turn|src/app-server/protocol/turn)(?:/.*)?[\"']$" }
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: app_server_protocol_source(), not { $source <: turn_protocol_source() } },
register_diagnostic(span=$stmt, message="Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol at its app-server boundary; feature state and UI must use Panel-owned models.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:(?:\.\./)+(?:application|app-server|host|panel)|src/(?:app-server|features/chat/(?:application|app-server|host|panel)))(?:/.*)?[\"']$"
},
register_diagnostic(span=$stmt, message="Chat UI modules must not import application, app-server, host, or panel layers; pass render-ready props and actions through UI contracts.", severity="error")
}

View file

@ -0,0 +1,13 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+workspace|src/workspace)(?:/.*)?[\"']$" },
register_diagnostic(span=$stmt, message="Chat modules must not import workspace modules; pass workspace capabilities through chat host contracts.", severity="error")
}

View file

@ -0,0 +1,30 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
private pattern root_outer_layer_source() {
or {
r"^[\"'](?:\.\./)+(?:app-server|settings|workspace|shared|features)(?:/.*)?[\"']$",
r"^[\"']src/(?:app-server|settings|workspace|shared|features)(?:/.*)?[\"']$",
r"^[\"']obsidian[\"']$"
}
}
private pattern chat_domain_outer_layer_source() { r"^[\"'](?:\.\./)+(?:application|host|panel|presentation|ui)(?:/.*)?[\"']$" }
private pattern domain_outer_layer_source() {
or {
root_outer_layer_source(),
chat_domain_outer_layer_source()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: domain_outer_layer_source() },
register_diagnostic(span=$stmt, message="Domain modules must stay pure; outer layers may depend on domain, not the reverse.", severity="error")
}

View file

@ -0,0 +1,13 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+workspace|src/workspace)(?:/.*)?[\"']$" },
register_diagnostic(span=$stmt, message="Feature modules must not import workspace modules; pass workspace capabilities through feature host contracts.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
private pattern generated_app_server_source() { r"^[\"'].*(?:src/)?generated/app-server(?:/.*)?[\"']$" }
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: generated_app_server_source() },
register_diagnostic(span=$stmt, message="Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", severity="error")
}

View file

@ -0,0 +1,13 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+features|src/features)(?:/.*)?[\"']$" },
register_diagnostic(span=$stmt, message="Do not import feature modules from this layer. Move reusable behavior to domain, shared DOM/Obsidian/runtime/UI, or app-server adapters.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:(?:\.\./)+app-server/(?:connection|services)|src/app-server/(?:connection|services))(?:/.*)?[\"']$"
},
register_diagnostic(span=$stmt, message="Thread workflows must depend on feature-owned ports instead of app-server clients or services.", severity="error")
}

View file

@ -0,0 +1,15 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:\.\./features/chat/(?:application|app-server|domain|panel|presentation|ui)|src/features/chat/(?:application|app-server|domain|panel|presentation|ui))(?:/.*)?[\"']$"
},
register_diagnostic(span=$stmt, message="Workspace modules may coordinate chat only through chat host contracts and Obsidian views.", severity="error")
}

View file

@ -0,0 +1,53 @@
engine biome(1.0)
language css
or {
// Keep colors on Obsidian or Codex Panel design tokens.
CssDeclarationWithSemicolon() as $decl where {
$decl <: r"\s*(?:background(?:-color)?|border(?:-(?:block|inline|top|right|bottom|left))?(?:-color)?|box-shadow|caret-color|color|fill|outline(?:-color)?|stroke|text-shadow): .*?(?:#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\(|\b(?:black|white)\b).*",
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel design tokens instead of hardcoded colors.")
},
CssDeclarationWithSemicolon() as $decl where {
$decl <: r".*--codex-panel-.*(?:accent|background|border|color|danger|error|faint|muted|ring|success|surface|text|warning).*: .*?(?:#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\(|\b(?:black|white)\b).*",
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel design tokens instead of hardcoded colors.")
},
CssDeclarationWithSemicolon() as $decl where {
$decl <: r".*\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\(.*",
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel design tokens instead of color functions.")
},
// Keep typography and layout dimensions on shared variables.
CssDeclarationWithSemicolon() as $decl where {
$decl <: r"\s*font-size: .*?\b\d*\.?\d+(?:px|rem|em|%)\b.*",
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel typography tokens instead of hardcoded font sizes.")
},
CssDeclarationWithSemicolon() as $decl where {
$decl <: r"\s*font-weight:\s*(?:[1-9]00|bold|bolder|lighter|normal)\s*;.*",
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel typography tokens instead of hardcoded font weights.")
},
CssDeclarationWithSemicolon() as $decl where {
$decl <: r"\s*line-height: .*?(?:\b\d*\.?\d+\b|\b\d*\.?\d+(?:px|rem|em|%)\b).*",
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel typography tokens instead of hardcoded line heights.")
},
CssDeclarationWithSemicolon() as $decl where {
$decl <: r"\s*(?:border(?:-(?:block|inline|top|right|bottom|left))?-width|border-radius|bottom|column-gap|gap|height|inset|inset-(?:block|inline)|left|margin|margin-(?:block|inline|top|right|bottom|left)|max-height|max-width|min-height|min-width|padding|padding-(?:block|inline|top|right|bottom|left)|right|row-gap|top|width): .*?\b\d*\.?\d+(?:px|rem|em)\b.*",
register_diagnostic(span=$decl, message="Prefer Obsidian or Codex Panel spacing and size tokens for layout dimensions.")
},
// Keep selectors local and predictable.
CssPseudoClassSelector() as $selector where {
$selector <: r".*:has.*",
register_diagnostic(span=$selector, message="Avoid :has() because it can cause broad selector invalidation.")
},
CssPseudoClassSelector() as $selector where {
$selector <: r".*:where[(].*(?:[.]|#|\[).*",
register_diagnostic(span=$selector, message="Do not hide class, id, or attribute selectors inside :where().")
},
CssIdSelector() as $selector where { register_diagnostic(span=$selector, message="Avoid ID selectors in Codex Panel CSS.") },
CssUniversalSelector() as $selector where {
register_diagnostic(span=$selector, message="Avoid universal selectors in Codex Panel CSS.")
},
CssAtRule() as $rule where {
$rule <: r"@keyframes ([a-zA-Z0-9_-]+)[\s\S]*"($name),
not $name <: r"codex-panel-[a-z0-9-]+",
register_diagnostic(span=$rule, message="Prefix keyframes with codex-panel-.")
}
}

View file

@ -0,0 +1,5 @@
language js
`$target.request($...)` as $stmt where {
register_diagnostic(span=$stmt, message="Keep direct app-server RPC calls behind app-server services and chat app-server adapters.", severity="error")
}

View file

@ -0,0 +1,18 @@
language js
or {
`Date.now()` as $stmt,
`new Date()` as $stmt,
`Math.random()` as $stmt,
`new AppServerClient($...)` as $stmt,
`new ConnectionManager($...)` as $stmt,
`new Notice($...)` as $stmt,
`setTimeout($...)` as $stmt,
`clearTimeout($...)` as $stmt,
`requestAnimationFrame($...)` as $stmt,
`document.$property` as $stmt,
`localStorage.$property` as $stmt,
`sessionStorage.$property` as $stmt
} where {
register_diagnostic(span=$stmt, message="Keep this state module deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", severity="error")
}

View file

@ -0,0 +1,8 @@
language js
or {
JsExportNamedFromClause() as $stmt,
JsExportFromClause() as $stmt
} where {
register_diagnostic(span=$stmt, message="Do not add hand-written re-exports. Import from the owning module directly.", severity="error")
}

View file

@ -0,0 +1,5 @@
language js
`$program` where {
register_diagnostic(span=$program, message="Keep TSX files in rendering-owned source folders; non-rendering source should use .ts.", severity="error")
}

View file

@ -0,0 +1,5 @@
language js
`$program` where {
register_diagnostic(span=$program, message="Keep responsibility-split source roots free of module files; add modules to the matching subfolder instead of the root.", severity="error")
}

View file

@ -0,0 +1,7 @@
language js
`const $name = new $constructor($callback)` as $stmt where {
or { $callback <: `() => $body`, $callback <: `function() { $body }` },
$body <: contains `$name`,
register_diagnostic(span=$stmt, message="Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.", severity="error")
}

View file

@ -0,0 +1,6 @@
language js
JsxAttribute(name=$name) as $stmt where {
or { $name <: `defaultValue`, $name <: `defaultChecked` },
register_diagnostic(span=$stmt, message="Keep Preact form state explicit with controlled value or checked props.", severity="error")
}

View file

@ -0,0 +1,5 @@
language js
`$iterator.next().value` as $stmt where {
register_diagnostic(span=$stmt, message="Avoid reading iterator.next().value directly; use for...of or inspect the typed IteratorResult first.", severity="error")
}

View file

@ -1,214 +0,0 @@
import path from "node:path";
import ts from "typescript";
if (process.argv.length > 2) {
console.error("Usage: node scripts/lint/check-import-cycles.mjs");
process.exit(1);
}
const cwd = process.cwd();
const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, "tsconfig.json");
if (!configPath) {
console.error("Could not find tsconfig.json.");
process.exit(1);
}
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
if (configFile.error) {
reportDiagnostic(configFile.error);
process.exit(1);
}
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath));
if (parsedConfig.errors.length > 0) {
for (const diagnostic of parsedConfig.errors) reportDiagnostic(diagnostic);
process.exit(1);
}
const sourceFiles = parsedConfig.fileNames.map((file) => path.resolve(file)).filter((file) => isCheckedSourceFile(relativePath(file)));
const sourceFileSet = new Set(sourceFiles);
const compilerHost = ts.createCompilerHost(parsedConfig.options, true);
const moduleResolutionCache = ts.createModuleResolutionCache(cwd, (file) => file, parsedConfig.options);
const graph = new Map(sourceFiles.map((file) => [relativePath(file), new Set()]));
for (const sourceFile of sourceFiles) {
const sourceText = ts.sys.readFile(sourceFile);
if (sourceText === undefined) continue;
const parsedSource = ts.createSourceFile(sourceFile, sourceText, parsedConfig.options.target ?? ts.ScriptTarget.ES2022, true);
const from = relativePath(sourceFile);
for (const edge of importEdges(parsedSource)) {
const resolved = ts.resolveModuleName(
edge.specifier,
sourceFile,
parsedConfig.options,
compilerHost,
moduleResolutionCache,
).resolvedModule;
if (!resolved) continue;
const resolvedFile = path.resolve(resolved.resolvedFileName);
if (!sourceFileSet.has(resolvedFile)) continue;
graph.get(from)?.add(relativePath(resolvedFile));
}
}
const cycleComponents = stronglyConnectedComponents(graph)
.filter((component) => component.length > 1 || hasSelfDependency(graph, component[0]))
.sort((left, right) => left[0].localeCompare(right[0]));
if (cycleComponents.length === 0) {
console.log("No import cycles found.");
process.exit(0);
}
console.error(`Import cycle check failed: found ${cycleComponents.length} cycle${cycleComponents.length === 1 ? "" : "s"}.`);
for (const [index, component] of cycleComponents.entries()) {
const cycle = sampleCycle(graph, component);
console.error(`${index + 1}. ${cycle.join(" -> ")}`);
}
process.exit(1);
function importEdges(sourceFile) {
const edges = [];
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) && hasRuntimeImportEdge(statement) && stringLiteralText(statement.moduleSpecifier)) {
edges.push({
specifier: stringLiteralText(statement.moduleSpecifier),
});
} else if (
ts.isExportDeclaration(statement) &&
hasRuntimeExportEdge(statement) &&
statement.moduleSpecifier &&
stringLiteralText(statement.moduleSpecifier)
) {
edges.push({
specifier: stringLiteralText(statement.moduleSpecifier),
});
}
}
return edges;
}
function hasRuntimeImportEdge(statement) {
const importClause = statement.importClause;
if (!importClause) return true;
if (importClause.isTypeOnly) return false;
if (importClause.name) return true;
const namedBindings = importClause.namedBindings;
if (!namedBindings) return false;
if (ts.isNamespaceImport(namedBindings)) return true;
return namedBindings.elements.some((element) => !element.isTypeOnly);
}
function hasRuntimeExportEdge(statement) {
if (statement.isTypeOnly) return false;
const exportClause = statement.exportClause;
if (!exportClause) return true;
if (ts.isNamespaceExport(exportClause)) return true;
return exportClause.elements.some((element) => !element.isTypeOnly);
}
function stringLiteralText(node) {
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null;
}
function stronglyConnectedComponents(graph) {
let nextIndex = 0;
const stack = [];
const onStack = new Set();
const indices = new Map();
const lowLinks = new Map();
const components = [];
for (const node of graph.keys()) {
if (!indices.has(node)) visit(node);
}
return components.map((component) => component.sort());
function visit(node) {
indices.set(node, nextIndex);
lowLinks.set(node, nextIndex);
nextIndex += 1;
stack.push(node);
onStack.add(node);
for (const dependency of graph.get(node) ?? []) {
if (!indices.has(dependency)) {
visit(dependency);
lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(dependency)));
} else if (onStack.has(dependency)) {
lowLinks.set(node, Math.min(lowLinks.get(node), indices.get(dependency)));
}
}
if (lowLinks.get(node) !== indices.get(node)) return;
const component = [];
let item;
do {
item = stack.pop();
onStack.delete(item);
component.push(item);
} while (item !== node);
components.push(component);
}
}
function sampleCycle(graph, component) {
const componentSet = new Set(component);
for (const start of component) {
if (graph.get(start)?.has(start)) return [start, start];
for (const dependency of graph.get(start) ?? []) {
if (!componentSet.has(dependency)) continue;
const pathToStart = findPath(graph, componentSet, dependency, start, new Set([start]));
if (pathToStart) return [start, ...pathToStart];
}
}
return component;
}
function findPath(graph, allowedNodes, current, target, seen) {
if (current === target) return [current];
if (seen.has(current)) return null;
seen.add(current);
for (const dependency of graph.get(current) ?? []) {
if (!allowedNodes.has(dependency)) continue;
const path = findPath(graph, allowedNodes, dependency, target, seen);
if (path) return [current, ...path];
}
return null;
}
function hasSelfDependency(graph, node) {
return Boolean(node && graph.get(node)?.has(node));
}
function isCheckedSourceFile(file) {
return (
file.startsWith("src/") &&
!file.startsWith("src/generated/") &&
(file.endsWith(".ts") || file.endsWith(".tsx")) &&
!file.endsWith(".d.ts")
);
}
function relativePath(file) {
return path.relative(cwd, file).split(path.sep).join("/");
}
function reportDiagnostic(diagnostic) {
console.error(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
}

View file

@ -1,67 +0,0 @@
import stylelint from "stylelint";
const ruleName = "codex-panel/no-specificity-where";
const messages = stylelint.utils.ruleMessages(ruleName, {
rejected: (selector) => `Do not hide class, id, or attribute selectors inside :where(): "${selector}"`,
});
function rule(primary) {
return (root, result) => {
if (primary === false) return;
root.walkRules((ruleNode) => {
const selector = ruleNode.selector;
if (!selector.includes(":where(")) return;
for (const range of whereRanges(selector)) {
const body = selector.slice(range.bodyStart, range.bodyEnd);
if (!/[.#[]/.test(body)) continue;
stylelint.utils.report({
message: messages.rejected,
messageArgs: [selector.slice(range.start, range.end)],
node: ruleNode,
result,
ruleName,
index: range.start,
endIndex: range.end,
});
}
});
};
}
function whereRanges(selector) {
const ranges = [];
let searchFrom = 0;
while (searchFrom < selector.length) {
const start = selector.indexOf(":where(", searchFrom);
if (start === -1) break;
const bodyStart = start + ":where(".length;
let depth = 1;
let index = bodyStart;
for (; index < selector.length && depth > 0; index += 1) {
const char = selector[index];
if (char === "(") depth += 1;
if (char === ")") depth -= 1;
}
if (depth === 0) {
ranges.push({ start, bodyStart, bodyEnd: index - 1, end: index });
searchFrom = index;
} else {
searchFrom = bodyStart;
}
}
return ranges;
}
rule.ruleName = ruleName;
rule.messages = messages;
export default stylelint.createPlugin(ruleName, rule);

View file

@ -1,16 +1,16 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { compareVersions, isExpectedNextVersion, parseVersion, readJson } from "../utils.mjs";
import { compareVersions, isExpectedNextVersion, parseVersion } from "./versioning.mjs";
function fail(message) {
console.error(`release check failed: ${message}`);
process.exitCode = 1;
}
const packageJson = await readJson("package.json");
const packageLockJson = await readJson("package-lock.json");
const manifestJson = await readJson("manifest.json");
const versionsJson = await readJson("versions.json");
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const packageLockJson = JSON.parse(await readFile("package-lock.json", "utf8"));
const manifestJson = JSON.parse(await readFile("manifest.json", "utf8"));
const versionsJson = JSON.parse(await readFile("versions.json", "utf8"));
const releaseVersion = process.env.RELEASE_VERSION || packageJson.version;
const currentVersion = parseVersion(releaseVersion);

73
scripts/release/notes.mjs Normal file
View file

@ -0,0 +1,73 @@
import { spawnSync } from "node:child_process";
import { pathToFileURL } from "node:url";
import conventionalCommits from "conventional-changelog-conventionalcommits";
import { CommitParser } from "conventional-commits-parser";
const releaseNoteTypes = new Set(["feat", "fix", "perf"]);
const parser = new CommitParser(conventionalCommits().parser);
function fail(message) {
throw new Error(`release notes generation failed: ${message}`);
}
function runGit(args, cwd) {
const result = spawnSync("git", args, {
cwd,
encoding: "utf8",
shell: false,
});
if (result.error) fail(result.error.message);
if (result.status !== 0) {
fail(`${args.join(" ")} exited with ${result.status}: ${result.stderr.trim()}`);
}
return result.stdout;
}
function formatSubject(subject) {
const sentence = subject.replace(/^([a-z])/, (letter) => letter.toUpperCase());
return /[.!?]$/.test(sentence) ? sentence : `${sentence}.`;
}
export function releaseNoteForCommit(message) {
const commit = parser.parse(message);
if (!commit.subject) return null;
const isBreaking = commit.notes.length > 0;
if (!isBreaking && !releaseNoteTypes.has(commit.type ?? "")) return null;
return formatSubject(commit.subject);
}
export function renderReleaseNotes(messages) {
const entries = messages.map(releaseNoteForCommit).filter((entry) => entry !== null);
const bullets = entries.length > 0 ? entries.map((entry) => `- ${entry}`).join("\n") : "- ";
return `## Changes\n\n${bullets}\n`;
}
export function readCommitMessagesSince(tag, cwd = process.cwd()) {
runGit(["rev-parse", "--verify", `refs/tags/${tag}`], cwd);
const output = runGit(["log", "--format=%B%x00", `${tag}..HEAD`], cwd);
return output
.split("\0")
.map((message) => message.trim())
.filter(Boolean);
}
export function generateReleaseNotes(tag, cwd = process.cwd()) {
return renderReleaseNotes(readCommitMessagesSince(tag, cwd));
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const previousTag = process.argv[2];
if (!previousTag) {
console.error("usage: npm run release:notes -- X.Y.Z");
process.exit(1);
}
try {
process.stdout.write(generateReleaseNotes(previousTag));
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
}

Some files were not shown because too many files have changed in this diff Show more