A turn could write a report and say "here it is" -- and there was nothing to
open. The engine had no way to hand a finished file to the person, and the
front end had no way to show one. This release adds the whole path: a tool to
deliver, a store that keeps the copy, a card in the transcript, and open /
share through the platform. It also fixes two bugs the phone exposed while it
was being tried there.
deliver_file (engine)
- cst_attachment_t.for_user marks a copy that belongs to the person, not to
the model; cst_attach_import_artifact() stores any bytes up to
CST_ARTIFACT_MAX_BYTES (64 MB) with the mime taken from the name's
extension. Attachments are unchanged (content decides the type, 16 MB).
- It is a read-policy engine tool: path resolves inside the workspace, a
directory is refused, title (optional) is the display name.
- The wire is what keeps it out of the conversation: parts_add_attachments()
skips for_user entries without even a "not sent" note, and a turn that only
delivered a file no longer invents an empty user message. tests/test_artifacts.c
pins that down end to end, including a session-file round trip.
The card (Qt front end)
- filesText() replaced imagesText(): one line per file -- kind, stored name,
name, origin, mime, bytes, pixels -- decoded by ImageStrip.qml. A for_user
entry is a file card (extension badge, name, type and size, Open, Share);
a picture stays a thumbnail and opens on tap. Both shapes are read, so
sessions written by an older version still draw.
- AgentCore::openArtifact() / shareArtifact() accept a name only when one of
the session's own messages carries it, and hand the display name to the
platform layer for the status line.
- Android opens through the app's FileProvider (content:// + ACTION_VIEW,
resolveActivity() checked, read permission granted) and shares through the
share sheet; a desktop uses QDesktopServices / the clipboard.
CST_APP_NO_OPEN=1 makes --uitest run without opening anything.
Two bugs the device found
- The session writer had its own copy of the attachment JSON and forgot
`for_user`: the file survived the file, but came back as an attachment --
no card, and put back in front of the model. It now uses the one writer
(cst_attachments_to_json()), with the round trip covered by a test.
- engineToolImages() looked the card up with findTool(), which only finds a
card that is still running -- and the engine reports the result *before*
the files, so the card was already complete and the files were dropped.
A live turn therefore showed no card while a replay of the same session
did. It now matches on the call id.
- A tap that could not work said nothing: ImageStrip.qml's errorText was
never drawn. The card now carries the reason ("no app on the phone opens
report.md").
Also
- The saved provider key reaches the engine on every start: AgentCore::open()
was handed an empty key and the engine never resolves credentials itself
(only the CLI did), so every start asked for it again. It resolves the same
order main.c uses (saved key, provider environment, CST_API_KEY) and wipes
the copy it made; --selftest checks it against a scratch credentials file.
- Android: notification_send reads the Java exception text into its result,
and keeps failing on the NX789J -- the build() returns null and Qt clears
the exception before C++ can read it, so the call has to be wrapped in Java
next (docs/android.md says so).
- --uitest grew the delivered-file case (card, Open, Share, a forged name
refused), the live order of result-then-files, and the startup row count.
Version 0.9.2.
221 lines
9.9 KiB
CMake
221 lines
9.9 KiB
CMake
cmake_minimum_required(VERSION 3.16)
|
|
project(cst-agent-app LANGUAGES C CXX VERSION 0.1.0)
|
|
|
|
# The Qt front end (app/) embeds the C engine in-process: it links libcstcore
|
|
# and never opens a port. Two targets:
|
|
#
|
|
# cst-app-selftest pure C smoke test of the embedding path (no Qt), so the
|
|
# C side is still guarded on machines without Qt Quick
|
|
# cst-agent-app the GUI (Qt 6 Quick + QML), built when the Qt modules
|
|
# are there
|
|
#
|
|
# Standalone build (builds the engine from ../ too):
|
|
# cmake -S app -B build-app -DCMAKE_BUILD_TYPE=Debug && cmake --build build-app -j
|
|
# Inside the repo build (the parent defines the cstcore target, shared with the CLI):
|
|
# cmake -B build -DCST_BUILD_APP=ON && cmake --build build --target cst-agent-app
|
|
#
|
|
# Extra QML import paths, e.g. a Kirigami installed away from the Qt prefix:
|
|
# -DCST_APP_QML_IMPORT=... (added to QML2_IMPORT_PATH / the app's runtime)
|
|
|
|
option(CST_APP_GUI "build the Qt GUI (needs Qt6 Quick, Qml, QuickControls2)" ON)
|
|
option(CST_APP_SELFTEST "build the C embedding smoke test" ON)
|
|
|
|
# --- the engine ----------------------------------------------------------
|
|
# Used as a subproject (../CMakeLists.txt already defined cstcore, engine
|
|
# compiled once for CLI and app) or built here from the sources next door.
|
|
if(NOT TARGET cstcore)
|
|
set(CST_APP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
|
include_directories("${CST_APP_ROOT}/include" "${CST_APP_ROOT}/vendor/cjson")
|
|
file(GLOB CST_APP_CORE_SRC
|
|
"${CST_APP_ROOT}/src/*.c"
|
|
"${CST_APP_ROOT}/src/tools/*.c"
|
|
"${CST_APP_ROOT}/vendor/cjson/cJSON.c")
|
|
list(REMOVE_ITEM CST_APP_CORE_SRC
|
|
"${CST_APP_ROOT}/src/main.c"
|
|
"${CST_APP_ROOT}/src/tui.c"
|
|
"${CST_APP_ROOT}/src/tui_stub.c"
|
|
"${CST_APP_ROOT}/src/textsel.c")
|
|
# The HTTP front end is not part of an in-process app: no server, no port.
|
|
list(REMOVE_ITEM CST_APP_CORE_SRC
|
|
"${CST_APP_ROOT}/src/net.c"
|
|
"${CST_APP_ROOT}/src/http_srv.c"
|
|
"${CST_APP_ROOT}/src/webui.c"
|
|
"${CST_APP_ROOT}/src/web_md.c")
|
|
if(EXISTS "${CST_APP_ROOT}/.gen/models_builtin.inc")
|
|
include_directories("${CST_APP_ROOT}/.gen")
|
|
endif()
|
|
add_library(cstcore STATIC ${CST_APP_CORE_SRC})
|
|
target_compile_definitions(cstcore PRIVATE CST_NO_WEBUI=1)
|
|
find_package(CURL REQUIRED)
|
|
find_package(Threads REQUIRED)
|
|
target_link_libraries(cstcore PUBLIC ${CURL_LIBRARIES} Threads::Threads m)
|
|
message(STATUS "app: building the engine from ${CST_APP_ROOT}")
|
|
else()
|
|
message(STATUS "app: linking the cstcore target of the parent build")
|
|
endif()
|
|
|
|
# --- the C smoke test ----------------------------------------------------
|
|
if(CST_APP_SELFTEST)
|
|
add_executable(cst-app-selftest tools/selftest.c)
|
|
target_link_libraries(cst-app-selftest PRIVATE cstcore)
|
|
enable_testing()
|
|
add_test(NAME cst-app-selftest COMMAND cst-app-selftest)
|
|
endif()
|
|
|
|
# --- the GUI -------------------------------------------------------------
|
|
if(CST_APP_GUI)
|
|
# Android's platform file talks to the platform through QtAndroidPrivate
|
|
# (the POST_NOTIFICATIONS probe), whose include path comes with CorePrivate.
|
|
if(ANDROID)
|
|
find_package(Qt6 6.4 COMPONENTS Core Gui Qml Quick QuickControls2 Test
|
|
CorePrivate QUIET)
|
|
else()
|
|
find_package(Qt6 6.4 COMPONENTS Core Gui Qml Quick QuickControls2 Test QUIET)
|
|
endif()
|
|
if(NOT Qt6_FOUND)
|
|
message(WARNING
|
|
"app: Qt 6 Quick/Qml not found, skipping cst-agent-app "
|
|
"(the C selftest still builds; install e.g. qt6.qtdeclarative / "
|
|
"qt6.qtquickcontrols2 and reconfigure)")
|
|
else()
|
|
qt_standard_project_setup()
|
|
# Qt 6.5+ policies: keep the module resource at :/qt/qml/<URI>/ (what
|
|
# main.cpp loads) and let CMake generate the qmldir of qml/ for us.
|
|
qt_policy(SET QTP0001 NEW)
|
|
qt_policy(SET QTP0004 NEW)
|
|
# The front end's platform tools: one file per platform, the same split
|
|
# core/ToolProvider.cpp makes. Android brings its own (JNI, owner
|
|
# "app:android"); the other platforms still use the Linux file, whose
|
|
# registration entry point is simply not called there.
|
|
if(ANDROID)
|
|
set(CST_APP_PLATFORM_SRC src/platform/android/AndroidTools.cpp)
|
|
else()
|
|
set(CST_APP_PLATFORM_SRC src/platform/linux/NotifyTool.cpp)
|
|
endif()
|
|
qt_add_executable(cst-agent-app
|
|
src/main.cpp
|
|
src/core/AgentCore.cpp
|
|
src/core/ToolProvider.cpp
|
|
src/ui/UiTest.cpp
|
|
src/ui/ThemeIcons.cpp
|
|
src/ui/SessionImages.cpp
|
|
${CST_APP_PLATFORM_SRC})
|
|
target_include_directories(cst-agent-app PRIVATE
|
|
src "${CMAKE_CURRENT_SOURCE_DIR}/../include")
|
|
target_compile_definitions(cst-agent-app PRIVATE CST_APP_VERSION="${PROJECT_VERSION}")
|
|
if(CST_APP_QML_IMPORT)
|
|
target_compile_definitions(cst-agent-app PRIVATE
|
|
CST_APP_QML_IMPORT="${CST_APP_QML_IMPORT}")
|
|
endif()
|
|
target_link_libraries(cst-agent-app PRIVATE
|
|
Qt6::Core Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Test cstcore)
|
|
if(ANDROID)
|
|
# app/src/platform/android/AndroidTools.cpp talks to the platform
|
|
# through QtAndroidPrivate (permissions) -- a private header, hence the
|
|
# private module for its include path.
|
|
target_link_libraries(cst-agent-app PRIVATE Qt6::CorePrivate)
|
|
endif()
|
|
if(WIN32)
|
|
set_target_properties(cst-agent-app PROPERTIES WIN32_EXECUTABLE TRUE)
|
|
endif()
|
|
|
|
# --- Android (APK) -----------------------------------------------------
|
|
# The engine is compiled in (top-level CMakeLists: -DCST_BUILD_TUI=OFF,
|
|
# -DCST_BUILD_WEBUI=OFF, so src/webui_stub.c stands in for the server and
|
|
# nothing listens on a port). The manifest, icons and strings live in
|
|
# app/android/, which androiddeployqt copies next to its own gradle
|
|
# project. minSdk 24 is the engine's own API_LEVEL (scripts/
|
|
# build_third_party_android.sh builds curl/mbedTLS for android-24).
|
|
if(ANDROID)
|
|
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/../include/cst_version.h"
|
|
_cst_ver_lines REGEX "#define CST_VERSION_(MAJOR|MINOR|PATCH) ")
|
|
foreach(_line ${_cst_ver_lines})
|
|
if(_line MATCHES "CST_VERSION_MAJOR ([0-9]+)")
|
|
set(_cst_vmaj "${CMAKE_MATCH_1}")
|
|
elseif(_line MATCHES "CST_VERSION_MINOR ([0-9]+)")
|
|
set(_cst_vmin "${CMAKE_MATCH_1}")
|
|
elseif(_line MATCHES "CST_VERSION_PATCH ([0-9]+)")
|
|
set(_cst_vpat "${CMAKE_MATCH_1}")
|
|
endif()
|
|
endforeach()
|
|
set_target_properties(cst-agent-app PROPERTIES
|
|
QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android"
|
|
QT_ANDROID_MIN_SDK_VERSION 24
|
|
QT_ANDROID_TARGET_SDK_VERSION 35
|
|
QT_ANDROID_VERSION_NAME "${_cst_vmaj}.${_cst_vmin}.${_cst_vpat}"
|
|
QT_ANDROID_VERSION_CODE 1
|
|
QT_ANDROID_ABIS "${ANDROID_ABI}")
|
|
message(STATUS "app: Android package "
|
|
"${_cst_vmaj}.${_cst_vmin}.${_cst_vpat}, ABI ${ANDROID_ABI}, "
|
|
"minSdk 24, targetSdk 35")
|
|
|
|
# Signing: androiddeployqt signs the APK itself and refuses a signed
|
|
# gradle output ("Could not copy unsigned APK"), so the keystore goes
|
|
# here, not into gradle. The path, alias and passwords arrive through
|
|
# the QT_ANDROID_KEYSTORE_* environment variables;
|
|
# scripts/build_apk_android.sh points them at the standard Android debug
|
|
# key it creates on first use, and CST_ANDROID_KEYSTORE overrides it.
|
|
if(DEFINED ENV{QT_ANDROID_KEYSTORE_PATH} AND NOT "$ENV{QT_ANDROID_KEYSTORE_PATH}" STREQUAL "")
|
|
set(QT_ANDROID_SIGN_APK TRUE)
|
|
message(STATUS "app: signing the APK with "
|
|
"$ENV{QT_ANDROID_KEYSTORE_PATH} ($ENV{QT_ANDROID_KEYSTORE_ALIAS})")
|
|
else()
|
|
message(STATUS "app: QT_ANDROID_KEYSTORE_PATH unset -- the APK stays "
|
|
"unsigned and 'adb install' will refuse it (INSTALL_PARSE_FAILED_NO_CERTIFICATES)")
|
|
endif()
|
|
endif()
|
|
|
|
# Theme is read by every component, so it is a QML singleton (one instance,
|
|
# no object needed in the tree).
|
|
set_source_files_properties(qml/Theme.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
|
# Keep the module flat in the resource tree (:qt/qml/CstAgent/Main.qml).
|
|
# Sources live in qml/, but a module whose files sit in a subdirectory of
|
|
# the URI makes Qt register the types with relative URLs ("qmlRegisterType
|
|
# requires absolute URLs" on every start).
|
|
foreach(_qml Main ChatView Sidebar SessionRow MessageCard ToolCard ThinkBlock
|
|
NoteLine TodoPanel Composer ConsentDialog AskDialog SettingsDialog
|
|
ThemeIcon Theme ImageStrip)
|
|
set_source_files_properties(qml/${_qml}.qml PROPERTIES QT_RESOURCE_ALIAS ${_qml}.qml)
|
|
endforeach()
|
|
qt_add_qml_module(cst-agent-app
|
|
URI CstAgent
|
|
VERSION 1.0
|
|
QML_FILES
|
|
qml/Main.qml
|
|
qml/ChatView.qml
|
|
qml/Sidebar.qml
|
|
qml/SessionRow.qml
|
|
qml/MessageCard.qml
|
|
qml/ToolCard.qml
|
|
qml/ThinkBlock.qml
|
|
qml/NoteLine.qml
|
|
qml/TodoPanel.qml
|
|
qml/Composer.qml
|
|
qml/ConsentDialog.qml
|
|
qml/AskDialog.qml
|
|
qml/SettingsDialog.qml
|
|
qml/ThemeIcon.qml
|
|
qml/ImageStrip.qml
|
|
qml/Theme.qml)
|
|
|
|
# The icons the window asks for, compiled in (app/icons, Breeze's scalable
|
|
# names copied at 24@3x). The desktop finds them in the platform's icon
|
|
# theme and only falls back to these (ThemeIcons.cpp); a machine without
|
|
# any theme -- Android -- has nothing else, and before this the QML glyph
|
|
# fallback was all that showed, which the phone's fonts cannot render.
|
|
set(CST_APP_ICONS
|
|
go-down mail-attachment application-menu settings-configure edit-copy
|
|
edit-delete arrow-down arrow-right list-add task-complete box
|
|
document-edit text-x-generic folder-open edit-find code-context
|
|
internet-services format-list-unordered help-hint)
|
|
set(_cst_icon_files "")
|
|
foreach(_icon ${CST_APP_ICONS})
|
|
list(APPEND _cst_icon_files "${CMAKE_CURRENT_SOURCE_DIR}/icons/${_icon}.svg")
|
|
endforeach()
|
|
qt_add_resources(cst-agent-app "cst_icons"
|
|
PREFIX "/cst-icons"
|
|
BASE "${CMAKE_CURRENT_SOURCE_DIR}/icons"
|
|
FILES ${_cst_icon_files})
|
|
endif()
|
|
endif()
|