Commit Graph

253 Commits

Author SHA1 Message Date
Yunfei He 5c60a0af63 fix: upgrade text stack to grapheme-aware slicing/width (closes #21 grapheme classes)
Bump slice-ansi@9, string-width@8, wrap-ansi@10 and add cli-truncate@6
(both the runtime dep and the pnpm catalog entry for string-width). Rewrite
wrapText truncate variants to delegate to cli-truncate, matching Ink's
wrap-text.ts: grapheme clusters (ZWJ emoji, combining marks) stay whole and
newlines are preserved. Adjust the horizontal-clip left-edge compensation in
paint.ts because slice-ansi@9 drops a straddling wide grapheme whole rather
than splitting it, so lineX must advance by the actually-dropped width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:57:52 +08:00
Yunfei He 1a1d9935d8 fix: skip painting display:none subtrees (closes #21 display-none class)
Adds an early-return guard in paintNode so nodes with DISPLAY_NONE
(already set on their Yoga node) are entirely skipped during paint,
matching Ink's renderNodeToOutput behavior. Without the guard, hidden
text/borders leaked onto visible siblings at x=0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:57:52 +08:00
Yunfei He cd5971e28b fix(runtime): round up scheduler delay; restore spies in scheduler tests 2026-05-28 16:12:31 +08:00
Yunfei He abaf2acbce feat(runtime): provide no-op animation scheduler in renderToString 2026-05-28 16:12:31 +08:00
Yunfei He 00dcaad206 feat(runtime): provide shared animation scheduler per app, dispose on teardown 2026-05-28 16:12:31 +08:00
Yunfei He a016bf3801 refactor(runtime): drive useAnimation via shared scheduler with elapsed-time frames 2026-05-28 16:12:31 +08:00
Yunfei He 68c09128df feat(runtime): add AnimationSchedulerKey injection key 2026-05-28 16:12:31 +08:00
Yunfei He 9468c34f35 feat(runtime): add shared animation scheduler 2026-05-28 16:12:31 +08:00
Yunfei He 81b068eda1 fix: waitUntilRenderFlush waits for scheduled-but-not-yet-pending commits
Codex review found waitUntilRenderFlush gated the scheduler flush on
hasPending() alone, missing the window after schedule() queues a commit
but before the post-flush callback sets hasPendingFlag — letting the
promise resolve before the pending render flushed. Now delegates to
flush() unconditionally; flush() already short-circuits when nothing is
scheduled or pending, so this only adds the missing wait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 7d03a110e1 fix: address /simplify review — frame-writer dedup desync, scheduler edge cases
Correctness fixes found by max-effort review of the branch diff:

- frame-writer.sync() now updates lastFrame alongside log-update's
  previousOutput. Previously the two dedup layers desynced after a sync()
  (the clearTerminal path), silently dropping a legitimately-changed frame
  and emitting an empty BSU/ESU pair. Adds a regression test.
- scheduler: the queuePostFlushCb callback now bails if scheduled was reset
  by cancel(), so a stale callback can't commit on a torn-down tree or
  re-arm an uncancellable trailing timer.
- scheduler.flush() now collects multiple concurrent waiters instead of
  overwriting a single resolver — fixes a hang when two waitUntilRenderFlush()
  calls await the same pending commit.
- render teardown nulls mountedClear so a post-unmount app.clear() can't
  write to a torn-down stream.
- test-streams getContentWrites imports bsu/esu instead of hardcoding the
  escape literals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 1eb30703f9 fix: address PR review — flush hang on cancel, BSU/ESU incremental wrapping
- scheduler.cancel() now resolves a pending flush() waiter, preventing
  waitUntilRenderFlush() from hanging when the app unmounts mid-flush
- BSU/ESU now wrap the actual stream writes instead of being embedded in
  the frame string, so synchronization survives log-update's incremental
  line diffing (matches Ink ink.tsx:1059-1097). Normal branch guarded by
  willRender() to avoid empty synchronized-update pairs on unchanged frames

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 1ffb847a65 feat: test parity final push — 64 new tests, BSU/ESU, clear() API
Closes remaining test gaps between vue-tui and Ink:

Features:
- Add synchronized output (BSU/ESU) via DEC private mode 2026
- Add clear() API to TuiApp for erasing rendered output

Bug fixes:
- Cancel scheduler trailing timer on teardown (prevents stale commits)
- Guard teardown writes against ended/destroyed streams
- Reorder teardown to cancel timer before final commit

Tests (64 new, 2 skipped for known feature gaps):
- 7 BSU/ESU shouldSynchronize tests
- 5 borderBackgroundColor tests
- 13 component edge cases (empty text, number child, OSC hyperlink
  wrap-width, bare-text-in-Box validation, transform multi-line,
  leading whitespace, link escape closing)
- 13 text-width/CJK tests (alignment, truncation, overlay edge cases)
- 4 throttle + unmount edge cases
- 10 waitUntilRenderFlush write-callback-level tests + 1 clear() test
- 3 exit re-entrance tests
- 5 PTY #450 regression tests + 4 inline #450 tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 6628dde35e refactor: deduplicate safeSliceEnd into shared export from text-measure 2026-05-28 10:06:36 +08:00
Yunfei He 2c994314e1 fix: reorder clip/transform pipeline so transforms are clipped correctly
Horizontal clipping now runs per-line AFTER transforms instead of before,
preventing Transform-widened text from escaping clip boundaries. Left-edge
clipping also uses the actual removed width to position subsequent text
correctly when a wide char straddles the boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:06:36 +08:00
Yunfei He fe98756666 fix: clip border lines to box width for wide corner chars (closes #17)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:06:36 +08:00
Yunfei He 1f871e1ba5 chore: bump all packages to 0.0.2
Bump @vue-tui/runtime, @vue-tui/cli, and @vue-tui/testing from 0.0.1 to 0.0.2.
2026-05-28 01:10:20 +08:00
Yunfei He 5d532f47d1 fix: clip wide glyphs that overflow grid/clip boundary (closes #10) (#14)
Absolute-positioned wide characters (CJK, emoji) could paint past the
right edge of a clipped box or the terminal grid, producing output wider
than the column limit. Three fixes:

- Safe-slice after sliceAnsi in clip logic to handle wide char overshoot
- Bounds check in grid write loop to skip chars exceeding grid width
- Width-aware border fill to account for measured corner char widths
2026-05-28 00:42:39 +08:00
Yunfei He 0e7d7753f9 feat: export measureText and kittyModifiers to match Ink public API (#13) 2026-05-27 23:12:20 +08:00
Yunfei He 9227ddf696 test: add Ink component/composable test parity (+130) and fix layout listener bug
* test: add text ANSI sanitization parity tests from Ink (+15)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add use-animation parity tests from Ink (+43)

Port 43 new tests from Ink's use-animation test suite covering:
- Multiple animations in sync, different rates, sibling unmount
- Timer cleanup/recreation on unmount and remount
- Inactive animations, timer leak prevention
- Edge intervals (NaN, Infinity, -Infinity, oversized, zero, negative)
- isActive toggle resets, pause/resume cycles
- Frame catch-up, time/delta tracking, reset() behavior
- Newly mounted/activated animations don't inherit elapsed time
- Wall clock monotonicity, getter function isActive support

Uses selective fake timers (setInterval + performance only) so that
render()'s internal setImmediate still works on real clocks. Fake timer
tests read refs directly to avoid Vue scheduler flush timing issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add use-box-metrics/measure parity tests from Ink (+16)

Port 16 missing tests from Ink's use-box-metrics, measure-element, and
measure-text test suites. Fix useBoxMetrics to reset metrics to zeros
when the tracked ref detaches (element unmounts or ref switches to null).

3 tests are skipped because vue-tui's useBoxMetrics uses watchPostEffect
(re-runs only when ref.value changes) rather than Ink's layout-commit
listener pattern, so sibling-content and resize-driven re-measurement
is not yet supported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add screen-reader parity tests from Ink (+11)

Add 11 screen-reader integration tests covering aria-label substitution on
Text/Box, ANSI styling omission, multiple/nested components, null component,
aria-state variants (busy, disabled, expanded), multi-line roles, and
multiselectable listbox.

Also fix component prop bug: Vue normalizes kebab-case prop names to camelCase
at runtime, so props["aria-label"] was always undefined. Switch Box/Text prop
declarations and access to camelCase (ariaLabel, ariaHidden, ariaRole, ariaState).

Add isScreenReaderEnabled option to renderToString() so tests can exercise
screen-reader output through the component pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add render-to-string parity tests from Ink (+18)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add cursor composable parity tests from Ink (+7)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add 6 missing screen-reader Ink parity tests

Add tests for aria-hidden, select input (list with roles/states/labels),
aria-state.multiline, aria-state.readonly, aria-state.required, and
nested multi-line text rendering in screen-reader mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fix render-to-string missing Ink parity tests (+10)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fix cursor composable missing Ink parity tests (+6)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fix use-box-metrics missing Ink parity tests (+4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: add layout listener so useBoxMetrics updates on resize and sibling changes

Adds a layout listener mechanism to TuiRoot matching Ink's architecture:
- TuiRoot.layoutListeners Set with addLayoutListener/emitLayoutListeners
- emitLayoutListeners called after every yoga.calculateLayout in commit()
- useBoxMetrics subscribes to layout listeners, diffs values before updating

Enables 4 previously-skipped tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:42:54 +08:00
Yunfei He 32fff4104c fix: address PR review comments
- sanitize-ansi.test.ts: convert raw ESC/C1 control chars to \u escapes
- frame-writer.test.ts: import cursor escapes from cursor-helpers instead
  of duplicating, add 7 createFrameWriter integration tests
2026-05-27 16:22:15 +08:00
Yunfei He 9f9ce66609 fix: safeSliceStart returns empty when ZWJ emoji exceeds width
slice-ansi can't split within a ZWJ grapheme cluster, so the retry
loop may never find a result that fits. Return empty string instead
of the oversized result.
2026-05-27 16:22:15 +08:00
Yunfei He ec67193250 test: add text-width parity tests from Ink (+9)
Port 9 text-width tests from Ink's test suite covering wide characters
in fixed-width Boxes, CJK width calculation, mixed ASCII+wide chars,
ANSI styled text layout, empty Text siblings, and CJK truncation
(end/middle/start/box-width). Also fix CJK truncation bug where
slice-ansi would overshoot on wide character boundaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00
Yunfei He f5de77a426 test: add frame-writer parity tests from Ink log-update (+41)
Port all 35 test declarations from Ink's log-update test suite,
producing 41 new runtime tests (6 cursor tests run in both standard
and incremental modes via describe.each). Covers standard rendering,
incremental rendering (surgical updates, shrink, grow), clear/done
reset, sync+update, cursor positioning, no-trailing-newline fullscreen
mode, and render-to-empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00
Yunfei He f098a8dc91 test: add cursor-helpers parity tests from Ink (+7)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00
Yunfei He 3a836c1e5b test: add input-parser parity tests from Ink (+45)
Port all 57 tests from Ink's input-parser test suite (43 named + 14
backspace/delete splitting via test.each). Replaces the previous 12
skeleton tests with full Ink parity covering: CSI parameters, SS3
sequences, meta+CSI/SS3 double escape, escaped printable/supplementary
codepoints, legacy ESC[[ sequences, incomplete holding states, flush
pending, empty chunks, text + incomplete escape, 14 backspace/delete
splitting cases, and 10 bracketed paste tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00
Yunfei He cc8f4e1a29 test: add ansi-tokenizer parity tests from Ink (+26)
Port all 26 tests from Ink's ansi-tokenizer test suite, covering CSI,
OSC, DCS, SOS, ESC, C1 control tokens and invalid/incomplete sequences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00
Yunfei He 6655e32f25 test: add sanitize-ansi parity tests from Ink (+24)
Port 24 missing tests from Ink's sanitize-ansi test suite covering:
- C1 OSC hyperlinks with various terminators (C1 ST, ESC ST, BEL)
- C1 CSI sequences (both SGR and non-SGR)
- DCS passthrough (tmux wrappers, incomplete, BEL in payload)
- SOS control strings (ESC and C1 variants, various terminators)
- Standalone C1 control characters and ST bytes
- ESC ST sequences, malformed ESC sequences, incomplete CSI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00
Yunfei He 8209f8440b docs: write/optimize READMEs for all published packages
- @vue-tui/runtime: rewrite with pitch, badges, SFC+shallowRef example,
  accurate component/composable tables, app lifecycle examples
- @vue-tui/cli: new README documenting vue-tui dev, HMR, crash recovery
- @vue-tui/testing: rewrite with @testing-library positioning, correct
  async render API, cleanup globals:true requirement
- Add early-stage warning to all sub-packages (matching root README)
- Fix CLI package.json: add license field, correct description

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:08:48 +08:00
Yunfei He 978a29145b fix: suppress input text for kitty release events
Release events now produce empty input to prevent character duplication
when reportEventTypes flag is enabled. key.eventType is still passed
through so handlers can detect release events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:50:48 +08:00
Yunfei He 935f998d15 test: add supplementary unicode text-as-codepoints test
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:50:48 +08:00
Yunfei He fa919ef166 test: add all kitty protocol parse-keypress unit tests (~57 tests) 2026-05-27 15:50:48 +08:00
Yunfei He 838aaaf806 feat: wire kitty keyboard controller into mount/teardown and add exports
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 15:50:48 +08:00
Yunfei He 387791c142 feat: add kitty query response filter and extend useInput Key interface
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 15:50:48 +08:00
Yunfei He 377e34ef37 feat: add kitty-keyboard module with types, query matching, lifecycle controller 2026-05-27 15:50:48 +08:00
Yunfei He 72cf46987c chore: optimize package.json descriptions for npm discoverability
Improve descriptions across all packages to include key differentiators
(Yoga flexbox, AI agent, React Ink lineage) and rename workspace root
from vue-tui to vue-tui-monorepo to avoid confusion with the taken npm name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:39:24 +08:00
Yunfei He 40aa144a82 feat: add alternateScreen option to MountOptions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:59:01 +08:00
Yunfei He 4db1ee4e8d fix: add try/catch and pending-render guard to final unmount commit
Codex review suggestions:
- Wrap mountedCommit() in try/catch so a crash in the final render
  doesn't block teardown cleanup (console restore, yoga detach, etc.)
- Skip final render if scheduler already has a pending commit (matches
  Ink's shouldRenderFinalFrame guard, prevents potential <Static> duplication)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:04:18 +08:00
Yunfei He 5b1035792c fix: add final unmount render to trigger shouldClearOnUnmount
Ink does an explicit onRender() with isUnmounting=true before unmount
(ink.tsx:755-761), which triggers clearTerminal for fullscreen apps.
vue-tui's teardown() was nulling scheduledCommit before unmount, making
shouldClearOnUnmount dead code. Now calls commit() synchronously before
disabling the scheduler, matching Ink's unmount render behavior.

Restores 2 erase PTY tests that were incorrectly attributed to a
React vs Vue rendering difference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:47:11 +08:00
Yunfei He d8dde1438d fix: fix PTY test failures — stdin double-processing, raw mode exit hang, fixture JSX
Three runtime bugs and fixture issues fixed:

1. stdin registered both "readable" and "data" handlers, causing double input
   processing in real PTY. Now only uses "data" handler (works for both real
   TTY and fake PassThrough streams).

2. dispose() didn't call stdin.unref() after restoring raw mode, keeping the
   event loop alive and causing raw mode exit tests to hang.

3. PTY fixtures used JSX syntax which tsx compiles without vue-jsx plugin,
   producing non-function slot values. Converted to h() with function slots.

Also: term.ts now passes rows arg to node-pty for viewport-dependent tests,
and exit-double-raw-mode fixture uses __READY__ protocol.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 01:28:29 +08:00
Yunfei He dd4c5aa3e1 fix: replace node-pty with child_process.spawn + force-tty preload
node-pty's posix_spawnp is blocked in sandboxed environments.
child_process.spawn works everywhere. force-tty.cjs patches
stdout.isTTY so fixtures behave as if running in a real terminal.

Also fix setRawMode this-binding bug (acquireRawMode on undefined).

72/82 PTY tests now pass. Remaining 10 need timing/assertion fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:55:52 +08:00
Yunfei He d8aeb2bc02 feat: add clearTerminal strategy, fullscreen suppression, log-update integration
- shouldClearTerminalForFrame() for overflow/fullscreen transitions
- renderInteractiveFrame with fullscreen newline suppression
- writeToStdout/Stderr restore via log-update clear + re-render
- incrementalRendering MountOption
- Static output capture inlined in commit paths

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:57:59 +08:00
Yunfei He 7403522c96 feat: port Ink's log-update and replace npm log-update dependency
Port Ink's self-built log-update with standard and incremental rendering
modes, cursor positioning, and sync support. Rewrite frame-writer to use
the new internal log-update instead of the npm package, adding sync,
setCursorPosition, and willRender methods while preserving existing
write/done/clear behavior. Remove the npm log-update dependency (-6
packages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:51:20 +08:00
Yunfei He 4a9d401b0f feat: port cursor-helpers from Ink for cursor positioning escape sequences
Add ansi-escapes to pnpm catalog and runtime dependencies. Port all
cursor helper functions (cursorPositionChanged, buildCursorSuffix,
buildReturnToBottom, buildReturnToBottomPrefix, buildCursorOnlySequence)
with 10 colocated unit tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:48:03 +08:00
Yunfei He 3422e6edc1 feat: stdin.ref/unref, raw mode default false, conditional listener, maxListeners
- stdin.ref() on raw mode enable, stdin.unref() on disable
- setEncoding("utf8") on raw mode enable
- Raw mode default changed from true to false (hooks acquire it)
- useStdin().setRawMode routed through ref-counted controller
- Stdin listeners attached only when raw mode is active
- EventEmitter maxListeners set to Infinity

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:22:58 +08:00
Yunfei He 05c4dd5076 feat: add beforeExit natural unmount + stdout write barrier
Register process.once('beforeExit') in waitUntilExit() so TUI apps
that finish naturally (no explicit exit/unmount) clean up before the
event loop drains. Add stdout write barrier in resolveExit() to ensure
all output is flushed before settling the exit promise.

Update testing render helper to await the additional async tick
introduced by the write barrier so early-error detection still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:18:56 +08:00
Yunfei He 222b520678 feat: add useWindowSize alias, terminal-size fallback, update public API + README
- useWindowSize alias for useTerminalSize (Ink API naming parity)
- terminal-size fallback when stdout.columns/rows are 0/missing
- Public API test covers all new exports
- README documents all MountOptions and composables

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:28:46 +08:00
Yunfei He 11c655c76e feat: add accessibility support (aria props, screen reader output)
- Box: aria-label, aria-hidden, aria-role (typed union), aria-state
- Text: aria-label, aria-hidden
- Transform: accessibilityLabel prop
- internal_accessibility on TuiBox node (role + state only)
- renderScreenReaderOutput() alternate render path
- Component-level aria-hidden hides, aria-label replaces content
- 14 new accessibility tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:26:10 +08:00
Yunfei He 83d9dd6ce8 feat: add useIsScreenReaderEnabled + MountOptions.isScreenReaderEnabled
- Default: true when INK_SCREEN_READER=true env var set
- Screen reader mode bypasses render throttle
- Composable exposes flag via provide/inject

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:16:31 +08:00
Yunfei He 2c186951ed feat: add useAnimation composable for frame-based animations
Drives animations with a frame counter, elapsed time, delta, and reset
function. Supports configurable interval and reactive isActive toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:13:43 +08:00
Yunfei He edf51bef3c feat: add useCursor composable for terminal cursor control
- setCursorPosition propagates to app context for frame restoration
- Cursor cleared on component unmount via onScopeDispose

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:10:51 +08:00