taskchef 8.6.1 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +8 -4
- package/assets/README.md +19 -0
- package/assets/taskchef-app-rounded.png +0 -0
- package/assets/taskchef-avatar-square.png +0 -0
- package/assets/taskchef-avatar-square.svg +8 -0
- package/assets/taskchef-sidebar-dark.svg +6 -0
- package/assets/taskchef-sidebar-light.svg +6 -0
- package/assets/taskchef-sidebar.svg +6 -0
- package/docs/advanced-guide.md +20 -29
- package/docs/dashboard-ui.md +15 -0
- package/docs/diagrams/taskchef-next-reply-lookup.drawio +38 -0
- package/docs/diagrams/taskchef-next-reply-lookup.drawio.png +0 -0
- package/docs/diagrams/taskchef-next-storage.drawio +41 -0
- package/docs/diagrams/taskchef-next-storage.drawio.png +0 -0
- package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
- package/docs/images/taskchef-unified-status-popup.jpg +0 -0
- package/docs/mcp-app.md +261 -6
- package/docs/spec.md +24 -69
- package/docs/taskchef-next-github.md +206 -0
- package/docs/taskchef-next-status-research.md +102 -0
- package/docs/taskchef-next-storage.md +167 -0
- package/docs/workflows.md +4 -2
- package/node_modules/@napi-rs/keyring/LICENSE +21 -0
- package/node_modules/@napi-rs/keyring/README.md +45 -0
- package/node_modules/@napi-rs/keyring/index.d.ts +232 -0
- package/node_modules/@napi-rs/keyring/index.js +706 -0
- package/node_modules/@napi-rs/keyring/keytar.d.ts +53 -0
- package/node_modules/@napi-rs/keyring/keytar.js +26 -0
- package/node_modules/@napi-rs/keyring/package.json +126 -0
- package/node_modules/@napi-rs/keyring-darwin-arm64/README.md +3 -0
- package/node_modules/@napi-rs/keyring-darwin-arm64/keyring.darwin-arm64.node +0 -0
- package/node_modules/@napi-rs/keyring-darwin-arm64/package.json +32 -0
- package/node_modules/@napi-rs/keyring-darwin-x64/README.md +3 -0
- package/node_modules/@napi-rs/keyring-darwin-x64/keyring.darwin-x64.node +0 -0
- package/node_modules/@napi-rs/keyring-darwin-x64/package.json +32 -0
- package/node_modules/@napi-rs/keyring-linux-arm64-gnu/README.md +3 -0
- package/node_modules/@napi-rs/keyring-linux-arm64-gnu/keyring.linux-arm64-gnu.node +0 -0
- package/node_modules/@napi-rs/keyring-linux-arm64-gnu/package.json +35 -0
- package/node_modules/@napi-rs/keyring-linux-arm64-musl/README.md +3 -0
- package/node_modules/@napi-rs/keyring-linux-arm64-musl/keyring.linux-arm64-musl.node +0 -0
- package/node_modules/@napi-rs/keyring-linux-arm64-musl/package.json +35 -0
- package/node_modules/@napi-rs/keyring-linux-x64-gnu/README.md +3 -0
- package/node_modules/@napi-rs/keyring-linux-x64-gnu/keyring.linux-x64-gnu.node +0 -0
- package/node_modules/@napi-rs/keyring-linux-x64-gnu/package.json +35 -0
- package/node_modules/@napi-rs/keyring-linux-x64-musl/README.md +3 -0
- package/node_modules/@napi-rs/keyring-linux-x64-musl/keyring.linux-x64-musl.node +0 -0
- package/node_modules/@napi-rs/keyring-linux-x64-musl/package.json +35 -0
- package/node_modules/@napi-rs/keyring-win32-arm64-msvc/README.md +3 -0
- package/node_modules/@napi-rs/keyring-win32-arm64-msvc/keyring.win32-arm64-msvc.node +0 -0
- package/node_modules/@napi-rs/keyring-win32-arm64-msvc/package.json +32 -0
- package/node_modules/@napi-rs/keyring-win32-x64-msvc/README.md +3 -0
- package/node_modules/@napi-rs/keyring-win32-x64-msvc/keyring.win32-x64-msvc.node +0 -0
- package/node_modules/@napi-rs/keyring-win32-x64-msvc/package.json +32 -0
- package/node_modules/@types/debug/LICENSE +21 -0
- package/node_modules/@types/debug/README.md +69 -0
- package/node_modules/@types/debug/index.d.ts +50 -0
- package/node_modules/@types/debug/package.json +58 -0
- package/node_modules/@types/mdast/LICENSE +21 -0
- package/node_modules/@types/mdast/README.md +15 -0
- package/node_modules/@types/mdast/index.d.ts +1123 -0
- package/node_modules/@types/mdast/package.json +47 -0
- package/node_modules/@types/ms/LICENSE +21 -0
- package/node_modules/@types/ms/README.md +82 -0
- package/node_modules/@types/ms/index.d.ts +63 -0
- package/node_modules/@types/ms/package.json +26 -0
- package/node_modules/@types/unist/LICENSE +21 -0
- package/node_modules/@types/unist/README.md +15 -0
- package/node_modules/@types/unist/index.d.ts +119 -0
- package/node_modules/@types/unist/package.json +60 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/dequal/dist/index.js +86 -0
- package/node_modules/dequal/dist/index.min.js +1 -0
- package/node_modules/dequal/dist/index.mjs +84 -0
- package/node_modules/dequal/index.d.ts +1 -0
- package/node_modules/dequal/license +21 -0
- package/node_modules/dequal/lite/index.d.ts +1 -0
- package/node_modules/dequal/lite/index.js +31 -0
- package/node_modules/dequal/lite/index.min.js +1 -0
- package/node_modules/dequal/lite/index.mjs +29 -0
- package/node_modules/dequal/package.json +57 -0
- package/node_modules/dequal/readme.md +112 -0
- package/node_modules/devlop/lib/default.js +9 -0
- package/node_modules/devlop/lib/development.d.ts +84 -0
- package/node_modules/devlop/lib/development.js +238 -0
- package/node_modules/devlop/license +22 -0
- package/node_modules/devlop/package.json +80 -0
- package/node_modules/devlop/readme.md +360 -0
- package/node_modules/mdast-util-from-markdown/dev/index.d.ts +23 -0
- package/node_modules/mdast-util-from-markdown/dev/index.js +2 -0
- package/node_modules/mdast-util-from-markdown/dev/lib/index.d.ts +53 -0
- package/node_modules/mdast-util-from-markdown/dev/lib/index.d.ts.map +1 -0
- package/node_modules/mdast-util-from-markdown/dev/lib/index.js +1348 -0
- package/node_modules/mdast-util-from-markdown/dev/lib/types.d.ts +295 -0
- package/node_modules/mdast-util-from-markdown/dev/lib/types.js +2 -0
- package/node_modules/mdast-util-from-markdown/index.d.ts +23 -0
- package/node_modules/mdast-util-from-markdown/index.js +2 -0
- package/node_modules/mdast-util-from-markdown/lib/index.d.ts +53 -0
- package/node_modules/mdast-util-from-markdown/lib/index.js +1177 -0
- package/node_modules/mdast-util-from-markdown/lib/types.d.ts +295 -0
- package/node_modules/mdast-util-from-markdown/lib/types.js +2 -0
- package/node_modules/mdast-util-from-markdown/license +22 -0
- package/node_modules/mdast-util-from-markdown/package.json +139 -0
- package/node_modules/mdast-util-from-markdown/readme.md +537 -0
- package/node_modules/mdast-util-to-string/index.d.ts +2 -0
- package/node_modules/mdast-util-to-string/index.js +5 -0
- package/node_modules/mdast-util-to-string/lib/index.d.ts +31 -0
- package/node_modules/mdast-util-to-string/lib/index.js +108 -0
- package/node_modules/mdast-util-to-string/license +22 -0
- package/node_modules/mdast-util-to-string/package.json +78 -0
- package/node_modules/mdast-util-to-string/readme.md +218 -0
- package/node_modules/micromark/dev/index.d.ts +82 -0
- package/node_modules/micromark/dev/index.d.ts.map +1 -0
- package/node_modules/micromark/dev/index.js +68 -0
- package/node_modules/micromark/dev/lib/compile.d.ts +16 -0
- package/node_modules/micromark/dev/lib/compile.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/compile.js +1162 -0
- package/node_modules/micromark/dev/lib/constructs.d.ts +73 -0
- package/node_modules/micromark/dev/lib/constructs.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/constructs.js +101 -0
- package/node_modules/micromark/dev/lib/create-tokenizer.d.ts +46 -0
- package/node_modules/micromark/dev/lib/create-tokenizer.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/create-tokenizer.js +718 -0
- package/node_modules/micromark/dev/lib/initialize/content.d.ts +4 -0
- package/node_modules/micromark/dev/lib/initialize/content.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/initialize/content.js +99 -0
- package/node_modules/micromark/dev/lib/initialize/document.d.ts +10 -0
- package/node_modules/micromark/dev/lib/initialize/document.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/initialize/document.js +449 -0
- package/node_modules/micromark/dev/lib/initialize/flow.d.ts +4 -0
- package/node_modules/micromark/dev/lib/initialize/flow.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/initialize/flow.js +86 -0
- package/node_modules/micromark/dev/lib/initialize/text.d.ts +8 -0
- package/node_modules/micromark/dev/lib/initialize/text.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/initialize/text.js +248 -0
- package/node_modules/micromark/dev/lib/parse.d.ts +10 -0
- package/node_modules/micromark/dev/lib/parse.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/parse.js +58 -0
- package/node_modules/micromark/dev/lib/postprocess.d.ts +9 -0
- package/node_modules/micromark/dev/lib/postprocess.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/postprocess.js +19 -0
- package/node_modules/micromark/dev/lib/preprocess.d.ts +13 -0
- package/node_modules/micromark/dev/lib/preprocess.d.ts.map +1 -0
- package/node_modules/micromark/dev/lib/preprocess.js +139 -0
- package/node_modules/micromark/dev/stream.d.ts +35 -0
- package/node_modules/micromark/dev/stream.d.ts.map +1 -0
- package/node_modules/micromark/dev/stream.js +270 -0
- package/node_modules/micromark/index.d.ts +82 -0
- package/node_modules/micromark/index.d.ts.map +1 -0
- package/node_modules/micromark/index.js +60 -0
- package/node_modules/micromark/lib/compile.d.ts +16 -0
- package/node_modules/micromark/lib/compile.d.ts.map +1 -0
- package/node_modules/micromark/lib/compile.js +1068 -0
- package/node_modules/micromark/lib/constructs.d.ts +73 -0
- package/node_modules/micromark/lib/constructs.d.ts.map +1 -0
- package/node_modules/micromark/lib/constructs.js +85 -0
- package/node_modules/micromark/lib/create-tokenizer.d.ts +46 -0
- package/node_modules/micromark/lib/create-tokenizer.d.ts.map +1 -0
- package/node_modules/micromark/lib/create-tokenizer.js +613 -0
- package/node_modules/micromark/lib/initialize/content.d.ts +4 -0
- package/node_modules/micromark/lib/initialize/content.d.ts.map +1 -0
- package/node_modules/micromark/lib/initialize/content.js +79 -0
- package/node_modules/micromark/lib/initialize/document.d.ts +10 -0
- package/node_modules/micromark/lib/initialize/document.d.ts.map +1 -0
- package/node_modules/micromark/lib/initialize/document.js +370 -0
- package/node_modules/micromark/lib/initialize/flow.d.ts +4 -0
- package/node_modules/micromark/lib/initialize/flow.d.ts.map +1 -0
- package/node_modules/micromark/lib/initialize/flow.js +58 -0
- package/node_modules/micromark/lib/initialize/text.d.ts +8 -0
- package/node_modules/micromark/lib/initialize/text.d.ts.map +1 -0
- package/node_modules/micromark/lib/initialize/text.js +216 -0
- package/node_modules/micromark/lib/parse.d.ts +10 -0
- package/node_modules/micromark/lib/parse.d.ts.map +1 -0
- package/node_modules/micromark/lib/parse.js +56 -0
- package/node_modules/micromark/lib/postprocess.d.ts +9 -0
- package/node_modules/micromark/lib/postprocess.d.ts.map +1 -0
- package/node_modules/micromark/lib/postprocess.js +18 -0
- package/node_modules/micromark/lib/preprocess.d.ts +13 -0
- package/node_modules/micromark/lib/preprocess.d.ts.map +1 -0
- package/node_modules/micromark/lib/preprocess.js +112 -0
- package/node_modules/micromark/license +22 -0
- package/node_modules/micromark/package.json +87 -0
- package/node_modules/micromark/readme.md +488 -0
- package/node_modules/micromark/stream.d.ts +35 -0
- package/node_modules/micromark/stream.d.ts.map +1 -0
- package/node_modules/micromark/stream.js +256 -0
- package/node_modules/micromark-core-commonmark/dev/index.d.ts +23 -0
- package/node_modules/micromark-core-commonmark/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/index.js +22 -0
- package/node_modules/micromark-core-commonmark/dev/lib/attention.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/attention.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/attention.js +294 -0
- package/node_modules/micromark-core-commonmark/dev/lib/autolink.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/autolink.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/autolink.js +269 -0
- package/node_modules/micromark-core-commonmark/dev/lib/blank-line.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/blank-line.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/blank-line.js +62 -0
- package/node_modules/micromark-core-commonmark/dev/lib/block-quote.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/block-quote.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/block-quote.js +162 -0
- package/node_modules/micromark-core-commonmark/dev/lib/character-escape.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/character-escape.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/character-escape.js +69 -0
- package/node_modules/micromark-core-commonmark/dev/lib/character-reference.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/character-reference.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/character-reference.js +166 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-fenced.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-fenced.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-fenced.js +472 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-indented.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-indented.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-indented.js +178 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-text.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-text.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/code-text.js +257 -0
- package/node_modules/micromark-core-commonmark/dev/lib/content.d.ts +7 -0
- package/node_modules/micromark-core-commonmark/dev/lib/content.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/content.js +185 -0
- package/node_modules/micromark-core-commonmark/dev/lib/definition.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/definition.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/definition.js +295 -0
- package/node_modules/micromark-core-commonmark/dev/lib/hard-break-escape.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/hard-break-escape.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/hard-break-escape.js +65 -0
- package/node_modules/micromark-core-commonmark/dev/lib/heading-atx.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/heading-atx.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/heading-atx.js +229 -0
- package/node_modules/micromark-core-commonmark/dev/lib/html-flow.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/html-flow.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/html-flow.js +938 -0
- package/node_modules/micromark-core-commonmark/dev/lib/html-text.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/html-text.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/html-text.js +783 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-end.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-end.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-end.js +658 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-start-image.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-start-image.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-start-image.js +119 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-start-link.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-start-link.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/label-start-link.js +71 -0
- package/node_modules/micromark-core-commonmark/dev/lib/line-ending.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/line-ending.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/line-ending.js +34 -0
- package/node_modules/micromark-core-commonmark/dev/lib/list.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/list.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/list.js +306 -0
- package/node_modules/micromark-core-commonmark/dev/lib/partial-non-lazy-continuation.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/partial-non-lazy-continuation.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/partial-non-lazy-continuation.js +67 -0
- package/node_modules/micromark-core-commonmark/dev/lib/setext-underline.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/setext-underline.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/setext-underline.js +213 -0
- package/node_modules/micromark-core-commonmark/dev/lib/thematic-break.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/dev/lib/thematic-break.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/dev/lib/thematic-break.js +120 -0
- package/node_modules/micromark-core-commonmark/index.d.ts +23 -0
- package/node_modules/micromark-core-commonmark/index.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/index.js +22 -0
- package/node_modules/micromark-core-commonmark/lib/attention.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/attention.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/attention.js +231 -0
- package/node_modules/micromark-core-commonmark/lib/autolink.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/autolink.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/autolink.js +233 -0
- package/node_modules/micromark-core-commonmark/lib/blank-line.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/blank-line.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/blank-line.js +61 -0
- package/node_modules/micromark-core-commonmark/lib/block-quote.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/block-quote.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/block-quote.js +143 -0
- package/node_modules/micromark-core-commonmark/lib/character-escape.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/character-escape.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/character-escape.js +64 -0
- package/node_modules/micromark-core-commonmark/lib/character-reference.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/character-reference.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/character-reference.js +149 -0
- package/node_modules/micromark-core-commonmark/lib/code-fenced.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/code-fenced.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/code-fenced.js +422 -0
- package/node_modules/micromark-core-commonmark/lib/code-indented.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/code-indented.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/code-indented.js +159 -0
- package/node_modules/micromark-core-commonmark/lib/code-text.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/code-text.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/code-text.js +225 -0
- package/node_modules/micromark-core-commonmark/lib/content.d.ts +7 -0
- package/node_modules/micromark-core-commonmark/lib/content.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/content.js +163 -0
- package/node_modules/micromark-core-commonmark/lib/definition.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/definition.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/definition.js +254 -0
- package/node_modules/micromark-core-commonmark/lib/hard-break-escape.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/hard-break-escape.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/hard-break-escape.js +60 -0
- package/node_modules/micromark-core-commonmark/lib/heading-atx.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/heading-atx.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/heading-atx.js +191 -0
- package/node_modules/micromark-core-commonmark/lib/html-flow.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/html-flow.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/html-flow.js +827 -0
- package/node_modules/micromark-core-commonmark/lib/html-text.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/html-text.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/html-text.js +678 -0
- package/node_modules/micromark-core-commonmark/lib/label-end.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/label-end.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/label-end.js +561 -0
- package/node_modules/micromark-core-commonmark/lib/label-start-image.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/label-start-image.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/label-start-image.js +110 -0
- package/node_modules/micromark-core-commonmark/lib/label-start-link.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/label-start-link.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/label-start-link.js +63 -0
- package/node_modules/micromark-core-commonmark/lib/line-ending.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/line-ending.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/line-ending.js +33 -0
- package/node_modules/micromark-core-commonmark/lib/list.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/list.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/list.js +213 -0
- package/node_modules/micromark-core-commonmark/lib/partial-non-lazy-continuation.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/partial-non-lazy-continuation.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/partial-non-lazy-continuation.js +61 -0
- package/node_modules/micromark-core-commonmark/lib/setext-underline.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/setext-underline.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/setext-underline.js +187 -0
- package/node_modules/micromark-core-commonmark/lib/thematic-break.d.ts +4 -0
- package/node_modules/micromark-core-commonmark/lib/thematic-break.d.ts.map +1 -0
- package/node_modules/micromark-core-commonmark/lib/thematic-break.js +102 -0
- package/node_modules/micromark-core-commonmark/license +22 -0
- package/node_modules/micromark-core-commonmark/package.json +62 -0
- package/node_modules/micromark-core-commonmark/readme.md +171 -0
- package/node_modules/micromark-factory-destination/dev/index.d.ts +42 -0
- package/node_modules/micromark-factory-destination/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-destination/dev/index.js +255 -0
- package/node_modules/micromark-factory-destination/index.d.ts +42 -0
- package/node_modules/micromark-factory-destination/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-destination/index.js +206 -0
- package/node_modules/micromark-factory-destination/license +22 -0
- package/node_modules/micromark-factory-destination/package.json +57 -0
- package/node_modules/micromark-factory-destination/readme.md +234 -0
- package/node_modules/micromark-factory-label/dev/index.d.ts +37 -0
- package/node_modules/micromark-factory-label/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-label/dev/index.js +172 -0
- package/node_modules/micromark-factory-label/index.d.ts +37 -0
- package/node_modules/micromark-factory-label/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-label/index.js +148 -0
- package/node_modules/micromark-factory-label/license +22 -0
- package/node_modules/micromark-factory-label/package.json +60 -0
- package/node_modules/micromark-factory-label/readme.md +224 -0
- package/node_modules/micromark-factory-space/dev/index.d.ts +60 -0
- package/node_modules/micromark-factory-space/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-space/dev/index.js +122 -0
- package/node_modules/micromark-factory-space/index.d.ts +60 -0
- package/node_modules/micromark-factory-space/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-space/index.js +116 -0
- package/node_modules/micromark-factory-space/license +22 -0
- package/node_modules/micromark-factory-space/package.json +46 -0
- package/node_modules/micromark-factory-space/readme.md +225 -0
- package/node_modules/micromark-factory-title/dev/index.d.ts +36 -0
- package/node_modules/micromark-factory-title/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-title/dev/index.js +169 -0
- package/node_modules/micromark-factory-title/index.d.ts +36 -0
- package/node_modules/micromark-factory-title/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-title/index.js +158 -0
- package/node_modules/micromark-factory-title/license +22 -0
- package/node_modules/micromark-factory-title/package.json +58 -0
- package/node_modules/micromark-factory-title/readme.md +229 -0
- package/node_modules/micromark-factory-whitespace/dev/index.d.ts +22 -0
- package/node_modules/micromark-factory-whitespace/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-whitespace/dev/index.js +53 -0
- package/node_modules/micromark-factory-whitespace/index.d.ts +22 -0
- package/node_modules/micromark-factory-whitespace/index.d.ts.map +1 -0
- package/node_modules/micromark-factory-whitespace/index.js +44 -0
- package/node_modules/micromark-factory-whitespace/license +22 -0
- package/node_modules/micromark-factory-whitespace/package.json +57 -0
- package/node_modules/micromark-factory-whitespace/readme.md +205 -0
- package/node_modules/micromark-util-character/dev/index.d.ts +195 -0
- package/node_modules/micromark-util-character/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-character/dev/index.js +252 -0
- package/node_modules/micromark-util-character/index.d.ts +195 -0
- package/node_modules/micromark-util-character/index.d.ts.map +1 -0
- package/node_modules/micromark-util-character/index.js +246 -0
- package/node_modules/micromark-util-character/license +22 -0
- package/node_modules/micromark-util-character/package.json +57 -0
- package/node_modules/micromark-util-character/readme.md +446 -0
- package/node_modules/micromark-util-chunked/dev/index.d.ts +41 -0
- package/node_modules/micromark-util-chunked/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-chunked/dev/index.js +89 -0
- package/node_modules/micromark-util-chunked/index.d.ts +41 -0
- package/node_modules/micromark-util-chunked/index.d.ts.map +1 -0
- package/node_modules/micromark-util-chunked/index.js +81 -0
- package/node_modules/micromark-util-chunked/license +22 -0
- package/node_modules/micromark-util-chunked/package.json +57 -0
- package/node_modules/micromark-util-chunked/readme.md +219 -0
- package/node_modules/micromark-util-classify-character/dev/index.d.ts +18 -0
- package/node_modules/micromark-util-classify-character/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-classify-character/dev/index.js +38 -0
- package/node_modules/micromark-util-classify-character/index.d.ts +18 -0
- package/node_modules/micromark-util-classify-character/index.d.ts.map +1 -0
- package/node_modules/micromark-util-classify-character/index.js +27 -0
- package/node_modules/micromark-util-classify-character/license +22 -0
- package/node_modules/micromark-util-classify-character/package.json +59 -0
- package/node_modules/micromark-util-classify-character/readme.md +205 -0
- package/node_modules/micromark-util-combine-extensions/index.d.ts +22 -0
- package/node_modules/micromark-util-combine-extensions/index.d.ts.map +1 -0
- package/node_modules/micromark-util-combine-extensions/index.js +143 -0
- package/node_modules/micromark-util-combine-extensions/license +22 -0
- package/node_modules/micromark-util-combine-extensions/package.json +52 -0
- package/node_modules/micromark-util-combine-extensions/readme.md +201 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/dev/index.d.ts +16 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/dev/index.js +42 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/index.d.ts +16 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/index.d.ts.map +1 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/index.js +32 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/license +22 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/package.json +59 -0
- package/node_modules/micromark-util-decode-numeric-character-reference/readme.md +184 -0
- package/node_modules/micromark-util-decode-string/dev/index.d.ts +14 -0
- package/node_modules/micromark-util-decode-string/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-decode-string/dev/index.js +53 -0
- package/node_modules/micromark-util-decode-string/index.d.ts +14 -0
- package/node_modules/micromark-util-decode-string/index.d.ts.map +1 -0
- package/node_modules/micromark-util-decode-string/index.js +45 -0
- package/node_modules/micromark-util-decode-string/license +22 -0
- package/node_modules/micromark-util-decode-string/package.json +63 -0
- package/node_modules/micromark-util-decode-string/readme.md +179 -0
- package/node_modules/micromark-util-edit-map/dev/index.d.ts +78 -0
- package/node_modules/micromark-util-edit-map/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-edit-map/dev/index.js +237 -0
- package/node_modules/micromark-util-edit-map/index.d.ts +78 -0
- package/node_modules/micromark-util-edit-map/index.d.ts.map +1 -0
- package/node_modules/micromark-util-edit-map/index.js +225 -0
- package/node_modules/micromark-util-edit-map/license +22 -0
- package/node_modules/micromark-util-edit-map/package.json +48 -0
- package/node_modules/micromark-util-edit-map/readme.md +234 -0
- package/node_modules/micromark-util-encode/index.d.ts +14 -0
- package/node_modules/micromark-util-encode/index.d.ts.map +1 -0
- package/node_modules/micromark-util-encode/index.js +33 -0
- package/node_modules/micromark-util-encode/license +22 -0
- package/node_modules/micromark-util-encode/package.json +47 -0
- package/node_modules/micromark-util-encode/readme.md +176 -0
- package/node_modules/micromark-util-html-tag-name/index.d.ts +30 -0
- package/node_modules/micromark-util-html-tag-name/index.d.ts.map +1 -0
- package/node_modules/micromark-util-html-tag-name/index.js +93 -0
- package/node_modules/micromark-util-html-tag-name/license +22 -0
- package/node_modules/micromark-util-html-tag-name/package.json +47 -0
- package/node_modules/micromark-util-html-tag-name/readme.md +193 -0
- package/node_modules/micromark-util-normalize-identifier/dev/index.d.ts +21 -0
- package/node_modules/micromark-util-normalize-identifier/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-normalize-identifier/dev/index.js +38 -0
- package/node_modules/micromark-util-normalize-identifier/index.d.ts +21 -0
- package/node_modules/micromark-util-normalize-identifier/index.d.ts.map +1 -0
- package/node_modules/micromark-util-normalize-identifier/index.js +33 -0
- package/node_modules/micromark-util-normalize-identifier/license +22 -0
- package/node_modules/micromark-util-normalize-identifier/package.json +58 -0
- package/node_modules/micromark-util-normalize-identifier/readme.md +187 -0
- package/node_modules/micromark-util-resolve-all/index.d.ts +22 -0
- package/node_modules/micromark-util-resolve-all/index.d.ts.map +1 -0
- package/node_modules/micromark-util-resolve-all/index.js +32 -0
- package/node_modules/micromark-util-resolve-all/license +22 -0
- package/node_modules/micromark-util-resolve-all/package.json +48 -0
- package/node_modules/micromark-util-resolve-all/readme.md +238 -0
- package/node_modules/micromark-util-sanitize-uri/dev/index.d.ts +36 -0
- package/node_modules/micromark-util-sanitize-uri/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-sanitize-uri/dev/index.js +124 -0
- package/node_modules/micromark-util-sanitize-uri/index.d.ts +36 -0
- package/node_modules/micromark-util-sanitize-uri/index.d.ts.map +1 -0
- package/node_modules/micromark-util-sanitize-uri/index.js +107 -0
- package/node_modules/micromark-util-sanitize-uri/license +22 -0
- package/node_modules/micromark-util-sanitize-uri/package.json +59 -0
- package/node_modules/micromark-util-sanitize-uri/readme.md +214 -0
- package/node_modules/micromark-util-subtokenize/dev/index.d.ts +12 -0
- package/node_modules/micromark-util-subtokenize/dev/index.d.ts.map +1 -0
- package/node_modules/micromark-util-subtokenize/dev/index.js +284 -0
- package/node_modules/micromark-util-subtokenize/dev/lib/splice-buffer.d.ts +161 -0
- package/node_modules/micromark-util-subtokenize/dev/lib/splice-buffer.d.ts.map +1 -0
- package/node_modules/micromark-util-subtokenize/dev/lib/splice-buffer.js +288 -0
- package/node_modules/micromark-util-subtokenize/index.d.ts +12 -0
- package/node_modules/micromark-util-subtokenize/index.d.ts.map +1 -0
- package/node_modules/micromark-util-subtokenize/index.js +228 -0
- package/node_modules/micromark-util-subtokenize/lib/splice-buffer.d.ts +161 -0
- package/node_modules/micromark-util-subtokenize/lib/splice-buffer.d.ts.map +1 -0
- package/node_modules/micromark-util-subtokenize/lib/splice-buffer.js +251 -0
- package/node_modules/micromark-util-subtokenize/license +22 -0
- package/node_modules/micromark-util-subtokenize/package.json +60 -0
- package/node_modules/micromark-util-subtokenize/readme.md +181 -0
- package/node_modules/micromark-util-symbol/lib/codes.d.ts +139 -0
- package/node_modules/micromark-util-symbol/lib/codes.d.ts.map +1 -0
- package/node_modules/micromark-util-symbol/lib/codes.js +158 -0
- package/node_modules/micromark-util-symbol/lib/constants.d.ts +37 -0
- package/node_modules/micromark-util-symbol/lib/constants.d.ts.map +1 -0
- package/node_modules/micromark-util-symbol/lib/constants.js +44 -0
- package/node_modules/micromark-util-symbol/lib/default.d.ts +5 -0
- package/node_modules/micromark-util-symbol/lib/default.d.ts.map +1 -0
- package/node_modules/micromark-util-symbol/lib/default.js +4 -0
- package/node_modules/micromark-util-symbol/lib/types.d.ts +106 -0
- package/node_modules/micromark-util-symbol/lib/types.d.ts.map +1 -0
- package/node_modules/micromark-util-symbol/lib/types.js +453 -0
- package/node_modules/micromark-util-symbol/lib/values.d.ts +102 -0
- package/node_modules/micromark-util-symbol/lib/values.d.ts.map +1 -0
- package/node_modules/micromark-util-symbol/lib/values.js +109 -0
- package/node_modules/micromark-util-symbol/license +22 -0
- package/node_modules/micromark-util-symbol/package.json +43 -0
- package/node_modules/micromark-util-symbol/readme.md +168 -0
- package/node_modules/micromark-util-types/index.d.ts +1315 -0
- package/node_modules/micromark-util-types/index.js +2 -0
- package/node_modules/micromark-util-types/license +22 -0
- package/node_modules/micromark-util-types/package.json +37 -0
- package/node_modules/micromark-util-types/readme.md +151 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +605 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +156 -0
- package/node_modules/smol-toml/dist/error.d.ts +37 -0
- package/node_modules/smol-toml/dist/error.js +67 -0
- package/node_modules/smol-toml/dist/extract.js +289 -0
- package/node_modules/smol-toml/dist/index.cjs +770 -0
- package/node_modules/smol-toml/dist/index.d.ts +44 -0
- package/node_modules/smol-toml/dist/index.js +34 -0
- package/node_modules/smol-toml/dist/parse.d.ts +73 -0
- package/node_modules/smol-toml/dist/parse.js +164 -0
- package/node_modules/smol-toml/dist/primitive.js +151 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
- package/node_modules/smol-toml/dist/stringify.js +219 -0
- package/node_modules/smol-toml/dist/struct.js +156 -0
- package/node_modules/smol-toml/dist/util.d.ts +43 -0
- package/node_modules/smol-toml/dist/util.js +57 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/node_modules/unist-util-stringify-position/index.d.ts +1 -0
- package/node_modules/unist-util-stringify-position/index.js +1 -0
- package/node_modules/unist-util-stringify-position/lib/index.d.ts +61 -0
- package/node_modules/unist-util-stringify-position/lib/index.js +84 -0
- package/node_modules/unist-util-stringify-position/license +22 -0
- package/node_modules/unist-util-stringify-position/package.json +80 -0
- package/node_modules/unist-util-stringify-position/readme.md +206 -0
- package/package.json +27 -8
- package/scripts/prepare-github-keyring.js +29 -0
- package/skills/taskchef-delegate/SKILL.md +5 -0
- package/src/codex-app.js +20 -2
- package/src/codex-pricing.js +79 -0
- package/src/codex-projects.js +90 -0
- package/src/codex-session-scanner.js +748 -0
- package/src/codex-session-usage.js +332 -0
- package/src/dashboard/app.js +2 -2
- package/src/dashboard/dist/app.js +135 -60
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/components/AnimatedUsage.test.tsx +78 -0
- package/src/dashboard/react/components/CardStatusIcons.test.tsx +146 -0
- package/src/dashboard/react/components/CardStatusIcons.tsx +138 -0
- package/src/dashboard/react/components/ImageGallery.tsx +22 -0
- package/src/dashboard/react/components/ModelSettings.test.tsx +4 -4
- package/src/dashboard/react/components/ModelSettings.tsx +1 -1
- package/src/dashboard/react/components/OpenChatButton.test.tsx +19 -0
- package/src/dashboard/react/components/OpenChatButton.tsx +2 -1
- package/src/dashboard/react/components/PrGlyph.tsx +35 -0
- package/src/dashboard/react/components/PullRequestBadges.tsx +18 -0
- package/src/dashboard/react/components/QueuedPrompts.test.tsx +42 -0
- package/src/dashboard/react/components/QueuedPrompts.tsx +34 -0
- package/src/dashboard/react/components/RelativeTime.test.tsx +100 -0
- package/src/dashboard/react/components/RelativeTime.tsx +46 -11
- package/src/dashboard/react/components/ReplyCover.test.tsx +30 -0
- package/src/dashboard/react/components/ReplyCover.tsx +33 -0
- package/src/dashboard/react/components/ReplyImagePreview.test.tsx +90 -0
- package/src/dashboard/react/components/ReplyImagePreview.tsx +34 -0
- package/src/dashboard/react/components/ReplyMarkdown.test.tsx +108 -0
- package/src/dashboard/react/components/ReplyMarkdown.tsx +112 -0
- package/src/dashboard/react/components/SavedUsageFormat.test.tsx +11 -0
- package/src/dashboard/react/components/SavedUsageFormat.tsx +68 -0
- package/src/dashboard/react/components/StatusBadge.tsx +2 -2
- package/src/dashboard/react/components/TaskBoard.test.tsx +256 -1
- package/src/dashboard/react/components/TaskBoard.tsx +136 -28
- package/src/dashboard/react/components/TaskCard.tsx +7 -4
- package/src/dashboard/react/components/TaskDetail.test.tsx +92 -1
- package/src/dashboard/react/components/TaskDetail.tsx +37 -10
- package/src/dashboard/react/components/TurnContent.test.tsx +44 -0
- package/src/dashboard/react/components/TurnContent.tsx +53 -0
- package/src/dashboard/react/components/UsagePanel.tsx +2 -2
- package/src/dashboard/react/image-cache.test.ts +23 -0
- package/src/dashboard/react/image-cache.ts +33 -0
- package/src/dashboard/react/presentation.test.tsx +31 -0
- package/src/dashboard/react/styles.css +126 -2
- package/src/dashboard/react/types.ts +55 -1
- package/src/dashboard/time.js +39 -0
- package/src/dashboard.js +4 -3
- package/src/data/codex-pricing.json +537 -0
- package/src/mcp-app/dist/index.html +216 -112
- package/src/mcp-app/react/AppearancePicker.test.tsx +50 -0
- package/src/mcp-app/react/AppearancePicker.tsx +56 -0
- package/src/mcp-app/react/GitHubConnection.test.tsx +78 -0
- package/src/mcp-app/react/GitHubConnection.tsx +78 -0
- package/src/mcp-app/react/NextNotificationCenter.test.tsx +121 -0
- package/src/mcp-app/react/NextNotificationCenter.tsx +94 -0
- package/src/mcp-app/react/NextTaskDetail.test.tsx +124 -0
- package/src/mcp-app/react/NextTaskDetail.tsx +147 -0
- package/src/mcp-app/react/ProjectPicker.tsx +49 -0
- package/src/mcp-app/react/main.test.tsx +499 -49
- package/src/mcp-app/react/main.tsx +359 -86
- package/src/mcp-app/react/styles.css +186 -9
- package/src/mcp-app/react/themes.test.ts +34 -0
- package/src/mcp-app/react/themes.ts +112 -0
- package/src/mcp-app.js +238 -120
- package/src/mcp.js +9 -2
- package/src/next-cache.js +16 -0
- package/src/next-github.js +273 -0
- package/src/next-notifications.js +90 -0
- package/src/recorded-usage-tracker.js +92 -0
- package/src/reply-image.js +65 -0
- package/src/usage-tracker.js +7 -6
- package/src/usage.js +7 -355
- package/src/user-prompt.js +55 -0
- package/src/version.js +0 -1
- package/node_modules/proper-lockfile/CHANGELOG.md +0 -108
- package/node_modules/which/CHANGELOG.md +0 -166
- package/src/resolve-ccusage.js +0 -17
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { MantineProvider } from "@mantine/core";
|
|
2
|
+
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
4
|
+
import { formatCardAge, formatCardTime, formatWorkedDuration } from "../../time.js";
|
|
5
|
+
import { ElapsedTime, RelativeTime, RelativeTimeProvider } from "./RelativeTime";
|
|
6
|
+
|
|
7
|
+
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
|
8
|
+
const local = (year: number, month: number, day: number, hour = 0, minute = 0) => new Date(year, month - 1, day, hour, minute).toISOString();
|
|
9
|
+
|
|
10
|
+
test("saved work durations use hours and minutes without inventing missing data", () => {
|
|
11
|
+
expect(formatWorkedDuration(54300000)).toBe("15h 5m");
|
|
12
|
+
expect(formatWorkedDuration(59999)).toBe("<1m");
|
|
13
|
+
expect(formatWorkedDuration(60000)).toBe("1m");
|
|
14
|
+
for (const value of [null, undefined, -1, NaN]) expect(formatWorkedDuration(value)).toBe("—");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("elapsed time ticks from the turn start, resets for a new turn, and cleans up", () => {
|
|
18
|
+
vi.useFakeTimers();
|
|
19
|
+
vi.setSystemTime(new Date("2026-10-09T07:12:34Z"));
|
|
20
|
+
const { rerender, unmount } = render(<MantineProvider><ElapsedTime startedAt="2026-10-09T07:00:00Z" /></MantineProvider>);
|
|
21
|
+
expect(screen.getByLabelText(/^Elapsed time:/)).toHaveTextContent("12m");
|
|
22
|
+
expect(screen.getByLabelText(/^Elapsed time:/).querySelector("svg")).not.toBeNull();
|
|
23
|
+
expect(screen.getByLabelText(/^Elapsed time:/)).toHaveAccessibleName(/Started/);
|
|
24
|
+
act(() => { vi.advanceTimersByTime(1000); });
|
|
25
|
+
expect(screen.getByLabelText(/^Elapsed time:/)).toHaveTextContent(/^12m$/);
|
|
26
|
+
act(() => { vi.advanceTimersByTime(25000); });
|
|
27
|
+
expect(screen.getByLabelText(/^Elapsed time:/)).toHaveTextContent(/^13m$/);
|
|
28
|
+
rerender(<MantineProvider><ElapsedTime startedAt="2026-10-09T06:00:00Z" /></MantineProvider>);
|
|
29
|
+
expect(screen.getByLabelText(/^Elapsed time:/)).toHaveTextContent("1h 13m");
|
|
30
|
+
rerender(<MantineProvider><ElapsedTime startedAt="2026-10-09T07:13:00Z" /></MantineProvider>);
|
|
31
|
+
expect(screen.getByLabelText(/^Elapsed time:/)).toHaveTextContent("<1m");
|
|
32
|
+
unmount();
|
|
33
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("elapsed time does not invent a start time and clamps clock skew", () => {
|
|
37
|
+
vi.useFakeTimers();
|
|
38
|
+
vi.setSystemTime(new Date("2026-10-09T07:00:00Z"));
|
|
39
|
+
const { rerender } = render(<MantineProvider><ElapsedTime startedAt="invalid" /></MantineProvider>);
|
|
40
|
+
expect(screen.getByLabelText(/^Elapsed time:/)).toHaveTextContent("—");
|
|
41
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
42
|
+
rerender(<MantineProvider><ElapsedTime startedAt="2026-10-09T07:00:01Z" /></MantineProvider>);
|
|
43
|
+
expect(screen.getByLabelText(/^Elapsed time:/)).toHaveTextContent("<1m");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("card dates use local calendar days across midnight and year boundaries", () => {
|
|
47
|
+
const now = new Date(2026, 9, 8, 14, 35).getTime();
|
|
48
|
+
expect(formatCardTime(local(2026, 10, 8, 14, 30), { now })).toBe("2:30PM");
|
|
49
|
+
expect(formatCardTime(local(2026, 10, 7, 23, 59), { now })).toBe("Yesterday");
|
|
50
|
+
expect(formatCardTime(local(2026, 7, 7), { now })).toBe("Jul 7th");
|
|
51
|
+
expect(formatCardTime(local(2025, 7, 7), { now })).toBe("Jul 7th, 2025");
|
|
52
|
+
expect(formatCardTime(local(2025, 12, 31), { now: new Date(2026, 0, 1, 0, 1).getTime() })).toBe("Yesterday");
|
|
53
|
+
expect(formatCardTime(local(2026, 10, 9), { now })).toBe("Oct 9th");
|
|
54
|
+
expect(formatCardTime(null, { now })).toBe("—");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("ordinal date labels handle teens and month endings", () => {
|
|
58
|
+
const now = new Date(2026, 9, 8).getTime();
|
|
59
|
+
for (const [day, suffix] of [[1, "st"], [2, "nd"], [3, "rd"], [11, "th"], [12, "th"], [13, "th"], [21, "st"], [31, "st"]] as const) {
|
|
60
|
+
expect(formatCardTime(local(2026, 7, day), { now })).toBe(`Jul ${day}${suffix}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("card time has no clock and preserves exact-time access", () => {
|
|
65
|
+
render(<MantineProvider><RelativeTimeProvider showCalendarDates now={new Date(2026, 9, 8, 15).getTime()}>
|
|
66
|
+
<RelativeTime calendar icon={false} label="Updated time" value={local(2026, 10, 8, 14, 30)} />
|
|
67
|
+
</RelativeTimeProvider></MantineProvider>);
|
|
68
|
+
const button = screen.getByRole("button", { name: "Updated time: 2:30PM. Show exact time" });
|
|
69
|
+
expect(button.querySelector("svg")).toBeNull();
|
|
70
|
+
fireEvent.click(button);
|
|
71
|
+
expect(button).toHaveAccessibleName(/Show card date/);
|
|
72
|
+
expect(button).toHaveTextContent("2026");
|
|
73
|
+
fireEvent.click(button);
|
|
74
|
+
expect(button).toHaveTextContent("2:30PM");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
test("compact card ages use minutes, hours, days, and months at their boundaries", () => {
|
|
79
|
+
const now = Date.parse("2026-10-10T12:00:00Z");
|
|
80
|
+
for (const [age, expected] of [[0, "<1m"], [59999, "<1m"], [60000, "1m"], [3600000, "1h"], [7200000, "2h"], [5 * 86400000, "5d"], [29 * 86400000, "29d"], [30 * 86400000, "1mo"], [75 * 86400000, "2mo"], [-60000, "<1m"]] as const) {
|
|
81
|
+
expect(formatCardAge(new Date(now - age).toISOString(), { now })).toBe(expected);
|
|
82
|
+
}
|
|
83
|
+
expect(formatCardAge("invalid", { now })).toBe("—");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("card format defaults to compact age and changes with the shared setting", () => {
|
|
87
|
+
const now = new Date(2026, 9, 8, 15).getTime();
|
|
88
|
+
const view = (showCalendarDates = false) => <MantineProvider><RelativeTimeProvider now={now} showCalendarDates={showCalendarDates}>
|
|
89
|
+
<RelativeTime calendar icon={false} label="Updated time" value={local(2026, 10, 8, 14, 30)} />
|
|
90
|
+
</RelativeTimeProvider></MantineProvider>;
|
|
91
|
+
const { rerender } = render(view());
|
|
92
|
+
expect(screen.getByRole("button", { name: /^Updated time:/ })).toHaveTextContent("30m");
|
|
93
|
+
fireEvent.click(screen.getByRole("button", { name: /^Updated time:/ }));
|
|
94
|
+
expect(screen.getByRole("button", { name: /^Updated time:/ })).toHaveAccessibleName(/Show relative time/);
|
|
95
|
+
fireEvent.click(screen.getByRole("button", { name: /^Updated time:/ }));
|
|
96
|
+
rerender(view(true));
|
|
97
|
+
expect(screen.getByRole("button", { name: /^Updated time:/ })).toHaveTextContent("2:30PM");
|
|
98
|
+
rerender(view());
|
|
99
|
+
expect(screen.getByRole("button", { name: /^Updated time:/ })).toHaveTextContent("30m");
|
|
100
|
+
});
|
|
@@ -1,40 +1,75 @@
|
|
|
1
1
|
import { Tooltip, UnstyledButton } from "@mantine/core";
|
|
2
|
-
import { IconClock } from "@tabler/icons-react";
|
|
3
|
-
import { createContext, useContext, useState, type ReactNode } from "react";
|
|
4
|
-
import { formatExactTime, formatRelativeTime } from "../../time.js";
|
|
2
|
+
import { IconClock, IconHourglass } from "@tabler/icons-react";
|
|
3
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
4
|
+
import { formatCardAge, formatCardTime, formatExactTime, formatRelativeTime, formatWorkedDuration } from "../../time.js";
|
|
5
5
|
|
|
6
|
+
const CardCalendarDates = createContext(false);
|
|
6
7
|
const RelativeTimeClock = createContext<number | null>(null);
|
|
7
8
|
|
|
8
|
-
export function RelativeTimeProvider({ children, now }: { children: ReactNode; now: number }) {
|
|
9
|
-
return <RelativeTimeClock value={now}>{children}</RelativeTimeClock>;
|
|
9
|
+
export function RelativeTimeProvider({ children, now, showCalendarDates = false }: { children: ReactNode; now: number; showCalendarDates?: boolean }) {
|
|
10
|
+
return <RelativeTimeClock value={now}><CardCalendarDates value={showCalendarDates}>{children}</CardCalendarDates></RelativeTimeClock>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ElapsedTime({ startedAt, tooltipEnabled = true }: { startedAt: string | null | undefined; tooltipEnabled?: boolean }) {
|
|
14
|
+
const [now, setNow] = useState(() => Date.now());
|
|
15
|
+
const start = startedAt ? Date.parse(startedAt) : NaN;
|
|
16
|
+
const available = Number.isFinite(start);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!available) return;
|
|
19
|
+
const tick = () => setNow(Date.now());
|
|
20
|
+
tick();
|
|
21
|
+
const timer = window.setInterval(tick, 1000);
|
|
22
|
+
return () => window.clearInterval(timer);
|
|
23
|
+
}, [available, startedAt]);
|
|
24
|
+
const text = available ? formatWorkedDuration(Math.max(0, now - start)) : "—";
|
|
25
|
+
const tooltip = available ? `Worked for ${text} · Started ${formatExactTime(startedAt)}` : "Turn start time unavailable";
|
|
26
|
+
return <Tooltip disabled={!tooltipEnabled} events={{ focus: true, hover: true, touch: false }} label={tooltip}>
|
|
27
|
+
<span aria-label={`Elapsed time: ${text}. ${tooltip}`} className="taskchef-time" tabIndex={0}>
|
|
28
|
+
<IconHourglass aria-hidden size={12} />
|
|
29
|
+
<bdi className="taskchef-time-label">{text}</bdi>
|
|
30
|
+
</span>
|
|
31
|
+
</Tooltip>;
|
|
10
32
|
}
|
|
11
33
|
|
|
12
34
|
export function RelativeTime({
|
|
13
35
|
icon,
|
|
36
|
+
calendar = false,
|
|
14
37
|
label,
|
|
15
38
|
value,
|
|
39
|
+
durationMs,
|
|
40
|
+
tooltipEnabled = true,
|
|
16
41
|
}: {
|
|
17
42
|
icon?: ReactNode;
|
|
43
|
+
calendar?: boolean;
|
|
18
44
|
label: string;
|
|
19
45
|
value: string | null | undefined;
|
|
46
|
+
durationMs?: number | null;
|
|
47
|
+
tooltipEnabled?: boolean;
|
|
20
48
|
}) {
|
|
21
49
|
const [exact, setExact] = useState(false);
|
|
22
|
-
useContext(RelativeTimeClock);
|
|
23
|
-
const
|
|
50
|
+
const now = useContext(RelativeTimeClock) ?? Date.now();
|
|
51
|
+
const showCalendarDates = useContext(CardCalendarDates);
|
|
52
|
+
const shortText = calendar
|
|
53
|
+
? showCalendarDates ? formatCardTime(value, { now }) : formatCardAge(value, { now })
|
|
54
|
+
: formatRelativeTime(value, { now });
|
|
55
|
+
const text = exact ? formatExactTime(value) : shortText;
|
|
24
56
|
const exactText = formatExactTime(value);
|
|
25
57
|
const unavailable = exactText === "—";
|
|
26
|
-
const
|
|
58
|
+
const duration = formatWorkedDuration(durationMs);
|
|
59
|
+
const tooltip = durationMs !== undefined
|
|
60
|
+
? duration === "—" ? "Run duration unavailable" : `Worked for ${duration}`
|
|
61
|
+
: unavailable ? "Updated time unavailable" : exact ? shortText : exactText;
|
|
27
62
|
return (
|
|
28
|
-
<Tooltip events={{ focus: true, hover: true, touch: false }} label={tooltip}>
|
|
63
|
+
<Tooltip disabled={!tooltipEnabled} events={{ focus: true, hover: true, touch: false }} label={tooltip}>
|
|
29
64
|
<UnstyledButton
|
|
30
65
|
aria-label={unavailable
|
|
31
66
|
? `${label}: unavailable.`
|
|
32
|
-
: `${label}: ${text}. ${exact ? "Show relative time" : "Show exact time"}`}
|
|
67
|
+
: `${label}: ${text}. ${exact ? calendar && showCalendarDates ? "Show card date" : "Show relative time" : "Show exact time"}`}
|
|
33
68
|
className="taskchef-time"
|
|
34
69
|
disabled={unavailable}
|
|
35
70
|
onClick={() => setExact((value) => !value)}
|
|
36
71
|
>
|
|
37
|
-
{icon
|
|
72
|
+
{icon === undefined ? <IconClock aria-hidden size={12} /> : icon}
|
|
38
73
|
<bdi className="taskchef-time-label">{text}</bdi>
|
|
39
74
|
</UnstyledButton>
|
|
40
75
|
</Tooltip>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
3
|
+
import { fixtureTask } from "../fixtures";
|
|
4
|
+
import { ReplyCover } from "./ReplyCover";
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
test("loads a local cover, opens details, and clears it on a new turn", async () => {
|
|
7
|
+
const task = fixtureTask({ title: "Design", turnId: "one", replyImage: { url: "/design.png", alt: "Design screenshot" } });
|
|
8
|
+
const load = vi.fn().mockResolvedValueOnce("data:image/png;base64,AAAA").mockResolvedValueOnce(null);
|
|
9
|
+
const open = vi.fn();
|
|
10
|
+
const { rerender } = render(<ReplyCover task={task} loadImage={load} onOpen={open} />);
|
|
11
|
+
expect(await screen.findByAltText("Design screenshot")).toHaveAttribute("src", "data:image/png;base64,AAAA");
|
|
12
|
+
fireEvent.click(screen.getByRole("button")); expect(open).toHaveBeenCalledOnce();
|
|
13
|
+
rerender(<ReplyCover task={{ ...task, turnId: "two" }} loadImage={load} onOpen={open} />);
|
|
14
|
+
await waitFor(() => expect(screen.queryByRole("button")).not.toBeInTheDocument());
|
|
15
|
+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
test("remote covers are hidden without starting an image request", async () => {
|
|
18
|
+
const task = fixtureTask({ title: "Chart", replyImage: {url: "https://example.com/chart.png", alt: "Chart"} });
|
|
19
|
+
const load = vi.fn();
|
|
20
|
+
render(<ReplyCover task={task} loadImage={load} onOpen={vi.fn()} />);
|
|
21
|
+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
|
|
22
|
+
expect(load).not.toHaveBeenCalled();
|
|
23
|
+
await waitFor(() => expect(screen.queryByRole("button")).not.toBeInTheDocument());
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a rejected local image read hides the cover", async () => {
|
|
27
|
+
const task = fixtureTask({ replyImage: {url: "/missing.png", alt: "Missing"} });
|
|
28
|
+
render(<ReplyCover task={task} loadImage={vi.fn().mockRejectedValue(new Error("Missing file"))} onOpen={vi.fn()} />);
|
|
29
|
+
await waitFor(() => expect(screen.queryByRole("button")).not.toBeInTheDocument());
|
|
30
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { Task } from "../types";
|
|
3
|
+
|
|
4
|
+
export function ReplyCover({ task, image = task.replyImage!, loadImage, onOpen }: { task: Task; image?: { url: string; alt: string }; loadImage?: (task: Task, url?: string) => Promise<string | null>; onOpen: () => void }) {
|
|
5
|
+
const root = useRef<HTMLButtonElement>(null);
|
|
6
|
+
const [src, setSrc] = useState<string | null>(null);
|
|
7
|
+
const [failed, setFailed] = useState(false);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
let cancelled = false;
|
|
10
|
+
let started = false;
|
|
11
|
+
setSrc(null); setFailed(false);
|
|
12
|
+
// Remote images remain click-only Markdown links, never automatic requests.
|
|
13
|
+
if (/^https:\/\//i.test(image.url)) { setFailed(true); return; }
|
|
14
|
+
const load = async () => {
|
|
15
|
+
if (started) return;
|
|
16
|
+
started = true;
|
|
17
|
+
try {
|
|
18
|
+
const url = await loadImage?.(task, image.url);
|
|
19
|
+
if (!cancelled) { setSrc(url || null); setFailed(!url); }
|
|
20
|
+
} catch { if (!cancelled) setFailed(true); }
|
|
21
|
+
};
|
|
22
|
+
const observer = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
|
|
23
|
+
if (entries.some((entry) => entry.isIntersecting)) { observer?.disconnect(); void load(); }
|
|
24
|
+
}, { rootMargin: "200px" });
|
|
25
|
+
if (observer && root.current) observer.observe(root.current); else void load();
|
|
26
|
+
return () => { cancelled = true; observer?.disconnect(); };
|
|
27
|
+
}, [image.url, task.turnId, loadImage]);
|
|
28
|
+
if (failed) return null;
|
|
29
|
+
return <button ref={root} type="button" className="taskchef-reply-cover" aria-label={`View image details for ${task.title}`} onClick={onOpen}>
|
|
30
|
+
{src ? <img src={src} alt={image.alt} referrerPolicy="no-referrer" loading="lazy" onError={() => setFailed(true)} />
|
|
31
|
+
: <span>Loading image…</span>}
|
|
32
|
+
</button>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { MantineProvider } from "@mantine/core";
|
|
2
|
+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
4
|
+
import { fixtureTask } from "../fixtures";
|
|
5
|
+
import { NextTaskDetail } from "../../../mcp-app/react/NextTaskDetail";
|
|
6
|
+
import { TurnContent } from "./TurnContent";
|
|
7
|
+
// Test TaskChef's rows and keyboard handlers without jsdom layout work.
|
|
8
|
+
// Browser QA separately exercises Mantine's real popup and positioning.
|
|
9
|
+
vi.mock("@mantine/core", async () => {
|
|
10
|
+
const actual = await vi.importActual<typeof import("@mantine/core")>("@mantine/core");
|
|
11
|
+
const React = await import("react");
|
|
12
|
+
const { createPortal } = await import("react-dom");
|
|
13
|
+
const Open = React.createContext(false);
|
|
14
|
+
const Popover = Object.assign(
|
|
15
|
+
({opened, children}: {opened: boolean; children: React.ReactNode}) => <Open value={opened}>{children}</Open>,
|
|
16
|
+
{
|
|
17
|
+
Target: ({children}: {children: React.ReactNode}) => children,
|
|
18
|
+
Dropdown: React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((props, ref) =>
|
|
19
|
+
React.useContext(Open) ? createPortal(<div {...props} ref={ref} />, document.body) : null),
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
return {...actual, Popover};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(cleanup);
|
|
26
|
+
const png = 'data:image/png;base64,iVBORw0KGgo=';
|
|
27
|
+
test('reply thumbnails open above Details and close with Escape', async () => {
|
|
28
|
+
const load=vi.fn().mockResolvedValue(png);
|
|
29
|
+
const close = vi.fn();
|
|
30
|
+
const { rerender } = render(<MantineProvider><NextTaskDetail task={{...fixtureTask(),title:'Chat details',replyText:'.png>)'}} opened busy={false} error={null} onClose={close} onOpenCodex={()=>{}} loadImage={load} /></MantineProvider>);
|
|
31
|
+
const thumbnail=await screen.findByRole('button',{name:'Enlarge Design'});
|
|
32
|
+
expect(load).toHaveBeenCalledWith(expect.anything(),'/local/design%20(2).png');
|
|
33
|
+
fireEvent.click(thumbnail);
|
|
34
|
+
await waitFor(()=>expect(screen.getByRole('dialog',{name:'Design'})).toBeVisible());
|
|
35
|
+
rerender(<MantineProvider><NextTaskDetail task={{...fixtureTask(),title:'Chat details',replyText:'.png>)'}} opened busy={false} error={null} onClose={close} onOpenCodex={()=>{}} loadImage={load} /></MantineProvider>);
|
|
36
|
+
expect(screen.getByRole('dialog',{name:'Design'})).toBeVisible();
|
|
37
|
+
fireEvent.keyDown(screen.getByRole('dialog',{name:'Design'}),{key:'Escape'});
|
|
38
|
+
await waitFor(()=>expect(screen.queryByRole('dialog',{name:'Design'})).not.toBeInTheDocument());
|
|
39
|
+
expect(screen.getByRole('dialog',{name:'Chat details'})).toBeVisible();
|
|
40
|
+
expect(close).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
test('missing local images are hidden and remote images stay links', async () => {
|
|
43
|
+
const load=vi.fn().mockResolvedValue(null);
|
|
44
|
+
render(<MantineProvider><TurnContent task={{...fixtureTask(),replyText:'\n'}} loadImage={load} /></MantineProvider>);
|
|
45
|
+
await waitFor(()=>expect(load).toHaveBeenCalledTimes(1));
|
|
46
|
+
expect(screen.queryByRole('img')).toBeNull();expect(screen.queryByRole('button',{name:/Enlarge/})).toBeNull();
|
|
47
|
+
expect(screen.getByRole('link',{name:'Remote'})).toHaveAttribute('href','https://example.com/image.png');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('all prompt images stay visible beside capped prompt text and open in the lightbox', async () => {
|
|
51
|
+
const load=vi.fn().mockResolvedValue(png); const close=vi.fn();
|
|
52
|
+
const task={...fixtureTask(),title:'Prompt images',promptText:'\n\n\n\nPlease review both.',promptImages:[{url:'/first.png',alt:'First'},{url:'/second.png',alt:'Second'}]};
|
|
53
|
+
render(<MantineProvider><NextTaskDetail task={task} opened busy={false} error={null} onClose={close} onOpenCodex={()=>{}} loadImage={load} /></MantineProvider>);
|
|
54
|
+
expect(await screen.findByRole('button',{name:'Enlarge First'})).toBeVisible();
|
|
55
|
+
const second=await screen.findByRole('button',{name:'Enlarge Second'});
|
|
56
|
+
expect(second.closest('.taskchef-turn-collapsed')).toBeNull();
|
|
57
|
+
expect(screen.getAllByRole('img')).toHaveLength(2);
|
|
58
|
+
fireEvent.click(second);
|
|
59
|
+
await waitFor(()=>expect(screen.getByRole('dialog',{name:'Second'})).toBeVisible());
|
|
60
|
+
fireEvent.keyDown(screen.getByRole('dialog',{name:'Second'}),{key:'Escape'});
|
|
61
|
+
await waitFor(()=>expect(screen.queryByRole('dialog',{name:'Second'})).toBeNull());
|
|
62
|
+
expect(close).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('Escape closes an annotation popup while keeping Details open', async () => {
|
|
66
|
+
const close=vi.fn();
|
|
67
|
+
render(<MantineProvider><NextTaskDetail task={{...fixtureTask(),title:'Annotated chat',replyText:':codex-annotation{index="1"}',annotations:[{text:'Selected text',annotation:'User comment'}]}} opened busy={false} error={null} onClose={close} onOpenCodex={()=>{}} /></MantineProvider>);
|
|
68
|
+
fireEvent.click(screen.getByRole('button',{name:'Annotation 1'}));
|
|
69
|
+
await waitFor(()=>expect(screen.getByRole('tooltip')).toBeVisible());
|
|
70
|
+
fireEvent.keyDown(screen.getByRole('button',{name:'Annotation 1'}),{key:'Escape'});
|
|
71
|
+
await waitFor(()=>expect(screen.queryByRole('tooltip')).toBeNull());
|
|
72
|
+
expect(screen.getByRole('dialog',{name:'Annotated chat'})).toBeVisible();
|
|
73
|
+
expect(close).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('prompt gallery reserves space and reveals images together after concurrent loading', async () => {
|
|
77
|
+
let first!: (value:string) => void; let second!: (value:string) => void;
|
|
78
|
+
const load=vi.fn().mockImplementation((_task, url) => new Promise<string>(resolve => { if(url==='/first.png') first=resolve; else second=resolve; }));
|
|
79
|
+
const task={...fixtureTask(),promptText:'Review both.',promptImages:[{url:'/first.png',alt:'First'},{url:'/second.png',alt:'Second'}]};
|
|
80
|
+
render(<MantineProvider><TurnContent task={task} loadImage={load} /></MantineProvider>);
|
|
81
|
+
expect(screen.getByRole('status',{name:'Loading First'})).toBeVisible();
|
|
82
|
+
expect(screen.getByRole('status',{name:'Loading Second'})).toBeVisible();
|
|
83
|
+
await waitFor(()=>expect(load).toHaveBeenCalledTimes(2));
|
|
84
|
+
first(png);
|
|
85
|
+
await waitFor(()=>expect(screen.queryByRole('button',{name:'Enlarge First'})).toBeNull());
|
|
86
|
+
second(png);
|
|
87
|
+
expect(await screen.findByRole('button',{name:'Enlarge First'})).toBeVisible();
|
|
88
|
+
expect(screen.getByRole('button',{name:'Enlarge Second'})).toBeVisible();
|
|
89
|
+
expect(load).toHaveBeenCalledTimes(2);
|
|
90
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Modal } from "@mantine/core";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import type { Task } from "../types";
|
|
4
|
+
|
|
5
|
+
export function ReplyImagePreview({ task, url, alt, loadImage, source, onLightboxChange }: { task: Task; url: string; alt: string; loadImage: (task: Task, url?: string) => Promise<string | null>; source?: string | null; onLightboxChange?: (opened: boolean) => void }) {
|
|
6
|
+
const [src, setSrc] = useState<string | null>(source ?? null);
|
|
7
|
+
const [failed, setFailed] = useState(false);
|
|
8
|
+
const [opened, setOpened] = useState(false);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
setSrc(source ?? null); setFailed(false);
|
|
12
|
+
if (source) return;
|
|
13
|
+
void loadImage(task, url).then(value => {
|
|
14
|
+
if (!cancelled) {
|
|
15
|
+
if (value && /^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/=]+$/.test(value)) setSrc(value);
|
|
16
|
+
else setFailed(true);
|
|
17
|
+
}
|
|
18
|
+
}).catch(() => { if (!cancelled) setFailed(true); });
|
|
19
|
+
return () => { cancelled = true; };
|
|
20
|
+
}, [task.id, task.turnId, url, loadImage, source]);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!opened) return;
|
|
23
|
+
onLightboxChange?.(true);
|
|
24
|
+
return () => onLightboxChange?.(false);
|
|
25
|
+
}, [opened, onLightboxChange]);
|
|
26
|
+
if (failed) return null;
|
|
27
|
+
if (!src) return <span className="taskchef-image-placeholder" role="status" aria-label={`Loading ${alt}`} />;
|
|
28
|
+
return <span className="taskchef-response-image">
|
|
29
|
+
<button type="button" className="taskchef-image-thumbnail" aria-label={`Enlarge ${alt}`} onClick={() => setOpened(true)}><img src={src} alt={alt} onError={() => setFailed(true)} /></button>
|
|
30
|
+
<Modal opened={opened} onClose={() => setOpened(false)} title={alt} centered size="auto" zIndex={400} classNames={{ content: "taskchef-image-lightbox" }}>
|
|
31
|
+
<img src={src} alt={alt} onError={() => { setFailed(true); setOpened(false); }} />
|
|
32
|
+
</Modal>
|
|
33
|
+
</span>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, expect, test, vi } from "vitest";
|
|
3
|
+
import { MantineProvider } from "@mantine/core";
|
|
4
|
+
import { ReplyMarkdown } from "./ReplyMarkdown";
|
|
5
|
+
|
|
6
|
+
// Test TaskChef's rows and keyboard handlers without jsdom layout work.
|
|
7
|
+
// Browser QA separately exercises Mantine's real popup and positioning.
|
|
8
|
+
vi.mock("@mantine/core", async () => {
|
|
9
|
+
const actual = await vi.importActual<typeof import("@mantine/core")>("@mantine/core");
|
|
10
|
+
const React = await import("react");
|
|
11
|
+
const { createPortal } = await import("react-dom");
|
|
12
|
+
const Open = React.createContext(false);
|
|
13
|
+
const Popover = Object.assign(
|
|
14
|
+
({opened, children}: {opened: boolean; children: React.ReactNode}) => <Open value={opened}>{children}</Open>,
|
|
15
|
+
{
|
|
16
|
+
Target: ({children}: {children: React.ReactNode}) => children,
|
|
17
|
+
Dropdown: React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((props, ref) =>
|
|
18
|
+
React.useContext(Open) ? createPortal(<div {...props} ref={ref} />, document.body) : null),
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
return {...actual, Popover};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(cleanup);
|
|
25
|
+
|
|
26
|
+
test("renders Markdown blocks, formatting, links, and GFM tables", () => {
|
|
27
|
+
const { container } = render(<ReplyMarkdown text={'# Review\n\n**Ready** with `npm test`.\n\n- First\n- Second\n\n```js\nconst ready = true;\n```\n\n| Check | Result |\n| --- | --- |\n| CI | Passed |\n\n[PR](https://github.com/example/repo/pull/1)'} />);
|
|
28
|
+
expect(screen.getByRole("heading", { name: "Review" })).toBeVisible();
|
|
29
|
+
expect(container.querySelector("strong")).toHaveTextContent("Ready");
|
|
30
|
+
expect(container.querySelectorAll("li")).toHaveLength(2);
|
|
31
|
+
expect(container.querySelector("pre code")).toHaveTextContent("const ready = true;");
|
|
32
|
+
expect(screen.getByRole("table")).toHaveTextContent("Passed");
|
|
33
|
+
expect(screen.getByRole("link", { name: "PR" })).toHaveAttribute("rel", "noopener noreferrer");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("compact previews keep formatting without exposing Markdown markers", () => {
|
|
37
|
+
const { container } = render(<ReplyMarkdown compact text={'**Ready**\n\n- Run `npm test`'} />);
|
|
38
|
+
expect(container.firstChild).toHaveClass("taskchef-markdown--compact");
|
|
39
|
+
expect(container.querySelector("strong")).toHaveTextContent("Ready");
|
|
40
|
+
expect(container.querySelector("code")).toHaveTextContent("npm test");
|
|
41
|
+
expect(container).not.toHaveTextContent("**Ready**");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("does not execute HTML, use unsafe links, or fetch saved images", () => {
|
|
45
|
+
const { container } = render(<ReplyMarkdown text={'<script>alert(1)</script>\n\n[Unsafe](javascript:alert%281%29) [Local](/tmp/test)\n\n\n\n'} />);
|
|
46
|
+
expect(container.querySelector("script")).toBeNull();
|
|
47
|
+
expect(container.querySelector("img")).toBeNull();
|
|
48
|
+
expect(screen.queryByRole("link", { name: "Unsafe" })).toBeNull();
|
|
49
|
+
expect(screen.queryByRole("link", { name: "Local" })).toBeNull();
|
|
50
|
+
expect(screen.queryByRole("link", { name: "Bad" })).toBeNull();
|
|
51
|
+
expect(screen.getByRole("link", { name: "Screenshot" })).toHaveAttribute("href", "https://example.com/private.png");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
test.each(["## My request:\nFix the filter.", "## My requests:\nFix the filter.", "My requests:\n\nFix the filter.", "My requests: Fix the filter.", "My requests:\nFix the filter.", "My requests: \nFix the filter.", "**My requests:** Fix the filter.", "## **My requests:**\nFix the filter.", "**My requests:** \nFix the filter."])("card previews hide request labels: %s", text => {
|
|
56
|
+
const { container, rerender } = render(<ReplyMarkdown compact hideRequestLabels text={text} />);
|
|
57
|
+
expect(container).toHaveTextContent("Fix the filter.");
|
|
58
|
+
expect(container).not.toHaveTextContent(/My requests?:/i);
|
|
59
|
+
rerender(<ReplyMarkdown text={text} />);
|
|
60
|
+
expect(container).toHaveTextContent(/My requests?:/i);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("card request-label cleanup preserves quoted and code examples", () => {
|
|
64
|
+
const { container } = render(<ReplyMarkdown compact hideRequestLabels text={"> ## My requests:\n\n```text\nMy requests:\n```"} />);
|
|
65
|
+
expect(container.querySelector("blockquote")).toHaveTextContent("My requests:");
|
|
66
|
+
expect(container.querySelector("pre code")).toHaveTextContent("My requests:");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("prompt cleanup preserves later user headings", () => {
|
|
70
|
+
const { container } = render(<ReplyMarkdown hideRequestLabels text={"## My request:\nReview this.\n\n## My requests:\nA real later section."} />);
|
|
71
|
+
expect(container.querySelectorAll("h2")).toHaveLength(1);
|
|
72
|
+
expect(container.querySelector("h2")).toHaveTextContent("My requests:");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test.each([false, true])("annotations render readable links with selected text and comment (compact=%s)", async compact => {
|
|
76
|
+
const {container}=render(<MantineProvider><ReplyMarkdown compact={compact} annotations={[{text:'Quoted selection',annotation:'Requested change'}]} text={'Final plan :codex-annotation{index="1"}'} /></MantineProvider>);
|
|
77
|
+
const button=screen.getByRole('button',{name:'Annotation 1'});
|
|
78
|
+
expect(container).not.toHaveTextContent(':codex-annotation');
|
|
79
|
+
fireEvent.focus(button);
|
|
80
|
+
expect(await screen.findByRole('tooltip')).toHaveTextContent('Selected text:Quoted selectionUser comment:Requested change');
|
|
81
|
+
});
|
|
82
|
+
test('code examples stay literal and unavailable annotations do not borrow another index', async () => {
|
|
83
|
+
const {container}=render(<MantineProvider><ReplyMarkdown annotations={[{text:'First selection',annotation:'First comment'}]} text={':codex-annotation{index="2"}\n\n`:codex-annotation{index="1"}`\n\n```text\n:codex-annotation{index="1"}\n```'} /></MantineProvider>);
|
|
84
|
+
expect(container.querySelectorAll('code')).toHaveLength(2);
|
|
85
|
+
expect(container.querySelector('code')).toHaveTextContent(':codex-annotation{index="1"}');
|
|
86
|
+
expect(screen.queryByRole('button',{name:'Annotation 1'})).toBeNull();
|
|
87
|
+
fireEvent.focus(screen.getByRole('button',{name:'Annotation 2'}));
|
|
88
|
+
expect(await screen.findByRole('tooltip')).toHaveTextContent('Annotation details unavailable.');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('an open annotation survives same-turn refresh and closes without opening a card', async () => {
|
|
92
|
+
const annotation={text:'Selected text',annotation:'User comment'};
|
|
93
|
+
const clicked=vi.fn();
|
|
94
|
+
const {rerender}=render(<MantineProvider><div onClick={clicked}><ReplyMarkdown text={':codex-annotation{index="1"}'} annotations={[annotation]} /></div></MantineProvider>);
|
|
95
|
+
fireEvent.click(screen.getByRole('button',{name:'Annotation 1'}));
|
|
96
|
+
expect(clicked).not.toHaveBeenCalled();
|
|
97
|
+
rerender(<MantineProvider><div onClick={clicked}><ReplyMarkdown text={':codex-annotation{index="1"}'} annotations={[{...annotation}]} /></div></MantineProvider>);
|
|
98
|
+
expect(screen.getByRole('tooltip')).toHaveTextContent('User comment');
|
|
99
|
+
fireEvent.keyDown(screen.getByRole('button',{name:'Annotation 1'}),{key:'Escape'});
|
|
100
|
+
expect(screen.queryByRole('tooltip')).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("annotation popups keep fixed labels even without a comment", async () => {
|
|
104
|
+
render(<MantineProvider><ReplyMarkdown text={':codex-annotation{index="1"}'} annotations={[{text:"A selection",annotation:" "}]} /></MantineProvider>);
|
|
105
|
+
fireEvent.mouseEnter(screen.getByRole("button",{name:"Annotation 1"}));
|
|
106
|
+
const popup=await screen.findByRole("tooltip");
|
|
107
|
+
expect(popup).toHaveTextContent("Selected text:A selectionUser comment:—");
|
|
108
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Popover } from "@mantine/core";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState, useId, type ReactNode } from "react";
|
|
3
|
+
import Markdown, { type Components } from "react-markdown";
|
|
4
|
+
import remarkGfm from "remark-gfm";
|
|
5
|
+
import type { PhrasingContent, Root, RootContent } from "mdast";
|
|
6
|
+
|
|
7
|
+
// Saved replies are untrusted text. Do not execute HTML or load remote images.
|
|
8
|
+
function replyUrl(url: string): string {
|
|
9
|
+
return /^(https?:|mailto:)/i.test(url) || url.startsWith("#") ? url : "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function localImagePath(url: string): boolean {
|
|
13
|
+
return (url.startsWith("/") && !url.startsWith("//")) || /^[A-Za-z]:[\\/]/.test(url);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Generated request labels add no information to cards or prompt bubbles.
|
|
17
|
+
// Keep labels inside quoted examples and code intact.
|
|
18
|
+
function requestLabelText(node: PhrasingContent): string | null {
|
|
19
|
+
if (node.type === "text") return node.value;
|
|
20
|
+
if ((node.type === "strong" || node.type === "emphasis") && node.children.length === 1 && node.children[0].type === "text") return node.children[0].value;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hideCardRequestLabels() {
|
|
25
|
+
return (tree: Root) => {
|
|
26
|
+
const leading = tree.children[0];
|
|
27
|
+
if (leading && (leading.type === "heading" || leading.type === "paragraph") && leading.children.length === 1
|
|
28
|
+
&& /^My requests?:\s*$/i.test(requestLabelText(leading.children[0]) ?? "")) tree.children.shift();
|
|
29
|
+
const first = tree.children[0];
|
|
30
|
+
if (first?.type === "paragraph" && first.children[0]) {
|
|
31
|
+
const label = first.children[0];
|
|
32
|
+
if (label.type === "text") {
|
|
33
|
+
label.value = label.value.replace(/^My requests?:[ \t]*(?:\r?\n)?/i, "");
|
|
34
|
+
if (!label.value) first.children.shift();
|
|
35
|
+
} else if (/^My requests?:\s*$/i.test(requestLabelText(label) ?? "")) {
|
|
36
|
+
first.children.shift();
|
|
37
|
+
} else return;
|
|
38
|
+
if (first.children[0]?.type === "break") first.children.shift();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Transform text nodes only: quoted code and existing links remain literal.
|
|
44
|
+
function annotationLinks() {
|
|
45
|
+
return (tree: Root) => {
|
|
46
|
+
const visit = (parent: Root | RootContent) => {
|
|
47
|
+
if (!("children" in parent) || parent.type === "link") return;
|
|
48
|
+
parent.children = parent.children.flatMap((child): (RootContent | PhrasingContent)[] => {
|
|
49
|
+
if (child.type !== "text") { visit(child as RootContent); return [child]; }
|
|
50
|
+
const parts: PhrasingContent[] = [];
|
|
51
|
+
let end = 0;
|
|
52
|
+
for (const match of child.value.matchAll(/:codex-annotation\{index="([1-9]\d*)"\}/g)) {
|
|
53
|
+
if (match.index! > end) parts.push({ type: "text", value: child.value.slice(end, match.index) });
|
|
54
|
+
parts.push({ type: "link", url: `#taskchef-annotation-${match[1]}`, children: [{ type: "text", value: `Annotation ${match[1]}` }] });
|
|
55
|
+
end = match.index! + match[0].length;
|
|
56
|
+
}
|
|
57
|
+
if (!parts.length) return [child];
|
|
58
|
+
if (end < child.value.length) parts.push({ type: "text", value: child.value.slice(end) });
|
|
59
|
+
return parts;
|
|
60
|
+
}) as typeof parent.children;
|
|
61
|
+
};
|
|
62
|
+
visit(tree);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Annotation = { text: string; annotation: string };
|
|
67
|
+
type AnnotationOpenChange = (id: string, opened: boolean) => void;
|
|
68
|
+
const AnnotationContext = createContext<{ items: Annotation[]; onOpenChange?: AnnotationOpenChange }>({ items: [] });
|
|
69
|
+
function AnnotationLink({ index }: { index: number }) {
|
|
70
|
+
const { items, onOpenChange } = useContext(AnnotationContext);
|
|
71
|
+
const annotation = items[index - 1];
|
|
72
|
+
const popupId = useId();
|
|
73
|
+
const label = annotation ? <div className="taskchef-annotation-popup">
|
|
74
|
+
<div className="taskchef-annotation-label">Selected text:</div><div>{annotation.text}</div>
|
|
75
|
+
<div className="taskchef-annotation-label">User comment:</div><div>{annotation.annotation.trim() || "—"}</div>
|
|
76
|
+
</div> : "Annotation details unavailable.";
|
|
77
|
+
const [opened, setOpened] = useState(false);
|
|
78
|
+
useEffect(() => { onOpenChange?.(popupId, opened); return () => onOpenChange?.(popupId, false); }, [popupId, opened, onOpenChange]);
|
|
79
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
80
|
+
const target = useRef<HTMLButtonElement>(null);
|
|
81
|
+
const popup = useRef<HTMLDivElement>(null);
|
|
82
|
+
const show = () => { if (timer.current) clearTimeout(timer.current); setOpened(true); };
|
|
83
|
+
const leave = () => {
|
|
84
|
+
if (timer.current) clearTimeout(timer.current);
|
|
85
|
+
timer.current = setTimeout(() => {
|
|
86
|
+
if (!target.current?.contains(document.activeElement) && !popup.current?.contains(document.activeElement)) setOpened(false);
|
|
87
|
+
}, 180);
|
|
88
|
+
};
|
|
89
|
+
useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
|
|
90
|
+
return <Popover opened={opened} onChange={setOpened} withinPortal width={320} zIndex={500} position="bottom-start" middlewares={{ flip: true, shift: { crossAxis: true, padding: 12 } }}>
|
|
91
|
+
<Popover.Target><button ref={target} type="button" className="taskchef-annotation-link" onMouseEnter={show} onMouseLeave={leave} onFocus={show} onBlur={leave}
|
|
92
|
+
onClick={event => { event.stopPropagation(); show(); }} onKeyDown={event => { event.stopPropagation(); if (event.key === "Escape") setOpened(false); }} aria-expanded={opened} aria-label={`Annotation ${index}`}>Annotation {index}</button></Popover.Target>
|
|
93
|
+
<Popover.Dropdown ref={popup} role="tooltip" maw="calc(100vw - 24px)" className="taskchef-status-tooltip" onMouseEnter={show} onMouseLeave={leave} onFocus={show} onBlur={leave}
|
|
94
|
+
onClick={event => event.stopPropagation()} onKeyDown={event => { event.stopPropagation(); if (event.key === "Escape") setOpened(false); }}>{label}</Popover.Dropdown>
|
|
95
|
+
</Popover>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function ReplyMarkdown({ text, compact = false, hideRequestLabels = false, renderImage, annotations = [], onAnnotationChange }: { text: string; compact?: boolean; hideRequestLabels?: boolean; renderImage?: (url: string, alt: string) => ReactNode; annotations?: Annotation[]; onAnnotationChange?: AnnotationOpenChange }) {
|
|
99
|
+
const components = useMemo<Components>(() => ({
|
|
100
|
+
a: ({ href, children }) => /^#taskchef-annotation-[1-9]\d*$/.test(href ?? "")
|
|
101
|
+
? <AnnotationLink index={Number(href!.split("-").at(-1))} /> : href
|
|
102
|
+
? <a href={href} target={href.startsWith("#") ? undefined : "_blank"} rel="noopener noreferrer">{children}</a>
|
|
103
|
+
: <span>{children}</span>,
|
|
104
|
+
img: ({ src, alt }) => src && renderImage && localImagePath(src) ? renderImage(src, alt || "Image") : src
|
|
105
|
+
? <a href={src} target="_blank" rel="noopener noreferrer">{alt || "Image"}</a>
|
|
106
|
+
: <span>{alt || "Image"}</span>,
|
|
107
|
+
table: ({ children }) => <div className="taskchef-markdown-table"><table>{children}</table></div>,
|
|
108
|
+
}), [renderImage]);
|
|
109
|
+
return <div className={`taskchef-markdown${compact ? " taskchef-markdown--compact" : ""}`}>
|
|
110
|
+
<AnnotationContext value={{ items: annotations, onOpenChange: onAnnotationChange }}><Markdown skipHtml remarkPlugins={hideRequestLabels ? [remarkGfm, hideCardRequestLabels, annotationLinks] : [remarkGfm, annotationLinks]} urlTransform={(url, key) => key === "src" && renderImage && localImagePath(url) ? url : replyUrl(url)} components={components}>{text}</Markdown></AnnotationContext>
|
|
111
|
+
</div>;
|
|
112
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { compactTokens, savedUsageCost } from "./SavedUsageFormat";
|
|
3
|
+
test("formats token units with two decimal places and promotes rounded boundaries", () => {
|
|
4
|
+
expect([0, 999, 3210, 4230000, 5390000000, 999999].map(compactTokens)).toEqual(["0", "999", "3.21K", "4.23M", "5.39B", "1.00M"]);
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
test("an unknown price keeps a plain cost placeholder", () => {
|
|
8
|
+
const usage={tokens:{input_tokens:10,cached_input_tokens:0,cache_write_input_tokens:0,output_tokens:1,reasoning_output_tokens:0,total_tokens:11},samples:1,costUsd:0,costPartial:true,partial:false};
|
|
9
|
+
expect(savedUsageCost(usage)).toBe("—");
|
|
10
|
+
expect(savedUsageCost({...usage,costPartial:false})).toBe("$0.00");
|
|
11
|
+
});
|