Commit Graph

35 Commits

Author SHA1 Message Date
Yunfei He c76a0f09b2 fix(runtime): raw-mode teardown matches Ink's clearInputState + disableRawMode (#101)
Two related fixes to the raw-mode controller, mirroring Ink's split
(App.tsx:212-224,357):

- Sync input-state clear (P6): on the last useInput release (refs→0), reset the
  input parser, clear the pending escape-flush timer, and detach the stdin
  listeners SYNCHRONOUSLY — only the terminal raw-mode toggle stays deferred.
  Previously everything was deferred in one microtask, so a same-tick useInput
  SWAP (old unmounts → refs 0 → queued; new mounts → refs 0→1; the queued reset
  then short-circuits on refs>0) left the parser un-reset and a partial escape
  buffered before the swap leaked into the replacement handler. Ink's
  clearInputState runs synchronously and unconditionally so this can't happen.

- Force raw-off (P7): the final disable now unconditionally setRawMode(false),
  matching Ink's disableRawMode. The previous prevRaw-restore re-captured
  stdin.isRaw at acquire while raw was still active on a sync false→true→false
  swap, snapshotting `true` and leaving the terminal in RAW mode after exit. No
  test locked the prevRaw-restore (an undocumented vue invention), so the field
  is removed entirely — eliminating the corruption and aligning with Ink.

The swap-keeps-raw-on behavior (deferred toggle short-circuits on refs>0) is
preserved. Tests lock the partial-escape no-leak on swap and the terminal being
restored (final setRawMode is false) via an isRaw-tracking stdin.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 22:25:42 +08:00
Yunfei He f99f23e346 fix(runtime): restoreLastOutput falls back on an empty frame, matching Ink (#100)
The frame-restore after an external stdout write (console.log / useStdout().write)
used `lastOutputToRender ?? lastOutput + "\n"`. `??` only falls back for
null/undefined, so an empty-string lastOutputToRender (the initial value, and the
value left by the screen-reader empty-frame path) restored "" — nothing — where Ink
restores lastOutput + "\n". Ink uses `||` (ink.tsx:507) and so does vue's own
mountedClear (render.ts:668); :518 was the lone inconsistent site. Changed `??` to
`||`. Locked by an SR-empty-frame + external-write test that re-emits "\n".

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 22:04:06 +08:00
Yunfei He aa1c8bfb07 fix(runtime): treat cross-realm Error as an error on exit + thrown paths (Ink parity) (#86)
app.exit(crossRealmError) resolved waitUntilExit() with the error as a result value,
and a component-thrown cross-realm Error was re-wrapped (losing the original) -- both
because `instanceof Error` fails across VM realms. Adds Ink's isErrorInput check
(instanceof Error || Object.prototype.toString.call(v) === "[object Error]", the
realm-independent [object Error] brand) and uses it at the 3 exit() classification
sites and the 2 error-boundary normalization sites. A cross-realm Error now rejects
(exit) / is preserved (throw), matching Ink; genuine non-Error values still resolve /
still wrap.

Flips the previously-locked "cross-realm resolves" test to Ink's reject (the behavior
was never in ink-divergences.md, so per the repo rule it was an unverified gap, not a
sanctioned divergence). Adds component-throw cross-realm + a non-Error-still-wraps guard.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 07:22:27 +08:00
Yunfei He 9b5eac3ba4 feat(runtime): port ErrorOverview to Ink's full error frame (parity) (#81)
ErrorOverview was a one-line `Name: message` stub; Ink v7.0.4 renders a full
ERROR overview. Ports it faithfully:

- white-on-red ` ERROR ` label + message, dim cwd-relative file:line:column origin
- a code excerpt around the throwing line (padded gutter, error line highlighted,
  `Line N` / `Line N, error` aria-labels)
- the parsed stack (`- fn (file:line:col)`, cwd-relative, StackUtils nodeInternals
  filtering, unparsable-line fallback, fs.existsSync guard)

Adds code-excerpt@4.0.0 + stack-utils@2.0.6 (the versions Ink uses). The error
boundary now keeps the raw thrown value for display (Ink stores the raw value;
ErrorOverview renders a stack only when one exists) -- so a non-Error throw no
longer shows a misleading synthetic framework stack. The exit/reject path still
receives a wrapped Error (semantics unchanged). The unparsable-stack fallback
emits a literal backslash-t to match Ink's JSX.

Showing String(value) for a non-Error message (vs Ink's blank) is a documented
additive divergence (ink-divergences.md).

Adds 4 error-overview tests.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:26:02 +08:00
Yunfei He e3202f345a fix(runtime): feed useCursor position to the interactive commit path (Ink parity) (#80)
On the interactive commit path the active cursor position was never forwarded to
the frame writer, so the cursor was never shown at the useCursor() position, never
followed input, and a cursor-only move on a byte-identical frame emitted nothing.
Aligns with Ink v7.0.4 by wiring three coupled defects together:

- render.ts setCursorPosition now forwards to writer.setCursorPosition, marking
  log-update's cursorDirty (Ink ink.tsx:494-497).
- the synchronized-update commit gate is split into Ink's two levels: the write is
  gated on willRender() || isCursorDirty(), but BSU/ESU wrap only when willRender()
  (Ink ink.tsx:1094 outer, :372-382 inner) -- an idle cursor-dirty re-render emits
  zero bytes, not an empty BSU/ESU pair.
- FrameWriter.write() bypasses its frame===lastFrame dedup when the cursor is dirty,
  so a cursor-only move still reaches log-update's buildCursorOnlySequence.

The mount-time hide-cursor write moves before originalMount so the first commit's
show is the last visibility change (Ink hides before its first render); a synchronous
mount throw now runs best-effort teardown (cursor/alt-screen restore) before
rethrowing the ORIGINAL error, matching Ink's constructor-wired signalExit.

Adds 8 interactive-TTY tests; the prior use-cursor tests used the debug render()
helper where log-update never runs, so they passed for the wrong reason.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 04:57:37 +08:00
Yunfei He bfd680490f refactor(runtime)!: rename useAppContext() to useApp() (full Ink alignment) (#73)
#69 added `useAppContext()` as a Vue-native rename of Ink's `useApp()`,
qualified to avoid reading as the Vue application instance. On reflection the
"Context" suffix borrowed the name of an internal grab-bag context and slightly
mislabels the hook — it returns app lifecycle controls, not that context. The
collision worry doesn't hold up: Vue has no `useApp()`, the returned
`{ exit, waitUntilRenderFlush }` is clearly not the Vue app instance, and "App"
in vue-tui already means the `TuiApp` from `createApp()`.

Rename to `useApp()` for full Ink fidelity (same name + same shape), and drop
the now-defunct "App composable" entry from ink-divergences.md — it ceases to
be a divergence.

Internal context cleanup (the grab-bag `AppContext` + the `StdinContext`
duplication) is intentionally out of scope here, tracked separately.

BREAKING CHANGE: `useAppContext()` is renamed to `useApp()`. Replace
`const { exit } = useAppContext()` with `const { exit } = useApp()`.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 23:32:22 +08:00
Yunfei He 9d1e4d7805 feat(runtime)!: replace useExit() with Ink-aligned useAppContext() (#69)
Ink's `useApp()` returns `{ exit, waitUntilRenderFlush }`. vue-tui previously
exposed only `exit()` via `useExit()` and kept `waitUntilRenderFlush` on the
`TuiApp` handle alone. Align the public surface with Ink: add `useAppContext()`
returning the same pair, and remove `useExit()`.

- thread `waitUntilRenderFlush` into the injected `AppContext` via a hoisted
  impl shared by the `TuiApp` handle and the composable, so both resolve
  identically
- add `useAppContext()`; delete `useExit()`; migrate all call sites, PTY
  fixtures, examples, READMEs and the public-API surface test
- port Ink's two "useApp waitUntilRenderFlush" tests; Ink's third relies on
  React concurrent mode (N/A in Vue)
- rewrite the ink-divergences entry: this is now a *naming* divergence
  (`useAppContext` vs `useApp`, mirroring `createApp` vs `render`), not a
  surface one — and fix the prior wrong claim that Ink's `useApp` returns
  stdin/stdout/stderr

The name is qualified (`useAppContext`, not `useApp`) so it doesn't read as the
Vue application instance (`createApp`/`app.mount`) — the same Vue-native naming
choice vue-tui already makes with `createApp()` vs Ink's `render()`.

BREAKING CHANGE: `useExit()` is removed. Replace `const exit = useExit()` with
`const { exit } = useAppContext()`.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 19:57:52 +08:00
Yunfei He f78121f6e7 fix(runtime): make exit() first-call-wins (Ink parity, G33) (#55)
The first exit() call now captures its value/error and initiates teardown
synchronously; subsequent exit() calls are complete no-ops, so waitUntilExit
resolves/rejects with the FIRST value rather than the last. This mirrors
Ink's handleAppExit guard (isUnmounted || isUnmounting → early return).

Previously each exit() queued a microtask that overwrote pendingExitResult/
pendingExitError before resolveExit ran, making it last-wins. An exitInitiated
flag set at the top of exit() now guards the value capture and re-resolve,
while the deferred microtask teardown (needed because exit() is called from
inside the Vue update cycle) is preserved.

Reverses the sweep-1 refutation: exit() was NOT already first-wins guarded.

Also guard unmount-in-progress (isUnmounting parity) + value→error test.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:43:35 +08:00
Yunfei He 2e9c938f5f fix(runtime): guard against two renderers on the same stdout (Ink parity, G14) (#43)
* fix(runtime): guard against two renderers on the same stdout (Ink parity, G14)

Make skipped-mount unmount() a pure no-op: add `skippedMount` flag set in
the instance-reuse guard branch; teardown() and resolveExit() return early
when set, so no write-barrier or stdout touch reaches the owner's stream.
Strengthen test: assert app2.unmount() writes nothing to process.stdout
(catches the pre-fix empty write-barrier) and that app1 still owns the
WeakMap entry after app2.unmount() (third mount on same stdout still warns).

* chore(parity): ledger — G14 pr-open, reconcile G13 merged

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 03:59:56 +08:00
Yunfei He f49a995d03 fix(runtime): clear+reset on terminal-width decrease during resize (Ink parity, G11) (#41)
* fix(runtime): clear+reset on terminal-width decrease during resize (Ink parity, G11)

Track lastTerminalWidth (initialized at mount via resolveSize); onResize clears
writer + resets frameState.lastOutput/lastOutputToRender when the new width is
narrower than the previous width, mirroring the logic in ink.tsx:459-474 that
prevents duplicate overlapping re-renders on terminal narrow. Width increases and
pure height changes are unaffected.

Preserve outputHeight on narrowing (matches Ink ink.tsx:462-466 which leaves
lastOutputHeight intact): zeroing it suppressed the clearTerminal path in
shouldClearTerminalForFrame for overflowing frames (hadPreviousFrame=false).
Adds overflow-path test that was RED with outputHeight=0 and GREEN with the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(parity): ledger — G11 pr-open, reconcile G10 merged

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 03:24:35 +08:00
Yunfei He 051e9f128e test(runtime-tests): move beforeExit listener assertion into sequential file
The test asserting process.listenerCount("beforeExit") lived in the
parallel wait-flush.test.tsx, conflicting with the branch's rule that
process-global-state tests live in *.sequential.test.* files. Move it into
leak.sequential.test.tsx (already the home for process exit/SIGINT listener-
count assertions) and extend that file's header comment. #26 (item 2).
2026-05-29 20:00:36 +08:00
Yunfei He f3bb5f268a fix(runtime): cancel pending throttled commit on resize
The resize handler painted synchronously via commit() but left the commit
scheduler's pending trailing throttle timer armed. If an update was sitting
in that timer, it fired a second doCommit() right after the resize paint —
and because shouldClearTerminalForFrame clears whenever the previous frame
overflowed the viewport, the second commit emitted a duplicate clearTerminal.

Cancel the pending trailing commit before the synchronous paint; the paint
already reflects the current tree, so the pending commit is redundant.

Regression test (test-first) in throttle.sequential.test.tsx reproduces the
double-clear (2 clears) and verifies the fix (1 clear). Closes #26 (item 1).
2026-05-29 20:00:36 +08:00
Yunfei He ee6004b8be test(runtime-tests): run the main suite concurrently by default
Enable sequence.concurrent: true in vite.config.ts so the non-PTY suite runs
concurrently like the PTY suite. Stress-verified stable (8/8 at maxForks=4);
the suite drops from ~13s to ~4-5s.

Three test patterns were incompatible with concurrency; handled per cause:

- Inline snapshots (background-color, borders): the module-level `expect`
  loses snapshot test context under concurrency. Fixed in place by using the
  context-local `expect` (async ({ expect }) => ...), so they stay concurrent.

- Process-global state (throttle/animation-scheduler use fake timers; leak
  asserts on process exit/SIGINT listener counts and live yoga nodes): a
  concurrent sibling clobbers the shared global mid-test. These genuinely
  require serial execution, so they move to *.sequential.test.* files with
  it.sequential / describe.sequential and a header explaining why.

`vp run ready` passes.
2026-05-29 16:54:13 +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 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 48beac5673 test: add render lifecycle parity tests from Ink (+10) (#12)
Port missing render lifecycle tests from Ink's test/render.tsx:
- onRender fires on input-triggered state update
- throttle renders to maxFps (leading+trailing pattern)
- immediate scheduler in debug mode commits every mutation
- screen reader mode bypasses throttle (immediate commits)
- exit(error) followed by exit(value) still rejects
- exit(value) resolves even when called rapidly twice
- unmount does not write to ended stdout stream
- non-interactive mode writes only last frame at unmount
- non-interactive mode does not emit erase or cursor sequences
- non-interactive unmount does not crash on ended stdout

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:40:47 +08:00
Yunfei He 666cbca73a test: alternate screen teardown-safety and debug-mode tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:59:01 +08:00
Yunfei He 34827b312f test: alternate screen lifecycle tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:59:01 +08:00
Yunfei He 0022c5cc49 test: alternate screen guard condition tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:59:01 +08:00
Yunfei He 2e01fc50a4 chore: remove 8 exit todo stubs (covered by PTY fixtures)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:23:39 +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 a87cc08d0d Revert "chore: remove 8 subprocess-fixture todo stubs (behaviors already covered)"
This reverts commit a5b0530c04.
2026-05-26 17:35:16 +08:00
Yunfei He a5b0530c04 chore: remove 8 subprocess-fixture todo stubs (behaviors already covered)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:25:06 +08:00
Yunfei He 3ee01dc278 feat: add patchConsole, waitUntilRenderFlush, onRender, and maxFps
Add four behavioral surfaces to the runtime:

- patchConsole (default: true): intercepts console.* methods to route
  output through writeToStdout/writeToStderr, preventing frame corruption.
  Disabled in debug mode. Restored before Vue cleanup on teardown.
- waitUntilRenderFlush(): flushes pending throttled renders and waits
  for the stdout write barrier, exposed on TuiApp and testing RenderResult.
- onRender callback: fires after each commit with { renderTime } in ms.
- maxFps option: overrides the scheduler's default 32ms throttle interval
  with 1000/maxFps. The scheduler now accepts a throttleMs option and
  exposes hasPending() for flush coordination.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:54:18 +08:00
Yunfei He f51079a5ea feat: rewrite stdin pipeline with input-parser, exitOnCtrlC via \x03
- Replace data event with readable + input-parser state machine
- Intercept \x03 in input pipeline (not process SIGINT)
- useInput now uses parse-keypress for semantic key mapping
- 20ms pending escape flush timer matching Ink
- Esc focus reset in input pipeline
- Delete old key-parser.ts (replaced by input-parser + parse-keypress)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:04:39 +08:00
Yunfei He 1106be867b feat: add error boundary with onErrorCaptured to catch descendant errors
Wrap user root component in an InternalErrorBoundary that catches all
descendant errors (setup, render, lifecycle) via onErrorCaptured and
routes them through exit(error). This prevents yoga WASM state corruption
that occurred when errors propagated uncaught during Vue's render phase.

- Add ErrorOverview component for rendering error frames
- Replace try-catch around mount with onErrorCaptured boundary
- Update testing render() to detect early exit errors
- Convert 3 todo tests to active tests (nested setup, unhandledRejection, post-mount)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:48:23 +08:00
Yunfei He 272f1f0c32 refactor!: tree validation throws on invalid nesting instead of warn+skip
Change Box-in-Text validation from dev-only warn+skip to unconditional
throw, matching Ink's reconciler behavior. Add text-outside-Text
validation that throws when non-empty text-leaf nodes are inserted into
box/root/static containers outside a Text context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:37:39 +08:00
Yunfei He 37c4905bbc fix: warn and skip insertion when Box is nested inside Text
Detect invalid <Box> inside <Text> nesting at the DOM insert level and
emit a dev warning instead of crashing the WASM yoga engine. The box
insertion is skipped to prevent layout corruption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:04:16 +08:00
Yunfei He e79c3660f5 test: verify error boundary routes post-mount errors through exit()
Vue's errorHandler (installed after mount) already catches render-time
errors from nested components and routes them through exit(err),
providing error boundary behavior matching Ink's design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:00:56 +08:00
Yunfei He 42c69d72f2 feat: hide cursor on mount, show on unmount (matching Ink)
Write ESC[?25l to stdout after mount and ESC[?25h during teardown,
matching Ink's cursor management. Only active in production mode
(debug: false) to avoid interfering with test streams.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:00:17 +08:00
Yunfei He bcfdaddec7 feat: exit() accepts result value, waitUntilExit resolves with it
Change exit(error?: Error) to exit(errorOrResult?: unknown). When an
Error is passed, waitUntilExit rejects; otherwise it resolves with the
value. This matches Ink's behavior where exit("hello") resolves the
promise with "hello".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:10:52 +08:00
Yunfei He 5e54e5742d refactor: replace ref() with shallowRef() in all test files
AGENTS.md requires shallowRef by default. All test state uses
reassignment (not mutation), so shallowRef is correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:28:06 +08:00
Yunfei He d1c9945cc4 test: port Ink lifecycle, error, and resize tests (15 tests)
Covers exit lifecycle, error handling, and terminal resize
reactivity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:14:35 +08:00
Yunfei He 562b07b60e test: port Ink component tests (38 tests)
Distributes across text, static, transform, newline-spacer,
error-handling, and prop-reset test files.

Tests requiring features not yet in vue-tui are marked test.todo:
- <Transform> inside <Text> (paint pass limitation)
- multi-line transform (transform nodes not yoga carriers)
- text/box nesting validation (no runtime validation yet)
- complex Static rerender patterns (Ink-specific API)

Also fixes wrap="hard" to use wordWrap:false so text breaks at exact
character boundaries rather than word boundaries, matching Ink behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:01:10 +08:00
Yunfei He a7f7d9957d feat(runtime-tests): integration test suite
21 test files covering components, composables, focus, lifecycle,
and scheduler through the public render() API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:13:53 +08:00