Lots of web workflow, C3 and title bar fixes

* Fixes #6221 - C3 hang on `import wifi`. Enabling the WiFi PHY was
  disabling USB. Now boards that use it set CONFIG_ESP_PHY_ENABLE_USB
  explicitly.
* Fixes #6655 - Allows pasting into the web serial page. Fixes reading
  more than 0xf bytes at a time.
* Fixes #6653 - Fixes web socket encoding of payloads >125 bytes. Can
  happen when printing a long string.
* Fixes C3 responsiveness when waiting for key to enter REPL. (It
  now correctly stops sleeping.)
* Disables title bar updates when in raw REPL. Related to #6548.
* Adds version to title bar.
This commit is contained in:
Scott Shawcroft 2022-07-27 17:00:38 -07:00
parent c4c15206e7
commit d6344812e8
No known key found for this signature in database
GPG Key ID: 0DFD512649C052DA
19 changed files with 262 additions and 71 deletions

4
main.c
View File

@ -57,6 +57,7 @@
#include "supervisor/shared/stack.h"
#include "supervisor/shared/status_leds.h"
#include "supervisor/shared/tick.h"
#include "supervisor/shared/title_bar.h"
#include "supervisor/shared/traceback.h"
#include "supervisor/shared/translate/translate.h"
#include "supervisor/shared/workflow.h"
@ -822,7 +823,9 @@ STATIC int run_repl(bool first_run) {
status_led_deinit();
#endif
if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) {
supervisor_title_bar_suspend();
exit_code = pyexec_raw_repl();
supervisor_title_bar_resume();
} else {
exit_code = pyexec_friendly_repl();
}
@ -914,6 +917,7 @@ int __attribute__((used)) main(void) {
run_boot_py(safe_mode);
supervisor_workflow_start();
supervisor_title_bar_start();
// Boot script is finished, so now go into REPL or run code.py.
int exit_code = PYEXEC_FORCED_EXIT;

View File

@ -30,6 +30,14 @@ CONFIG_PARTITION_TABLE_FILENAME="esp-idf-config/partitions-4MB-no-uf2.csv"
#
# Component config
#
#
#
# PHY
#
CONFIG_ESP_PHY_ENABLE_USB=y
# end of PHY
#
# ESP System Settings
#

View File

@ -1,3 +1,9 @@
#
# PHY
#
CONFIG_ESP_PHY_ENABLE_USB=y
# end of PHY
#
# LWIP
#

View File

@ -1,3 +1,9 @@
#
# PHY
#
CONFIG_ESP_PHY_ENABLE_USB=y
# end of PHY
#
# LWIP
#

View File

@ -44,6 +44,7 @@ wifi_radio_obj_t common_hal_wifi_radio_obj;
#include "components/log/include/esp_log.h"
#include "supervisor/port.h"
#include "supervisor/shared/title_bar.h"
#include "supervisor/workflow.h"
#include "esp_ipc.h"
@ -55,7 +56,7 @@ wifi_radio_obj_t common_hal_wifi_radio_obj;
static const char *TAG = "CP wifi";
STATIC void schedule_background_on_cp_core(void *arg) {
supervisor_workflow_request_background();
supervisor_title_bar_request_update(false);
// CircuitPython's VM is run in a separate FreeRTOS task from wifi callbacks. So, we have to
// notify the main task every time in case it's waiting for us.

@ -1 +1 @@
Subproject commit 0180c0cb80f052919badc15df164e8edde6344ad
Subproject commit ddb7ddbcb613a582e0a91eda8b1d2510dd0a2d83

View File

@ -30,7 +30,6 @@
#include "supervisor/board.h"
#include "supervisor/port.h"
#include "py/runtime.h"
#include "supervisor/esp_port.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@ -390,6 +389,14 @@ void port_wake_main_task() {
xTaskNotifyGive(circuitpython_task);
}
void port_wake_main_task_from_isr() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(circuitpython_task, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
void sleep_timer_cb(void *arg) {
port_wake_main_task();
}

View File

@ -28,19 +28,23 @@
#include "py/ringbuf.h"
#include "py/runtime.h"
#include "py/mphal.h"
#include "usb_serial_jtag.h"
#include "supervisor/port.h"
#include "supervisor/usb_serial_jtag.h"
#include "hal/usb_serial_jtag_ll.h"
#include "esp_intr_alloc.h"
#include "soc/periph_defs.h"
#include "supervisor/esp_port.h"
#define USB_SERIAL_JTAG_BUF_SIZE (64)
STATIC ringbuf_t ringbuf;
STATIC uint8_t buf[128];
STATIC bool connected;
STATIC volatile bool connected;
#if CIRCUITPY_ESP_USB_SERIAL_JTAG && !CONFIG_ESP_PHY_ENABLE_USB
#error "CONFIG_ESP_PHY_ENABLE_USB must be enabled in sdkconfig"
#endif
static void usb_serial_jtag_isr_handler(void *arg) {
uint32_t flags = usb_serial_jtag_ll_get_intsts_mask();
@ -49,6 +53,11 @@ static void usb_serial_jtag_isr_handler(void *arg) {
usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SOF);
}
if (flags & USB_SERIAL_JTAG_INTR_TOKEN_REC_IN_EP1) {
usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_TOKEN_REC_IN_EP1);
connected = true;
}
if (flags & USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT) {
usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT);
size_t req_len = ringbuf_num_empty(&ringbuf);
@ -64,25 +73,19 @@ static void usb_serial_jtag_isr_handler(void *arg) {
ringbuf_put(&ringbuf, rx_buf[i]);
}
}
vTaskNotifyGiveFromISR(circuitpython_task, NULL);
port_wake_main_task_from_isr();
}
}
void usb_serial_jtag_init(void) {
ringbuf_init(&ringbuf, buf, sizeof(buf));
usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SOF | USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT);
usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SOF | USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT);
usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SOF | USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT | USB_SERIAL_JTAG_INTR_TOKEN_REC_IN_EP1);
usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SOF | USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT | USB_SERIAL_JTAG_INTR_TOKEN_REC_IN_EP1);
ESP_ERROR_CHECK(esp_intr_alloc(ETS_USB_SERIAL_JTAG_INTR_SOURCE, ESP_INTR_FLAG_LEVEL1,
usb_serial_jtag_isr_handler, NULL, NULL));
}
bool usb_serial_jtag_connected(void) {
// Make connected sticky. Otherwise we'll be disconnected every time the SOF
// index is 0. (It's only ~15 bits so it wraps around frequently.)
if (connected) {
return true;
}
connected = USB_SERIAL_JTAG.fram_num.sof_frame_index > 0;
return connected;
}

View File

@ -99,8 +99,14 @@ void port_background_task(void);
void port_start_background_task(void);
void port_finish_background_task(void);
// Some ports need special handling to wake the main task from an interrupt
// context or other task. The port must implement the necessary code in this
// function. A default weak implementation is provided that does nothing.
// Some ports need special handling to wake the main task from another task. The
// port must implement the necessary code in this function. A default weak
// implementation is provided that does nothing.
void port_wake_main_task(void);
// Some ports need special handling to wake the main task from an interrupt
// context. The port must implement the necessary code in this function. A
// default weak implementation is provided that does nothing.
void port_wake_main_task_from_isr(void);
#endif // MICROPY_INCLUDED_SUPERVISOR_PORT_H

View File

@ -3,7 +3,7 @@
*
* The MIT License (MIT)
*
* Copyright (c) 2019 Lucian Copeland for Adafruit Industries
* Copyright (c) 2022 Scott Shawcroft for Adafruit Industries
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -24,12 +24,10 @@
* THE SOFTWARE.
*/
#ifndef MICROPY_INCLUDED_ESPRESSIF_SUPERVISOR_PORT_H
#define MICROPY_INCLUDED_ESPRESSIF_SUPERVISOR_PORT_H
#include "supervisor/port.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
MP_WEAK void port_wake_main_task(void) {
}
extern TaskHandle_t circuitpython_task;
#endif // MICROPY_INCLUDED_ESPRESSIF_SUPERVISOR_PORT_H
MP_WEAK void port_wake_main_task_from_isr(void) {
}

View File

@ -207,10 +207,17 @@ char serial_read(void) {
#if CIRCUITPY_WEB_WORKFLOW
if (websocket_available()) {
return websocket_read_char();
char c = websocket_read_char();
if (c != -1) {
return c;
}
}
#endif
if (port_serial_bytes_available() > 0) {
return port_serial_read();
}
#if CIRCUITPY_USB_CDC
if (!usb_cdc_console_enabled()) {
return -1;
@ -220,9 +227,6 @@ char serial_read(void) {
return (char)tud_cdc_read_char();
#endif
if (port_serial_bytes_available() > 0) {
return port_serial_read();
}
return -1;
}

View File

@ -0,0 +1,93 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2022 Scott Shawcroft for Adafruit Industries
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <stdbool.h>
#include "genhdr/mpversion.h"
#include "py/mpconfig.h"
#include "supervisor/background_callback.h"
#include "supervisor/serial.h"
#include "supervisor/shared/title_bar.h"
#if CIRCUITPY_WEB_WORKFLOW
#include "supervisor/shared/web_workflow/web_workflow.h"
#endif
static background_callback_t title_bar_background_cb;
static bool _forced_dirty = false;
static bool _suspended = false;
static void title_bar_background(void *data) {
if (_suspended) {
return;
}
bool dirty = _forced_dirty;
#if CIRCUITPY_WEB_WORKFLOW
dirty = dirty || supervisor_web_workflow_status_dirty();
#endif
if (!dirty) {
return;
}
_forced_dirty = false;
#if CIRCUITPY_STATUS_BAR
// Neighboring "" "" are concatenated by the compiler. Without this separation, the hex code
// doesn't get terminated after two following characters and the value is invalid.
// This is the OSC command to set the title and the icon text. It can be up to 255 characters
// but some may be cut off.
serial_write("\x1b" "]0;");
serial_write("🐍 ");
#if CIRCUITPY_WEB_WORKFLOW
supervisor_web_workflow_status();
#endif
serial_write("|");
serial_write(MICROPY_GIT_TAG);
// Send string terminator
serial_write("\x1b" "\\");
#endif
}
void supervisor_title_bar_start(void) {
title_bar_background_cb.fun = title_bar_background;
title_bar_background_cb.data = NULL;
supervisor_title_bar_request_update(true);
}
void supervisor_title_bar_request_update(bool force_dirty) {
if (force_dirty) {
_forced_dirty = true;
}
background_callback_add_core(&title_bar_background_cb);
}
void supervisor_title_bar_suspend(void) {
_suspended = true;
}
void supervisor_title_bar_resume(void) {
_suspended = false;
supervisor_title_bar_request_update(false);
}

View File

@ -0,0 +1,34 @@
/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2022 Scott Shawcroft for Adafruit Industries
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#pragma once
#include <stdbool.h>
void supervisor_title_bar_start(void);
void supervisor_title_bar_suspend(void);
void supervisor_title_bar_resume(void);
void supervisor_title_bar_request_update(bool force_dirty);

View File

@ -63,10 +63,12 @@ input.addEventListener("beforeinput", function(e) {
input.value = "";
input.focus();
e.preventDefault();
} else if (e.inputType == "insertText") {
} else if (e.inputType == "insertText" || e.inputType == "insertFromPaste") {
ws.send(e.data);
} else if (e.inputType == "deleteContentBackward") {
ws.send("\b");
} else {
console.log(e);
}
});

View File

@ -95,7 +95,12 @@ typedef struct {
char websocket_key[24 + 1];
} _request;
static wifi_radio_error_t wifi_status = WIFI_RADIO_ERROR_NONE;
static wifi_radio_error_t _wifi_status = WIFI_RADIO_ERROR_NONE;
// Store last status state to compute dirty.
static bool _last_enabled = false;
static uint32_t _last_ip = 0;
static wifi_radio_error_t _last_wifi_status = WIFI_RADIO_ERROR_NONE;
static mdns_server_obj_t mdns;
static uint32_t web_api_port = 80;
@ -108,6 +113,7 @@ static _request active_request;
static char _api_password[64];
// Store the encoded IP so we don't duplicate work.
static uint32_t _encoded_ip = 0;
static char _our_ip_encoded[4 * 4];
@ -170,25 +176,41 @@ static bool _base64_in_place(char *buf, size_t in_len, size_t out_len) {
return true;
}
STATIC void _update_encoded_ip(void) {
uint32_t ipv4_address = 0;
if (common_hal_wifi_radio_get_enabled(&common_hal_wifi_radio_obj)) {
ipv4_address = wifi_radio_get_ipv4_address(&common_hal_wifi_radio_obj);
}
if (_encoded_ip != ipv4_address) {
uint8_t *octets = (uint8_t *)&ipv4_address;
snprintf(_our_ip_encoded, sizeof(_our_ip_encoded), "%d.%d.%d.%d", octets[0], octets[1], octets[2], octets[3]);
_encoded_ip = ipv4_address;
}
}
bool supervisor_web_workflow_status_dirty(void) {
return common_hal_wifi_radio_get_enabled(&common_hal_wifi_radio_obj) != _last_enabled ||
_encoded_ip != _last_ip ||
_last_wifi_status != _wifi_status;
}
void supervisor_web_workflow_status(void) {
serial_write_compressed(translate("Wi-Fi: "));
if (common_hal_wifi_radio_get_enabled(&common_hal_wifi_radio_obj)) {
_last_enabled = common_hal_wifi_radio_get_enabled(&common_hal_wifi_radio_obj);
if (_last_enabled) {
uint32_t ipv4_address = wifi_radio_get_ipv4_address(&common_hal_wifi_radio_obj);
if (wifi_status == WIFI_RADIO_ERROR_AUTH_EXPIRE ||
wifi_status == WIFI_RADIO_ERROR_AUTH_FAIL) {
_last_wifi_status = _wifi_status;
if (_wifi_status == WIFI_RADIO_ERROR_AUTH_EXPIRE ||
_wifi_status == WIFI_RADIO_ERROR_AUTH_FAIL) {
serial_write_compressed(translate("Authentication failure"));
} else if (wifi_status != WIFI_RADIO_ERROR_NONE) {
mp_printf(&mp_plat_print, "%d", wifi_status);
} else if (_wifi_status != WIFI_RADIO_ERROR_NONE) {
mp_printf(&mp_plat_print, "%d", _wifi_status);
} else if (ipv4_address == 0) {
_last_ip = 0;
serial_write_compressed(translate("No IP"));
} else {
if (_encoded_ip != ipv4_address) {
uint8_t *octets = (uint8_t *)&ipv4_address;
snprintf(_our_ip_encoded, sizeof(_our_ip_encoded), "%d.%d.%d.%d", octets[0], octets[1], octets[2], octets[3]);
_encoded_ip = ipv4_address;
}
_update_encoded_ip();
_last_ip = _encoded_ip;
mp_printf(&mp_plat_print, "%s", _our_ip_encoded);
if (web_api_port != 80) {
mp_printf(&mp_plat_print, ":%d", web_api_port);
@ -231,11 +253,11 @@ void supervisor_start_web_workflow(void) {
// We can all connect again because it will return early if we're already connected to the
// network. If we are connected to a different network, then it will disconnect before
// attempting to connect to the given network.
wifi_status = common_hal_wifi_radio_connect(
_wifi_status = common_hal_wifi_radio_connect(
&common_hal_wifi_radio_obj, (uint8_t *)ssid, ssid_len, (uint8_t *)password, password_len,
0, 0.1, NULL, 0);
if (wifi_status != WIFI_RADIO_ERROR_NONE) {
if (_wifi_status != WIFI_RADIO_ERROR_NONE) {
common_hal_wifi_radio_set_enabled(&common_hal_wifi_radio_obj, false);
return;
}
@ -290,8 +312,6 @@ void supervisor_start_web_workflow(void) {
}
// TODO:
// GET /cp/serial.txt
// - Most recent 1k of serial output.
// GET /edit/
// - Super basic editor
#endif
@ -406,6 +426,7 @@ static bool _origin_ok(const char *origin) {
return true;
}
_update_encoded_ip();
end = origin + strlen(http) + strlen(_our_ip_encoded);
if (strncmp(origin + strlen(http), _our_ip_encoded, strlen(_our_ip_encoded)) == 0 &&
(end[0] == '\0' || end[0] == ':')) {
@ -710,6 +731,7 @@ static void _reply_with_version_json(socketpool_socket_obj_t *socket, _request *
mp_print_t _socket_print = {socket, _print_chunk};
const char *hostname = common_hal_mdns_server_get_hostname(&mdns);
_update_encoded_ip();
// Note: this leverages the fact that C concats consecutive string literals together.
mp_printf(&_socket_print,
"{\"web_api_version\": 1, "

View File

@ -31,6 +31,7 @@
// This background function should be called repeatedly. It cannot be done based
// on events.
void supervisor_web_workflow_background(void);
bool supervisor_web_workflow_status_dirty(void);
void supervisor_web_workflow_status(void);
void supervisor_start_web_workflow(void);
void supervisor_stop_web_workflow(void);

View File

@ -26,6 +26,8 @@
#include "supervisor/shared/web_workflow/websocket.h"
#include "supervisor/shared/title_bar.h"
// TODO: Remove ESP specific stuff. For now, it is useful as we refine the server.
#include "esp_log.h"
@ -59,6 +61,9 @@ void websocket_handoff(socketpool_socket_obj_t *socket) {
// Mark the original socket object as closed without telling the lower level.
socket->connected = false;
socket->num = -1;
// Send the title bar for the new client.
supervisor_title_bar_request_update(true);
}
bool websocket_connected(void) {
@ -94,7 +99,7 @@ static void _read_next_frame_header(void) {
}
if (cp_serial.frame_index == 1 && _read_byte(&h)) {
cp_serial.frame_index++;
uint8_t len = h & 0xf;
uint8_t len = h & 0x7f;
cp_serial.masked = (h >> 7) == 1;
if (len <= 125) {
cp_serial.payload_remaining = len;
@ -194,7 +199,9 @@ bool websocket_available(void) {
char websocket_read_char(void) {
uint8_t c;
_read_next_payload_byte(&c);
if (!_read_next_payload_byte(&c)) {
c = -1;
}
return c;
}
@ -215,13 +222,20 @@ static void _websocket_send(_websocket *ws, const char *text, size_t len) {
}
frame_header[1] = payload_len;
_send_raw(&ws->socket, (const uint8_t *)frame_header, 2);
uint8_t extended_len[4];
if (payload_len == 126) {
_send_raw(&ws->socket, (const uint8_t *)&len, 2);
extended_len[0] = (len >> 8) & 0xff;
extended_len[1] = len & 0xff;
_send_raw(&ws->socket, extended_len, 2);
} else if (payload_len == 127) {
uint32_t zero = 0;
// 64 bits where top four bytes are zero.
_send_raw(&ws->socket, (const uint8_t *)&zero, 4);
_send_raw(&ws->socket, (const uint8_t *)&len, 4);
extended_len[0] = (len >> 24) & 0xff;
extended_len[1] = (len >> 16) & 0xff;
extended_len[2] = (len >> 8) & 0xff;
extended_len[3] = len & 0xff;
_send_raw(&ws->socket, extended_len, 4);
}
_send_raw(&ws->socket, (const uint8_t *)text, len);
char copy[len];

View File

@ -46,27 +46,7 @@
#endif
static background_callback_t workflow_background_cb;
#if CIRCUITPY_STATUS_BAR
static void supervisor_workflow_update_status_bar(void) {
// Neighboring "" "" are concatenated by the compiler. Without this separation, the hex code
// doesn't get terminated after two following characters and the value is invalid.
// This is the OSC command to set the title and the icon text. It can be up to 255 characters
// but some may be cut off.
serial_write("\x1b" "]0;");
serial_write("🐍 ");
#if CIRCUITPY_WEB_WORKFLOW
supervisor_web_workflow_status();
#endif
// Send string terminator
serial_write("\x1b" "\\");
}
#endif
static void workflow_background(void *data) {
#if CIRCUITPY_STATUS_BAR
supervisor_workflow_update_status_bar();
#endif
#if CIRCUITPY_WEB_WORKFLOW
supervisor_web_workflow_background();
#endif

View File

@ -8,12 +8,14 @@ SRC_SUPERVISOR = \
supervisor/shared/lock.c \
supervisor/shared/memory.c \
supervisor/shared/micropython.c \
supervisor/shared/port.c \
supervisor/shared/reload.c \
supervisor/shared/safe_mode.c \
supervisor/shared/serial.c \
supervisor/shared/stack.c \
supervisor/shared/status_leds.c \
supervisor/shared/tick.c \
supervisor/shared/title_bar.c \
supervisor/shared/traceback.c \
supervisor/shared/translate/translate.c \
supervisor/shared/workflow.c