Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
storage. Closing a popup cancels without joining; the main loop reaps finished
jobs and metaserver teardown joins all workers before endpoint/toolkit cleanup.
Retained workers share a bounded job budget, including cancelled requests.
- The optional shared chat pilot owns one nonpersistent local-echo widget in
`shared_chat_pilot`; it has no network authority. Keep its build/runtime gates,
bounded input drain, control-epoch cancellation, exact whole-frame presentation
acknowledgment and native text ownership barriers. See `doc/SHARED_CHAT_PILOT.md`.
- Capture privacy is owned by `capture_privacy`: private UI retention and frame
composition deny new screenshots, video frames, and diagnostic captures until
a successfully presented clean frame. UI close/reset and renderer recovery
Expand Down
1 change: 1 addition & 0 deletions client/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ endif ()

# Compile the sources into an executable.
add_executable(${EXECUTABLE} WIN32 ${SOURCES})
include(cmake/SharedChatPilot.cmake)
if (PACKAGE_TYPE STREQUAL "appimage")
set(ATRINIK_APPIMAGE_SOURCE_REVISION "" CACHE STRING
"Exact source revision embedded in the AppImage identity")
Expand Down
4 changes: 4 additions & 0 deletions client/INSTALL
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,7 @@ Windows toolchain and recursive runtime packaging requirements are documented
in `../docs/WINDOWS_TOOLCHAIN.md`. Use the required neutral
`ATRINIK_WINDOWS_TOOLCHAIN_FILE`, `ATRINIK_WINDOWS_TARGET` and
`ATRINIK_WINDOWS_RUNTIME_DIR` producer variables with plain CMake.

The experimental shared local-echo panel is OFF by default and requires explicit
external SDK and verified font inputs. See `doc/SHARED_CHAT_PILOT.md`; enabling
it is a source-build experiment and does not change default package dependencies.
38 changes: 38 additions & 0 deletions client/cmake/SharedChatPilot.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2026 The Atrinik Project
# SPDX-License-Identifier: GPL-2.0-or-later
option(ATRINIK_SHARED_CHAT_PILOT "Build experimental local-only shared chat panel" OFF)
if(ATRINIK_SHARED_CHAT_PILOT)
set(ATRINIK_SHARED_CHAT_INCLUDE_DIR "" CACHE PATH "Explicit ABI 2 header directory")
set(ATRINIK_SHARED_CHAT_LIBRARY "" CACHE FILEPATH "Explicit CPU prototype library")
set(ATRINIK_SHARED_CHAT_LINK_LIBRARIES "" CACHE STRING "Explicit platform static-library closure")
set(ATRINIK_SHARED_CHAT_FONT_MANIFEST "" CACHE FILEPATH "Explicit verified font-pack manifest")
foreach(input ATRINIK_SHARED_CHAT_INCLUDE_DIR ATRINIK_SHARED_CHAT_LIBRARY ATRINIK_SHARED_CHAT_FONT_MANIFEST)
if(NOT IS_ABSOLUTE "${${input}}" OR NOT EXISTS "${${input}}")
message(FATAL_ERROR "Shared chat pilot requires explicit existing absolute ${input}")
endif()
endforeach()
if(NOT EXISTS "${ATRINIK_SHARED_CHAT_INCLUDE_DIR}/tilewright_ui.h")
message(FATAL_ERROR "Shared chat pilot header missing")
endif()
execute_process(COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tools/shared_chat_fonts.py"
"${ATRINIK_SHARED_CHAT_FONT_MANIFEST}" "${CMAKE_CURRENT_BINARY_DIR}/shared_chat_fonts.h"
RESULT_VARIABLE font_result)
if(NOT font_result EQUAL 0)
message(FATAL_ERROR "Shared chat pilot font validation failed")
endif()
target_sources(${EXECUTABLE} PRIVATE src/client/shared_chat_pilot.c)
target_compile_definitions(${EXECUTABLE} PRIVATE ATRINIK_SHARED_CHAT_PILOT TWUI_CPU_PROTOTYPE)
target_include_directories(${EXECUTABLE} PRIVATE "${ATRINIK_SHARED_CHAT_INCLUDE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(${EXECUTABLE} PRIVATE "${ATRINIK_SHARED_CHAT_LIBRARY}" ${ATRINIK_SHARED_CHAT_LINK_LIBRARIES})
endif()
if(ATRINIK_SHARED_CHAT_PILOT AND BUILD_TESTING)
add_executable(client-shared-chat-pilot-tests src/tests/shared_chat_pilot.c src/client/capture_privacy.c)
atrinik_configure_target(client-shared-chat-pilot-tests)
target_compile_definitions(client-shared-chat-pilot-tests PRIVATE ATRINIK_SHARED_CHAT_PILOT TWUI_CPU_PROTOTYPE)
target_include_directories(client-shared-chat-pilot-tests PRIVATE src/include
"${ATRINIK_SHARED_CHAT_INCLUDE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(client-shared-chat-pilot-tests PRIVATE SDL3::SDL3
"${ATRINIK_SHARED_CHAT_LIBRARY}" ${ATRINIK_SHARED_CHAT_LINK_LIBRARIES})
add_test(NAME client-shared-chat-pilot COMMAND client-shared-chat-pilot-tests)
set_tests_properties(client-shared-chat-pilot PROPERTIES ENVIRONMENT "SDL_VIDEODRIVER=dummy")
endif()
82 changes: 82 additions & 0 deletions client/doc/SHARED_CHAT_PILOT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Optional local shared chat panel

This experimental client adapter puts a shared CPU-rendered chat panel in one
existing widget slot. It is disabled at build time and at runtime. Existing
Classic chat is unchanged. The panel labels itself **Local echo**: all text,
including slash-prefixed text, is accepted into its private in-memory history.
It never sends a command or packet. This is a candidate embedding experiment,
not chat replacement or a qualified distribution feature.

## Explicit source-build inputs

Configure the client with these additional CMake options in an owned
Classic-derived wrapper profile/build plan:

```
-DATRINIK_SHARED_CHAT_PILOT=ON
-DATRINIK_SHARED_CHAT_INCLUDE_DIR=/absolute/sdk/include
-DATRINIK_SHARED_CHAT_LIBRARY=/absolute/sdk/lib/libtilewright_ui_ffi.a
-DATRINIK_SHARED_CHAT_LINK_LIBRARIES=dl;pthread;m
-DATRINIK_SHARED_CHAT_FONT_MANIFEST=/absolute/font-pack/manifest.json
```

The library must provide the embedded ChatPanel ABI 2 and CPU prototype exports.
The linker closure shown is for the Linux static candidate only; other targets
need their own verified closure. No Rust compiler, downloads or new fonts are
required by the default build. CMake does not fetch or build the optional SDK.
Default packages do not install any SDK or font-pack assets. Supplying this
option does not establish redistribution or platform qualification.

The explicit schema-1 font manifest supplies an ordered `fonts` array. Each
entry names `family`, relative `path`, `bytes` and `sha256`. Configure verifies
font sizes, hashes and containment in the manifest directory and emits a private
build header. Runtime reads only those named paths and the shared library
reverifies their hashes. The pack is bounded to 32 fonts, 32 MiB each and 64 MiB
total. Missing, changed or incompatible inputs leave a visible generic error;
text is never logged in that error.

Run only an isolated wrapper-managed client with
`ATRINIK_SHARED_CHAT_PILOT=1` to create the panel. The fixed 480×240 widget does
not persist its layout, transcript, draft, selection or history. It is admitted
only when fully inside the window at 1:1 scale. Click its composer to focus;
Escape releases focus. An outside click releases focus and is consumed. Standard
selection keys and explicit Ctrl+A/C/X/V gestures use shared text state.
Existing popups, menus and focused Classic chat/command inputs retain precedence.
Release the existing input (Escape) before focusing the pilot. After
they close, click the pilot to focus again.

## Boundaries and validation

The host retains its single SDL window, event poller, GPU renderer and presenter.
The adapter copies at most 256 native events per drain, bounds copied text to
4096 bytes and fences both physical Enter keys on any text/composition activity
in that drain. Ordinary editing and clipboard keys remain usable alongside
committed text; non-Enter keys are quarantined during actual native composition.
Control-epoch changes cancel held Classic movement and pointer
gestures, even when the final shared state allows world input. Host key
quarantine survives widget/context destruction and reconstruction until matching
key-up events arrive; a copied native drain also finishes after destruction. Native text
sessions are restarted on ownership barriers; timestamps retire late text.
Clipboard and visual movement additionally require shared physical-key effect
permission. There is no implicit world hit from the embedded panel.

The panel renders a bounded 480×218 premultiplied RGBA surface through
`gpu_renderer_draw_surface`, inside normal widget order. The host acknowledges
its exact prepared stamp only after successful whole-frame presentation. Move,
resize, focus, modal and renderer-recovery barriers invalidate displayed
geometry before input. IME rectangles come from the acknowledged shared layout.

History is limited to 32 messages and 32 KiB; the composer and messages to
4096 bytes; pending actions to eight and prepared frames to three. Acceptance
copies an action once into local history and acknowledges its exact batch ID.
Retained pilot content denies screenshot/video/diagnostic admission through
`capture_privacy`. Hiding retains privacy. Destruction drops private memory but
capture remains denied until a successfully presented clean frame.

The enabled `client-shared-chat-pilot` CTest exercises the production adapter
with the actual shared ABI/CPU renderer and a small fake Classic host. It checks
ownership barriers, physical Enter identities, composition fences, output
backpressure, exact presentation, stale attachment geometry, alpha mode and
private-content lifetime. It uses the SDL dummy video driver and is **not** a
real GPU or game embedding qualification. Required wrapper builds, native
coverage and isolated real-runtime acceptance remain separate evidence.
12 changes: 10 additions & 2 deletions client/src/client/capture_privacy.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
static bool blocked = true;
static bool composing;
static bool composition_sensitive;
static bool shared_chat_retained;

void capture_privacy_shared_chat_retained(bool retained) {
shared_chat_retained = retained;
if (retained) {
capture_privacy_block();
}
}

void capture_privacy_block(void) {
blocked = true;
Expand All @@ -16,7 +24,7 @@ void capture_privacy_block(void) {
void capture_privacy_frame_begin(bool sensitive_active) {
blocked = true;
composing = true;
composition_sensitive = sensitive_active;
composition_sensitive = sensitive_active || shared_chat_retained;
}

void capture_privacy_frame_end(bool presented) {
Expand All @@ -27,5 +35,5 @@ void capture_privacy_frame_end(bool presented) {
}

bool capture_privacy_allowed(bool sensitive_active) {
return !blocked && !composing && !sensitive_active;
return !blocked && !composing && !sensitive_active && !shared_chat_retained;
}
4 changes: 4 additions & 0 deletions client/src/client/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <animations.h>
#include <book.h>
#include <capture_privacy.h>
#include <shared_chat_pilot.h>
#include <access_resolver.h>
#include <client.h>
#include <client_celestial.h>
Expand Down Expand Up @@ -859,6 +860,7 @@ bool gpu_renderer_recovery_republish_test(void) {
static bool gpu_renderer_recover_frame(unsigned int *attempts,
const char *context,
const gpu_renderer_recreation_diagnostic_t *consumed) {
shared_chat_pilot_invalidate();
capture_privacy_block();
HARD_ASSERT(attempts != NULL);
HARD_ASSERT(context != NULL);
Expand Down Expand Up @@ -1448,6 +1450,7 @@ int main(int argc, char *argv[]) {
}
render_profiler_end(RENDER_PROFILE_GAME, profile_game_started);

shared_chat_pilot_sync();
update = 0;

if (window_is_active()) {
Expand Down Expand Up @@ -1548,6 +1551,7 @@ int main(int argc, char *argv[]) {
bool frame_presented = false;
if (update) {
bool presented = gpu_renderer_present();
shared_chat_pilot_presented(presented);
frame_presented = presented;
capture_privacy_frame_end(presented);
map_benchmark_statistics_present(presented);
Expand Down
Loading