diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e196c49..cff30e5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,10 +13,7 @@ concurrency:
cancel-in-progress: true
env:
- AISUITE_REVISION: ff130d741ec50ed7dba1890f7981bc62f26f15db
- SNODEC_REVISION: bc43179dbee2b5a0286420a61d8f1ceaef01530d
- CMAKE_BUILD_PARALLEL_LEVEL: 2
- CTEST_PARALLEL_LEVEL: 2
+ CMAKE_BUILD_PARALLEL_LEVEL: 8
LD_LIBRARY_PATH: ${{ github.workspace }}/_stage/aisuite/lib:${{ github.workspace }}/_stage/snodec/lib
LDFLAGS: -Wl,-rpath-link,${{ github.workspace }}/_stage/aisuite/lib -Wl,-rpath-link,${{ github.workspace }}/_stage/snodec/lib
@@ -35,18 +32,18 @@ jobs:
${{ github.event_name == 'pull_request' &&
github.event.pull_request.head.sha || github.sha }}
- - name: Check out accepted SNode.C 2.0.0
+ - name: Check out SNode.C master
uses: actions/checkout@v5
with:
repository: SNodeC/snode.c
- ref: ${{ env.SNODEC_REVISION }}
+ ref: master
path: _deps/snodec
- - name: Check out required AISuite 0.6.0
+ - name: Check out canonical AISuite Codex
uses: actions/checkout@v5
with:
repository: SNodeC/AISuite
- ref: ${{ env.AISUITE_REVISION }}
+ ref: master
path: _deps/aisuite
- name: Install build dependencies
@@ -65,7 +62,7 @@ jobs:
git config --global --add safe.directory "$GITHUB_WORKSPACE/_deps/snodec"
git config --global --add safe.directory "$GITHUB_WORKSPACE/_deps/aisuite"
- - name: Verify exact source revisions
+ - name: Verify CodexUI source revision
shell: bash
env:
CODEXUI_EXPECTED_HEAD: >-
@@ -74,8 +71,6 @@ jobs:
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$CODEXUI_EXPECTED_HEAD"
- test "$(git -C _deps/snodec rev-parse HEAD)" = "$SNODEC_REVISION"
- test "$(git -C _deps/aisuite rev-parse HEAD)" = "$AISUITE_REVISION"
- name: Build and install SNode.C
run: |
@@ -87,38 +82,26 @@ jobs:
cmake --build _build/snodec --target all
cmake --install _build/snodec
- - name: Build and install AISuite 0.6.0
+ - name: Build and install canonical AISuite Codex
run: |
cmake -S _deps/aisuite -B _build/aisuite -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_INSTALL_PREFIX="$GITHUB_WORKSPACE/_stage/aisuite" \
-DCMAKE_PREFIX_PATH="$GITHUB_WORKSPACE/_stage/snodec" \
- -DAISUITE_BUILD_TESTS=OFF \
- -DAISUITE_BUILD_APPS=OFF \
- -DAISUITE_BUILD_CODEX_FRONTEND_CLIENT=ON \
- -DAISUITE_ENABLE_CODEX_FRONTEND_TLS=OFF \
- -DAISUITE_ENABLE_CODEX_FRONTEND_WEBSOCKET=OFF \
- -DAISUITE_ENABLE_CODEX_FRONTEND_RFCOMM=OFF
- cmake --build _build/aisuite --target all
+ -DAISUITE_BUILD_CODEX_TESTS=OFF \
+ -DAISUITE_BUILD_APPS=ON
+ cmake --build _build/aisuite --target all --parallel 2
cmake --install _build/aisuite
- grep -F 'set(PACKAGE_VERSION "0.6.0")' \
- _stage/aisuite/lib/cmake/AISuite/AISuiteConfigVersion.cmake
- name: Configure CodexUI
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
- -DCMAKE_PREFIX_PATH="$GITHUB_WORKSPACE/_stage/aisuite;$GITHUB_WORKSPACE/_stage/snodec" \
- -DBUILD_TESTING=ON
+ -DCMAKE_PREFIX_PATH="$GITHUB_WORKSPACE/_stage/aisuite;$GITHUB_WORKSPACE/_stage/snodec"
- name: Build CodexUI
run: cmake --build build --target all
- - name: Run CodexUI tests headlessly
- env:
- QT_QPA_PLATFORM: offscreen
- run: ctest --test-dir build --output-on-failure
-
- name: Check changed lines
shell: bash
env:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 655970e..a9a08fb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,206 +4,353 @@ cmake_minimum_required(VERSION 3.20)
project(CodexUI LANGUAGES CXX)
-include(CTest)
include(GNUInstallDirs)
+include(CTest)
set(CMAKE_AUTOMOC ON)
-find_package(AISuite 0.6.0 CONFIG REQUIRED)
-find_package(Qt6 REQUIRED COMPONENTS Concurrent Network Widgets)
+find_package(AISuite CONFIG REQUIRED)
+find_package(
+ snodec 2.0 CONFIG REQUIRED
+ COMPONENTS net-un-stream-legacy net-in-stream-legacy net-in6-stream-legacy
+)
+find_package(
+ snodec 2.0 CONFIG QUIET
+ COMPONENTS net-in-stream-tls net-in6-stream-tls
+)
+find_package(
+ snodec 2.0 CONFIG QUIET
+ COMPONENTS net-rc-stream-legacy net-rc-stream-tls
+)
+find_package(Qt6 REQUIRED COMPONENTS Widgets)
+find_package(Threads REQUIRED)
+
+set(
+ CODEXUI_CODEX_COMMON_SOURCES
+ src/codex/ClientRuntime.cpp
+ src/codex/ClientRuntime.h
+ src/codex/Configuration.cpp
+ src/codex/Configuration.h
+ src/codex/ConnectionDialog.cpp
+ src/codex/ConnectionDialog.h
+ src/codex/DiffViewer.cpp
+ src/codex/DiffViewer.h
+ src/codex/FrontendSession.cpp
+ src/codex/FrontendSession.h
+ src/codex/FileSelectionDialog.cpp
+ src/codex/FileSelectionDialog.h
+ src/codex/MainWindow.cpp
+ src/codex/MainWindow.h
+ src/codex/NewThreadDialog.cpp
+ src/codex/NewThreadDialog.h
+ src/codex/PresentationModel.cpp
+ src/codex/PresentationModel.h
+ src/codex/PresentationProtocol.cpp
+ src/codex/PresentationProtocol.h
+ src/codex/ProtocolNormalizer.cpp
+ src/codex/ProtocolNormalizer.h
+ src/codex/ipc/QtSocketPairEndpoint.cpp
+ src/codex/ipc/QtSocketPairEndpoint.h
+ src/codex/ipc/SNodeSocketPairEndpoint.cpp
+ src/codex/ipc/SNodeSocketPairEndpoint.h
+ src/codex/ipc/SocketPair.cpp
+ src/codex/ipc/SocketPair.h
+ src/codex/main.cpp
+ src/codex/ui/ExpandingPromptEditor.cpp
+ src/codex/ui/ExpandingPromptEditor.h
+ src/codex/ui/BrandMark.cpp
+ src/codex/ui/BrandMark.h
+ src/codex/ui/UiStyle.cpp
+ src/codex/ui/UiStyle.h
+)
+
+set(
+ CODEXUI_GREENFIELD_MIDDLE_SOURCES
+ src/greenfield/codex/ShellWidget.cpp
+ src/greenfield/codex/ShellWidget.h
+ src/greenfield/codex/middle/ComposerPane.cpp
+ src/greenfield/codex/middle/ComposerPane.h
+ src/greenfield/codex/middle/ConversationCards.cpp
+ src/greenfield/codex/middle/ConversationCards.h
+ src/greenfield/codex/middle/ConversationProjection.cpp
+ src/greenfield/codex/middle/ConversationProjection.h
+ src/greenfield/codex/middle/ConversationView.cpp
+ src/greenfield/codex/middle/ConversationView.h
+ src/greenfield/codex/middle/InspectorPane.cpp
+ src/greenfield/codex/middle/InspectorPane.h
+ src/greenfield/codex/middle/MiddleRegionWidget.cpp
+ src/greenfield/codex/middle/MiddleRegionWidget.h
+ src/greenfield/codex/middle/MiddleTypes.cpp
+ src/greenfield/codex/middle/MiddleTypes.h
+ src/greenfield/codex/middle/PromptCoordinator.cpp
+ src/greenfield/codex/middle/PromptCoordinator.h
+ src/greenfield/codex/middle/ThreadPane.cpp
+ src/greenfield/codex/middle/ThreadPane.h
+)
qt_add_executable(
codex-ui
- src/app/Application.cpp
- src/app/Application.h
- src/app/AttachmentManager.cpp
- src/app/AttachmentManager.h
- src/app/FrontendSession.cpp
- src/app/FrontendSession.h
- src/app/FrontendSessionWorker.cpp
- src/app/FrontendSessionWorker.h
- src/main.cpp
- src/ui/AnchoredTurnSurface.cpp
- src/ui/AnchoredTurnSurface.h
- src/ui/ConversationWidget.cpp
- src/ui/ConversationWidget.h
- src/ui/ExpandingPromptEditor.cpp
- src/ui/ExpandingPromptEditor.h
- src/ui/InspectorWidget.cpp
- src/ui/InspectorWidget.h
- src/ui/InteractiveRequestDialog.cpp
- src/ui/InteractiveRequestDialog.h
- src/ui/MainWindow.cpp
- src/ui/MainWindow.h
- src/ui/PresentationRefreshAccumulator.cpp
- src/ui/PresentationRefreshAccumulator.h
- src/ui/SidebarWidget.cpp
- src/ui/SidebarWidget.h
- src/ui/ThreadSetupDialog.cpp
- src/ui/ThreadSetupDialog.h
- src/ui/UpcomingTurnDock.cpp
- src/ui/UpcomingTurnDock.h
- src/ui/UiStyle.cpp
- src/ui/UiStyle.h
- src/ui/WorkbenchWidget.cpp
- src/ui/WorkbenchWidget.h
+ ${CODEXUI_CODEX_COMMON_SOURCES}
+ ${CODEXUI_GREENFIELD_MIDDLE_SOURCES}
+ src/codex/PendingRequestDialog.cpp
+ src/codex/PendingRequestDialog.h
+ src/codex/TurnSettingsWidget.cpp
+ src/codex/TurnSettingsWidget.h
)
-target_compile_features(codex-ui PRIVATE cxx_std_20)
-target_include_directories(codex-ui PRIVATE src)
-target_link_libraries(
- codex-ui
- PRIVATE
- AISuite::OpenAICodexFrontendClient
- Qt6::Concurrent
- Qt6::Network
- Qt6::Widgets
+qt_add_executable(
+ codex-ui-harness
+ ${CODEXUI_CODEX_COMMON_SOURCES}
+ src/codex/WorkbenchWidget.cpp
+ src/codex/WorkbenchWidget.h
)
-install(
- TARGETS codex-ui
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
- BUNDLE DESTINATION .
-)
+function(configure_codexui_target target)
+ target_compile_features(${target} PRIVATE cxx_std_20)
+ target_include_directories(${target} PRIVATE src)
+ target_link_libraries(
+ ${target}
+ PRIVATE
+ AISuite::OpenAICodex
+ Qt6::Widgets
+ Threads::Threads
+ snodec::net-un-stream-legacy
+ snodec::net-in-stream-legacy
+ snodec::net-in6-stream-legacy
+ )
+endfunction()
+
+configure_codexui_target(codex-ui)
+target_include_directories(codex-ui BEFORE PRIVATE src/greenfield)
+configure_codexui_target(codex-ui-harness)
+target_compile_definitions(codex-ui-harness PRIVATE CODEXUI_DEVELOPMENT_HARNESS=1)
+
+if(TARGET snodec::net-in-stream-tls AND TARGET snodec::net-in6-stream-tls)
+ foreach(target codex-ui codex-ui-harness)
+ target_compile_definitions(${target} PRIVATE CODEXUI_CODEX_FRONTEND_TLS=1)
+ target_link_libraries(
+ ${target} PRIVATE snodec::net-in-stream-tls snodec::net-in6-stream-tls
+ )
+ endforeach()
+endif()
+
+if(TARGET snodec::net-rc-stream-legacy AND TARGET snodec::net-rc-stream-tls)
+ foreach(target codex-ui codex-ui-harness)
+ target_compile_definitions(${target} PRIVATE CODEXUI_CODEX_FRONTEND_RFCOMM=1)
+ target_link_libraries(
+ ${target} PRIVATE snodec::net-rc-stream-legacy snodec::net-rc-stream-tls
+ )
+ endforeach()
+endif()
+
+if(TARGET snodec::http-client AND TARGET snodec::websocket-client)
+ foreach(target codex-ui codex-ui-harness)
+ target_compile_definitions(${target} PRIVATE CODEXUI_CODEX_FRONTEND_WEBSOCKET=1)
+ target_link_libraries(${target} PRIVATE snodec::http-client snodec::websocket-client)
+ endforeach()
+endif()
if(BUILD_TESTING)
add_executable(
- CodexUIAttachmentManagerTest
- tests/AttachmentManagerTest.cpp
- src/app/AttachmentManager.cpp
- src/app/AttachmentManager.h
+ codexui-socketpair-contract-test
+ tests/codex/SocketPairContractTest.cpp
+ src/codex/ipc/QtSocketPairEndpoint.cpp
+ src/codex/ipc/QtSocketPairEndpoint.h
+ src/codex/ipc/SNodeSocketPairEndpoint.cpp
+ src/codex/ipc/SNodeSocketPairEndpoint.h
+ src/codex/ipc/SocketPair.cpp
+ src/codex/ipc/SocketPair.h
+ )
+ target_compile_features(
+ codexui-socketpair-contract-test PRIVATE cxx_std_20
+ )
+ target_include_directories(
+ codexui-socketpair-contract-test PRIVATE src
)
- target_compile_features(CodexUIAttachmentManagerTest PRIVATE cxx_std_20)
- target_include_directories(CodexUIAttachmentManagerTest PRIVATE src)
- target_link_libraries(CodexUIAttachmentManagerTest PRIVATE Qt6::Network)
- add_test(NAME CodexUIAttachmentManagerTest COMMAND CodexUIAttachmentManagerTest)
-
- add_executable(
- CodexUIPromptLimitTest
- tests/PromptLimitTest.cpp
- src/app/FrontendSession.cpp
- src/app/FrontendSession.h
- src/app/FrontendSessionWorker.cpp
- src/app/FrontendSessionWorker.h
- )
- target_compile_features(CodexUIPromptLimitTest PRIVATE cxx_std_20)
- target_include_directories(CodexUIPromptLimitTest PRIVATE src)
target_link_libraries(
- CodexUIPromptLimitTest
- PRIVATE
- AISuite::OpenAICodexFrontendClient
- Qt6::Network
+ codexui-socketpair-contract-test
+ PRIVATE Qt6::Core Threads::Threads snodec::net-un-stream-legacy
+ )
+ add_test(
+ NAME codexui-socketpair-contract
+ COMMAND codexui-socketpair-contract-test
+ )
+ set_tests_properties(
+ codexui-socketpair-contract PROPERTIES TIMEOUT 10
)
- add_test(NAME CodexUIPromptLimitTest COMMAND CodexUIPromptLimitTest)
add_executable(
- CodexUIFrontendSessionTest
- tests/FrontendSessionTest.cpp
- src/app/FrontendSession.cpp
- src/app/FrontendSession.h
- src/app/FrontendSessionWorker.cpp
- src/app/FrontendSessionWorker.h
- )
- target_compile_features(CodexUIFrontendSessionTest PRIVATE cxx_std_20)
- target_include_directories(CodexUIFrontendSessionTest PRIVATE src)
+ codexui-presentation-pipeline-test
+ tests/codex/PresentationPipelineTest.cpp
+ src/codex/PresentationModel.cpp
+ src/codex/PresentationModel.h
+ src/codex/PresentationProtocol.cpp
+ src/codex/PresentationProtocol.h
+ src/codex/ProtocolNormalizer.cpp
+ src/codex/ProtocolNormalizer.h
+ )
+ target_compile_features(
+ codexui-presentation-pipeline-test PRIVATE cxx_std_20
+ )
+ target_include_directories(
+ codexui-presentation-pipeline-test PRIVATE src
+ )
target_link_libraries(
- CodexUIFrontendSessionTest
- PRIVATE
- AISuite::OpenAICodexFrontendClient
- Qt6::Network
+ codexui-presentation-pipeline-test
+ PRIVATE AISuite::OpenAICodex
+ )
+ add_test(
+ NAME codexui-presentation-pipeline
+ COMMAND codexui-presentation-pipeline-test
+ )
+ set_tests_properties(
+ codexui-presentation-pipeline PROPERTIES TIMEOUT 10
)
- add_test(NAME CodexUIFrontendSessionTest COMMAND CodexUIFrontendSessionTest)
add_executable(
- CodexUIPresentationRefreshAccumulatorTest
- tests/PresentationRefreshAccumulatorTest.cpp
- src/ui/PresentationRefreshAccumulator.cpp
- src/ui/PresentationRefreshAccumulator.h
+ codexui-greenfield-projection-test
+ tests/codex/GreenfieldProjectionTest.cpp
+ src/greenfield/codex/middle/ConversationProjection.cpp
+ src/greenfield/codex/middle/ConversationProjection.h
+ src/greenfield/codex/middle/MiddleTypes.cpp
+ src/greenfield/codex/middle/MiddleTypes.h
+ src/greenfield/codex/middle/PromptCoordinator.cpp
+ src/greenfield/codex/middle/PromptCoordinator.h
+ )
+ target_compile_features(
+ codexui-greenfield-projection-test PRIVATE cxx_std_20
+ )
+ target_include_directories(
+ codexui-greenfield-projection-test BEFORE PRIVATE src/greenfield src
)
- target_compile_features(CodexUIPresentationRefreshAccumulatorTest PRIVATE cxx_std_20)
- target_include_directories(CodexUIPresentationRefreshAccumulatorTest PRIVATE src)
target_link_libraries(
- CodexUIPresentationRefreshAccumulatorTest
- PRIVATE
- AISuite::OpenAICodexFrontendClient
- Qt6::Widgets
+ codexui-greenfield-projection-test PRIVATE Qt6::Widgets
)
add_test(
- NAME CodexUIPresentationRefreshAccumulatorTest
- COMMAND CodexUIPresentationRefreshAccumulatorTest
+ NAME codexui-greenfield-projection
+ COMMAND codexui-greenfield-projection-test
+ )
+ set_tests_properties(
+ codexui-greenfield-projection PROPERTIES TIMEOUT 10
)
- add_executable(
- CodexUIConversationLayoutTest
- tests/ConversationLayoutTest.cpp
- src/app/AttachmentManager.cpp
- src/app/AttachmentManager.h
- src/ui/AnchoredTurnSurface.cpp
- src/ui/AnchoredTurnSurface.h
- src/ui/ConversationWidget.cpp
- src/ui/ConversationWidget.h
- src/ui/ExpandingPromptEditor.cpp
- src/ui/ExpandingPromptEditor.h
- src/ui/InspectorWidget.cpp
- src/ui/InspectorWidget.h
- src/ui/PresentationRefreshAccumulator.cpp
- src/ui/PresentationRefreshAccumulator.h
- src/ui/UpcomingTurnDock.cpp
- src/ui/UpcomingTurnDock.h
- )
- target_compile_features(CodexUIConversationLayoutTest PRIVATE cxx_std_20)
- target_include_directories(CodexUIConversationLayoutTest PRIVATE src)
+ qt_add_executable(
+ codexui-greenfield-middle-test
+ tests/codex/GreenfieldMiddleTest.cpp
+ src/greenfield/codex/middle/ConversationCards.cpp
+ src/greenfield/codex/middle/ConversationCards.h
+ src/greenfield/codex/middle/ConversationView.cpp
+ src/greenfield/codex/middle/ConversationView.h
+ src/greenfield/codex/middle/MiddleTypes.cpp
+ src/greenfield/codex/middle/MiddleTypes.h
+ )
+ target_compile_features(
+ codexui-greenfield-middle-test PRIVATE cxx_std_20
+ )
+ target_include_directories(
+ codexui-greenfield-middle-test BEFORE PRIVATE src/greenfield src
+ )
target_link_libraries(
- CodexUIConversationLayoutTest
- PRIVATE
- AISuite::OpenAICodexFrontendClient
- Qt6::Widgets
+ codexui-greenfield-middle-test PRIVATE Qt6::Widgets
+ )
+ add_test(
+ NAME codexui-greenfield-middle
+ COMMAND codexui-greenfield-middle-test
+ )
+ set_tests_properties(
+ codexui-greenfield-middle
+ PROPERTIES TIMEOUT 15 ENVIRONMENT "QT_QPA_PLATFORM=offscreen"
)
- add_test(NAME CodexUIConversationLayoutTest COMMAND CodexUIConversationLayoutTest)
- set_tests_properties(CodexUIConversationLayoutTest PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
- add_executable(
- CodexUIPhase1ThreadTurnUxTest
- tests/Phase1ThreadTurnUxTest.cpp
- src/app/AttachmentManager.cpp
- src/app/AttachmentManager.h
- src/ui/AnchoredTurnSurface.cpp
- src/ui/AnchoredTurnSurface.h
- src/ui/ExpandingPromptEditor.cpp
- src/ui/ExpandingPromptEditor.h
- src/ui/SidebarWidget.cpp
- src/ui/SidebarWidget.h
- src/ui/ThreadSetupDialog.cpp
- src/ui/ThreadSetupDialog.h
- src/ui/UpcomingTurnDock.cpp
- src/ui/UpcomingTurnDock.h
- src/ui/UiStyle.cpp
- src/ui/UiStyle.h
- )
- target_compile_features(CodexUIPhase1ThreadTurnUxTest PRIVATE cxx_std_20)
- target_include_directories(CodexUIPhase1ThreadTurnUxTest PRIVATE src)
+ qt_add_executable(
+ codexui-greenfield-layout-test
+ tests/codex/GreenfieldLayoutTest.cpp
+ src/codex/DiffViewer.cpp
+ src/codex/DiffViewer.h
+ src/codex/FileSelectionDialog.cpp
+ src/codex/FileSelectionDialog.h
+ src/codex/PresentationModel.cpp
+ src/codex/PresentationModel.h
+ src/codex/PresentationProtocol.cpp
+ src/codex/PresentationProtocol.h
+ src/codex/TurnSettingsWidget.cpp
+ src/codex/TurnSettingsWidget.h
+ src/codex/ui/ExpandingPromptEditor.cpp
+ src/codex/ui/ExpandingPromptEditor.h
+ src/greenfield/codex/middle/ComposerPane.cpp
+ src/greenfield/codex/middle/ComposerPane.h
+ src/greenfield/codex/middle/ConversationCards.cpp
+ src/greenfield/codex/middle/ConversationCards.h
+ src/greenfield/codex/middle/ConversationView.cpp
+ src/greenfield/codex/middle/ConversationView.h
+ src/greenfield/codex/middle/InspectorPane.cpp
+ src/greenfield/codex/middle/InspectorPane.h
+ src/greenfield/codex/middle/MiddleRegionWidget.cpp
+ src/greenfield/codex/middle/MiddleRegionWidget.h
+ src/greenfield/codex/middle/MiddleTypes.cpp
+ src/greenfield/codex/middle/MiddleTypes.h
+ src/greenfield/codex/middle/ThreadPane.cpp
+ src/greenfield/codex/middle/ThreadPane.h
+ )
+ target_compile_features(
+ codexui-greenfield-layout-test PRIVATE cxx_std_20
+ )
+ target_include_directories(
+ codexui-greenfield-layout-test BEFORE PRIVATE src/greenfield src
+ )
target_link_libraries(
- CodexUIPhase1ThreadTurnUxTest
- PRIVATE
- AISuite::OpenAICodexFrontendClient
- Qt6::Widgets
+ codexui-greenfield-layout-test PRIVATE AISuite::OpenAICodex Qt6::Widgets
+ )
+ add_test(
+ NAME codexui-greenfield-layout
+ COMMAND codexui-greenfield-layout-test
+ )
+ set_tests_properties(
+ codexui-greenfield-layout
+ PROPERTIES TIMEOUT 15 ENVIRONMENT "QT_QPA_PLATFORM=offscreen"
)
- add_test(NAME CodexUIPhase1ThreadTurnUxTest COMMAND CodexUIPhase1ThreadTurnUxTest)
- set_tests_properties(CodexUIPhase1ThreadTurnUxTest PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
- add_executable(
- CodexUIInteractiveRequestDialogTest
- tests/InteractiveRequestDialogTest.cpp
- src/ui/InteractiveRequestDialog.cpp
- src/ui/InteractiveRequestDialog.h
+ set(CODEXUI_GREENFIELD_SHELL_TEST_SOURCES ${CODEXUI_CODEX_COMMON_SOURCES})
+ list(
+ REMOVE_ITEM CODEXUI_GREENFIELD_SHELL_TEST_SOURCES
+ src/codex/main.cpp
+ src/codex/MainWindow.cpp
+ src/codex/MainWindow.h
)
- target_compile_features(CodexUIInteractiveRequestDialogTest PRIVATE cxx_std_20)
- target_include_directories(CodexUIInteractiveRequestDialogTest PRIVATE src)
- target_link_libraries(
- CodexUIInteractiveRequestDialogTest
- PRIVATE
- AISuite::OpenAICodexFrontendClient
- Qt6::Widgets
+ qt_add_executable(
+ codexui-greenfield-shell-test
+ tests/codex/GreenfieldShellIntegrationTest.cpp
+ ${CODEXUI_GREENFIELD_SHELL_TEST_SOURCES}
+ ${CODEXUI_GREENFIELD_MIDDLE_SOURCES}
+ src/codex/PendingRequestDialog.cpp
+ src/codex/PendingRequestDialog.h
+ src/codex/TurnSettingsWidget.cpp
+ src/codex/TurnSettingsWidget.h
+ )
+ configure_codexui_target(codexui-greenfield-shell-test)
+ target_include_directories(
+ codexui-greenfield-shell-test BEFORE PRIVATE src/greenfield
+ )
+ add_test(
+ NAME codexui-greenfield-shell
+ COMMAND codexui-greenfield-shell-test
+ )
+ set_tests_properties(
+ codexui-greenfield-shell
+ PROPERTIES TIMEOUT 20 ENVIRONMENT "QT_QPA_PLATFORM=offscreen"
)
- add_test(NAME CodexUIInteractiveRequestDialogTest COMMAND CodexUIInteractiveRequestDialogTest)
- set_tests_properties(CodexUIInteractiveRequestDialogTest PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
+
endif()
+
+install(
+ TARGETS codex-ui codex-ui-harness
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ BUNDLE DESTINATION .
+)
+install(
+ FILES resources/icons/codex-ui.svg
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps
+)
+install(
+ FILES resources/applications/codex-ui.desktop
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
+)
diff --git a/README.md b/README.md
index 70ca7ec..480f8f2 100644
--- a/README.md
+++ b/README.md
@@ -1,59 +1,64 @@
# CodexUI
-CodexUI is a native Qt 6 Widgets graphical Codex client built on AISuite's
-public Codex frontend C++ SDK. The UI shell implements the Figma v2 workbench
-([node 4:2](https://www.figma.com/design/IScmS9lHPduDueN2sVEsiO/Codex-UI-Prototype-v0.1?node-id=4-2)).
+CodexUI is a native Qt 6 Widgets frontend for the AISuite `codex-bridge`.
+It presents the Codex app-server protocol without introducing another backend,
+semantic cache, snapshot store, or persistence authority.
+
+The canonical process has two threads:
```text
-CodexUI
- |
- | AISuite public frontend C++ SDK
- v
-codex-backend
- |
- v
-codex app-server
+Qt GUI thread
+ <-> bounded nonblocking Unix socketpair
+SNode.C client thread
+ <-> codex-bridge
+ <-> Codex app-server
```
-CodexUI connects automatically to the local `codex-backend` Unix frontend
-socket through Qt and delegates authentication, protocol handling, and state
-synchronization to AISuite's public frontend SDK. The sidebar displays real
-synchronized threads, and the center work area renders the selected thread's
-real turns, messages, semantic items, activity, token usage, and failures from
-the current immutable frontend State. The composer can lazily acquire
-controller ownership, create a thread, start or continue a real turn, and
-interrupt the selected active turn. Live output continues to arrive entirely
-through AISuite's immutable State projection.
-
-Sending on a persisted thread automatically resumes it in the running Codex
-App Server before starting the turn; selecting a thread still performs only the
-read-side synchronization needed to render it.
-
-CodexUI also presents real command and file-change approvals and typed
-user-input requests from AISuite's canonical pending-request collection. A real
-attention count opens the compact request dialog, responses acquire controller
-ownership only when submitted, and requests remain visible until a subsequent
-immutable State update removes or changes them.
-
-The Inspector follows the selected thread and renders its latest plan,
-subagent/collaboration activity, reported file changes, and compact factual
-thread/turn/synchronization information from public typed AISuite projections.
-Unavailable or truncated fields remain visibly absent rather than being
-reconstructed from protocol JSON or the local filesystem.
-
-Attachments, advanced thread management, and settings/persistence remain
-outside the current interactive core.
+The Qt thread owns widgets and `PresentationModel`. The SNode.C thread owns the
+event loop, selected transport, `AISuite::OpenAICodex` frontend proxy SDK,
+native protocol normalization, and connection/controller telemetry. They
+exchange only bounded `codexui.presentation` JSONL commands and events.
+
+## Applications
+
+- `codex-ui`: the normal visual application.
+- `codex-ui-harness`: the permanent protocol and reducer development harness.
+
+Both applications use the same transport, socketpair, normalization,
+presentation protocol, and model implementation. Only their top-level Qt
+consumer differs.
## Build
-Qt 6 Widgets, Qt 6 Network, and an installed AISuite package that exports its
-public Codex frontend client are required.
+Qt 6 Widgets, Threads, SNode.C `master`/HEAD, and an installed canonical
+AISuite package exporting `AISuite::OpenAICodex` are required.
```sh
-cmake -S . -B build -G Ninja
-cmake --build build --parallel 28
+cmake -S . -B "${BUILD_DIR}" -G Ninja \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DCMAKE_PREFIX_PATH="/path/to/aisuite;/path/to/snodec"
+cmake --build "${BUILD_DIR}" --parallel 8
+ctest --test-dir "${BUILD_DIR}" --output-on-failure --parallel 8
+cmake --install "${BUILD_DIR}"
```
+Installation includes the `codex-ui` executable, desktop entry, and SVG icon.
+The executable name, application ID, `StartupWMClass`, desktop entry, and icon
+name intentionally match so Linux launchers and taskbars associate the window
+with the installed CodexUI application.
+
+## Architecture
+
+The complete thread model, presentation protocol, authority rules, normalized
+event vocabulary, public APIs, shell behavior, implementation report, and test
+boundaries are documented in
+[`docs/codex-architecture.md`](docs/codex-architecture.md).
+
+Current message routing, pending-prompt acknowledgment, scrolling, composer
+geometry, shell-output, Inspector, and desktop-integration decisions are
+documented in
+[`docs/ui-behavior.md`](docs/ui-behavior.md).
+
## License
CodexUI is available under the LGPL-3.0-or-later OR MIT dual license.
diff --git a/design/t1-item-upsert-investigation.md b/design/t1-item-upsert-investigation.md
deleted file mode 100644
index fed1db2..0000000
--- a/design/t1-item-upsert-investigation.md
+++ /dev/null
@@ -1,256 +0,0 @@
-# T1 item-upsert reconciliation investigation
-
-Status: implementation gate triggered; no `src/` change is justified by the
-current T1 brief.
-
-Reviewed targets:
-
-- CodexUI `3679e38` (merged PR #38)
-- AISuite `61ed370`
-- CodexUI history `7e19d8c`, failed attempt `c680e37`, and revert `f7b931c`
-
-## Standing safeguards
-
-- No `git merge`, `git pull`, `git rebase`, `git cherry-pick`. Linear history only.
-- One concern per PR. If the diff touches a second concern, stop and split.
-- Per-commit deletion census: count of removed lines under `src/`, and a reason for every removal longer than five lines.
-- Call-site audit before implementation: how many call sites exist, how many will change, how many are deliberately left, and why.
-- Tests are runtime proof. A test asserting on source text, policy, or the presence of a symbol does not count.
-- Do not refresh a golden hash or protocol fingerprint without naming, in the commit message, the semantic change that moved it.
-- If a task says "investigate and report before implementing", the first commit contains no `src/` changes.
-
-## Finding in one sentence
-
-`c680e37` failed only when a valid exact content append and an item upsert were
-coalesced: the exact append succeeded and returned before topology
-reconciliation, while the existing reconciliation is too entangled and not
-exhaustively keyed enough to satisfy T1's performance and widget-identity
-invariants by adding only a third scope set.
-
-## 1. What `requiresFullRefresh` concretely changes
-
-`requiresFullRefresh` is not an input to `ConversationWidget::render`. It exists
-only in `WorkbenchWidget::scheduleStateRefresh` at
-`src/ui/WorkbenchWidget.cpp:357-364`.
-
-When it is true, Workbench:
-
-1. sets `selectedPresentationFullRefreshPending`;
-2. clears every accumulated exact-content update and its byte count;
-3. reaches the 16 ms flush with `exactContentOnly == false`; and
-4. calls `ConversationWidget::render` without exact-content hints.
-
-When it is false:
-
-- an affected conversation with no exact identity is promoted back to a full
- refresh at `src/ui/WorkbenchWidget.cpp:389-396`;
-- exact identities are retained and, only when no Inspector, Sidebar, or
- pending turn/resume work also needs the frame, Workbench calls
- `updateExactMessageContent` directly at
- `src/ui/WorkbenchWidget.cpp:453-457`; otherwise it forwards the hints to
- `render`; and
-- if Workbench reaches `render` with valid exact appends, `render` tries the
- same exact updater and returns on success at
- `src/ui/ConversationWidget.cpp:3004-3010`.
-
-The segment topology/key reconciliation at
-`src/ui/ConversationWidget.cpp:3221-3588` therefore does not run after a
-successful exact append. It runs for the no-hint/full route and as a fallback
-after an exact update cannot be applied. There is no separate full-refresh
-branch inside `render`.
-
-The reconciliation does not blindly recreate every widget. It preserves
-compatible turns and segments, skips equal presentation keys, updates supported
-widgets in place, and creates or replaces only entries it considers
-incompatible (`src/ui/ConversationWidget.cpp:3290-3583`). The comment in
-`c680e37` implying that `fullyAffectedThreadIds` itself invalidated all retained
-widgets was therefore factually wrong.
-
-There is a second limitation: the compatibility test is not an exhaustive
-keyed diff. It accepts a removable old prefix followed by an aligned surviving
-sequence. An insertion or regrouping in the middle clears that turn's item
-layout at `src/ui/ConversationWidget.cpp:3411-3427`, deleting unchanged later
-widgets. Reusing the existing block cannot prove the broad invariant that every
-unchanged presentation key retains the same `QWidget*`.
-
-## 2. Why the reverted attempt failed
-
-A lone `ItemUpsertedChange` under `c680e37` still worked. Because it carried no
-exact content identity, Workbench promoted it to the general reconciliation.
-
-The failing sequence was:
-
-1. an `ItemContentAppendedChange` and an `ItemUpsertedChange` reached the same
- scope/mailbox/16 ms presentation window;
-2. `c680e37` kept the thread out of `fullyAffectedThreadIds`;
-3. Workbench retained the append and classified the refresh as exact-content
- only;
-4. `updateExactMessageContent` applied the append and returned `true`; and
-5. `render` returned before deriving the new segment list, so the new item never
- received a widget.
-
-The existing mixed-scope test could not reproduce this. It combines an item
-upsert with `ItemContentReplacedChange`, whose append payload is absent.
-`updateExactMessageContent` rejects that input at
-`src/ui/ConversationWidget.cpp:3623-3624`, so the test always falls through to
-the safe reconciliation path. The valid append fixture is tested separately
-and never combined with an upsert.
-
-## 3. Operations bundled with general reconciliation
-
-Before and alongside the segment reconciliation, the current `render` path
-performs:
-
-- canonical upcoming-turn configuration synchronization;
-- the viewport freeze check;
-- the incomplete-history containment proof;
-- generation, follow-tail, anchor, and thread-switch pin bookkeeping;
-- thread title and detail rewriting;
-- a forward scan over all ordered turns to find the last retained turn;
-- latest-turn summary and failure recomputation;
-- `latestTimelineWindow` recomputation;
-- segment regeneration for the selected window;
-- turn and segment compatibility checks and presentation-key computation; and
-- conditional timeline layout and scroll settling.
-
-Inspector rendering is separate, but `ItemUpsertedChange` marks the relevant
-Inspector thread, so Workbench refreshes it. Workbench's selected-presentation
-refresh also rebuilds breadcrumb/context text and scans the latest turn's items
-for its activity count at `src/ui/WorkbenchWidget.cpp:629-666`.
-
-## 4. Required work for a pure item upsert
-
-A pure item upsert requires:
-
-- the existing freeze ordering;
-- selection of the current bounded window, because a tail addition can evict
- the oldest visible item or turn;
-- segment regeneration for that window, because an activity bucket can retain
- its segment ID while gaining a row;
-- presentation-key comparison, because an item upsert may replace an existing
- same-identity item as well as add one;
-- exhaustive keyed topology reconciliation that preserves every unchanged
- widget;
-- updated rendered item-range and item-identity bookkeeping;
-- geometry/follow-tail settling when the visible structure changes; and
-- retention and application of every coalesced exact-content append;
-- the separately gated Inspector refresh, because item changes can affect its
- plan, activity, and file-change projections; and
-- Workbench's latest-turn agent-activity status update, because a new
- collaboration/subagent item can change that count.
-
-It does not require thread title/detail rewriting, the all-turn current-turn
-scan, turn summary/failure reconstruction, workspace breadcrumb reconstruction,
-thread-switch pinning, or unrelated attachment/settings/controller work.
-
-The bounded window calculation itself is necessary. What T1 must avoid is a
-destructive or monolithic full presentation rebuild, not the bounded calculation
-needed to know which entries currently belong in a capped window.
-
-## 5. Mandatory gate result
-
-A structural boolean could suppress the exact-content early return and enter
-the existing late reconciliation. That would make new widgets appear, but it
-would not satisfy the stated invariant:
-
-- it would execute the monolithic metadata, all-turn, summary, and general
- presentation work identified above;
-- an exact message append would go through canonical message reconstruction
- rather than retaining the O(delta) direct append path; and
-- a middle insertion or regrouping could still destroy later widgets whose
- presentation keys did not change.
-
-There is no callable boundary that performs only a fully keyed timeline
-reconciliation. Reconciliation and the unnecessary full-render work are
-inseparable in the current structure. The brief explicitly says to stop in
-this case because the task becomes a `render()` split. This investigation
-therefore makes no `src/` change.
-
-The proposed thread-ID-only structural set also cannot honor a literal ban on
-window re-derivation. By the time it reaches `ConversationWidget`, it has lost
-the changed turn/item identity and whether the upsert added or replaced an
-item. A richer structural delta or a separately maintained window delta would
-be required to patch the capped window without recalculating it.
-
-## 6. Correction to the proposed scope taxonomy
-
-The expected shape in the brief cannot be applied literally at AISuite
-`61ed370`:
-
-- the public `client::Change` variant has no `TurnRemovedChange` or
- `ItemRemovedChange`; it exposes only `ThreadRemovedChange` for explicit
- removals;
-- `TurnUpsertedOccurrence` carries an internal `replaceItems` bit;
-- legacy `turn.updated` sets `replaceItems = true` at AISuite
- `Occurrence.cpp:2318-2332`;
-- the reducer then replaces the turn's ordered items and deletes omitted
- descendants at `Occurrence.cpp:2705-2724`; and
-- the public client collapses this to `TurnUpsertedChange` without exposing the
- replacement bit at `Client.cpp:449-454`.
-
-CodexUI leaves the SDK's supported legacy-v1 fallback enabled. Consequently,
-`TurnUpsertedChange` must remain fully affected under the current public
-contract. Moving it into an add-only structural set could reintroduce stale
-widgets after a legitimate descendant deletion.
-
-`ItemUpsertedChange` is narrower: its reducer upserts one composite item
-identity and does not interpret other descendants' omission as deletion. A
-future structural scope may therefore classify resolved item upserts, but not
-all turn upserts. Thread-read publications map to `ThreadUpsertedChange` and
-must remain fully affected.
-
-## 7. Call-site audit for the follow-up implementation
-
-- `ConversationWidget::render` has one production caller and 68 direct test
- calls. A new structural entry point or mode would change the one production
- caller and new targeted tests; existing tests should deliberately remain on
- the default general-render contract.
-- `StateUpdateScope` has one canonical per-update producer,
- `stateUpdateScope`, plus broad manually constructed worker scopes for
- lifecycle/discovery boundaries. Only the two resolved parent branches of
- `ItemUpsertedChange` should become structural. Broad scopes deliberately
- remain broad.
-- `mergeScope` is the single GUI-mailbox merge site. A future structural set
- must use the same bounded unique-union, overflow-to-`allThreadsAffected`, and
- clear-on-all discipline as the existing thread sets.
-- `WorkbenchWidget::scheduleStateRefresh` is the single production consumer of
- the distinction. Full/deletion-capable input must dominate structural input;
- structural input must not clear exact-content updates.
-
-## 8. Runtime proof required in the follow-up PR
-
-1. Render an existing live turn, retain every original segment address, apply
- 50 item upserts, and assert that every new item has a widget while every
- unchanged presentation key retains the exact original `QWidget*`.
-2. Apply deletion-capable authority to the same thread and assert the removed
- widget is both absent from lookup and destroyed (`QPointer` becomes null).
-3. Coalesce a valid exact append with an item upsert in both arrival orders.
- Assert the appended text, all new widgets, original widget identities, and
- unchanged `sourceMaterializationCount`/incremental append instrumentation.
-4. Add an insertion/regrouping case, not only tail appends, so the keyed reuse
- invariant is actually proved.
-5. Exercise mailbox merging so structural scope and exact append metadata
- survive in both orders while a later full scope still dominates.
-
-## Baseline runtime result
-
-Before any edit, both relevant runtime suites passed:
-
-```text
-CodexUIFrontendSessionTest ....... Passed
-CodexUIConversationLayoutTest .... Passed
-100% tests passed, 0 tests failed out of 2
-```
-
-Command:
-
-```sh
-cmake --build build \
- --target CodexUIFrontendSessionTest CodexUIConversationLayoutTest
-ctest --test-dir build \
- --output-on-failure \
- -R 'CodexUI(FrontendSession|ConversationLayout)Test'
-```
-
-That green baseline confirms the current suite does not expose the mixed
-append/upsert defect.
diff --git a/design/ux-decisions/thread-turn-model.md b/design/ux-decisions/thread-turn-model.md
index 24e8737..393832c 100644
--- a/design/ux-decisions/thread-turn-model.md
+++ b/design/ux-decisions/thread-turn-model.md
@@ -1,505 +1,144 @@
-# CodexUI UX Decision: Thread and Turn Model
+# CodexUI UX Decision: Threads, Turns, and Prompt Admission
-Status: **Agreed design baseline**
+Status: **Implemented design baseline**
-This document records the current UX decisions for thread creation, persistent execution settings, turns, resume/fork behavior, and thread actions. It is intended as source material for the upcoming Figma reconciliation and redesign work.
+## Domain model
-## 1. Core mental model
-
-CodexUI distinguishes four concepts:
-
-```text
-THREAD
-|
-+-- Creation properties
-+-- Foundational instructions
-+-- Current execution settings
-+-- Turns
- +-- turn-specific input/settings
- +-- historical effective settings
-```
-
-The central UX rule is:
-
-> **Every mutable execution setting has exactly one editable representation: at the next-turn composer.**
-
-There are no separate editable "thread reasoning" and "turn reasoning" controls.
-
-The authoritative values are persistent thread settings, but they are presented beside the next-turn composer because that is the point at which the user needs to understand or change what the next Codex turn will use.
-
-## 2. New Thread
-
-`+ New Thread` opens a fresh conversation immediately. There is no mandatory configuration dialog.
-
-Conceptually:
-
-```text
-NEW THREAD
-
-+----------------------------------------------------+
-| What do you want Codex to work on? |
-| |
-+----------------------------------------------------+
-
-Model | Reasoning | Workspace | Access | Approval
-
-> Advanced thread options
-
- Start
-```
-
-The initial values come from the **Codex/app-server defaults**. CodexUI does not introduce artificial concepts such as "Starting configuration" or "Initial reasoning".
-
-The controls beside the composer show what the **first turn will use**. They may be changed before pressing **Start**.
-
-## 3. Advanced Thread Options
-
-Only genuine thread-creation/context properties belong here:
-
-```text
-Advanced thread options
-
-[ ] Temporary thread
- Do not persist this thread to history
-
-Base instructions
-Default Edit...
-
-Developer instructions
-Default Edit...
-```
-
-Protocol fields are not exposed merely because they exist.
-
-## 4. Foundational instructions
-
-CodexUI distinguishes:
-
-```text
-Base instructions
- -> Fundamental behavior of the Codex agent
-
-Developer instructions
- -> Project/workflow/architecture/testing rules
-
-User prompt
- -> Concrete task
-```
-
-For normal use, Base and Developer Instructions inherit the Codex/app-server defaults.
-
-They may be explicitly changed during:
-
-- New Thread
-- Fork...
-- Resume with options...
-
-They are not ordinary next-turn execution settings.
-
-For an already active thread they are informational/read-only unless the user explicitly chooses an operation that permits changing them.
-
-## 5. Next-turn configuration
-
-Every composer, including the first one, shows the effective execution configuration that the next turn will use.
-
-Conceptually:
-
-```text
-+----------------------------------------------------+
-| Ask Codex... |
-| |
-+----------------------------------------------------+
-
-Model | Reasoning | Workspace | Access | Approval
-
- Send
-```
-
-Persistent mutable execution settings include, where supported and useful in the UI:
-
-- Model
-- Reasoning effort
-- Workspace / cwd
-- Sandbox / workspace access
-- Approval policy / reviewer
-- Service tier
-- Reasoning summary
-- Personality
-- Collaboration mode
-
-The final Figma design will decide which settings deserve first-level visibility and which belong under a compact secondary control such as `More...`.
-
-## 6. Meaning of changing a next-turn setting
-
-Suppose the current inherited reasoning value is `High` and the user selects `XHigh` before the next turn.
-
-The meaning is:
-
-```text
-Current inherited value
- High
- |
- v
-user selects XHigh
- |
- v
-Next turn uses XHigh
- +
-XHigh becomes the inherited value
-for subsequent turns
-```
-
-The UI should make this persistence rule discoverable, for example through a tooltip:
-
-> Changes apply to this and subsequent turns.
-
-The control is positioned at the next-turn boundary because that is when the value matters to the user, even though the resulting value is stored persistently with the thread.
-
-## 7. True turn-specific data
-
-These values belong only to one turn and do not become inherited thread configuration:
-
-- Prompt / UserInput
-- Attachments, images, and other turn input
-- Output schema
-- Client message identity
-
-Conceptually:
-
-```text
-NEXT TURN
-|
-+-- inherited execution settings
-| +-- Model
-| +-- Reasoning
-| +-- Workspace
-| +-- Access
-| +-- Approval
-| +-- ...
-|
-+-- turn-local data
- +-- Prompt
- +-- Attachments
- +-- Output requirements
-```
-
-## 8. Historical turns
-
-Historical turns should expose the **effective settings they actually used**, where the Codex app-server and AISuite can provide them.
-
-These values are read-only:
-
-```text
-TURN DETAILS
-
-Model GPT-5.x Codex
-Reasoning XHigh
-Workspace ~/AISuite
-Access Workspace Write
-Approval On Request
-```
-
-This permits truthful history even after the current thread configuration has changed:
-
-```text
-Turn 12 High
-Turn 13 High
-Turn 14 XHigh
-Turn 15 XHigh
-
-Current next-turn setting:
- Medium
-```
-
-A reconnected CodexUI should ideally receive these historical effective values from authoritative app-server/AISuite state rather than infer them from the current thread settings.
-
-Historical effective settings are never editable.
-
-## 9. Thread header
-
-The thread header should identify the conversation and workspace clearly:
-
-```text
-AISuite Performance
-~/Projects/SNodeC/AISuite
-```
-
-It may also contain a quiet read-only summary such as:
+CodexUI presents the current Codex app-server model directly:
```text
-GPT-5.x Codex | XHigh | Workspace Write
+Thread
+├── stable identity and lifetime
+├── foundational instructions
+├── current upcoming-turn configuration
+└── Turns
+ ├── user input and attachments
+ ├── activity and output
+ └── completion or failure
```
-Such a summary must not look like a second set of editable controls. The single editable execution controls remain beside the next-turn composer.
+AISuite and the app-server are authoritative for thread, turn, item, and
+configuration semantics. CodexUI retains only bounded presentation state and
+client-local interaction state.
-## 10. Normal open / resume
+## Thread selection and routing
-Selecting an existing thread should simply work:
+The visible thread selection is user-owned. Background events, reconnects,
+thread refreshes, and activity in another thread do not change it.
-```text
-click thread
- |
- v
-load/resume if necessary
- |
- v
-conversation appears
-```
+Send and Steer resolve the destination from the visibly selected row and its
+stable thread ID. Missing or inconsistent selection is an error; it never
+causes implicit thread creation. New thread creation requires an explicit New
+Thread intent.
-There is no normal resume dialog. Whether CodexUI internally performs `thread/resume` is an implementation detail that should normally remain invisible.
+## New thread
-Existing foundational instructions and thread context are retained.
+New Thread opens a dialog for creation-only properties:
-## 11. Thread context menu
+- workspace;
+- optional name;
+- optional base instructions;
+- optional developer instructions;
+- ephemeral lifetime.
-The context menu is state-dependent.
+The accepted dialog creates a local draft. The app-server thread is created
+when the first prompt is admitted. Prompts entered while creation is in flight
+remain attached to the draft and move to the returned thread ID when creation
+succeeds.
-### Idle thread
-
-```text
-Open
-Rename...
-Fork...
---------------------
-Resume with options...
---------------------
-Archive
-Delete...
-```
+## Upcoming-turn configuration
-### Running thread
+The single editable settings surface is adjacent to the composer. It contains
+the supported model, reasoning, workspace, sandbox/access, network, approval,
+personality, service-tier, reasoning-summary, permission-profile, reviewer, and
+collaboration choices.
-```text
-Open
-Rename...
-Fork...
---------------------
-Interrupt
---------------------
-Resume with options... if applicable
---------------------
-Archive possibly disabled while running
-Delete... possibly disabled while running
-```
+Thread-creation properties are not duplicated in this surface. Untouched
+settings remain omitted from native operations so CodexUI does not replace
+provider state with inferred defaults.
-### Archived thread
+## Prompt admission and acknowledgment
-```text
-Open
-Fork...
---------------------
-Unarchive
-Delete...
-```
-
-Developer-oriented utilities may live under a secondary menu rather than cluttering the primary menu:
-
-```text
-More
-+-- Copy Thread ID
-```
-
-## 12. Open
-
-`Open` is the normal operation.
-
-If necessary, CodexUI automatically resumes the thread using its existing context and settings. No configuration interaction is required.
-
-## 13. Rename
-
-`Rename...` changes the human-readable thread title. It does not affect execution state, instructions, or Codex context.
-
-## 14. Fork
-
-`Fork...` creates a **new thread derived from the selected thread**.
-
-Because it creates a new thread, foundational context becomes editable again:
-
-```text
-FORK THREAD
-
-Base instructions
-[ editable ]
-
-Developer instructions
-[ editable ]
-
-[ ] Temporary thread
-
- Cancel Fork
-```
+Clicking Send or Steer performs local admission immediately:
-The new fork then uses the normal next-turn composer and its execution-setting controls.
+1. create a pending user card in the destination thread;
+2. clear the submitted prompt and attachments from the composer;
+3. leave the composer enabled for more input;
+4. dispatch the operation when it reaches the front of that thread's queue.
-Forking is the preferred way to continue historical context under changed foundational instructions because it creates a clean semantic boundary.
+A pending card is muted blue and has a brighter highlight sweeping left and
+right. Its visual identity is a process-wide local submission ID, so assigning
+an app-server thread ID to a creation draft and switching threads do not replace
+or relocate it.
-## 15. Resume with options
+Only one prompt operation per thread is unacknowledged at a time. Further
+prompts are admitted and displayed immediately but dispatched in order. This
+ensures that a later prompt observes the active-turn state published by the
+preceding acknowledgment. Queues belonging to different threads are
+independent.
-`Resume with options...` is an explicit advanced operation:
+Only the correlated `turn.start` or `turn.steer` completion callback can
+acknowledge a prompt. A successful callback begins a 500-millisecond accepted
+transition. Every submission carries a unique `clientUserMessageId`, allowing
+the authoritative user item to inherit the local card's stable visual key even
+when multiple prompts have identical text. Failure stops the animation and
+leaves an explicit error card.
-```text
-RESUME THREAD WITH OPTIONS
-
-Base instructions
-[ editable ]
-
-Developer instructions
-[ editable ]
-
- Cancel Resume
-```
-
-It permits the same historical thread to continue under modified foundational instructions.
-
-This is considered an **expert operation**. Normal `Open` resumes using the existing context without interruption.
-
-## 16. Interrupt
-
-`Interrupt` is visible when the thread has a running turn.
-
-```text
-Thread A Working...
- right-click
- |
- v
- Interrupt
-```
-
-This is important because multiple Codex threads can be running concurrently. The user should not have to switch to a thread merely to stop its active work.
-
-## 17. Archive / Unarchive
-
-`Archive` is the normal non-destructive way to remove completed threads from the active workspace.
-
-```text
-Active Threads
- |
- +-- Archive --> Archived Threads
-```
-
-Archived threads remain discoverable and expose `Unarchive` in their context menu.
-
-Archive should generally be preferred over Delete.
-
-## 18. Delete
-
-`Delete...` is destructive and visually separated from ordinary actions. It requires explicit confirmation, for example:
-
-> Permanently delete "AISuite Performance"?
-
-If deletion is unsafe while a turn is running, it should be disabled rather than implicitly interrupting and deleting.
-
-## 19. Temporary threads
-
-Temporary/ephemeral behavior is a creation-time property:
-
-```text
-[ ] Temporary thread
- Do not persist this thread to history
-```
-
-It belongs under Advanced Thread Options because it changes the lifetime and persistence semantics of the thread. It is not a next-turn setting.
-
-## 20. Overall lifecycle
-
-```text
- + NEW THREAD
- |
- v
- app-server defaults
- |
- optional Advanced
- +-- Temporary
- +-- Base instructions
- +-- Developer instructions
- |
- v
- FIRST TURN
- +-----------------+
- | Prompt |
- | Model |
- | Reasoning |
- | Workspace |
- | Access |
- | Approval |
- +-----------------+
- |
- Start
- |
- v
- THREAD
- |
- v
- NEXT TURN
- +-----------------+
- | Prompt |
- | current Model |
- | current Reason. |
- | current Access |
- | ... |
- +-----------------+
- |
- optionally change
- |
- Send
- |
- v
- changed values become
- current inherited values
- |
- v
- NEXT TURN...
-```
-
-Thread actions are orthogonal to that turn lifecycle:
-
-```text
-Existing Thread
- |
- +-- Open
- | +-- automatic resume
- |
- +-- Rename
- |
- +-- Fork
- | +-- new thread
- | foundational instructions editable
- |
- +-- Resume with options
- | +-- same thread
- | foundational instructions editable
- |
- +-- Interrupt
- | +-- active turn only
- |
- +-- Archive / Unarchive
- |
- +-- Delete
-```
+Prompt dispatch waits for once-per-connection-generation thread hydration. A
+provider-marked `notLoaded` thread is resumed first. A transient
+thread-not-found submission result triggers one resume-and-retry; a repeated
+failure becomes the card's terminal error. Failed hydration leaves the composer
+draft intact and requires an explicit reload before admission. Dispatch
+rechecks connection and recovery ownership at its queued execution boundary, so
+a disconnect cannot send and an in-flight resume cannot overlap a hydration
+read or another turn operation.
-## 21. Design principles carried into Figma
+## Start, steer, and interrupt
-The Figma redesign should preserve these principles:
+- An idle loaded thread uses `turn.start`.
+- An active thread uses `turn.steer` with the stable active turn ID.
+- A not-loaded thread is resumed before starting its turn.
+- Stop uses `turn.interrupt` for the stable active turn ID.
-1. **One editable control per mutable execution setting.** Do not duplicate Model, Reasoning, Access, etc. at thread and turn level.
-2. **Place mutable execution settings at the next-turn boundary.** This is where users need to understand and modify them.
-3. **Use Codex/app-server defaults for new threads.** Avoid an unnecessary creation-configuration step.
-4. **Keep creation-only and foundational options progressive.** Temporary mode and foundational instructions belong under Advanced Thread Options.
-5. **Historical truth is read-only.** A historical turn may display its effective execution settings but never edits them.
-6. **Normal resume is invisible.** Opening an existing thread should not present protocol mechanics to the user.
-7. **Fork is the preferred semantic boundary for changed foundational instructions.** Resume with Options remains available for expert use.
-8. **Thread actions are state-dependent.** Interrupt, Archive/Unarchive, and destructive operations appear only when meaningful and safe.
-9. **Archive before delete.** Non-destructive lifecycle management should be easier than permanent deletion.
-10. **Protocol capability does not automatically imply UI exposure.** Every control must justify itself as a useful user-facing concept.
+CodexUI does not fabricate turns or infer active identity from row position.
-## 22. Open design questions
+## Conversation hierarchy
-The following are intentionally left for later UX/Figma exploration:
+The app-server's authoritative hierarchy supplies the single semantic grouping
+level in the conversation: one section per turn, with items retained in their
+exact server order. A turn boundary is taken from the stable turn ID and never
+inferred from the presence of a visible user-message card; reviews, automation,
+or another controlling client may initiate work without a prompt typed in this
+window, while Steer adds input to an existing turn.
+
+Operational items remain individual cards inside their turn; there is no
+second Activity batch or arbitrary visible grouping. Pending prompts remain
+thread-local presentation cards until acknowledgment supplies their
+authoritative turn and item position.
-- Exact visual layout of the next-turn execution controls.
-- Which execution settings remain permanently visible versus move under `More...`.
-- Exact thread-header presentation and how much current configuration it summarizes.
-- Historical turn-detail presentation and which effective settings are useful enough to show.
-- Archived-thread discovery/filtering UI.
-- Exact confirmation and disabled-state behavior for destructive operations while work is running.
-- Keyboard shortcuts for Open, New Thread, Interrupt, Archive, and related actions.
+`PresentationModel` is the retained normalized source. A pure projection adds
+local prompt admissions and emits stable keyed turn sections and cards. Initial
+display and all updates use the same reconcile path; retained card widgets are
+mutated in place, and a visually identical projection performs no layout work.
+
+## Thread lifecycle actions
+
+The thread context menu operates on the stable ID under the pointer and offers
+Reload, Rename, Fork, Archive/Unarchive, and Delete. Opening the menu does not
+change the selected thread. Mutating actions require controller authority.
+
+## Conversation and composer layout
+
+The conversation is independently scrollable above a bottom-anchored settings
+and composer surface. The message viewport reserves the surface's canonical
+height. Further composer growth overlays the viewport without resizing it. A
+matching trailing spacer in the conversation supplies enough additional range
+to scroll the final message above the overlay with the normal visual gap.
+
+Composer growth preserves the current reading position even when it was at the
+old bottom. Reaching the extended range's new bottom enables automatic
+following again. Composer contraction removes the spacer; a Qt range clamp at
+the former bottom is accepted. Incoming content follows with a short,
+interruptible animation only while the conversation is at its current bottom.
+While following is paused, the first visible stable card and its pixel offset
+anchor the reading position across appends, card reflow, and reconstruction.
diff --git a/docs/codex-architecture.md b/docs/codex-architecture.md
new file mode 100644
index 0000000..c8eea69
--- /dev/null
+++ b/docs/codex-architecture.md
@@ -0,0 +1,1432 @@
+# CodexUI Architecture
+
+## 1. Purpose
+
+CodexUI is a remote frontend for `codex-bridge`. It uses the AISuite
+`ai::openai::codex` frontend proxy SDK and presents Codex app-server behavior
+without introducing another backend, protocol authority, or retained semantic
+store.
+
+The architecture has three explicit boundaries:
+
+```text
+Qt presentation
+ <-> normalized UI command/event protocol
+SNode.C client runtime + codex frontend proxy SDK
+ <-> slim codex-bridge envelope over a selected SNode.C transport
+codex-bridge
+ <-> native Codex app-server JSON-RPC
+Codex app-server
+```
+
+The Codex app-server remains authoritative for Codex account, configuration,
+model, thread, turn, item, plan, tool, approval, and persistence semantics.
+`codex-bridge` adds multi-client routing and telemetry. CodexUI adds only
+client-local interaction and presentation state.
+
+## 2. Runtime Object Graph
+
+CodexUI has two main operating-system threads. A Codex conversation thread is
+a protocol object and is unrelated to these execution threads.
+
+```text
+ CodexUI process
+
+ Qt GUI thread SNode.C client thread
+ +------------------------+ +---------------------------+
+ | Qt application loop | | SNode.C event loop |
+ | widgets | | selected client transport |
+ | presentation model | | ClientConnection |
+ | normalized UI events | | frontend proxy SDK |
+ | user interaction | | protocol normalizer |
+ +-----------+------------+ +-------------+-------------+
+ | |
+ | bounded full-duplex Unix socketpair |
+ +--------------------------------------------+
+ |
+ v
+ codex-bridge
+ |
+ v
+ Codex app-server
+```
+
+The SNode.C side has the same principal application shape as
+`codex-bridge-client`. The socketpair gateway replaces that application's
+interactive stdin parser and terminal presenter:
+
+```text
+Qt command gateway
+ -> ClientSession-equivalent dispatcher
+ -> ai::openai::codex::frontend::CodexBridge
+ -> frontend::client::ClientConnection
+ -> exactly one enabled SNode.C client transport
+```
+
+All objects have explicit application ownership. Socket contexts,
+subprotocols, and factories borrow the SDK/mediator they need. No singleton is
+required.
+
+## 3. Thread Ownership
+
+### 3.1 Qt GUI Thread
+
+The Qt thread exclusively owns:
+
+- `QApplication`, the Qt event loop, and all GUI objects;
+- selected thread, selected tab, scroll, expansion, draft, and focus state;
+- the `PresentationModel`, which is the sole retained authoritative store for
+ normalized presentation state;
+- rendering and user-action translation;
+- correlation of normalized UI operation results with UI intents.
+
+Only the Qt thread may mutate Qt objects or presentation state. It performs no
+bridge transport, app-server framing, JSON-RPC correlation, or typed app-server
+decoding.
+
+### 3.2 SNode.C Client Thread
+
+The SNode.C thread exclusively owns:
+
+- the SNode.C event loop;
+- the selected frontend transport and its connection lifecycle;
+- `ai::openai::codex::frontend::CodexBridge`;
+- `frontend::client::ClientConnection` and transport adapters;
+- frontend SDK method execution and callbacks;
+- bridge-envelope and native app-server message classification;
+- app-server JSON-RPC request/response/server-request correlation;
+- typed protocol decoding and normalization into bounded UI events;
+- bridge connection, role, and diagnostic telemetry.
+
+The SNode.C thread never accesses widgets or Qt presentation objects.
+
+Conversation discovery remains latency-sensitive. On connection CodexUI asks
+only for the thread list; selecting a thread can therefore issue its
+`thread/read` without waiting behind unrelated catalog traffic. The complete
+account, configuration, model, permission, skill, hook, plugin, app, and MCP
+catalog set is queried lazily when its presentation surface is opened. These
+are fresh app-server requests, not a CodexUI or bridge cache.
+
+The shared provider handshake is owned by `codex-bridge`, not by any frontend.
+Its `initialize` request advertises `experimentalApi: true`, making the complete
+generated experimental feature types and typed list/enablement operations
+available through the frontend proxy SDK. CodexUI does not perform a second
+provider initialization.
+
+## 4. Inter-Thread Socketpair
+
+One unnamed full-duplex Unix socketpair is the only cross-thread transport:
+
+```text
+Qt endpoint: commands ->, events <-
+ AF_UNIX SOCK_STREAM socketpair
+SNode.C endpoint: commands <-, events ->
+```
+
+The implementation uses:
+
+- `AF_UNIX`, `SOCK_STREAM`, `SOCK_NONBLOCK`, and `SOCK_CLOEXEC`;
+- one endpoint registered with Qt through `QSocketNotifier`;
+- one endpoint registered with the SNode.C descriptor event system;
+- bounded JSONL frames in both directions;
+- bounded socket and application write queues;
+- exclusive endpoint ownership and deterministic close behavior.
+
+The socket buffers are both the bounded queues and the readiness mechanism. No
+parallel in-memory queue, condition variable, eventfd, or other wakeup
+descriptor is added.
+
+The local `SocketPair` follows the ownership shape of SNode.C's
+`core::pipe::Pipe`: movable, noncopyable, error-reporting, and responsible for
+closing descriptors it still owns. Its endpoint adapters contain no CodexUI
+presentation policy so the primitive can move into SNode.C later.
+
+Named pipes/FIFOs are not used. They add names, filesystem cleanup, directional
+composition, and discovery semantics that two threads in one process do not
+need. Socketpair overhead is immaterial for the expected control/event volume.
+
+## 5. CodexUI Presentation Protocol v1
+
+### 5.1 Boundary and Reuse
+
+The Codex app-server protocol terminates in the SNode.C thread. Every normal
+socketpair message uses the presentation protocol identified by:
+
+```json
+{"protocol":"codexui.presentation","version":1}
+```
+
+No bridge envelope, JSON-RPC envelope, native app-server method name, Qt object
+name, widget pointer, or widget identifier is part of the normal contract. Qt
+does not parse app-server methods or correlate app-server JSON-RPC IDs.
+
+The protocol is transport-neutral JSON. A stream transport carries one bounded
+JSON object per JSONL line. A browser WebSocket carries the same object in one
+text message. Browser and Qt consumers therefore share the same reducer and
+event semantics without sharing Qt classes or the internal socketpair.
+
+### 5.2 Frame Grammar
+
+Exactly three frame kinds cross the socketpair:
+
+| `kind` | Direction | Purpose |
+| --- | --- | --- |
+| `command` | UI to SNode.C | Asynchronous user or lifecycle intent |
+| `result` | SNode.C to UI | One terminal result for a correlated command |
+| `event` | SNode.C to UI | Unsolicited presentation-state or diagnostic update |
+
+All frames contain `protocol`, `version`, and `kind`. A command contains
+`action` and `data`; commands expecting a result also contain
+`correlationId`. A result contains `action`, `correlationId`, `ok`, and either
+`data` or `error`. An event contains `type` and `data`.
+
+Every SNode.C-to-UI frame contains:
+
+- `sequence`: process-local, monotonically increasing output sequence;
+- `generation`: bridge connection generation;
+- `authority`: `none`, `merge`, `replace`, or `remove`;
+- optional `scope`: stable `threadId`, `turnId`, `itemId`, `requestId`, or
+ `processId` identities represented by the frame.
+
+`correlationId` identifies one asynchronous command/result exchange. It never
+identifies a widget or a presentation entity. Widgets are reached indirectly
+through the reducer using stable IDs in `scope` and domain data.
+
+Sequence zero is reserved for a Qt-local diagnostic that did not cross the
+socketpair. Such a diagnostic has no state authority.
+
+### 5.3 Authority
+
+Authority has one meaning across all domains:
+
+- `none`: telemetry, notice, or diagnostics; no retained-domain authority;
+- `merge`: update only represented fields and preserve omitted fields;
+- `replace`: replace exactly the represented scope and collection completeness;
+- `remove`: remove exactly the stable scope identified by the frame.
+
+An omitted field is unchanged. It is never an implicit deletion. Empty data is
+authoritative only when accompanied by `replace` or `remove` for an explicit
+scope. Unknown event types and diagnostics never mutate retained conversation
+state.
+
+### 5.4 UI-to-SNode.C Commands
+
+The v1 command catalog used by the application is:
+
+| Action | Result | Meaning |
+| --- | --- | --- |
+| `runtime.shutdown` | no | Orderly SNode.C runtime shutdown |
+| `connection.connect` | no | Connect the selected configured frontend transport |
+| `connection.disconnect` | no | Explicitly disconnect the selected frontend transport |
+| `connection.reconnect` | no | Explicit bridge transport reconnect |
+| `connection.configure` | yes | Apply a transient endpoint selection and connect it |
+| `controller.claim` | no | Request controller ownership |
+| `controller.release` | no | Release controller ownership |
+| `threads.list` | yes | Discover threads without deletion authority |
+| `thread.read` | yes | Read one thread with full turns where available |
+| `thread.create` | yes | Start a thread |
+| `thread.resume` | yes | Resume a thread through app-server semantics |
+| `thread.fork` | yes | Fork a thread through app-server semantics |
+| `thread.rename` | yes | Set a thread name |
+| `thread.archive` | yes | Archive a thread |
+| `thread.unarchive` | yes | Unarchive a thread |
+| `thread.delete` | yes | Delete a thread |
+| `models.list` | yes | Read the available model catalog |
+| `turn.start` | yes | Start a turn in an idle thread |
+| `turn.steer` | yes | Steer the identified active turn |
+| `turn.interrupt` | yes | Interrupt the identified active turn |
+| `pending-request.resolve` | no | Send typed result/error for a server request |
+| `diagnostic.raw.send` | no | Explicit development-only native JSON path |
+
+The implemented typed action catalog additionally covers:
+
+- thread goals, metadata, sections, compaction, rollback, shell commands,
+ guardian decisions, item injection, loaded-thread discovery, and unsubscribe;
+- reviews and experimental-feature listing and enablement;
+- account read, login, login cancellation, logout, rate limits, token usage,
+ reset-credit consumption, credit nudges, and workspace messages;
+- configuration read, requirements read, single-value write, and batch write;
+- model-provider capabilities and permission-profile discovery;
+- skills, hooks, marketplaces, plugins, plugin sharing, and apps;
+- MCP status, refresh, OAuth login, resource reads, and tool calls;
+- filesystem reads, writes, metadata, directory operations, copy/remove, and
+ watch management;
+- one-off command execution, stdin writes, resize, and termination;
+- external-agent configuration discovery/import/history, fuzzy file search,
+ feedback upload, and Windows sandbox setup/readiness.
+
+Every action is dispatched through its generated AISuite codex operation type.
+Qt sends semantic presentation action names and typed `data`; native app-server
+method names do not cross the regular socketpair contract. `initialize` and
+`initialized` are deliberately absent because the bridge owns the one shared
+provider handshake.
+
+Commands are asynchronous. No Qt call blocks waiting for SNode.C. Unsupported
+correlated actions receive one `result` with `ok:false` and a structured error.
+
+### 5.5 SNode.C-to-UI Results
+
+Results preserve their originating `action` and `correlationId`. The currently
+reduced result payloads are:
+
+- `threads.list`: `threads`, `nextCursor`, and `backwardsCursor`, with `merge`;
+- `thread.read`: returned `thread`, with `merge` because the current app-server
+ read projection can omit live-only Plan, Agent, command, and Changes detail;
+- `thread.create`, `thread.resume`, and `thread.fork`: returned `thread`, with
+ `merge`;
+- `thread.rename`, `thread.archive`, `thread.unarchive`, and `thread.delete`:
+ terminal operation status; their app-server notifications carry state
+ authority;
+- `models.list`: `models` and `nextCursor`, with `replace`;
+- `turn.start`: returned `turn`, with `merge` scoped to its thread;
+- all other successful actions: typed result data with `none` until a reducer
+ explicitly declares a presentation scope.
+
+A failed result contains a structured `error` and has no state authority.
+
+### 5.6 Event Vocabulary
+
+The core retained-state events are:
+
+- `thread.upsert`, `thread.name.changed`, `thread.status.changed`,
+ `thread.lifecycle`, and `thread.removed`;
+- `turn.upsert`, `turn.diff.changed`, `turn.moderation.changed`, and
+ `plan.replaced`;
+- `conversation.item.upsert`, `conversation.item.append`,
+ `conversation.command.interaction`, `conversation.file-change.output-appended`,
+ `conversation.file-change.patch-replaced`, `conversation.mcp.progress`, and
+ `conversation.reasoning.part-added`;
+- `agents.activity.upsert`;
+- `pending-request.upsert` and `pending-request.removed`.
+
+Connection and operational events are:
+
+- `connection.lifecycle`, `connection.bridge`, `connection.controller`, and
+ `connection.remote-control.changed`;
+- `terminal.command.output-appended`, `terminal.process.output-appended`, and
+ `terminal.process.completed`;
+- `activity.hook.started` and `activity.hook.completed`;
+- `approval.review.started`, `approval.review.completed`, and
+ `approval.strict-review.required`.
+
+Catalog, account, settings, and workspace events are:
+
+- `account.changed`, `account.rate-limits.changed`, and
+ `account.login.completed`;
+- `catalog.skills.invalidated` and `catalog.apps.changed`;
+- `integration.mcp.login-completed`, `integration.mcp.status-changed`, and
+ `integration.mcp.event`;
+- `workspace.project.changed`, `workspace.files.changed`,
+ `workspace.search.changed`, and `workspace.search.completed`;
+- `settings.external-agent-import.progress` and
+ `settings.external-agent-import.completed`;
+- thread goal, queue, project, environment, settings, token-usage, compacted,
+ and reverted events under the `thread.*` namespace;
+- model reroute, verification, and safety-buffering events under `model.*`.
+
+Realtime and platform events are normalized under `realtime.*` and `system.*`.
+Warnings and errors use `notice.added`. Unknown or malformed input uses
+`system.diagnostic`. Every generated app-server notification is either mapped
+to one of these semantic event types or produces a diagnostic-only event; it is
+never forwarded as generic presentation state.
+
+### 5.7 Pending Requests
+
+All app-server server-request families normalize to
+`pending-request.upsert`. Its data contains the native stable request ID, a
+presentation category, and typed request data. Categories are:
+
+- `command-approval`, `file-change-approval`, `user-input`,
+ `mcp-elicitation`, and `permissions-approval`;
+- `dynamic-tool-call`, `authentication-refresh`, and `attestation`.
+
+Resolution uses `pending-request.resolve` in the other direction and
+`pending-request.removed` when authoritative resolution is observed. Secret
+request content is not copied into diagnostics.
+
+### 5.8 Raw JSON and Compatibility
+
+The codex SDK preserves complete native app-server JSON and unknown fields in
+its generated C++ values on the SNode.C side. The regular socketpair boundary
+carries bounded normalized presentation data, including only the native fields
+needed to render and answer a pending request. The request object is retained
+transiently until that request is resolved and is never rendered as a raw dump.
+Arbitrary raw JSON crosses the boundary only through the explicit bounded
+`diagnostic.raw.send` development action. Raw data is not normal UI state,
+deletion authority, or an escape from typed normalization.
+
+Consumers reject an unsupported protocol name or major version. They ignore
+unknown semantic event types without deleting state. New optional fields,
+actions, and event types are backward-compatible within version 1 when old
+consumers can safely ignore them. Any change to frame meaning, authority, or
+identity requires a new major version.
+
+## 6. Presentation Authority and Reduction
+
+Qt owns `PresentationModel`, the sole retained authoritative store for
+normalized presentation state. Widgets and projections read from it; they do
+not retain competing copies of thread, turn, item, plan, agent, request, or
+global-domain state. The app-server remains the semantic and persistence
+authority, so the model is not a persistence layer or substitute for
+app-server history.
+
+Presentation reduction follows these rules:
+
+1. Stable `threadId`, `turnId`, `itemId`, agent-thread ID, and request ID define
+ identity; row position never defines identity.
+2. Incremental events merge only fields they represent.
+3. Deltas append to the identified field of the identified item.
+4. A richer completed item is not degraded by a later partial item view.
+5. Authoritative replacement is honored only when the normalized event marks
+ the represented scope and completeness explicitly.
+6. Explicit removals remove exactly their identified scope.
+7. Unknown, malformed, stale-generation, or diagnostic-only events do not
+ mutate retained presentation content.
+8. Thread/turn completion does not itself remove completed activity.
+
+This prevents an incomplete publication from acquiring accidental deletion
+authority while preserving the app-server's explicit authority.
+
+## 7. Thread Selection and Interaction
+
+The selected Codex thread is user-owned UI state.
+
+- A thread created or updated by another frontend does not change selection.
+- Incoming activity in a parallel thread does not change selection.
+- Controller changes, reconnects, list refreshes, and read completions do not
+ change selection merely because another thread is newer.
+- User selection changes the selected thread.
+- A user-initiated local new-thread action may select its returned thread as
+ part of that same explicit intent.
+- An explicitly removed selected thread may clear selection.
+
+There is no automatic switch to the newest, active, or newly created thread.
+
+For an idle selected thread, submitting a prompt starts a new turn. For an
+active selected turn, a steering action uses the app-server steering operation
+rather than fabricating another local turn. Interrupt targets the stable active
+turn ID.
+
+Switching threads or inspector tabs while turns, plans, commands, agents, or
+requests are changing must not stop, reset, or reorder those lifecycles.
+
+Selecting a thread hydrates it once per bridge connection, including when the
+thread-list projection already reports materialized or active turns. The
+`thread.read` result is merge-authoritative: it fills reconstruction data but
+does not erase retained live-only Plan, Agent, or Changes details that the
+provider omits. This explicit hydration state prevents a partial discovery
+projection from being mistaken for an operation-ready thread. Reload is the
+explicit forced fresh-read operation.
+
+### 7.1 Upcoming-Turn Settings
+
+The real shell has a codex-native upcoming-turn settings surface backed by the
+normalized `PresentationModel`. Its primary controls are:
+
+- model and model-constrained reasoning effort;
+- sandbox access and the sandbox-native network choice;
+- workspace;
+- approval policy;
+- personality/style.
+
+The compact More menu contains the named permission profile, approval
+reviewer, service tier, reasoning summary, and collaboration mode. Model,
+effort, service-tier, and permission-profile choices are populated from fresh
+app-server catalogs. A named permission profile and a sandbox policy are
+mutually exclusive, matching the native app-server contract.
+
+The settings object is a transient draft bound to the stable selected thread
+identity. User changes are serialized into native `thread/start` and
+`turn/start` fields; untouched fields remain omitted so UI defaults cannot
+replace provider state. Collaboration mode is the deliberate exception:
+app-server may retain Plan mode without returning it from a later
+`thread/read`, so every `turn/start` explicitly sends the Code or Plan mode
+currently displayed by CodexUI. The new-thread workspace always has an
+explicit local fallback. Settings are disabled while steering because
+`turn/steer` does not accept upcoming-turn configuration. No setting is
+persisted by CodexUI or treated as canonical before the app-server publishes
+it.
+
+### 7.2 Thread Creation and Per-Thread Actions
+
+New thread creation starts with a canonical custom dialog. It captures the
+workspace, optional thread name, optional base and developer instructions, and
+the native ephemeral flag. The dialog creates only a transient draft. CodexUI
+does not create an empty provider thread until the user submits the first
+prompt, so canceling or switching away cannot leave a phantom app-server
+thread. Model, reasoning, access, permission, style, service-tier, reviewer,
+and collaboration choices remain in the shared upcoming-turn controls rather
+than being duplicated in the dialog.
+
+Workspace selection uses the shared custom file browser in directory-only
+mode. It validates that the selected directory exists and returns an absolute
+local path. The accepted workspace is encoded as the native `thread/start`
+`cwd`; CodexUI does not persist it as an application preference.
+
+The visual shell's thread sidebar has no global More menu. A right-click
+context menu is created for the stable thread ID under the pointer and exposes
+Reload, Rename, Fork, Archive/Unarchive, and Delete. Read-only Reload remains
+available to an observer; mutations require the connected controller role.
+Opening or invoking the menu does not select the row or disturb the thread
+currently being reviewed. The permanent development harness may retain compact
+diagnostic controls that are not part of the product UI/UX.
+
+### 7.3 Message Attachments
+
+The composer opens the same custom file browser in multi-file mode. It supports
+up to sixteen unique files and reports detected MIME type and size. Local
+admission moves the prompt and attachments into a per-thread pending card and
+immediately clears the composer so another prompt can be entered. Images become
+native `localImage` input, audio becomes `localAudio`, and other files become
+native `mention` input.
+
+These are app-server local-path references, not bytes uploaded through
+`codex-bridge`. The provider must be able to access the selected path. This is
+correct for a local CodexUI/app-server workspace and remains explicit for a
+remote bridge topology; adding remote file transfer would require a separate
+bounded protocol and security design.
+
+### 7.4 Changes and Diff Presentation
+
+The Changes inspector uses a dedicated diff viewer. It prefers the latest
+authoritative `turn.diff.changed` domain for the selected thread. When that
+live domain is absent after reconstruction, it may display diffs explicitly
+retained in app-server `fileChange` items and labels that source as a fallback.
+It never invents a patch by comparing local files.
+
+Unified diffs are separated by file, counted for additions and deletions, and
+rendered read-only with fixed-width text and addition, deletion, header, and
+hunk highlighting. A user can select a file, copy its patch, or open an
+expanded viewer. The file list and patch view retain canonical CodexUI sizing,
+colors, controls, and scrollbars.
+
+### 7.5 Conversation Projection and Prompt Admission
+
+The selected conversation is a pure projection of `PresentationModel` plus
+client-local prompt admissions. Its one structural grouping level is the
+app-server turn: each retained turn contributes one transparent section, and
+its items remain in exact server order. A turn is identified only by its stable
+turn ID; CodexUI does not infer a turn boundary from a user-message card.
+
+Authoritative cards use the stable `(threadId, turnId, itemId)` identity.
+Locally admitted cards use a process-wide submission identity that remains
+stable when a new-thread draft receives its app-server thread ID. Initial
+render and later updates use the same keyed reconcile path. Existing widgets
+are updated in place, absent keys are removed, new keys are inserted at their
+projected positions, and an identical typed projection is a true visual no-op.
+
+Prompt admission and app-server acknowledgment are separate states. On Send or
+Steer, CodexUI immediately appends a client-local pending user card to the
+destination thread. The card uses a muted blue user-prompt treatment and a
+Qt-painted highlight sweeping left and right until the correlated app-server
+result callback arrives. Only the matching `turn.start` or `turn.steer`
+completion callback acknowledges the prompt; conversation events cannot infer
+acknowledgment. Each request carries a unique `clientUserMessageId`, allowing
+the resulting user item to bind exactly even when prompts have identical text.
+A fast successful result retains a 500-millisecond accepted transition so the
+state change remains visible. Pending cards survive thread switching and
+become normal authoritative user messages when the corresponding app-server
+item materializes. The pending and authoritative forms share one visual key
+and anchor during replacement. Once materialization and the accepted transition
+are complete, the prompt coordinator releases dispatch-only payload while
+retaining that lightweight identity alias. Failure produces a retained error
+card.
+
+The composer remains enabled while acknowledgments are outstanding. Multiple
+prompts may be admitted, but CodexUI dispatches them sequentially per thread so
+each operation observes the turn state established by the preceding result.
+Queues for different threads are independent. New-thread prompts remain bound
+to the explicit creation draft until `thread.create` returns its stable ID.
+Dispatch waits for explicit connection-generation thread hydration. A
+provider-marked `notLoaded` thread is resumed first, and a transient
+thread-not-found submission result permits exactly one resume-and-retry before
+becoming a terminal error. Failed hydration rejects local admission without
+clearing the composer draft; an explicit reload is required before sending.
+Transport eligibility is rechecked at the queued dispatch boundary: a
+disconnect leaves the prompt queued until bridge-open re-drives dispatch, and
+an in-flight resume gates both hydration reads and turn operations.
+
+The conversation smoothly follows new content only while its vertical scrollbar
+is at the bottom. Geometry bursts retarget a short monotonic animation to the
+latest maximum. Manual upward scrolling interrupts that animation immediately
+and pauses following until the user returns to the bottom. Programmatic Qt
+range clamps from card reflow do not change this user-owned state. While paused,
+the first visible stable card and its viewport offset anchor the reading position
+across appends, card reflow, and reconstruction. Wheel and touchpad events over
+non-scrollable center-pane chrome and splitter handles are forwarded to the
+conversation. Nested scrollable controls consume an event only while they can
+move in that direction and return edge events to the conversation.
+Follow/pause mode and the stable anchor are stored per thread and restored when
+the user returns to that thread.
+
+The update pipeline compares each card's typed visible projection.
+Protocol-only changes cannot mutate widgets or scroll state. All visible item
+changes in one reconcile are measured and applied inside one paint-suppressed
+layout transaction, followed by one scroll settlement. This is especially
+important for Command execution cards, whose streaming output and bounded
+nested viewer alter geometry. New authoritative items are inserted at their
+server-ordered layout position without rebuilding retained cards. While
+following is paused, the effective history window expands with appends so its
+stable visual anchor cannot be evicted; the requested bound is restored when
+following resumes.
+
+The bottom composer overlay has a canonical in-layout reservation. As multiline
+input, attachments, settings, or attention controls grow beyond that height,
+the conversation viewport keeps its geometry and the composer overlays its
+lower portion. A content-owned trailing spacer grows by the same extra height,
+extending the natural `QScrollArea` range so the final card can be scrolled
+above the overlay with the normal gap. The scrollbar maximum is never assigned
+manually.
+
+Spacer growth temporarily suppresses range-driven bottom following and restores
+the previous scrollbar value, so existing messages do not move. Reaching the
+new maximum re-enables following. Composer contraction removes the spacer; Qt
+may clamp the value to the reduced range, and being at that maximum re-enables
+following for later content.
+
+### 7.6 Command Execution Output and Info Viewers
+
+Command execution output controls exist only for printable, non-whitespace
+output after terminal control sequences are ignored. Empty, whitespace-only, and
+ANSI/control-only results create no black output surface. A shown control grows
+from zero content height to a 220-pixel maximum. Its width-dependent content
+height is measured synchronously inside the conversation update transaction.
+Streaming output and command completion mutate the retained outer card in
+place; a protocol update with an unchanged visible fingerprint touches neither
+the widget nor scroll state. Beyond the maximum the output control uses the
+shared styled vertical scrollbar. It follows appended output only while already
+at its bottom; manual upward scrolling pauses following, and the state is
+retained across in-place output updates.
+
+The Info tab's State and Protocol viewers use the same scrollbar styling and
+show vertical scrollbars only when required. The Protocol log owns the tab's
+expanding region and its statistics summary is placed below the log. Inspector
+content is read from retained per-thread presentation snapshots; selecting an
+already materialized thread does not temporarily clear Plan, Agents, Changes,
+or Requests while unrelated frames are processed.
+
+## 8. Plans and Agents
+
+### 8.1 Plans
+
+`turn/plan/updated` is the canonical structured plan update. A normalized plan
+replacement carries the thread ID, turn ID, optional explanation, and ordered
+steps with `pending`, `inProgress`, or `completed` status.
+
+Plan presentation is retained across tab and thread switching. It changes only
+for the identified turn and is cleared only by an explicit authoritative empty
+or replacement event for that turn. A completed textual plan item may be shown
+as conversation activity. When no structured plan survives a fresh
+`thread/read`, the Plan inspector renders the newest retained textual plan item
+as a read-only compatibility view; it never overrides a newer authoritative
+structured turn plan.
+
+### 8.2 Agents
+
+Agent presentation is derived from typed collaboration data, especially
+`collabAgentToolCall` and `subAgentActivity` items. It retains, when supplied:
+
+- tool operation and stable item ID;
+- sender thread ID;
+- receiver/agent thread IDs;
+- prompt;
+- requested model and reasoning effort;
+- current tool-call status;
+- last known per-agent state and path/activity details.
+
+Completion must not collapse this information into only a generic
+"Subagent activity completed" row. Completed and failed agent activity remains
+inspectable as part of its owning turn. Later partial events may update status
+without erasing richer agent identity or prompt data.
+
+Only spawn operations create agent rows. Provisional spawn starts without a
+child identity are not independently presented, and `wait`, `sendInput`, and
+other collaboration operations update an already identified child rather than
+being counted as additional agents. Once supplied, the child thread ID is the
+stable presentation identity across spawn completion, child activity, wait,
+and result events.
+
+App-server may publish a parent `subAgentActivity(kind=started)` and later
+complete the child thread without replacing the parent item with a completed
+variant. CodexUI correlates those authoritative records by `agentThreadId` and
+projects child turn status and retained child result into the original parent
+activity. This is transient presentation correlation, not backend state.
+Identified subagent implementation threads remain addressable for correlation
+but are omitted from the ordinary top-level thread list. When one is already
+user-selected, the sidebar retains that visible row across subsequent
+navigation for the session. An authoritative thread removal still drops it.
+
+The Agents view follows the currently selected thread; it never selects an
+agent thread or parent thread automatically.
+
+## 9. Pending Requests and Attention State
+
+App-server-initiated requests are normalized into explicit pending-request
+events using the native stable JSON-RPC request ID and associated thread ID.
+Supported request families include approvals, user input, MCP elicitation,
+permission approval, dynamic tool calls, and other generated server-request
+types.
+
+The Requests view presents each pending request independently. Command and
+file-change approvals use native decision enums, user-input answers preserve
+question IDs and support options/free text/secret input, MCP form responses
+return structured JSON, and permission approvals preserve the requested
+permission object and selected turn/session scope. Dynamic tools unavailable
+in CodexUI return a typed failed-tool response. Authentication, attestation,
+and unknown capabilities receive an explicit JSON-RPC error rather than
+remaining pending indefinitely. Canceling the dialog itself does not resolve
+the request.
+
+The UI attention/brown state is derived only from currently unresolved pending
+requests associated with that thread. It is not inferred from historical item
+status or retained across process restart without fresh provider evidence.
+
+A pending request is retired exactly once when:
+
+- its typed response/error is accepted and the corresponding resolution is
+ observed; or
+- `serverRequest/resolved` identifies that same request; or
+- the owning connection/generation terminates and the request can no longer be
+ answered by this frontend.
+
+Resolution matching uses stable request identity plus available thread and
+connection generation context. The presentation request record retains that
+generation. A mismatch is diagnostic and must not retire an
+unrelated request. Resolved request content is removed from actionable UI while
+non-secret lifecycle diagnostics may remain observable.
+
+## 10. Controller and Observer Roles
+
+The bridge permits one controller and multiple observers.
+
+- The controller may mutate Codex state, steer turns, and answer server
+ requests.
+- Observers receive fanout events and may use bridge-approved read operations.
+- Mutating observer operations fail visibly rather than appearing accepted.
+- Controller claim and release are explicit.
+- No frontend silently steals control.
+- A disconnected controller is not replaced by automatic promotion.
+- Thread selection is independent of controller ownership.
+
+CodexUI displays connection identity and role. Controls requiring authority are
+disabled or produce a precise role error while CodexUI is an observer. A local
+policy may request initial control explicitly, but role assignment remains a
+bridge decision reported through telemetry.
+
+Connection controls sit immediately to the left of Claim/Release control
+because transport lifecycle and controller ownership are distinct operations.
+The menu exposes Configure, Connect, Disconnect, and Reconnect. It never claims
+control as a side effect.
+
+## 11. Recovery, History, and No-Cache Policy
+
+CodexUI does not request or reconstruct an AISuite-owned snapshot because
+codex has no snapshot authority, replay store, frontend `State`, or backend
+semantic cache.
+
+Connection and process recovery uses fresh app-server queries through the
+bridge:
+
+1. establish the frontend transport and observe bridge readiness/role;
+2. issue `thread/list` for discovery;
+3. issue `thread/read(includeTurns=true)` for the selected materialized thread;
+4. continue applying normalized live events.
+
+A refresh result applies its declared authority. In particular, `thread.read`
+merges represented content and has no deletion authority because the current
+provider projection is incomplete. Explicit scoped remove events remain
+authoritative. Temporary disconnect, incomplete discovery, request failure, or
+an unknown message does not authorize clearing the existing presentation.
+
+Current app-server behavior may return `itemsView: "notLoaded"`, reject
+`includeTurns` for an unmaterialized thread, or reconstruct less live detail
+than was previously emitted under its active history mode. CodexUI reports
+that provider limitation; it does not invent missing items or add an implicit
+long-term history cache. Adding caching later requires a separate explicit
+architecture decision covering authority, bounds, persistence, and eviction.
+
+## 12. External Transport and Configuration
+
+The socketpair is internal only. The SNode.C thread connects to `codex-bridge`
+through exactly one configured frontend transport supported by codex and
+SNode.C:
+
+- Unix stream;
+- IPv4 or IPv6 stream;
+- IPv4 or IPv6 TLS stream;
+- IPv4 or IPv6 WebSocket;
+- IPv4 or IPv6 WSS;
+- RFCOMM or RFCOMM TLS where available.
+
+Transport and encryption do not change normalized UI semantics. WebSocket
+changes framing; TLS changes transport protection. Neither creates state,
+authority, or authentication semantics.
+
+There is no bearer-token or other codex-bridge authentication layer. Native
+Codex account/login operations remain app-server protocol features and are
+handled through typed SDK operations when exposed by the UI.
+
+Command-line configuration uses the SNode.C configuration subsystem. Any
+CodexUI-specific configuration class is a `utils::SubCommand`. Existing SNode.C
+instance options remain authoritative for addresses, Unix paths, IPv4/IPv6,
+TLS certificates, WebSocket setup, reconnect behavior, timeouts, and queue
+limits; CodexUI must not duplicate those semantics.
+
+The connection dialog reads the effective SNode.C client configurations to
+enumerate compiled transports and provide current endpoint defaults. A user may
+override the selected Unix path, IP host/port, WebSocket path, or RFCOMM
+address/channel for the running CodexUI session. TLS certificate and
+verification configuration remains in the corresponding SNode.C config
+object. Runtime overrides are intentionally transient and are not written to a
+CodexUI data file.
+
+Changing transport uses one asynchronous lifecycle: disconnect the attached
+frontend SDK, terminate the selected SNode.C flow, wait for both to detach,
+apply the new selection, then connect once. Repeated logical connect requests
+cannot create parallel flows or reuse an attached SDK. Local disconnect,
+reconnect, and transport-switch reasons remain distinguishable from remote
+closure in normalized diagnostics.
+
+Quiet Codex sessions are normal, so transport inactivity read/write timeouts
+default to zero (unlimited). Frame bounds, write-queue bounds, connect errors,
+and explicit lifecycle controls remain enforced.
+
+## 13. Startup and Shutdown
+
+The process lifecycle is:
+
+```text
+QApplication construction and Qt argument handling
+ -> core::SNodeC::init(argc, argv)
+ -> construct socketpair and both ownership graphs
+ -> start the SNode.C client thread
+ -> core::SNodeC::start() inside that thread
+ -> run the Qt event loop on the main thread
+ -> request inner transport shutdown
+ -> core::SNodeC::stop()
+ -> close socketpair endpoints and join the client thread
+```
+
+There is no `core::SNodeC::free()` call. Shutdown is asynchronous and
+idempotent. Qt does not destroy objects still used by the client thread, and
+the process does not exit while the SNode.C thread is still running.
+
+EOF or terminal failure on either socketpair endpoint initiates orderly
+shutdown. External bridge disconnect does not terminate CodexUI; it produces a
+normalized disconnected state and follows configured reconnect policy.
+
+## 14. Boundedness and Failure Semantics
+
+Every boundary is bounded:
+
+- bridge transport frame size;
+- socketpair frame size;
+- socketpair and transport write queues;
+- bytes processed per readiness callback;
+- retained diagnostics;
+- UI presentation work scheduled per event-loop pass.
+
+No operation may block the Qt event loop or wait synchronously across threads.
+Backpressure, oversized frames, malformed JSON, queue rejection, transport
+closure, and callback failure produce classified diagnostics. They are not
+silently converted into generic disconnects or state deletion.
+
+Outstanding normalized UI operations complete once with success or a concrete
+failure. A disconnect clears ephemeral request correlation and role telemetry,
+not Codex presentation content. Reconnect starts a new connection generation so
+late results from an old generation cannot resolve new operations or pending
+requests.
+
+## 15. Protocol Compatibility
+
+AISuite codex generates concrete C++ datatypes for the complete exported Codex
+app-server protocol, including client requests, client notifications, server
+requests, server notifications, responses, errors, nested objects, enums, and
+unions. Every generated value preserves its native JSON through `getRaw()` and
+preserves unknown fields.
+
+CodexUI uses those generated types and typed callbacks in the SNode.C thread.
+Every generated server notification and request is classified into a v1
+presentation event or a diagnostic-only event. No known message silently falls
+through as generic state. Unknown future messages remain observable through
+bounded diagnostics and cannot mutate presentation state. The app-server
+source/schema checkout is read-only and is never modified by CodexUI.
+
+The app-server wire is JSON-RPC-shaped but may omit the optional
+`"jsonrpc": "2.0"` member. The frontend SDK owns that compatibility; Qt never
+depends on the member's presence.
+
+## 16. Permanent Development Harness
+
+The codex workbench is retained as a deliberately plain development harness
+alongside the final visual UI/UX shell. Both consumers use the same
+`codexui.presentation` contract and `PresentationModel`; the harness does not
+receive native app-server JSON through a privileged path.
+
+The harness provides:
+
+- explicit thread selection with no automatic selection changes;
+- conversation, plan, correlated agent-activity, and generation-aware
+ pending-request inspection;
+- read-only State inspection for model, account, configuration, permission,
+ feature, skills, hooks, plugin, app, MCP, and other retained domains;
+- controller/observer role and connection-generation visibility;
+- a bounded protocol log containing timestamp, sequence, generation, frame
+ kind, action/event type, authority, stable scope IDs, correlation ID, and
+ result status;
+- explicit sequence-gap and non-monotonic-sequence diagnostics;
+- bounded model counters for discovered threads and selected-thread turns and
+ items.
+
+The protocol log retains at most 2,000 display records. It is telemetry only:
+it cannot replay frames, hydrate the presentation model, supply deletion
+authority, or conceal a missing app-server result. `thread/list` discovery
+preserves the order supplied by app-server for listed IDs, while IDs omitted
+from a non-authoritative discovery page remain retained after that page.
+
+The presentation model separately retains at most 256 authority-free telemetry
+records for status and warning presentation. These records cannot mutate or
+hydrate conversation state. Authoritative normalized domains not requiring a
+specialized reducer remain accessible at their stable global, thread, turn, or
+item scope; their `merge`, `replace`, and `remove` semantics are applied before
+the visual shell consumes them.
+
+The harness is used first when extending normalization or reduction. Once a
+path is proven there, the visual shell consumes the same model and command
+surface through narrow Qt adapters. The shell has no semantic snapshot or
+parallel state authority.
+
+## 17. Implemented Components and APIs
+
+The canonical CodexUI implementation contains both the complete visual shell
+and the permanent functional presentation harness. `ExpandingPromptEditor` and
+the visual style helpers live under
+`src/codex/ui` because they contain no protocol authority.
+
+The implementation is divided into the following concrete components:
+
+| Component | Responsibility |
+| --- | --- |
+| `Configuration` | CodexUI `utils::SubCommand`; adds only CodexUI-specific frame-size and WebSocket-path options |
+| `SocketPair` | Movable RAII owner for the unnamed nonblocking `AF_UNIX` socketpair |
+| `QtSocketPairEndpoint` | Qt-thread descriptor adapter using `QSocketNotifier`, bounded reads, and bounded writes |
+| `SNodeSocketPairEndpoint` | SNode.C-thread descriptor adapter using `ReadEventReceiver` and `WriteEventReceiver`, bounded reads, and bounded writes |
+| `FrontendSession` | Qt-side asynchronous command facade, correlation registry, lifecycle owner, and socketpair JSONL endpoint |
+| `ClientRuntime` | SNode.C-thread application graph, selected transport, frontend proxy SDK dispatch, reconnect, and shutdown |
+| `ProtocolNormalizer` | Native app-server/bridge input to `codexui.presentation` result/event conversion |
+| `PresentationProtocol` | Frame construction, validation, authority, sequence, generation, and scope utilities |
+| `PresentationModel` | Qt-owned stable-ID reducer for threads, turns, items, plans, agents, requests, global domains, and telemetry |
+| `WorkbenchWidget` | Permanent development harness and user-intent adapter |
+| `ShellWidget` | Product shell and protocol/application coordinator; owns stable selection, hydration, recovery, and command dispatch |
+| `MiddleRegionWidget` | Three-pane visual composition and center-region wheel routing |
+| `ThreadPane` | Stable-ID thread-list projection and thread actions |
+| `ConversationProjection` | Pure thread-to-turn-to-card projection over `PresentationModel` and local prompts |
+| `ConversationView` | Stable-key reconciliation, card geometry, per-thread follow/pause state, and anchor-preserving scrolling |
+| `ConversationCard` implementations | In-place typed card presentation, including pending prompts and bounded Command execution output |
+| `PromptCoordinator` | Per-thread prompt admission queues, callback-only acknowledgment, and authoritative-item correlation |
+| `ComposerPane` | Bottom-anchored upcoming-turn controls, attachments, prompt editor, and overlay-height reporting |
+| `InspectorPane` | Retained Plan, Agents, Changes, Requests, State, and Protocol presentation |
+| `TurnSettingsWidget` | Codex-native transient settings draft and native thread/turn option encoder |
+| `NewThreadDialog` | Transient native thread-start draft with workspace selection and instructions |
+| `FileSelectionDialog` | Canonical directory or bounded multi-file browser shared by workspace and attachments |
+| `ConnectionDialog` | Session-only selector over effective compiled SNode.C client configurations |
+| `DiffViewer` | Authoritative live or retained-provider unified-diff presentation |
+| `PendingRequestDialog` | Typed, generation-preserving UI for app-server server-request families |
+| `MainWindow` | Top-level Qt window ownership only |
+| `BrandMark` and desktop resources | Shared visual mark and the consistent `codex-ui` executable/application/window/icon identity |
+
+### 17.1 FrontendSession API
+
+`FrontendSession` is the normal Qt-side entry point. It provides asynchronous
+methods for thread discovery/read/create/resume/fork/rename/archive/delete,
+model and environment discovery, turn start/steer/interrupt, controller
+claim/release, transport connect/disconnect/reconnect/configure, raw diagnostic
+send, and typed server-request
+resolution. Every correlated method returns a presentation correlation ID and
+optionally invokes a Qt-thread response callback. It never blocks the GUI
+thread or exposes a transport socket.
+
+The generic operation method:
+
+```cpp
+request(std::string operation,
+ nlohmann::json parameters,
+ ResponseHandler handler = {})
+```
+
+supports the complete generated AISuite operation catalog without adding one
+Qt facade method per rarely used operation. Frequently used UI actions have
+narrow named methods such as `listThreads()`, `readThread()`, `startTurn()`,
+`steerTurn()`, `configureConnection()`, and `respondToServerRequest()`.
+
+Lifecycle is explicit: `start()` creates the endpoint/runtime graph,
+`shutdown()` requests orderly asynchronous termination, and `wait()` joins the
+SNode.C thread. `setEventHandler()` receives normalized frames and
+`setRuntimeStoppedHandler()` reports terminal worker shutdown.
+
+### 17.2 Normalizer and reducer APIs
+
+`ProtocolNormalizer` accepts transport lifecycle, bridge telemetry, typed
+server notifications, server requests, raw inbound observation, operation
+success, and operation rejection. Its only output is a validated bounded
+presentation frame through its sink. `knownServerMethod()` makes coverage gaps
+observable rather than silently treating an unknown method as state.
+
+`PresentationModel::applyEvent()` is the single public reduction entry point.
+The model exposes stable thread ordering and lookup, active-turn lookup,
+generation-aware pending-request queries, retained global domains, bounded
+telemetry, and pending-request presentation records. Internal upsert helpers
+preserve complete fields across partial events, correlate child-agent threads,
+and apply explicit merge/replace/remove authority.
+
+### 17.3 Transport availability
+
+The executable always builds Unix, IPv4, and IPv6 JSONL clients. TLS, RFCOMM,
+WebSocket, and WSS clients are compiled when their SNode.C targets are
+available. Exactly one configured client instance may be enabled. Address,
+certificate, timeout, queue, reconnect, and instance-enable options come from
+the corresponding SNode.C client configuration; CodexUI adds no duplicate
+transport configuration.
+
+The build produces two independently launchable applications:
+
+- `codex-ui` is the normal visual UI/UX shell;
+- `codex-ui-harness` is the permanent plain protocol/reducer harness.
+
+They compile the same `FrontendSession`, `ClientRuntime`, socketpair,
+normalizer, protocol, and presentation-model sources. Only the top-level Qt
+consumer differs. Neither executable has a privileged transport or state path.
+
+The current build links the codex AISuite frontend library as
+`AISuite::OpenAICodex`, Qt Widgets, Threads, and the selected SNode.C client
+modules. CodexUI CI consumes AISuite from `master`/HEAD and does not pin a
+particular AISuite revision. The canonical AISuite change must therefore be
+merged before the dependent CodexUI change. The AISuite dependency build is
+limited to two compiler jobs because its generated protocol translation units
+can otherwise exceed the hosted runner's aggregate memory.
+
+### 17.4 Shell settings and pending-request APIs
+
+`TurnSettingsWidget` owns only an upcoming-turn draft. The shell supplies fresh
+provider context and catalogs through:
+
+```cpp
+setContext(std::string identity,
+ const nlohmann::json &canonical,
+ const nlohmann::json &models,
+ const nlohmann::json &permissionProfiles);
+setControlsEnabled(bool enabled);
+```
+
+`workspace()` resolves the visible workspace against the caller's local
+fallback. `threadStartOptions()` emits only native `thread/start` fields, while
+`turnStartOptions()` emits only native `turn/start` fields. Untouched fields are
+omitted except that collaboration mode is always explicit because app-server
+does not reliably reconstruct its retained value. Explicitly selecting a
+provider default emits `null`; named permissions and sandbox policy remain
+mutually exclusive. The three app-server
+`thread/start` sandbox strings are encoded directly. The richer
+`externalSandbox` object is emitted only as a `turn/start.sandboxPolicy`, where
+the native protocol defines it. Reasoning efforts, service tiers, default tier,
+and personality availability follow the selected model catalog.
+
+The native collaboration object is not a partial mask: its nested `model` is
+mandatory, while `reasoning_effort` and `developer_instructions` use the
+app-server schema's snake-case names. When the UI shows `Codex default`, the
+encoder resolves the catalog entry marked `isDefault` and sends its concrete
+model ID. Until that fresh catalog is available, CodexUI omits the otherwise
+explicit collaboration object rather than constructing an invalid one.
+
+`PendingRequestDialog::present()` accepts one generation-preserving
+`PendingRequestPresentation` and returns either no value when the user closes
+the dialog or a `PendingRequestResponse` containing exactly one native result
+or JSON-RPC error. `negativeResponse()` constructs the family-specific explicit
+decline used by the Requests surface. The caller resolves through
+`FrontendSession::respondToServerRequest()` with the stable connection
+generation and request ID; the dialog never mutates presentation state itself.
+
+`ShellWidget` is the sole visual command adapter. It translates selection,
+composer, settings, controller, thread-management, and request-review actions
+into `FrontendSession` calls. Agent messages, plan text, reasoning summaries,
+and agent results pass through `QTextDocument::setMarkdown()` with
+`MarkdownNoHTML`; user prompts, commands, and command output remain literal.
+Its custom dialogs return transient value objects and never mutate the
+presentation model directly. The composer owns attachment drafts; the
+connection dialog edits only the SNode.C runtime selection; and `DiffViewer`
+is a read-only consumer of normalized model domains and retained provider
+items.
+
+### 17.5 Essential Automated Architecture Tests
+
+The permanent automated-test policy protects architectural boundaries rather
+than individual fixes, widget details, or lines of implementation. A defect
+correction does not automatically justify another test. A test belongs in the
+codex suite only when it validates a boundary whose failure would undermine
+the application architecture independently of the particular symptom that
+revealed it.
+
+Six focused CTest executables form the essential suite. They use production
+classes directly and are built when standard CMake `BUILD_TESTING` is enabled.
+CTest enables that option by default; disabling it remains the conventional
+packaging choice and does not select a different runtime implementation.
+
+#### Socketpair Contract
+
+`codexui-socketpair-contract-test` exercises the actual two-thread IPC
+mechanism:
+
+```text
+QCoreApplication / Qt event loop
+ -> QtSocketPairEndpoint
+ -> nonblocking AF_UNIX SOCK_STREAM socketpair
+ -> SNodeSocketPairEndpoint
+ -> SNode.C event loop on its worker thread
+```
+
+The test constructs the production `SocketPair`, gives one descriptor to the
+production Qt endpoint and the other to the production SNode.C endpoint, and
+runs both framework event loops. Multiple newline-delimited records travel in
+both directions as separately queued writes. The test establishes that byte
+ordering is preserved across partial/coalesced stream delivery, both endpoint
+queue bounds reject an oversized write without replacing the bound with an
+unbounded buffer, and closing the Qt endpoint produces orderly closure on the
+SNode.C side without a transport error. It also requires the SNode.C event loop
+to terminate cleanly. The test does not introduce another IPC implementation,
+polling loop, mock event loop, or synchronous cross-thread method call.
+
+This test deliberately treats the socketpair as an ordered byte stream. JSONL
+framing and semantic interpretation remain above this boundary; duplicating
+the AISuite `JsonLineFramer` tests here would test another project rather than
+CodexUI's thread boundary.
+
+#### Presentation Pipeline
+
+`codexui-presentation-pipeline-test` exercises the production semantic
+path without a bridge substitute:
+
+```text
+representative native app-server and bridge records
+ -> ProtocolNormalizer
+ -> codexui.presentation v1 frames
+ -> PresentationModel::applyEvent()
+ -> coherent Qt-owned presentation state
+```
+
+The representative lifecycle includes connection and controller publication,
+effective transport-settings publication, thread discovery, an authoritative
+full thread read, a later live turn, command start, command output, command
+completion, authoritative turn-diff publication, and turn completion. The
+test verifies the contract at architectural granularity: every emitted frame
+has the expected protocol version, monotonic sequence, and connection
+generation; connection settings reduce coherently; list/read/live updates
+converge on stable thread, turn, and item identities; and the completed model
+contains one coherent command result and scoped diff with no active turn left
+behind. It does not enumerate every generated app-server method, every
+presentation field, or every historical correction.
+
+The normalizer sink is connected directly to the reducer because the
+socketpair itself is independently covered by the first test. This keeps a
+failure attributable to either inter-thread transport or semantic reduction
+instead of repeating both mechanisms in every case.
+
+#### Conversation Projection
+
+`codexui-greenfield-projection-test` verifies the pure typed projection and
+prompt coordinator: one section per app-server turn, stable card identity and
+server ordering, per-thread prompt queues, dispatch-time Start/Steer choice,
+callback-only acknowledgment, exact `clientUserMessageId` correlation,
+duplicate-prompt ordering, history bounds, resolved-payload compaction, and
+Command execution output visibility.
+
+#### Middle-Region Behavior
+
+`codexui-greenfield-middle-test` exercises the actual conversation widgets
+programmatically. It verifies smooth follow, user-owned pause, stable
+card-and-pixel anchoring across every card type and width-dependent reflow,
+per-thread restoration, composer trailing space, pending-prompt animation,
+and independent Command execution output sizing and scroll ownership.
+
+`codexui-greenfield-layout-test` verifies the three-pane constraints, composer
+overlay geometry, complete center-region wheel routing, thread-list selection
+projection, nested-scroll handoff, and retained Inspector/Info behavior. These
+are state and geometry assertions over Qt widgets, not golden-screenshot or
+pixel-perfect visual baselines. The pending-animation check compares two
+transient card rasters only to prove that motion exists.
+
+#### Shell Integration
+
+`codexui-greenfield-shell-test` drives the production `ShellWidget` and
+`FrontendSession` across their real socketpair presentation boundary. It
+verifies exact visible-thread routing, independent prompt queues, real result
+acknowledgment, background completion, retained Plan/Agents state, monotonic
+hydration across reconnect, terminal callbacks, failed-hydration draft
+retention, bounded child-thread reads, and one-shot thread-not-found recovery.
+
+#### Explicit Exclusions
+
+The permanent automated suite does not include:
+
+- a fake or scripted codex-bridge;
+- a fake app-server or synthetic network server;
+- external GUI-driving automation, golden screenshots, or pixel-perfect
+ styling baselines;
+- one test per fixed issue, setting, request family, widget, or source branch;
+- the Unix/IPv4/IPv6/TLS/WebSocket/RFCOMM transport matrix already owned by
+ AISuite and SNode.C;
+- authenticated model execution, approval interaction, or assumptions about
+ nondeterministic model output.
+
+A real app-server-to-bridge-to-CodexUI turn remains a manual live acceptance
+procedure. It depends on external authentication, service availability,
+credits, approval policy, and model behavior, so presenting it as a
+deterministic CI test would be misleading. The persistent live topology and
+independent bridge observer provide that evidence without introducing a fake
+bridge into the CodexUI repository.
+
+The six focused tests can be built and run directly:
+
+```sh
+cmake --build "${BUILD_DIR}" --parallel 8 \
+ --target codexui-socketpair-contract-test \
+ codexui-presentation-pipeline-test \
+ codexui-greenfield-projection-test \
+ codexui-greenfield-middle-test \
+ codexui-greenfield-layout-test \
+ codexui-greenfield-shell-test
+ctest --test-dir "${BUILD_DIR}" --output-on-failure \
+ -R '^codexui-(socketpair-contract|presentation-pipeline|greenfield-(projection|middle|layout|shell))$'
+```
+
+Each test has a 10-to-20-second CTest ceiling. Normal successful execution is
+substantially shorter and requires no network listener, credentials, isolated
+Codex home, or user interaction.
+
+## 18. Live Harness Validation
+
+The harness was exercised against one persistent real topology:
+
+```text
+Codex app-server over IPv4 WebSocket
+ <-> codex-bridge over IPv4 WebSocket
+ <-> CodexUI harness over IPv4 WebSocket
+```
+
+An independent `codex-bridge-client` observer remained connected to the same
+bridge while CodexUI held controller ownership. The run used an authenticated
+isolated Codex home and an existing persistent bridge process rather than a
+simulated provider.
+
+Validated behavior includes:
+
+- initial connection, explicit controller claim/release, and observer fanout;
+- fresh thread discovery followed by selected `thread/read(includeTurns=true)`;
+- no automatic thread selection when another client or subagent creates a
+ thread;
+- multiple turns, steering, structured plan updates, command execution,
+ command output/completion, and final answers;
+- pending-request presentation and resolution without stale brown attention;
+- parent/child agent correlation, child history hydration, and retained child
+ result presentation in the parent Agents view;
+- switching among Conversation, Plan, Agents, Requests, State, and Protocol
+ surfaces while turns and agents were active;
+- retention of an early completed marker command while later commands, plan
+ transitions, subagent activity, and final output arrived;
+- stable presentation after turn completion, with no observed disconnect,
+ sequence gap, stale pending request, or retained-item disappearance.
+
+The final validation turn lasted about 36 seconds and included a completed
+marker command, a three-step completed plan, one subagent thread, later Command
+execution activity, and a final answer. At the final checkpoint the normalized
+model held one top-level selected thread, three turns, seventeen items, zero pending
+requests, and the retained marker and later activity simultaneously.
+
+A new CodexUI process was then validated against the same persistent bridge.
+Selecting the completed parent thread retained all top-level rows and
+hydrated the Conversation. The Plan inspector reconstructed the retained
+textual plan with Markdown formatting; Changes displayed the explicit
+read-only empty state; Requests remained at zero; and opening Info lazily
+populated the environment State without clearing or blocking Conversation.
+Controller and connection status remained stable throughout these tab
+transitions. The fresh Agents view correctly remained empty because the
+authoritative `thread/read` omitted all prior collaboration items, as documented
+below.
+
+A final focused live turn requested exactly one subagent. Raw observer events
+contained one completed `spawnAgent` item with child thread ID, one `wait`
+operation, the child command/result, and the parent final answer. During the
+turn the shell reported `1 agent | 1 active`; after completion it reported
+`1 agent | 0 active` and retained one completed agent card with model, effort,
+prompt, child thread ID, and result. No provisional spawn or wait row appeared.
+The settings controls also displayed explicit chevrons. This run exposed one
+additional compatibility defect: Code was displayed after a fresh read while
+`turn/start` omitted collaboration mode and app-server silently continued its
+retained Plan mode. The encoder now sends the displayed collaboration mode on
+every new turn once the mandatory model has been resolved from the fresh
+catalog.
+
+The post-fix live acceptance used frontend connection `frontend-27` and a fresh
+thread. Its raw `turn/start` contained `mode: "default"`, catalog-resolved model
+`gpt-5.6-sol`, and native `reasoning_effort: null` and
+`developer_instructions: null` fields. App-server accepted the request,
+published matching Default collaboration settings, completed the turn without
+tools, and returned the requested `CODE_MODE_OK` response.
+
+Startup latency was traced to eager account/configuration/plugin/app catalogs
+queued before the selected thread read. Startup now requests thread discovery
+plus the small model and permission-profile catalogs required by the composer.
+The larger environment catalog is fetched lazily when Info is first opened,
+allowing the selected conversation to hydrate promptly without introducing a
+cache.
+
+This live run proves the implemented paths exercised by the scenario; it is
+not a claim that every generated operation or every optional transport has
+received equivalent live coverage. The canonical build and `git diff --check`
+completed successfully. Automated coverage is intentionally limited to the
+socketpair contract and presentation pipeline described in Section 17.5; the
+real authenticated topology remains the manual live acceptance boundary.
+
+## 19. Provider Limitations Observed Live
+
+### 19.1 Reconstruction Shortcomings
+
+Three app-server reconstruction shortcomings were observed live:
+
+1. Live parent events include `collabAgentToolCall` and child-agent activity,
+ but a later `thread/read(includeTurns=true)` returned both parent turns while
+ omitting every collaboration and subagent item. A fresh no-cache CodexUI
+ therefore cannot reconstruct historical Agents content. During a continuous
+ connection CodexUI correlates the authoritative live records by child thread
+ ID and keeps implementation threads out of the ordinary top-level list.
+2. `turn/plan/updated` notifications produced and updated the Plan view
+ correctly during the live session, but a later
+ `thread/read(includeTurns=true)` did not return those completed plan updates
+ or an equivalent current-plan field. During the current session, the merge
+ authority of `thread.read` preserves the live structured plan. On a fresh
+ process, a completed textual plan item is used as the Plan inspector's
+ read-only fallback when available.
+3. Under the configured app-server history representation, a later
+ `thread/read` can reconstruct generic
+ item IDs and omit a live command-execution item even though the live event
+ stream contained the richer item.
+
+CodexUI preserves already observed live presentation when an incomplete read
+omits it, but it does not synthesize content that the process has never
+observed. A fresh process therefore remains limited to the provider's
+reconstruction. This merge policy is bounded in-memory presentation retention,
+not a semantic cache or persistence authority.
+
+### 19.2 Capability Limitations
+
+The current app-server does not support `historyMode: "paginated"` and returns
+`paginated_threads is not supported yet`. A newly started thread is also not
+materialized for `thread/read(includeTurns=true)` until it receives its first
+user message.
+
+These are provider-boundary discrepancies. CodexUI reports and renders the
+authoritative result it receives; it does not hide them with a bridge snapshot,
+AISuite cache, or CodexUI persistence layer. A future caching design requires a
+separate explicit authority and retention decision.
+
+## 20. Visual Shell Integration Boundary
+
+The CodexUI shell is implemented in codex-owned Qt widgets. Those widgets
+consume only `PresentationModel` and call only `FrontendSession`. The permanent
+harness remains available as the protocol/reducer diagnostic surface and is not
+itself the product shell.
+
+The implemented shell contains the 64-pixel top bar, hideable work sidebar,
+thread list, conversation timeline and composer, hideable inspector, Plan,
+Agents, Changes, Requests, and Info surfaces, explicit controller control,
+connection lifecycle/configuration, canonical new-thread/workspace/attachment
+dialogs, per-thread context actions, complete upcoming-turn settings, a
+first-class diff viewer, pending-prompt cards, request status, and the 40-pixel
+status bar. Agent
+messages, plans, reasoning summaries, and agent results are
+rendered with Qt Markdown parsing while embedded HTML is disabled. User text,
+commands, and command output remain literal. State and Protocol diagnostics
+remain nested under Info rather than dominating normal use.
+
+Pending-request presentation exposes category, stable request ID, connection
+generation, owning thread, and a bounded set of safe typed details. The native
+request object remains transiently available to the typed response dialog but
+is never dumped to the shell, harness State view, notice banner, or protocol
+log. Secret answers are held only by password editors until the dialog is
+destroyed.
+
+Operation errors, provider notices, protocol diagnostics, and connection
+failures produce a dismissible latest-notice banner. Its text is extracted
+only from bounded message/detail fields. The complete bounded frame chronology
+remains in Info/Protocol. Neither surface has state authority.
+
+Further shell work remains presentation-only. It must not change the socketpair
+protocol, app-server normalization, model authority, bridge role semantics,
+recovery policy, or AISuite codex implementation unless a proven missing
+contract requires a separately reviewed change.
+
+## 21. Architectural Invariants
+
+1. The app-server is Codex semantic and persistence authority.
+2. `codex-bridge` is a thin multi-client router with telemetry, not a cache.
+3. The codex frontend SDK is a typed proxy, not a frontend state store.
+4. SNode.C owns transport, SDK execution, protocol decoding, and normalization.
+5. Qt owns widgets, interaction, selection, and transient presentation state.
+6. Only normalized commands/events form the regular inter-thread contract.
+7. Cross-thread work is asynchronous and bounded.
+8. Partial omission is not deletion authority.
+9. Stable protocol IDs, never row order, define identity.
+10. Controller transfer and thread selection are explicit; neither auto-switches.
+11. Plans, completed commands, and completed agent activity remain visible until
+ an authoritative scoped update says otherwise.
+12. Pending attention exists only while a matching server request is unresolved.
+13. Recovery queries app-server; no snapshot, replay store, or semantic cache is
+ introduced.
+14. Generic Qt and SNode.C socket classes remain free of Codex-specific methods.
+15. Native app-server and bridge transport failures remain distinguishable.
+16. Prompt routing always uses the stable visibly selected thread; thread
+ creation requires an explicit new-thread intent.
+17. Pending prompts are presentation state until acknowledged and are
+ dispatched sequentially per thread without disabling the composer.
+18. Conversation and nested-output following is enabled exactly while the
+ corresponding scrollbar is at its bottom. Conversation following is smooth
+ and user-interruptible; its paused state preserves a stable visual anchor.
+19. Composer growth overlays the unchanged message viewport and adds equal
+ trailing content space without automatically moving existing messages.
+20. Thread selection hydrates once per bridge connection; prompt dispatch waits
+ for readiness and permits at most one resume-and-retry after a transient
+ thread-not-found result.
+21. Nonvisual item updates do not reconstruct cards; one coalesced refresh uses
+ one hidden layout transaction and one scroll settlement.
+22. Conversation hierarchy has exactly one semantic grouping level: stable
+ app-server turns containing stable server-ordered items.
+23. `PresentationModel` is the only retained normalized presentation store;
+ conversation and inspector views are keyed projections, not parallel state
+ authorities.
+
+## 22. Resolved Presentation Decisions
+
+The remaining presentation-level choices are implemented as follows:
+
+- every incoming frame is reduced immediately; the selected conversation then
+ takes one typed projection snapshot and one stable-key reconcile, with
+ identical visible projections producing no widget or geometry work;
+- the Info/Protocol view retains at most 2,000 text blocks and the presentation
+ model retains at most 256 authority-free telemetry records; the protocol
+ statistics summary is below the expanding log;
+- pending prompt acknowledgment uses a per-thread animated card rather than an
+ application-wide busy state or composer lock;
+- reaching the conversation bottom re-enables automatic following, including
+ after scrolling through composer-added trailing space or a contraction clamp;
+- paused conversation updates preserve the first visible stable card and its
+ pixel offset through appends, reflow, and reconstruction;
+- typed operation errors and provider notices use a dismissible latest-notice
+ banner, while unknown/malformed protocol input remains visible in bounded
+ diagnostics and never mutates retained presentation state.
+
+No architectural decision remains open in the canonical CodexUI implementation.
+Interactive visual validation covered settings, Markdown, plans, pending
+requests, and live agent lifecycle. Provider-omitted history remains visible as
+an explicit reconstruction boundary rather than being hidden by client state.
+The current CodexUI acceptance boundary requires focused build/tests and live
+visual acceptance of thread routing, prompt acknowledgment, scrolling,
+composer geometry, attachments, connection, and diff surfaces. No semantic
+cache is part of this boundary.
diff --git a/docs/ui-behavior.md b/docs/ui-behavior.md
new file mode 100644
index 0000000..4651a8a
--- /dev/null
+++ b/docs/ui-behavior.md
@@ -0,0 +1,191 @@
+# CodexUI Interaction and Presentation Decisions
+
+This document defines the current CodexUI interaction contract. AISuite and the
+Codex app-server own protocol and domain semantics; CodexUI owns only local
+presentation, input, selection, and scroll state.
+
+## Conversation source and structure
+
+`PresentationModel` is the sole retained authoritative store for normalized UI
+state. The message view is a projection of its selected thread plus
+client-local prompt admissions; cards and inspectors do not maintain a second
+domain store.
+
+The conversation has one semantic grouping level: an app-server turn contains
+its items in server order. Authoritative cards are keyed by stable thread,
+turn, and item IDs; local prompt cards are keyed by their submission IDs. The
+same keyed reconcile path handles initial display and updates, mutating a card
+in place when its visible data changes. An identical visible projection does
+not rebuild widgets or change geometry.
+
+Local prompt admission resumes bottom following when the only pause was caused
+by composer overlay growth, so the complete pending prompt becomes visible.
+It never overrides a pause created by user scrolling.
+
+Mouse-wheel and touchpad gestures use Qt's native platform/device scroll
+handling. CodexUI only records whether the resulting position follows the
+bottom or is owned by the user.
+
+## Thread identity and prompt routing
+
+- The selected thread is identified by its stable app-server thread ID.
+- Once selected, a hydrated thread remains visible in the sidebar for the
+ session even when it is outside the ordinary top-level thread ordering; an
+ authoritative removal still removes it, and other rows retain app-server
+ ordering.
+- Sending always targets the visibly selected thread. CodexUI validates the
+ visible selection before dispatch and never creates a thread as an implicit
+ fallback for missing or inconsistent selection state.
+- A new thread is created only from an explicit New Thread intent. Its dialog
+ captures the workspace, optional name, instructions, and ephemeral state.
+- Background thread activity, list refreshes, reconnects, and creation by
+ another frontend never change the user's selected thread.
+- Selecting a thread hydrates it once per bridge connection even when the
+ discovery result already contains an active turn. The full read is merged
+ into the retained per-thread presentation, so live Plan, Agents, and Changes
+ state cannot be erased by an incomplete reconstruction. Reload remains the
+ explicit forced fresh-read action.
+
+## Prompt submission and acknowledgment
+
+Submitting a prompt creates a client-local pending prompt card at the bottom of
+the destination thread immediately. The card uses a muted version of the normal
+blue user-card treatment, with a brighter blue highlight sweeping left and
+right across it until the app-server acknowledges the operation.
+
+Each pending prompt has a process-wide client-local submission ID and remains
+associated with its destination thread. It therefore remains visible when the
+user switches threads and returns. On
+successful acknowledgment, the card shows a short accepted sweep before it
+becomes a normal user message. If the authoritative app-server item arrives
+during that transition, it inherits the pending card's stable visual anchor and
+replaces it after the 500-millisecond transition completes. Only the correlated
+`turn.start` or `turn.steer` completion callback acknowledges a prompt;
+conversation events never infer acknowledgment. Each operation carries a
+unique `clientUserMessageId`, which binds the authoritative user item without
+confusing identical prompt text. A failed submission remains visible with an
+explicit error state.
+
+The composer is cleared immediately after local admission and remains enabled.
+Users may enter additional prompts while earlier prompts await acknowledgment.
+CodexUI queues submissions per thread and dispatches them in order: only one
+unacknowledged prompt operation is in flight for a thread. After each result,
+the next queued prompt is sent using the app-server state produced by the
+preceding acknowledgment. Different threads remain independent.
+
+Submission waits until the destination thread has completed its connection-
+generation hydration. A provider-marked `notLoaded` thread is resumed before
+the turn operation. If a submission still receives a transient thread-not-found
+result, CodexUI performs one bounded resume-and-retry; a repeated failure is
+shown on the pending card rather than retried indefinitely. If hydration has
+failed, submission is rejected without clearing the composer draft; Reload
+must succeed before that prompt can be admitted. A disconnect between admission
+and dispatch leaves the pending card in place and unsent until bridge-open
+re-drives it. An active resume prevents a concurrent hydration read or turn
+operation for the same thread.
+
+For an explicit new-thread draft, prompts entered while `thread.create` is in
+flight remain attached to that draft. When creation succeeds, all pending
+prompts move to the returned stable thread ID and are dispatched in order.
+
+## Conversation scrolling
+
+The message view smoothly follows incoming content only while it is already at
+the bottom. Consecutive geometry changes retarget one short, monotonic animation
+to the newest bottom. If the user scrolls upward, the animation stops
+immediately and automatic following pauses so the current text can be read.
+Returning to the bottom re-enables following.
+
+Follow/pause mode and the visible-card/pixel-offset anchor are retained per
+thread and restored when the user switches back.
+
+This policy applies to new messages, streaming updates, pending prompt cards,
+and card reconstruction. It is based on the scroll bar's actual bottom state,
+not on turn activity.
+
+While following is paused, CodexUI anchors the first visible card and its pixel
+offset. Appends below the viewport keep the scrollbar value unchanged; card
+reflow or reconstruction restores that visual anchor after Qt completes layout.
+Incoming data therefore cannot move the user's reading position merely because
+content above or below it changed size. Protocol updates that do not change a
+card's visible projection do not rebuild that card. Multiple visible card
+changes from one refresh are applied as one paint-suppressed layout transaction
+with one anchor restoration, including streaming Command execution updates.
+New authoritative cards are inserted at their server-ordered position without
+reconstructing retained cards. While following is paused, the effective history
+window expands with incoming cards so its visible anchor is not evicted; the
+requested bound is restored after following resumes.
+
+User scrolling to the current bottom re-enables following. A generic Qt range
+clamp caused by card reflow does not count as user intent and cannot silently
+re-enable following. Composer contraction is the explicit exception: after its
+trailing space is removed, CodexUI recomputes whether the resulting clamped
+position is the new bottom.
+
+The complete center region is wheel- and touchpad-scroll sensitive. Wheel
+events over non-scrollable center chrome and the horizontal splitter handles
+are forwarded to the message view. A nested scrollable control, such as Command
+execution output, consumes an event while it can scroll in that direction and
+hands an edge event back to the conversation.
+
+## Composer geometry
+
+The upcoming-turn controls are anchored to the bottom of the center pane. The
+prompt editor starts at one line, grows upward for multiline input, and stops at
+its configured maximum height, after which it scrolls internally.
+
+The message-view layout reserves only the composer's canonical height. When
+prompt text, attachments, settings, or attention controls increase that height,
+the composer grows upward as an overlay: the viewport keeps its normal geometry
+and may be partly covered. An equal-height trailing spacer is added to the
+scrollable conversation content so the final card can still be moved into the
+visible region with the normal gap above the composer.
+
+Growing this spacer preserves the current scrollbar value and does not move the
+messages automatically. Reaching its new maximum re-enables bottom-follow for
+subsequent content. When the composer returns to canonical height, the spacer
+is removed; Qt may clamp a former bottom position to the reduced range, after
+which the normal viewport state and bottom-follow policy apply again.
+
+## Command execution cards
+
+The card's visible label is **Command execution**.
+
+Command execution output boxes are created only when output contains printable,
+non-whitespace text after terminal control sequences are ignored; empty,
+whitespace-only, and ANSI/control-only output has no output surface. A shown box
+has no non-content minimum height, grows from zero to a maximum of 220 pixels,
+and exposes a styled vertical scrollbar only when content exceeds that limit.
+Its content height is measured synchronously during the outer layout
+transaction. Streaming output, completion status, and metadata update the
+retained outer Command execution card in place; they do not replace it. Output
+follows its bottom while already at the bottom. A manual upward scroll pauses
+following until the user returns to the bottom. Each output card retains its
+own follow/pause position across in-place output updates.
+
+## Inspector and Info presentation
+
+The State and Protocol viewers use the common CodexUI scrollbar styling and
+show vertical scrollbars only when needed. The Protocol log occupies the
+expanding area of its tab; protocol statistics are displayed below the log.
+Protocol and State data are diagnostic presentation only and do not create
+domain authority. Plan, Agents, Changes, and Requests use retained per-thread
+presentation snapshots, so revisiting a materialized thread does not clear or
+flash those surfaces while unrelated frames arrive.
+
+## Desktop identity
+
+The application identity is `codex-ui`. The executable, desktop entry,
+`StartupWMClass`, application icon name, and installed SVG icon use that same
+identity so Linux desktop environments associate the running window with the
+correct launcher and taskbar icon.
+
+## Progress indication
+
+Long-running operations need scoped progress presentation rather than a global
+busy state. Candidate scopes include prompt acknowledgment, thread creation,
+and loading a long thread. Pending prompt acknowledgment already has its own
+animated highlight sweep. Any additional progress indicator must preserve input
+and navigation that can safely remain interactive, identify the operation it
+represents, and avoid suggesting that unrelated threads are blocked. No general
+spinner contract is defined yet.
diff --git a/resources/applications/codex-ui.desktop b/resources/applications/codex-ui.desktop
new file mode 100644
index 0000000..797f331
--- /dev/null
+++ b/resources/applications/codex-ui.desktop
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: LGPL-3.0-or-later OR MIT
+[Desktop Entry]
+Type=Application
+Name=CodexUI
+Comment=Codex agent workspace
+Exec=codex-ui
+Icon=codex-ui
+Terminal=false
+Categories=Development;IDE;
+StartupNotify=true
+StartupWMClass=codex-ui
diff --git a/resources/icons/codex-ui.svg b/resources/icons/codex-ui.svg
new file mode 100644
index 0000000..e9442ac
--- /dev/null
+++ b/resources/icons/codex-ui.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/src/app/Application.cpp b/src/app/Application.cpp
deleted file mode 100644
index 170f700..0000000
--- a/src/app/Application.cpp
+++ /dev/null
@@ -1,20 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later OR MIT
-
-#include "app/Application.h"
-
-namespace codexui {
-
-Application::~Application()
-{
- // Complete/cancel frontend operations while MainWindow and its callback
- // targets are still alive. Member destruction happens after this body.
- frontendSession.shutdown();
-}
-
-void Application::show()
-{
- mainWindow.show();
- frontendSession.connectToBackend();
-}
-
-} // namespace codexui
diff --git a/src/app/Application.h b/src/app/Application.h
deleted file mode 100644
index 4cd3572..0000000
--- a/src/app/Application.h
+++ /dev/null
@@ -1,24 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later OR MIT
-
-#ifndef CODEXUI_APP_APPLICATION_H
-#define CODEXUI_APP_APPLICATION_H
-
-#include "app/FrontendSession.h"
-#include "ui/MainWindow.h"
-
-namespace codexui {
-
-class Application
-{
-public:
- ~Application();
- void show();
-
-private:
- FrontendSession frontendSession;
- MainWindow mainWindow{frontendSession};
-};
-
-} // namespace codexui
-
-#endif // CODEXUI_APP_APPLICATION_H
diff --git a/src/app/AttachmentManager.cpp b/src/app/AttachmentManager.cpp
deleted file mode 100644
index cc61394..0000000
--- a/src/app/AttachmentManager.cpp
+++ /dev/null
@@ -1,765 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later OR MIT
-
-#include "app/AttachmentManager.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-
-namespace codexui {
-namespace {
-
-constexpr QFileDevice::Permissions PrivateDirectoryPermissions =
- QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner;
-constexpr QFileDevice::Permissions PrivateFilePermissions =
- QFileDevice::ReadOwner | QFileDevice::WriteOwner;
-constexpr auto StagingRegistryGroup = "attachmentStaging/v1";
-
-QString errorWithPath(const QString& message, const QString& path)
-{
- return QStringLiteral("%1\n\n%2").arg(message, QDir::toNativeSeparators(path));
-}
-
-QString safeFileName(const QString& name)
-{
- QString result = QFileInfo(name).fileName();
- if (result.isEmpty() || result == QStringLiteral(".") || result == QStringLiteral(".."))
- result = QStringLiteral("attachment");
- for (QChar& character : result) {
- if (character.unicode() < 0x20 || character == QLatin1Char('/')
- || character == QLatin1Char('\\'))
- character = QLatin1Char('_');
- }
- return result;
-}
-
-QString uniqueDestinationName(const QString& requested, QSet& occupiedNames)
-{
- const QFileInfo info(requested);
- const QString suffix = info.completeSuffix();
- const QString base = info.completeBaseName().isEmpty()
- ? QStringLiteral("attachment") : info.completeBaseName();
- QString candidate = requested;
- int ordinal = 2;
- while (occupiedNames.contains(candidate.toCaseFolded())) {
- candidate = suffix.isEmpty()
- ? QStringLiteral("%1-%2").arg(base).arg(ordinal)
- : QStringLiteral("%1-%2.%3").arg(base).arg(ordinal).arg(suffix);
- ++ordinal;
- }
- occupiedNames.insert(candidate.toCaseFolded());
- return candidate;
-}
-
-bool copyFileAtomically(const QString& sourcePath,
- const QString& destinationPath,
- QString* errorMessage,
- const AttachmentManager::CancellationCheck& cancelled)
-{
- const auto reportCancellation = [errorMessage]() {
- if (errorMessage)
- *errorMessage = QStringLiteral("Attachment preparation was cancelled.");
- };
- if (cancelled && cancelled()) {
- reportCancellation();
- return false;
- }
-
- QFile source(sourcePath);
- if (!source.open(QIODevice::ReadOnly)) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to open the attachment: %1").arg(source.errorString()),
- sourcePath);
- return false;
- }
-
- QSaveFile destination(destinationPath);
- destination.setDirectWriteFallback(true);
- if (!destination.open(QIODevice::WriteOnly)) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to create the staged attachment: %1")
- .arg(destination.errorString()),
- destinationPath);
- return false;
- }
-
- const auto cancelDestination = [&]() {
- destination.cancelWriting();
- // QSaveFile's direct-write fallback cannot roll back by itself. This
- // path is always a fresh file inside a fresh staging directory.
- (void)QFile::remove(destinationPath);
- reportCancellation();
- return false;
- };
-
- constexpr qint64 chunkSize = 1024 * 1024;
- QByteArray buffer(static_cast(chunkSize), Qt::Uninitialized);
- while (!source.atEnd()) {
- if (cancelled && cancelled())
- return cancelDestination();
- const qint64 count = source.read(buffer.data(), chunkSize);
- if (count < 0 || (count > 0 && destination.write(buffer.constData(), count) != count)) {
- destination.cancelWriting();
- if (errorMessage)
- *errorMessage = errorWithPath(
- count < 0
- ? QStringLiteral("Unable to read the attachment: %1").arg(source.errorString())
- : QStringLiteral("Unable to write the staged attachment: %1")
- .arg(destination.errorString()),
- count < 0 ? sourcePath : destinationPath);
- return false;
- }
- if (count == 0)
- break;
- if (cancelled && cancelled())
- return cancelDestination();
- }
- if (cancelled && cancelled())
- return cancelDestination();
- // Apply the final private mode to QSaveFile's temporary inode before its
- // atomic rename publishes that inode at destinationPath.
- if (!destination.setPermissions(PrivateFilePermissions)) {
- destination.cancelWriting();
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to make the staged attachment private."),
- destinationPath);
- return false;
- }
- if (!destination.commit()) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to finish the staged attachment: %1")
- .arg(destination.errorString()),
- destinationPath);
- return false;
- }
- return true;
-}
-
-QString stagingThreadToken(const QString& threadId)
-{
- const QByteArray source = threadId.isEmpty() ? QByteArrayLiteral("pending-thread")
- : threadId.toUtf8();
- return QString::fromLatin1(
- QCryptographicHash::hash(source, QCryptographicHash::Sha256).toHex().left(16));
-}
-
-bool ensurePrivateDirectory(const QString& path,
- const QString& failureMessage,
- QString* errorMessage)
-{
- QFileInfo info(path);
- if (info.exists() && (!info.isDir() || info.isSymLink())) {
- if (errorMessage)
- *errorMessage = errorWithPath(failureMessage, path);
- return false;
- }
- if (!info.exists() && !QDir().mkpath(path)) {
- if (errorMessage)
- *errorMessage = errorWithPath(failureMessage, path);
- return false;
- }
- info.refresh();
- if (!info.isDir() || info.isSymLink()
- || !QFile::setPermissions(path, PrivateDirectoryPermissions)) {
- if (errorMessage)
- *errorMessage = errorWithPath(failureMessage, path);
- return false;
- }
- return true;
-}
-
-bool ensurePrivateStagingRoot(const QString& workspace,
- QString* rootPath,
- QString* errorMessage)
-{
- QDir workspaceDirectory(workspace);
- const QString metadataPath = workspaceDirectory.filePath(QStringLiteral(".codex-ui"));
- const QString attachmentsPath = QDir(metadataPath).filePath(QStringLiteral("attachments"));
- const QString directoryError = QStringLiteral(
- "Unable to create a private CodexUI attachment directory.");
- if (!ensurePrivateDirectory(metadataPath, directoryError, errorMessage)
- || !ensurePrivateDirectory(attachmentsPath, directoryError, errorMessage)) {
- return false;
- }
-
- const QString ignorePath = QDir(attachmentsPath).filePath(QStringLiteral(".gitignore"));
- QFileInfo ignoreInfo(ignorePath);
- if (ignoreInfo.exists() && (!ignoreInfo.isFile() || ignoreInfo.isSymLink())) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to secure the CodexUI attachment ignore file."),
- ignorePath);
- return false;
- }
- QByteArray ignoreContents;
- bool appendIgnoreRule = true;
- if (ignoreInfo.exists()) {
- QFile existingIgnoreFile(ignorePath);
- if (!existingIgnoreFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to read the CodexUI attachment ignore file."),
- ignorePath);
- return false;
- }
- ignoreContents = existingIgnoreFile.readAll();
- const QList lines = ignoreContents.split('\n');
- for (auto iterator = lines.crbegin(); iterator != lines.crend(); ++iterator) {
- const QByteArray line = iterator->trimmed();
- if (line.isEmpty() || line.startsWith('#'))
- continue;
- appendIgnoreRule = line != QByteArrayLiteral("*");
- break;
- }
- }
- if (appendIgnoreRule) {
- if (!ignoreContents.isEmpty() && !ignoreContents.endsWith('\n'))
- ignoreContents.append('\n');
- ignoreContents.append("# Transient files staged by CodexUI\n*\n");
- QSaveFile ignoreFile(ignorePath);
- ignoreFile.setDirectWriteFallback(true);
- if (!ignoreFile.open(QIODevice::WriteOnly | QIODevice::Text)
- || ignoreFile.write(ignoreContents) != ignoreContents.size() || !ignoreFile.commit()) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to secure the CodexUI attachment ignore file."),
- ignorePath);
- return false;
- }
- }
- if (!QFile::setPermissions(ignorePath, PrivateFilePermissions)) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to secure the CodexUI attachment ignore file."),
- ignorePath);
- return false;
- }
- if (rootPath)
- *rootPath = attachmentsPath;
- return true;
-}
-
-bool validRegistryId(const QString& registryId)
-{
- const QUuid id(QStringLiteral("{%1}").arg(registryId));
- return !id.isNull() && id.toString(QUuid::WithoutBraces) == registryId;
-}
-
-QString cleanAbsolutePath(const QString& path)
-{
- if (!QDir::isAbsolutePath(path))
- return {};
- return QDir::cleanPath(QFileInfo(path).absoluteFilePath());
-}
-
-bool isSafeStagingPath(const QString& workspace,
- const QString& stagingDirectory,
- const QStringList& stagedFiles)
-{
- const QString cleanWorkspace = cleanAbsolutePath(workspace);
- const QString cleanStagingDirectory = cleanAbsolutePath(stagingDirectory);
- if (cleanWorkspace.isEmpty() || cleanStagingDirectory.isEmpty())
- return false;
-
- const QFileInfo workspaceInfo(cleanWorkspace);
- if (!workspaceInfo.exists() || !workspaceInfo.isDir() || workspaceInfo.isSymLink()
- || workspaceInfo.canonicalFilePath() != cleanWorkspace) {
- return false;
- }
-
- const QString metadataDirectory = QDir(cleanWorkspace).filePath(QStringLiteral(".codex-ui"));
- const QString stagingRoot = QDir(metadataDirectory).filePath(QStringLiteral("attachments"));
- const QFileInfo stagingInfo(cleanStagingDirectory);
- if (QDir::cleanPath(stagingInfo.absolutePath()) != QDir::cleanPath(stagingRoot)
- || stagingInfo.fileName().isEmpty() || stagingInfo.fileName() == QStringLiteral(".")
- || stagingInfo.fileName() == QStringLiteral("..")) {
- return false;
- }
-
- for (const QString& component : {metadataDirectory, stagingRoot, cleanStagingDirectory}) {
- const QFileInfo info(component);
- if (info.exists() && (info.isSymLink() || !info.isDir()))
- return false;
- }
-
- QSet uniqueFiles;
- for (const QString& path : stagedFiles) {
- const QString cleanPath = cleanAbsolutePath(path);
- const QFileInfo fileInfo(cleanPath);
- if (cleanPath.isEmpty()
- || QDir::cleanPath(fileInfo.absolutePath()) != cleanStagingDirectory
- || fileInfo.fileName().isEmpty() || fileInfo.fileName() == QStringLiteral(".")
- || fileInfo.fileName() == QStringLiteral("..")
- || uniqueFiles.contains(cleanPath)) {
- return false;
- }
- if (fileInfo.exists() && (fileInfo.isSymLink() || fileInfo.isDir()))
- return false;
- uniqueFiles.insert(cleanPath);
- }
- return !stagedFiles.isEmpty();
-}
-
-bool stagingArtifactsAreAbsent(const QString& stagingDirectory,
- const QStringList& stagedFiles)
-{
- const QString cleanStagingDirectory = cleanAbsolutePath(stagingDirectory);
- if (cleanStagingDirectory.isEmpty())
- return false;
-
- const QFileInfo directoryInfo(cleanStagingDirectory);
- if (directoryInfo.exists() || directoryInfo.isSymLink())
- return false;
- return std::ranges::all_of(stagedFiles, [](const QString& path) {
- const QString cleanPath = cleanAbsolutePath(path);
- if (cleanPath.isEmpty())
- return false;
- const QFileInfo info(cleanPath);
- return !info.exists() && !info.isSymLink();
- });
-}
-
-bool synchronizePrivateSettings(QSettings& settings, QString* errorMessage)
-{
- const QString fileName = settings.fileName();
- QFileInfo settingsInfo(fileName);
- if (settingsInfo.isSymLink()
- || (settingsInfo.exists()
- && (!settingsInfo.isFile()
- || !QFile::setPermissions(fileName, PrivateFilePermissions)))) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to make the attachment staging registry private."),
- fileName);
- return false;
- }
-
- settings.sync();
- if (settings.status() != QSettings::NoError) {
- if (errorMessage)
- *errorMessage = QStringLiteral("Unable to update the attachment staging registry.");
- return false;
- }
-
- settingsInfo.refresh();
- if (settingsInfo.exists()
- && (!settingsInfo.isFile() || settingsInfo.isSymLink()
- || !QFile::setPermissions(fileName, PrivateFilePermissions))) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("Unable to make the attachment staging registry private."),
- fileName);
- return false;
- }
- return true;
-}
-
-} // namespace
-
-AttachmentStagingLease::AttachmentStagingLease(QString workspace, QString directory)
- : workspaceDirectory(std::move(workspace))
- , stagingDirectory(std::move(directory))
-{
-}
-
-AttachmentStagingLease::~AttachmentStagingLease()
-{
- if (!dispatched)
- (void)cleanup();
-}
-
-void AttachmentStagingLease::trackFile(QString path)
-{
- stagedFiles.append(std::move(path));
-}
-
-const QString& AttachmentStagingLease::directory() const noexcept
-{
- return stagingDirectory;
-}
-
-bool AttachmentStagingLease::cleanup() noexcept
-{
- if (stagingDirectory.isEmpty())
- return true;
-
- QFileInfo directoryInfo(stagingDirectory);
- if (directoryInfo.exists() && (!directoryInfo.isDir() || directoryInfo.isSymLink()))
- return false;
-
- bool removed = true;
- QStringList remainingFiles;
- const QString expectedDirectory = QDir(stagingDirectory).absolutePath();
- for (const QString& path : std::as_const(stagedFiles)) {
- const QFileInfo fileInfo(path);
- if (!fileInfo.exists() && !fileInfo.isSymLink())
- continue;
- if (fileInfo.absolutePath() != expectedDirectory
- || fileInfo.isDir() || !QFile::remove(path)) {
- removed = false;
- remainingFiles.append(path);
- }
- }
- stagedFiles = std::move(remainingFiles);
-
- directoryInfo.refresh();
- if (directoryInfo.exists()) {
- if (!directoryInfo.isDir() || directoryInfo.isSymLink()
- || !QDir().rmdir(stagingDirectory)) {
- removed = false;
- }
- }
- return removed;
-}
-
-void AttachmentStagingLease::markDispatched() noexcept
-{
- dispatched = true;
-}
-
-void AttachmentStagingLease::cancelDispatch() noexcept
-{
- dispatched = false;
-}
-
-bool AttachmentManager::inspectFile(const QString& path,
- AttachmentInfo* result,
- QString* errorMessage)
-{
- if (!result) {
- if (errorMessage)
- *errorMessage = QStringLiteral("No attachment result object was provided.");
- return false;
- }
- QFileInfo info(path);
- const QString canonicalPath = info.canonicalFilePath();
- if (canonicalPath.isEmpty()) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("The selected attachment does not exist or cannot be resolved."),
- path);
- return false;
- }
- info.setFile(canonicalPath);
- if (!info.isFile() || !info.isReadable()) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- info.isFile() ? QStringLiteral("The selected attachment is not readable.")
- : QStringLiteral("The selected attachment is not a regular file."),
- canonicalPath);
- return false;
- }
- if (info.size() > MaximumSingleFileBytes) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("The selected attachment exceeds the %1 per-file limit.")
- .arg(formatSize(MaximumSingleFileBytes)),
- canonicalPath);
- return false;
- }
-
- QMimeDatabase mimeDatabase;
- QString mimeType = mimeDatabase.mimeTypeForFile(
- canonicalPath, QMimeDatabase::MatchExtension).name();
- if (mimeType.isEmpty())
- mimeType = QStringLiteral("application/octet-stream");
- *result = AttachmentInfo{
- canonicalPath,
- info.fileName(),
- mimeType,
- info.size(),
- isSupportedLocalImage(canonicalPath, mimeType)
- ? AttachmentInfo::Kind::Image : AttachmentInfo::Kind::File};
- return true;
-}
-
-bool AttachmentManager::validateForWorkspace(const QList& attachments,
- const QString& workspace,
- QString* errorMessage)
-{
- if (totalSize(attachments) > MaximumTotalBytes) {
- if (errorMessage)
- *errorMessage = QStringLiteral(
- "The selected attachments exceed the %1 total attachment limit.")
- .arg(formatSize(MaximumTotalBytes));
- return false;
- }
- const bool hasGenericFile = std::ranges::any_of(attachments, [](const auto& attachment) {
- return attachment.kind == AttachmentInfo::Kind::File;
- });
- if (!hasGenericFile)
- return true;
-
- const QFileInfo workspaceInfo(workspace.trimmed());
- if (!workspaceInfo.exists() || !workspaceInfo.isDir()
- || !workspaceInfo.isReadable() || !workspaceInfo.isWritable()) {
- if (errorMessage)
- *errorMessage = errorWithPath(
- QStringLiteral("A readable and writable workspace is required for non-image attachments."),
- workspace.trimmed());
- return false;
- }
- return true;
-}
-
-bool AttachmentManager::prepare(const QList& attachments,
- const QString& workspace,
- const QString& threadId,
- AttachmentPreparation* result,
- QString* errorMessage,
- CancellationCheck cancelled)
-{
- if (!result) {
- if (errorMessage)
- *errorMessage = QStringLiteral("No attachment preparation result object was provided.");
- return false;
- }
- *result = {};
- if (cancelled && cancelled()) {
- if (errorMessage)
- *errorMessage = QStringLiteral("Attachment preparation was cancelled.");
- return false;
- }
- if (!validateForWorkspace(attachments, workspace, errorMessage))
- return false;
-
- const bool hasGenericFile = std::ranges::any_of(attachments, [](const auto& attachment) {
- return attachment.kind == AttachmentInfo::Kind::File;
- });
- QString stagingDirectory;
- const QString canonicalWorkspace = QFileInfo(workspace).canonicalFilePath();
- if (hasGenericFile) {
- QString stagingRoot;
- if (!ensurePrivateStagingRoot(canonicalWorkspace, &stagingRoot, errorMessage))
- return false;
- stagingDirectory = QDir(stagingRoot).filePath(
- QStringLiteral("%1-%2-%3")
- .arg(stagingThreadToken(threadId),
- QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd-HHmmss-zzz")),
- QUuid::createUuid().toString(QUuid::WithoutBraces)));
- if (!ensurePrivateDirectory(
- stagingDirectory,
- QStringLiteral("Unable to create a private directory for staged attachments."),
- errorMessage)) {
- (void)QDir().rmdir(stagingDirectory);
- return false;
- }
- result->stagingDirectory = stagingDirectory;
- result->stagingLease = std::shared_ptr(
- new AttachmentStagingLease(canonicalWorkspace, stagingDirectory));
- }
-
- QSet occupiedNames;
- QStringList promptLines;
- for (const AttachmentInfo& attachment : attachments) {
- if (cancelled && cancelled()) {
- if (errorMessage)
- *errorMessage = QStringLiteral("Attachment preparation was cancelled.");
- if (result->stagingLease)
- (void)result->stagingLease->cleanup();
- *result = {};
- return false;
- }
- PreparedAttachment prepared;
- prepared.source = attachment;
- if (attachment.kind == AttachmentInfo::Kind::Image) {
- prepared.effectivePath = attachment.sourcePath;
- result->imagePaths.append(prepared.effectivePath);
- } else {
- const QString destinationName = uniqueDestinationName(
- safeFileName(attachment.displayName), occupiedNames);
- prepared.effectivePath = QDir(stagingDirectory).filePath(destinationName);
- prepared.staged = true;
- if (!copyFileAtomically(attachment.sourcePath,
- prepared.effectivePath,
- errorMessage,
- cancelled)) {
- (void)result->stagingLease->cleanup();
- *result = {};
- return false;
- }
- result->stagingLease->trackFile(prepared.effectivePath);
- prepared.workspaceRelativePath = QDir::fromNativeSeparators(
- QDir(canonicalWorkspace).relativeFilePath(prepared.effectivePath));
- if (!prepared.workspaceRelativePath.startsWith(QStringLiteral("./")))
- prepared.workspaceRelativePath.prepend(QStringLiteral("./"));
- promptLines.append(QStringLiteral("- `%1` (original `%2`, %3, `%4`)")
- .arg(prepared.workspaceRelativePath,
- attachment.displayName,
- formatSize(attachment.sizeBytes),
- attachment.mimeType));
- }
- result->items.append(std::move(prepared));
- }
- if (!promptLines.isEmpty()) {
- result->genericFilePrompt = QStringLiteral(
- "The following local files were attached by the user and copied into the workspace by CodexUI. "
- "Read them as input for this turn. Archives are not unpacked automatically; inspect or extract "
- "them only when needed and permitted.\n%1")
- .arg(promptLines.join(QLatin1Char('\n')));
- }
- return true;
-}
-
-QString AttachmentManager::composePrompt(const QString& userPrompt,
- const AttachmentPreparation& preparation)
-{
- QString result = userPrompt;
- if (!preparation.genericFilePrompt.isEmpty()) {
- if (!result.trimmed().isEmpty())
- result += QStringLiteral("\n\n");
- else
- result.clear();
- result += preparation.genericFilePrompt;
- }
- return result;
-}
-
-QString AttachmentManager::formatSize(qint64 sizeBytes)
-{
- static const QStringList units{QStringLiteral("B"), QStringLiteral("KiB"),
- QStringLiteral("MiB"), QStringLiteral("GiB")};
- double value = static_cast(std::max(0, sizeBytes));
- qsizetype unit = 0;
- while (value >= 1024.0 && unit + 1 < units.size()) {
- value /= 1024.0;
- ++unit;
- }
- const int precision = unit == 0 ? 0 : (value >= 100.0 ? 0 : 1);
- return QStringLiteral("%1 %2").arg(value, 0, 'f', precision).arg(units.at(unit));
-}
-
-qint64 AttachmentManager::totalSize(const QList& attachments)
-{
- qint64 total = 0;
- for (const auto& attachment : attachments) {
- if (attachment.sizeBytes > 0 && total > MaximumTotalBytes - attachment.sizeBytes)
- return MaximumTotalBytes + 1;
- total += std::max(0, attachment.sizeBytes);
- }
- return total;
-}
-
-QString AttachmentManager::createStagingRegistryId()
-{
- return QUuid::createUuid().toString(QUuid::WithoutBraces);
-}
-
-bool AttachmentManager::persistDispatchedStaging(
- QSettings& settings,
- const QString& registryId,
- const QString& threadId,
- const QString& turnId,
- const AttachmentStagingLeasePtr& stagingLease,
- QString* errorMessage)
-{
- if (!stagingLease || !validRegistryId(registryId) || threadId.isEmpty()
- || !isSafeStagingPath(stagingLease->workspaceDirectory,
- stagingLease->stagingDirectory,
- stagingLease->stagedFiles)) {
- if (errorMessage)
- *errorMessage = QStringLiteral("Invalid attachment staging ownership metadata.");
- return false;
- }
-
- settings.beginGroup(QString::fromLatin1(StagingRegistryGroup));
- settings.beginGroup(registryId);
- settings.setValue(QStringLiteral("workspace"), stagingLease->workspaceDirectory);
- settings.setValue(QStringLiteral("directory"), stagingLease->stagingDirectory);
- settings.setValue(QStringLiteral("files"), stagingLease->stagedFiles);
- settings.setValue(QStringLiteral("threadId"), threadId);
- settings.setValue(QStringLiteral("turnId"), turnId);
- settings.endGroup();
- settings.endGroup();
- if (!synchronizePrivateSettings(settings, errorMessage))
- return false;
- stagingLease->markDispatched();
- return true;
-}
-
-bool AttachmentManager::recoverDispatchedStaging(
- QSettings& settings,
- QList* result,
- QString* errorMessage)
-{
- if (!result) {
- if (errorMessage)
- *errorMessage = QStringLiteral("No attachment staging recovery result was provided.");
- return false;
- }
- result->clear();
- if (!synchronizePrivateSettings(settings, errorMessage))
- return false;
-
- settings.beginGroup(QString::fromLatin1(StagingRegistryGroup));
- const QStringList registryIds = settings.childGroups();
- bool removedStaleRecord = false;
- for (const QString& registryId : registryIds) {
- if (!validRegistryId(registryId))
- continue;
- settings.beginGroup(registryId);
- const QString workspace = settings.value(QStringLiteral("workspace")).toString();
- const QString directory = settings.value(QStringLiteral("directory")).toString();
- const QStringList files = settings.value(QStringLiteral("files")).toStringList();
- const QString threadId = settings.value(QStringLiteral("threadId")).toString();
- const QString turnId = settings.value(QStringLiteral("turnId")).toString();
- settings.endGroup();
- if (stagingArtifactsAreAbsent(directory, files)) {
- settings.remove(registryId);
- removedStaleRecord = true;
- continue;
- }
- if (threadId.isEmpty() || !isSafeStagingPath(workspace, directory, files))
- continue;
-
- auto stagingLease = std::shared_ptr(
- new AttachmentStagingLease(workspace, directory));
- for (const QString& file : files)
- stagingLease->trackFile(cleanAbsolutePath(file));
- stagingLease->markDispatched();
- result->append(PersistedAttachmentStaging{
- registryId, threadId, turnId, std::move(stagingLease)});
- }
- settings.endGroup();
- return !removedStaleRecord || synchronizePrivateSettings(settings, errorMessage);
-}
-
-bool AttachmentManager::forgetDispatchedStaging(QSettings& settings,
- const QString& registryId,
- QString* errorMessage)
-{
- if (!validRegistryId(registryId)) {
- if (errorMessage)
- *errorMessage = QStringLiteral("Invalid attachment staging registry identity.");
- return false;
- }
- settings.beginGroup(QString::fromLatin1(StagingRegistryGroup));
- settings.remove(registryId);
- settings.endGroup();
- return synchronizePrivateSettings(settings, errorMessage);
-}
-
-bool AttachmentManager::isSupportedLocalImage(const QString& path, const QString& mimeType)
-{
- static const QSet extensions{QStringLiteral("png"), QStringLiteral("jpg"),
- QStringLiteral("jpeg"), QStringLiteral("webp"),
- QStringLiteral("gif"), QStringLiteral("bmp")};
- return mimeType.startsWith(QStringLiteral("image/"))
- && extensions.contains(QFileInfo(path).suffix().toLower());
-}
-
-} // namespace codexui
diff --git a/src/app/AttachmentManager.h b/src/app/AttachmentManager.h
deleted file mode 100644
index 023fdf6..0000000
--- a/src/app/AttachmentManager.h
+++ /dev/null
@@ -1,134 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later OR MIT
-
-#ifndef CODEXUI_APP_ATTACHMENTMANAGER_H
-#define CODEXUI_APP_ATTACHMENTMANAGER_H
-
-#include
-#include
-#include
-
-#include
-#include
-
-class QSettings;
-
-namespace codexui {
-
-struct AttachmentInfo
-{
- enum class Kind { File, Image };
-
- QString sourcePath;
- QString displayName;
- QString mimeType;
- qint64 sizeBytes = 0;
- Kind kind = Kind::File;
-
- bool operator==(const AttachmentInfo&) const = default;
-};
-
-struct PreparedAttachment
-{
- AttachmentInfo source;
- QString effectivePath;
- QString workspaceRelativePath;
- bool staged = false;
-};
-
-class AttachmentStagingLease final
-{
-public:
- ~AttachmentStagingLease();
-
- AttachmentStagingLease(const AttachmentStagingLease&) = delete;
- AttachmentStagingLease& operator=(const AttachmentStagingLease&) = delete;
-
- [[nodiscard]] const QString& directory() const noexcept;
- // Prepared-but-unsubmitted leases clean up on destruction. Dispatch makes
- // cleanup explicit so destroying the frontend cannot break an active turn.
- [[nodiscard]] bool cleanup() noexcept;
- void markDispatched() noexcept;
- void cancelDispatch() noexcept;
-
-private:
- friend class AttachmentManager;
- AttachmentStagingLease(QString workspace, QString directory);
- void trackFile(QString path);
-
- QString workspaceDirectory;
- QString stagingDirectory;
- QStringList stagedFiles;
- bool dispatched = false;
-};
-
-using AttachmentStagingLeasePtr = std::shared_ptr;
-
-struct AttachmentPreparation
-{
- QList items;
- QStringList imagePaths;
- QString genericFilePrompt;
- QString stagingDirectory;
- AttachmentStagingLeasePtr stagingLease;
-};
-
-struct PersistedAttachmentStaging
-{
- QString registryId;
- QString threadId;
- QString turnId;
- AttachmentStagingLeasePtr stagingLease;
-};
-
-class AttachmentManager final
-{
-public:
- // Generic-file staging is bounded and may run off the GUI thread. Callers
- // can cooperatively cancel it between fixed-size copy chunks. Image
- // contents travel by local path, not on the frontend protocol wire.
- static constexpr qint64 MaximumSingleFileBytes = 64LL * 1024LL * 1024LL;
- static constexpr qint64 MaximumTotalBytes = 256LL * 1024LL * 1024LL;
-
- using CancellationCheck = std::function;
-
- [[nodiscard]] static bool inspectFile(const QString& path,
- AttachmentInfo* result,
- QString* errorMessage = nullptr);
- [[nodiscard]] static bool validateForWorkspace(const QList& attachments,
- const QString& workspace,
- QString* errorMessage = nullptr);
- [[nodiscard]] static bool prepare(const QList& attachments,
- const QString& workspace,
- const QString& threadId,
- AttachmentPreparation* result,
- QString* errorMessage = nullptr,
- CancellationCheck cancelled = {});
- [[nodiscard]] static QString composePrompt(const QString& userPrompt,
- const AttachmentPreparation& preparation);
- [[nodiscard]] static QString formatSize(qint64 sizeBytes);
- [[nodiscard]] static qint64 totalSize(const QList& attachments);
- [[nodiscard]] static QString createStagingRegistryId();
- [[nodiscard]] static bool persistDispatchedStaging(
- QSettings& settings,
- const QString& registryId,
- const QString& threadId,
- const QString& turnId,
- const AttachmentStagingLeasePtr& stagingLease,
- QString* errorMessage = nullptr);
- [[nodiscard]] static bool recoverDispatchedStaging(
- QSettings& settings,
- QList* result,
- QString* errorMessage = nullptr);
- [[nodiscard]] static bool forgetDispatchedStaging(
- QSettings& settings,
- const QString& registryId,
- QString* errorMessage = nullptr);
-
-private:
- [[nodiscard]] static bool isSupportedLocalImage(const QString& path,
- const QString& mimeType);
-};
-
-} // namespace codexui
-
-#endif // CODEXUI_APP_ATTACHMENTMANAGER_H
diff --git a/src/app/FrontendSession.cpp b/src/app/FrontendSession.cpp
deleted file mode 100644
index 743ed56..0000000
--- a/src/app/FrontendSession.cpp
+++ /dev/null
@@ -1,1280 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later OR MIT
-
-#include "app/FrontendSession.h"
-#include "app/FrontendSessionWorker.h"
-
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include