Compare commits
5 Commits
a8e3040d1e
...
82a3380426
| Author | SHA1 | Date | |
|---|---|---|---|
| 82a3380426 | |||
| c2698e6a22 | |||
| d929f8e2f1 | |||
| 32b838f9c4 | |||
| 2154dba080 |
@ -175,10 +175,13 @@ mod tests {
|
|||||||
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
|
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
|
||||||
use prometeu_drivers::hardware::Hardware;
|
use prometeu_drivers::hardware::Hardware;
|
||||||
use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource};
|
use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource};
|
||||||
|
use prometeu_hal::color::Color;
|
||||||
|
use prometeu_hal::primitives::Rect;
|
||||||
use prometeu_hal::syscalls::caps;
|
use prometeu_hal::syscalls::caps;
|
||||||
use prometeu_system::CrashReport;
|
use prometeu_system::CrashReport;
|
||||||
use prometeu_system::process::ProcessState;
|
use prometeu_system::process::ProcessState;
|
||||||
use prometeu_system::task::TaskState;
|
use prometeu_system::task::{TaskId, TaskState};
|
||||||
|
use prometeu_system::windows::WindowOwner;
|
||||||
|
|
||||||
fn halting_program() -> Vec<u8> {
|
fn halting_program() -> Vec<u8> {
|
||||||
let code = assemble("HALT").expect("assemble");
|
let code = assemble("HALT").expect("assemble");
|
||||||
@ -262,6 +265,34 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_shell_running_firmware() -> (Firmware, Hardware, InputSignals, TaskId) {
|
||||||
|
let mut firmware = Firmware::new(None);
|
||||||
|
let mut hardware = Hardware::new();
|
||||||
|
let signals = InputSignals::default();
|
||||||
|
|
||||||
|
firmware.load_cartridge(valid_cartridge(AppMode::Shell));
|
||||||
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
|
let task_id = match &firmware.state {
|
||||||
|
FirmwareState::ShellRunning(step) => step.task_id,
|
||||||
|
other => panic!("expected ShellRunning state, got {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
(firmware, hardware, signals, task_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_window(firmware: &mut Firmware, owner: WindowOwner) {
|
||||||
|
let mut windows = firmware.os.windows();
|
||||||
|
windows.remove_all_windows();
|
||||||
|
let id = windows.add_window(
|
||||||
|
"Test Window".to_string(),
|
||||||
|
owner,
|
||||||
|
Rect { x: 0, y: 0, w: 10, h: 10 },
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
windows.set_focus(id);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() {
|
fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() {
|
||||||
let mut firmware = Firmware::new(None);
|
let mut firmware = Firmware::new(None);
|
||||||
@ -362,14 +393,79 @@ mod tests {
|
|||||||
|
|
||||||
firmware.load_cartridge(valid_cartridge(AppMode::Shell));
|
firmware.load_cartridge(valid_cartridge(AppMode::Shell));
|
||||||
firmware.tick(&signals, &mut hardware);
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
let task_id = match &firmware.state {
|
||||||
|
FirmwareState::ShellRunning(step) => step.task_id,
|
||||||
|
other => panic!("expected ShellRunning state, got {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
let tick_index_before_system_update = firmware.os.vm().tick_index();
|
let tick_index_before_system_update = firmware.os.vm().tick_index();
|
||||||
firmware.tick(&signals, &mut hardware);
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
assert!(matches!(firmware.state, FirmwareState::ShellRunning(_)));
|
assert!(matches!(firmware.state, FirmwareState::ShellRunning(_)));
|
||||||
|
assert!(firmware.os.windows().focused_window_belongs_to_task(task_id));
|
||||||
assert_eq!(firmware.os.vm().tick_index(), tick_index_before_system_update + 1);
|
assert_eq!(firmware.os.vm().tick_index(), tick_index_before_system_update + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_running_closes_task_and_returns_hub_home_without_focused_window() {
|
||||||
|
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();
|
||||||
|
|
||||||
|
firmware.os.windows().remove_all_windows();
|
||||||
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
|
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||||
|
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
|
||||||
|
assert_eq!(
|
||||||
|
firmware.os.lifecycle().process_state_for_task(task_id),
|
||||||
|
Ok(ProcessState::Stopped)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_running_closes_task_and_returns_hub_home_for_focused_hub_window() {
|
||||||
|
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();
|
||||||
|
|
||||||
|
focus_window(&mut firmware, WindowOwner::Hub);
|
||||||
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
|
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||||
|
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
|
||||||
|
assert_eq!(
|
||||||
|
firmware.os.lifecycle().process_state_for_task(task_id),
|
||||||
|
Ok(ProcessState::Stopped)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_running_closes_task_and_returns_hub_home_for_focused_overlay_window() {
|
||||||
|
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();
|
||||||
|
|
||||||
|
focus_window(&mut firmware, WindowOwner::Overlay);
|
||||||
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
|
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||||
|
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
|
||||||
|
assert_eq!(
|
||||||
|
firmware.os.lifecycle().process_state_for_task(task_id),
|
||||||
|
Ok(ProcessState::Stopped)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_running_closes_task_and_returns_hub_home_for_other_task_window() {
|
||||||
|
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();
|
||||||
|
|
||||||
|
focus_window(&mut firmware, WindowOwner::Task(TaskId(task_id.0 + 1)));
|
||||||
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
|
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||||
|
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
|
||||||
|
assert_eq!(
|
||||||
|
firmware.os.lifecycle().process_state_for_task(task_id),
|
||||||
|
Ok(ProcessState::Stopped)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_cartridge_routes_game_apps_to_game_running() {
|
fn load_cartridge_routes_game_apps_to_game_running() {
|
||||||
let mut firmware = Firmware::new(None);
|
let mut firmware = Firmware::new(None);
|
||||||
@ -380,5 +476,6 @@ mod tests {
|
|||||||
firmware.tick(&signals, &mut hardware);
|
firmware.tick(&signals, &mut hardware);
|
||||||
|
|
||||||
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
|
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
|
||||||
|
assert_eq!(firmware.os.windows().window_count(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,10 +66,13 @@ impl ShellRunningStep {
|
|||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !outcome.focused_window_active {
|
if !ctx.os.windows().focused_window_belongs_to_task(self.task_id) {
|
||||||
ctx.os.info(
|
ctx.os.info(
|
||||||
LogSource::Hub,
|
LogSource::Hub,
|
||||||
format!("Closing shell task {:?}: no focused window active", self.task_id),
|
format!(
|
||||||
|
"Closing shell task {:?}: focused window does not belong to task",
|
||||||
|
self.task_id,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if let Err(error) = ctx.os.lifecycle().close_task(self.task_id) {
|
if let Err(error) = ctx.os.lifecycle().close_task(self.task_id) {
|
||||||
ctx.os.error(
|
ctx.os.error(
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use crate::os::SystemOS;
|
use crate::os::SystemOS;
|
||||||
|
use crate::task::TaskId;
|
||||||
use crate::windows::{Window, WindowId, WindowOwner};
|
use crate::windows::{Window, WindowId, WindowOwner};
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::primitives::Rect;
|
use prometeu_hal::primitives::Rect;
|
||||||
@ -26,6 +27,10 @@ impl<'a> WindowFacade<'a> {
|
|||||||
self.os.window_manager.focused
|
self.os.window_manager.focused
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focused_window_belongs_to_task(&self, task_id: TaskId) -> bool {
|
||||||
|
self.os.window_manager.focused_window_belongs_to_task(task_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn close_window(&mut self, id: WindowId) {
|
pub fn close_window(&mut self, id: WindowId) {
|
||||||
self.os.window_manager.remove_window(id);
|
self.os.window_manager.remove_window(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,6 @@ pub struct PrometeuHub;
|
|||||||
|
|
||||||
pub struct SystemProfileUpdate {
|
pub struct SystemProfileUpdate {
|
||||||
pub crash: Option<CrashReport>,
|
pub crash: Option<CrashReport>,
|
||||||
pub focused_window_active: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PrometeuHub {
|
impl Default for PrometeuHub {
|
||||||
@ -103,9 +102,6 @@ impl PrometeuHub {
|
|||||||
self.render(os, hw);
|
self.render(os, hw);
|
||||||
hw.gfx_mut().present();
|
hw.gfx_mut().present();
|
||||||
|
|
||||||
SystemProfileUpdate {
|
SystemProfileUpdate { crash }
|
||||||
crash,
|
|
||||||
focused_window_active: os.windows().focused_window().is_some(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use crate::services::windows::window::{Window, WindowId};
|
use crate::services::windows::window::{Window, WindowId};
|
||||||
|
use crate::task::TaskId;
|
||||||
use crate::windows::WindowOwner;
|
use crate::windows::WindowOwner;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::primitives::Rect;
|
use prometeu_hal::primitives::Rect;
|
||||||
@ -51,6 +52,17 @@ impl WindowManager {
|
|||||||
window.has_focus = window.id == id;
|
window.has_focus = window.id == id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focused_window_belongs_to_task(&self, task_id: TaskId) -> bool {
|
||||||
|
let Some(focused_id) = self.focused else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.windows
|
||||||
|
.iter()
|
||||||
|
.find(|window| window.id == focused_id)
|
||||||
|
.is_some_and(|window| window.owner == WindowOwner::Task(task_id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -103,4 +115,91 @@ mod tests {
|
|||||||
assert_eq!(wm.windows.len(), 0);
|
assert_eq!(wm.windows.len(), 0);
|
||||||
assert_eq!(wm.focused, None);
|
assert_eq!(wm.focused, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_window_belongs_to_task_returns_false_without_focus() {
|
||||||
|
let mut wm = WindowManager::new();
|
||||||
|
wm.add_window(
|
||||||
|
"Task Window".to_string(),
|
||||||
|
WindowOwner::Task(crate::task::TaskId(1)),
|
||||||
|
Rect { x: 0, y: 0, w: 10, h: 10 },
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!wm.focused_window_belongs_to_task(crate::task::TaskId(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_window_belongs_to_task_matches_focused_task_owner() {
|
||||||
|
let mut wm = WindowManager::new();
|
||||||
|
let id = wm.add_window(
|
||||||
|
"Task Window".to_string(),
|
||||||
|
WindowOwner::Task(crate::task::TaskId(1)),
|
||||||
|
Rect { x: 0, y: 0, w: 10, h: 10 },
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
wm.set_focus(id);
|
||||||
|
|
||||||
|
assert!(wm.focused_window_belongs_to_task(crate::task::TaskId(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_window_belongs_to_task_rejects_other_task_owner() {
|
||||||
|
let mut wm = WindowManager::new();
|
||||||
|
let id = wm.add_window(
|
||||||
|
"Task Window".to_string(),
|
||||||
|
WindowOwner::Task(crate::task::TaskId(2)),
|
||||||
|
Rect { x: 0, y: 0, w: 10, h: 10 },
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
wm.set_focus(id);
|
||||||
|
|
||||||
|
assert!(!wm.focused_window_belongs_to_task(crate::task::TaskId(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_window_belongs_to_task_rejects_hub_owner() {
|
||||||
|
let mut wm = WindowManager::new();
|
||||||
|
let id = wm.add_window(
|
||||||
|
"Hub Window".to_string(),
|
||||||
|
WindowOwner::Hub,
|
||||||
|
Rect { x: 0, y: 0, w: 10, h: 10 },
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
wm.set_focus(id);
|
||||||
|
|
||||||
|
assert!(!wm.focused_window_belongs_to_task(crate::task::TaskId(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_window_belongs_to_task_rejects_overlay_owner() {
|
||||||
|
let mut wm = WindowManager::new();
|
||||||
|
let id = wm.add_window(
|
||||||
|
"Overlay Window".to_string(),
|
||||||
|
WindowOwner::Overlay,
|
||||||
|
Rect { x: 0, y: 0, w: 10, h: 10 },
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
wm.set_focus(id);
|
||||||
|
|
||||||
|
assert!(!wm.focused_window_belongs_to_task(crate::task::TaskId(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_window_belongs_to_task_rejects_stale_focus() {
|
||||||
|
let mut wm = WindowManager::new();
|
||||||
|
wm.add_window(
|
||||||
|
"Task Window".to_string(),
|
||||||
|
WindowOwner::Task(crate::task::TaskId(1)),
|
||||||
|
Rect { x: 0, y: 0, w: 10, h: 10 },
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
wm.focused = Some(WindowId(999));
|
||||||
|
|
||||||
|
assert!(!wm.focused_window_belongs_to_task(crate::task::TaskId(1)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{"type":"meta","next_id":{"DSC":35,"AGD":35,"DEC":27,"PLN":59,"LSN":44,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":36,"AGD":36,"DEC":28,"PLN":62,"LSN":45,"CLSN":1}}
|
||||||
|
{"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
|
||||||
{"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
|
{"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
|
||||||
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"discussion/lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"discussion/lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
id: LSN-0044
|
||||||
|
ticket: task-owned-shell-windows
|
||||||
|
title: Task Window Liveness Belongs to the Task
|
||||||
|
created: 2026-05-15
|
||||||
|
tags: [runtime, os, task, window-manager, shell, lifecycle]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task Window Liveness Belongs to the Task
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The Prometeu OS model separates execution, navigation, and visual presence:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Process = technical execution
|
||||||
|
Task = navigable presence
|
||||||
|
Window = visual presence of the task
|
||||||
|
```
|
||||||
|
|
||||||
|
Before `DSC-0035`, shell execution used a generic `focused_window_active`
|
||||||
|
signal. That signal only answered whether any window was focused. It did not
|
||||||
|
prove that the focused window represented the shell task currently driven by
|
||||||
|
`ShellRunningStep`.
|
||||||
|
|
||||||
|
The implemented contract replaced that generic signal with a task-owned window
|
||||||
|
predicate:
|
||||||
|
|
||||||
|
```text
|
||||||
|
os.windows().focused_window_belongs_to_task(task_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Shell liveness requires the task's own focused window
|
||||||
|
|
||||||
|
**What:** `ShellRunningStep` continues only when the task is foreground and the
|
||||||
|
global focused window is owned by `WindowOwner::Task(task_id)`.
|
||||||
|
|
||||||
|
**Why:** A shell task in foreground must have its own visual presence. Any
|
||||||
|
focused window is not enough; the focused window must belong to the same task.
|
||||||
|
|
||||||
|
**Trade-offs:** This v1 rule is strict. It does not preserve a shell task under
|
||||||
|
focused overlays, minimized windows, background execution, or app switcher
|
||||||
|
state. Those behaviors need their own explicit contracts.
|
||||||
|
|
||||||
|
### Window ownership is queried through the window facade
|
||||||
|
|
||||||
|
**What:** `WindowFacade` exposes
|
||||||
|
`focused_window_belongs_to_task(task_id)`.
|
||||||
|
|
||||||
|
**Why:** The question is about the global focused window and its owner. It is a
|
||||||
|
window-domain query, not a lifecycle transition.
|
||||||
|
|
||||||
|
**Trade-offs:** Firmware still coordinates across domains: it asks the window
|
||||||
|
facade for ownership and then calls lifecycle when the predicate is false.
|
||||||
|
|
||||||
|
### Lifecycle still owns state transitions
|
||||||
|
|
||||||
|
**What:** When the shell task loses its eligible focused window,
|
||||||
|
`ShellRunningStep` closes the task through `os.lifecycle().close_task(task_id)`
|
||||||
|
before returning to `HubHome`.
|
||||||
|
|
||||||
|
**Why:** Returning to Hub without lifecycle closure would leave task and process
|
||||||
|
state dependent on implicit cleanup.
|
||||||
|
|
||||||
|
**Trade-offs:** A visual close becomes a lifecycle event for foreground shell
|
||||||
|
apps. That is correct for v1 shell apps, but it must not be generalized to
|
||||||
|
background services without a separate decision.
|
||||||
|
|
||||||
|
## Patterns and Algorithms
|
||||||
|
|
||||||
|
Use this split when OS behavior crosses services:
|
||||||
|
|
||||||
|
```text
|
||||||
|
WindowFacade
|
||||||
|
answers questions about windows, focus, and ownership
|
||||||
|
|
||||||
|
LifecycleFacade
|
||||||
|
changes TaskState and ProcessState
|
||||||
|
|
||||||
|
Firmware step
|
||||||
|
coordinates policy between the two domains
|
||||||
|
```
|
||||||
|
|
||||||
|
The useful pattern is not "windows close tasks". The pattern is:
|
||||||
|
|
||||||
|
1. derive a visual-presence predicate from the window domain;
|
||||||
|
2. let the firmware step decide what the predicate means in that state;
|
||||||
|
3. perform lifecycle transitions through `SystemOS` lifecycle.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Do not use a generic "some window is focused" signal as a task liveness
|
||||||
|
contract.
|
||||||
|
- Do not treat `Hub` or `Overlay` windows as shell task windows.
|
||||||
|
- Do not model game cartridges as `WindowManager` windows. Games remain
|
||||||
|
fullscreen `GameRunningStep` sessions.
|
||||||
|
- Do not add overlay preservation, minimization, app switcher, or background
|
||||||
|
service behavior as incidental side effects of shell liveness.
|
||||||
|
- Do not return to Hub without closing the shell task through lifecycle.
|
||||||
|
|
||||||
|
## Takeaways
|
||||||
|
|
||||||
|
- Task-owned shell windows bind shell liveness to the task's own visual
|
||||||
|
presence.
|
||||||
|
- Ownership predicates belong in the window facade; state mutation belongs in
|
||||||
|
lifecycle.
|
||||||
|
- Strict v1 semantics are easier to evolve than an implicit "active window"
|
||||||
|
concept.
|
||||||
|
- Future overlay or background behavior should be introduced by explicit
|
||||||
|
policy, not by weakening the shell liveness predicate.
|
||||||
@ -31,6 +31,14 @@ The VM does not own the machine lifecycle. Firmware does.
|
|||||||
- **AppMode**: cartridge mode, currently `Game` or `System`.
|
- **AppMode**: cartridge mode, currently `Game` or `System`.
|
||||||
- **Logical frame**: execution unit completed when the app reaches `FRAME_SYNC`.
|
- **Logical frame**: execution unit completed when the app reaches `FRAME_SYNC`.
|
||||||
- **Host tick**: the host-driven outer update tick.
|
- **Host tick**: the host-driven outer update tick.
|
||||||
|
- **Process**: technical execution owned by POS lifecycle.
|
||||||
|
- **Task**: navigable presence associated with an app flow.
|
||||||
|
- **Window**: visual presence for a shell/system task when that task is
|
||||||
|
represented by the system window model.
|
||||||
|
- **WindowOwner**: ownership marker for a system window. The v1 owners are
|
||||||
|
`Hub`, `Task(TaskId)`, and `Overlay`.
|
||||||
|
- **Shell task window**: a `WindowManager` window whose owner is
|
||||||
|
`WindowOwner::Task(TaskId)`.
|
||||||
|
|
||||||
## 3 POS Responsibilities
|
## 3 POS Responsibilities
|
||||||
|
|
||||||
@ -69,6 +77,8 @@ Game-mode cartridges:
|
|||||||
- transition firmware into the game-running state;
|
- transition firmware into the game-running state;
|
||||||
- run as the active app flow;
|
- run as the active app flow;
|
||||||
- present through the main frame path.
|
- present through the main frame path.
|
||||||
|
- run as fullscreen firmware sessions;
|
||||||
|
- are not `WindowManager` windows.
|
||||||
|
|
||||||
### System
|
### System
|
||||||
|
|
||||||
@ -77,8 +87,59 @@ System-mode cartridges:
|
|||||||
- are initialized by POS;
|
- are initialized by POS;
|
||||||
- are integrated into the Hub/window environment;
|
- are integrated into the Hub/window environment;
|
||||||
- do not replace the firmware state with the game-running path.
|
- do not replace the firmware state with the game-running path.
|
||||||
|
- are represented as shell task windows when they expose a visual shell
|
||||||
|
presence.
|
||||||
|
|
||||||
## 6 Cartridge Load Flow
|
## 6 Task-Owned Shell Windows
|
||||||
|
|
||||||
|
The shell window contract separates execution, navigation, and visual
|
||||||
|
presence:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Process = technical execution
|
||||||
|
Task = navigable presence
|
||||||
|
Window = visual presence of the task
|
||||||
|
```
|
||||||
|
|
||||||
|
For shell/system apps managed by the window model, the visible shell presence
|
||||||
|
must be a `WindowManager` window owned by `WindowOwner::Task(TaskId)`.
|
||||||
|
|
||||||
|
`Hub` windows are not shell task windows. `Overlay` windows are not shell task
|
||||||
|
windows in this v1 contract. Overlay/modal policy, minimization, app switching,
|
||||||
|
dock behavior, background services, and complex multi-window semantics are
|
||||||
|
future contracts and are not defined here.
|
||||||
|
|
||||||
|
Game cartridges must not be represented as `WindowManager` windows. They remain
|
||||||
|
fullscreen sessions driven by `GameRunningStep`.
|
||||||
|
|
||||||
|
For v1, `ShellRunningStep` continues a shell task only while both conditions are
|
||||||
|
true:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TaskState::Foreground
|
||||||
|
+
|
||||||
|
focused window owner == WindowOwner::Task(task_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
The runtime query for the focused-window ownership predicate is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
os.windows().focused_window_belongs_to_task(task_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
If a foreground shell task loses its eligible focused window,
|
||||||
|
`ShellRunningStep` must close the task through `SystemOS` lifecycle before
|
||||||
|
returning to Hub:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Shell task loses its eligible focused window
|
||||||
|
-> os.lifecycle().close_task(task_id)
|
||||||
|
-> TaskState::Closed
|
||||||
|
-> ProcessState::Stopped
|
||||||
|
-> FirmwareState::HubHome
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7 Cartridge Load Flow
|
||||||
|
|
||||||
Current high-level flow:
|
Current high-level flow:
|
||||||
|
|
||||||
@ -93,7 +154,7 @@ If VM initialization fails, firmware transitions to the crash path.
|
|||||||
|
|
||||||
POS selects the cartridge and its execution context, but the cartridge's initial callable is not chosen by firmware metadata. Execution starts from the cartridge boot protocol defined in [`13-cartridge.md`](13-cartridge.md), currently `func_id = 0`.
|
POS selects the cartridge and its execution context, but the cartridge's initial callable is not chosen by firmware metadata. Execution starts from the cartridge boot protocol defined in [`13-cartridge.md`](13-cartridge.md), currently `func_id = 0`.
|
||||||
|
|
||||||
## 7 Firmware States
|
## 8 Firmware States
|
||||||
|
|
||||||
The current firmware state model includes:
|
The current firmware state model includes:
|
||||||
|
|
||||||
@ -107,7 +168,7 @@ The current firmware state model includes:
|
|||||||
|
|
||||||
These states express machine orchestration above the VM. They are not guest-visible bytecode states.
|
These states express machine orchestration above the VM. They are not guest-visible bytecode states.
|
||||||
|
|
||||||
## 8 Execution Contract
|
## 9 Execution Contract
|
||||||
|
|
||||||
For game-mode execution, firmware/runtime coordination preserves:
|
For game-mode execution, firmware/runtime coordination preserves:
|
||||||
|
|
||||||
@ -118,7 +179,7 @@ For game-mode execution, firmware/runtime coordination preserves:
|
|||||||
|
|
||||||
This keeps the machine model deterministic and observable.
|
This keeps the machine model deterministic and observable.
|
||||||
|
|
||||||
## 9 Crash Handling
|
## 10 Crash Handling
|
||||||
|
|
||||||
Firmware owns terminal fault presentation.
|
Firmware owns terminal fault presentation.
|
||||||
|
|
||||||
@ -130,7 +191,7 @@ When a terminal app fault occurs:
|
|||||||
|
|
||||||
Crash handling is outside the guest VM execution model.
|
Crash handling is outside the guest VM execution model.
|
||||||
|
|
||||||
## 10 Relationship to Other Specs
|
## 11 Relationship to Other Specs
|
||||||
|
|
||||||
- [`02-vm-instruction-set.md`](02-vm-instruction-set.md) defines the VM subsystem run by firmware.
|
- [`02-vm-instruction-set.md`](02-vm-instruction-set.md) defines the VM subsystem run by firmware.
|
||||||
- [`09-events-and-concurrency.md`](09-events-and-concurrency.md) defines the frame-boundary model used by firmware.
|
- [`09-events-and-concurrency.md`](09-events-and-concurrency.md) defines the frame-boundary model used by firmware.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user