implements PLN-0163
This commit is contained in:
parent
fa0a768b84
commit
73c8246c8d
@ -1099,6 +1099,64 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_kill_control_click_terminates_resident_game_and_stays_home() {
|
||||
let library = discover_games_root(repository_test_cartridges_root());
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.hub.set_game_library(library);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_click =
|
||||
InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() };
|
||||
firmware.tick(&stress_click, &mut platform);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_task = match &firmware.state {
|
||||
FirmwareState::GameRunning(step) => step.task_id,
|
||||
other => panic!("expected Stress GameRunning state, got {:?}", other),
|
||||
};
|
||||
|
||||
firmware.request_home_from_host();
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(stress_task));
|
||||
|
||||
let kill_click =
|
||||
InputSignals { f_signal: true, x_pos: 300, y_pos: 240, ..Default::default() };
|
||||
firmware.tick(&kill_click, &mut platform);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), None);
|
||||
assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed));
|
||||
assert_eq!(
|
||||
firmware.os.lifecycle().process_state_for_task(stress_task),
|
||||
Ok(ProcessState::Stopped)
|
||||
);
|
||||
assert_eq!(
|
||||
firmware.os.sessions().vm_session_for_task(stress_task).map(|session| session.task_id),
|
||||
None
|
||||
);
|
||||
assert!(!firmware.game_lifecycle_audio_paused());
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
firmware.os.recent_logs(32).iter().any(|event| {
|
||||
event.level == LogLevel::Info
|
||||
&& event.source == LogSource::Hub
|
||||
&& event.msg == "Resident game killed from Hub: Stress Console"
|
||||
}),
|
||||
"Hub kill control should use the canonical manual kill log"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_game_switch_returns_home_without_restoring_previous_game() {
|
||||
let root = TestGameRoot::new();
|
||||
|
||||
@ -106,6 +106,18 @@ impl HubHomeStep {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(SystemProfileAction::KillResidentGame) => {
|
||||
if let Err(error) =
|
||||
resident_game_termination::terminate_resident_game(ctx.os, ctx.platform, true)
|
||||
{
|
||||
ctx.os.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Hub,
|
||||
0,
|
||||
format!("Failed to kill resident game from Hub: {error:?}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(SystemProfileAction::CloseShell) | None => {}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use crate::task::TaskId;
|
||||
use crate::{CrashReport, GameLibrary, GameSwitchTarget, SystemOS};
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::primitives::Rect;
|
||||
use prometeu_hal::{
|
||||
@ -19,6 +20,7 @@ const GAME_ROW_W: i32 = 336;
|
||||
const GAME_ROW_H: i32 = 22;
|
||||
const GAME_ROW_GAP: i32 = 6;
|
||||
const MAX_HOME_GAME_ROWS: usize = 3;
|
||||
const SUSPENDED_GAME_KILL_CONTROL: Rect = Rect { x: 288, y: 230, w: 168, h: 24 };
|
||||
#[cfg(test)]
|
||||
const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 };
|
||||
|
||||
@ -31,6 +33,8 @@ const COLOR_TEXT: Color = Color::rgb(230, 236, 224);
|
||||
const COLOR_MUTED: Color = Color::rgb(91, 112, 128);
|
||||
const COLOR_BUTTON: Color = Color::rgb(45, 59, 82);
|
||||
const COLOR_BUTTON_ACTIVE: Color = Color::rgb(72, 91, 122);
|
||||
const COLOR_KILL: Color = Color::rgb(164, 32, 40);
|
||||
const COLOR_KILL_ACTIVE: Color = Color::rgb(206, 46, 54);
|
||||
|
||||
/// PrometeuHub: Launcher and system UI environment.
|
||||
pub struct PrometeuHub {
|
||||
@ -83,6 +87,7 @@ impl NativeShellApp {
|
||||
pub enum SystemProfileAction {
|
||||
LaunchNativeShell(NativeShellApp),
|
||||
LaunchGame { target: GameSwitchTarget },
|
||||
KillResidentGame,
|
||||
CloseShell,
|
||||
}
|
||||
|
||||
@ -131,12 +136,14 @@ impl PrometeuHub {
|
||||
}
|
||||
|
||||
let input = platform.input();
|
||||
let suspended_game = if in_shell { None } else { resident_game_indicator(os) };
|
||||
if let Some(action) = action_for_click(
|
||||
in_shell,
|
||||
input.touch().x(),
|
||||
input.touch().y(),
|
||||
input.touch().f().pressed,
|
||||
self.game_library.entries(),
|
||||
suspended_game.as_ref(),
|
||||
) {
|
||||
return Some(action);
|
||||
}
|
||||
@ -149,7 +156,13 @@ impl PrometeuHub {
|
||||
let pointer_y = platform.input().touch().y();
|
||||
|
||||
let packet = if os.windows().window_count() == 0 {
|
||||
render_home_packet(pointer_x, pointer_y, self.game_library.entries())
|
||||
let suspended_game = resident_game_indicator(os);
|
||||
render_home_packet(
|
||||
pointer_x,
|
||||
pointer_y,
|
||||
self.game_library.entries(),
|
||||
suspended_game.as_ref(),
|
||||
)
|
||||
} else {
|
||||
let mut commands = Vec::new();
|
||||
for window in os.windows().windows() {
|
||||
@ -232,6 +245,7 @@ fn action_for_click(
|
||||
y: i32,
|
||||
pressed: bool,
|
||||
game_entries: &[crate::GameLibraryEntry],
|
||||
suspended_game: Option<&SuspendedGameIndicator>,
|
||||
) -> Option<SystemProfileAction> {
|
||||
if !pressed {
|
||||
return None;
|
||||
@ -241,6 +255,10 @@ fn action_for_click(
|
||||
return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell);
|
||||
}
|
||||
|
||||
if suspended_game.is_some() && rect_contains(SUSPENDED_GAME_KILL_CONTROL, x, y) {
|
||||
return Some(SystemProfileAction::KillResidentGame);
|
||||
}
|
||||
|
||||
if let Some(entry) = game_entries
|
||||
.iter()
|
||||
.take(MAX_HOME_GAME_ROWS)
|
||||
@ -257,6 +275,23 @@ fn action_for_click(
|
||||
.map(|button| SystemProfileAction::LaunchNativeShell(button.app))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct SuspendedGameIndicator {
|
||||
title: String,
|
||||
}
|
||||
|
||||
fn resident_game_indicator(os: &mut SystemOS) -> Option<SuspendedGameIndicator> {
|
||||
let task_id = os.lifecycle().resident_game_task()?;
|
||||
let sessions = os.sessions();
|
||||
let session = sessions.vm_session_for_task(task_id)?;
|
||||
|
||||
if session.app_mode != AppMode::Game {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(SuspendedGameIndicator { title: session.title.clone() })
|
||||
}
|
||||
|
||||
fn game_row_rect(index: usize) -> Rect {
|
||||
Rect {
|
||||
x: GAME_ROW_X,
|
||||
@ -281,6 +316,7 @@ fn render_home_packet(
|
||||
pointer_x: i32,
|
||||
pointer_y: i32,
|
||||
game_entries: &[crate::GameLibraryEntry],
|
||||
suspended_game: Option<&SuspendedGameIndicator>,
|
||||
) -> ShellUiFramePacket {
|
||||
let mut commands = Vec::new();
|
||||
commands.push(GfxUiCommand::Clear { color: COLOR_BG });
|
||||
@ -320,6 +356,11 @@ fn render_home_packet(
|
||||
draw_button(&mut commands, button.rect, button.label, hovered);
|
||||
}
|
||||
|
||||
if let Some(suspended_game) = suspended_game {
|
||||
let hovered = rect_contains(SUSPENDED_GAME_KILL_CONTROL, pointer_x, pointer_y);
|
||||
draw_suspended_game_kill_control(&mut commands, suspended_game, hovered);
|
||||
}
|
||||
|
||||
ShellUiFramePacket::new(commands)
|
||||
}
|
||||
|
||||
@ -422,6 +463,39 @@ fn draw_game_row(commands: &mut Vec<GfxUiCommand>, rect: Rect, label: &str, hove
|
||||
draw_buffer_text(commands, rect.x + 8, rect.y + 7, label, COLOR_TEXT);
|
||||
}
|
||||
|
||||
fn draw_suspended_game_kill_control(
|
||||
commands: &mut Vec<GfxUiCommand>,
|
||||
suspended_game: &SuspendedGameIndicator,
|
||||
hovered: bool,
|
||||
) {
|
||||
let fill = if hovered { COLOR_KILL_ACTIVE } else { COLOR_KILL };
|
||||
let border = if hovered { COLOR_HILITE } else { COLOR_BORDER };
|
||||
fill_rect(commands, SUSPENDED_GAME_KILL_CONTROL, fill);
|
||||
draw_rect(commands, SUSPENDED_GAME_KILL_CONTROL, border);
|
||||
draw_buffer_text(
|
||||
commands,
|
||||
SUSPENDED_GAME_KILL_CONTROL.x + 8,
|
||||
SUSPENDED_GAME_KILL_CONTROL.y + 8,
|
||||
&fit_text_to_control(&suspended_game.title, SUSPENDED_GAME_KILL_CONTROL.w - 16),
|
||||
COLOR_TEXT,
|
||||
);
|
||||
}
|
||||
|
||||
fn fit_text_to_control(text: &str, max_width: i32) -> String {
|
||||
let max_chars = (max_width / 8).max(0) as usize;
|
||||
if text.chars().count() <= max_chars {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
if max_chars <= 2 {
|
||||
return text.chars().take(max_chars).collect();
|
||||
}
|
||||
|
||||
let mut fitted: String = text.chars().take(max_chars - 2).collect();
|
||||
fitted.push_str("..");
|
||||
fitted
|
||||
}
|
||||
|
||||
fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) {
|
||||
commands.push(GfxUiCommand::DrawText { x, y, text: text.to_string(), color });
|
||||
}
|
||||
@ -462,7 +536,14 @@ mod tests {
|
||||
#[test]
|
||||
fn shell_a_button_click_emits_launch_action() {
|
||||
assert_eq!(
|
||||
action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true, &[]),
|
||||
action_for_click(
|
||||
false,
|
||||
HOME_BUTTONS[0].rect.x,
|
||||
HOME_BUTTONS[0].rect.y,
|
||||
true,
|
||||
&[],
|
||||
None
|
||||
),
|
||||
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
|
||||
);
|
||||
}
|
||||
@ -475,7 +556,8 @@ mod tests {
|
||||
SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1,
|
||||
SHELL_B_BUTTON.y,
|
||||
true,
|
||||
&[]
|
||||
&[],
|
||||
None
|
||||
),
|
||||
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
|
||||
);
|
||||
@ -489,7 +571,8 @@ mod tests {
|
||||
SHELL_A_BUTTON.x + SHELL_A_BUTTON.w,
|
||||
SHELL_A_BUTTON.y,
|
||||
true,
|
||||
&[]
|
||||
&[],
|
||||
None
|
||||
),
|
||||
None
|
||||
);
|
||||
@ -499,7 +582,8 @@ mod tests {
|
||||
SHELL_A_BUTTON.x,
|
||||
SHELL_A_BUTTON.y + SHELL_A_BUTTON.h,
|
||||
true,
|
||||
&[]
|
||||
&[],
|
||||
None
|
||||
),
|
||||
None
|
||||
);
|
||||
@ -507,7 +591,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn held_pointer_does_not_emit_click_action() {
|
||||
assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[]), None);
|
||||
assert_eq!(
|
||||
action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[], None),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -535,8 +622,14 @@ mod tests {
|
||||
discovered_at: std::time::SystemTime::UNIX_EPOCH,
|
||||
};
|
||||
|
||||
let action =
|
||||
action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry));
|
||||
let action = action_for_click(
|
||||
false,
|
||||
GAME_ROW_X,
|
||||
GAME_ROW_Y,
|
||||
true,
|
||||
std::slice::from_ref(&entry),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
action,
|
||||
@ -554,10 +647,10 @@ mod tests {
|
||||
#[test]
|
||||
fn close_button_click_emits_close_only_in_shell() {
|
||||
assert_eq!(
|
||||
action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]),
|
||||
action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[], None),
|
||||
Some(SystemProfileAction::CloseShell)
|
||||
);
|
||||
assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), None);
|
||||
assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[], None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -571,7 +664,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn home_render_produces_shell_ui_packet_commands() {
|
||||
let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[]);
|
||||
let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[], None);
|
||||
|
||||
assert!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. })));
|
||||
assert!(packet.commands.iter().any(|command| {
|
||||
@ -579,6 +672,123 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_game_indicator_uses_only_resident_game_sessions() {
|
||||
let mut os = SystemOS::new(None);
|
||||
assert_eq!(resident_game_indicator(&mut os), None);
|
||||
|
||||
let shell = os.sessions().create_vm_shell_task(7, "VM Shell");
|
||||
assert_eq!(resident_game_indicator(&mut os), None);
|
||||
os.lifecycle().suspend_task(shell).expect("shell suspend should succeed");
|
||||
assert_eq!(resident_game_indicator(&mut os), None);
|
||||
|
||||
let game = os.sessions().create_vm_game_task(42, "Stress Console");
|
||||
os.lifecycle().suspend_task(game).expect("game suspend should succeed");
|
||||
|
||||
assert_eq!(
|
||||
resident_game_indicator(&mut os),
|
||||
Some(SuspendedGameIndicator { title: "Stress Console".to_string() })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_render_shows_suspended_game_kill_control_when_game_is_resident() {
|
||||
let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() };
|
||||
let packet = render_home_packet(
|
||||
SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
&[],
|
||||
Some(&suspended_game),
|
||||
);
|
||||
|
||||
assert!(packet.commands.iter().any(|command| {
|
||||
matches!(
|
||||
command,
|
||||
GfxUiCommand::FillRect { rect, color }
|
||||
if *rect == SUSPENDED_GAME_KILL_CONTROL && *color == COLOR_KILL_ACTIVE
|
||||
)
|
||||
}));
|
||||
assert!(packet.commands.iter().any(|command| {
|
||||
matches!(
|
||||
command,
|
||||
GfxUiCommand::DrawText { x, y, text, .. }
|
||||
if *x == SUSPENDED_GAME_KILL_CONTROL.x + 8
|
||||
&& *y == SUSPENDED_GAME_KILL_CONTROL.y + 8
|
||||
&& text == "Stress Console"
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_render_omits_suspended_game_kill_control_without_resident_game() {
|
||||
let packet = render_home_packet(
|
||||
SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
&[],
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!packet.commands.iter().any(|command| {
|
||||
matches!(
|
||||
command,
|
||||
GfxUiCommand::FillRect { rect, color }
|
||||
if *rect == SUSPENDED_GAME_KILL_CONTROL
|
||||
&& (*color == COLOR_KILL || *color == COLOR_KILL_ACTIVE)
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspended_game_kill_control_click_emits_kill_action() {
|
||||
let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() };
|
||||
|
||||
assert_eq!(
|
||||
action_for_click(
|
||||
false,
|
||||
SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
true,
|
||||
&[],
|
||||
Some(&suspended_game),
|
||||
),
|
||||
Some(SystemProfileAction::KillResidentGame)
|
||||
);
|
||||
assert_eq!(
|
||||
action_for_click(
|
||||
false,
|
||||
SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
true,
|
||||
&[],
|
||||
None,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspended_game_kill_control_truncates_long_titles() {
|
||||
let long_title =
|
||||
SuspendedGameIndicator { title: "Extremely Long Resident Game Title".to_string() };
|
||||
let packet = render_home_packet(0, 0, &[], Some(&long_title));
|
||||
let label = packet
|
||||
.commands
|
||||
.iter()
|
||||
.find_map(|command| match command {
|
||||
GfxUiCommand::DrawText { x, y, text, .. }
|
||||
if *x == SUSPENDED_GAME_KILL_CONTROL.x + 8
|
||||
&& *y == SUSPENDED_GAME_KILL_CONTROL.y + 8 =>
|
||||
{
|
||||
Some(text)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("kill control text should render");
|
||||
|
||||
assert!(label.ends_with(".."));
|
||||
assert!(label.chars().count() <= ((SUSPENDED_GAME_KILL_CONTROL.w - 16) / 8) as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_close_button_sits_inside_shell_frame() {
|
||||
assert!(rect_contains(SHELL_FRAME, CLOSE_BUTTON.x, CLOSE_BUTTON.y));
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{"type":"meta","next_id":{"DSC":45,"AGD":48,"DEC":41,"PLN":167,"LSN":53,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0044","status":"in_progress","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0040","file":"DEC-0040-hub-suspended-game-kill-affordance.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0047"}],"plans":[{"id":"PLN-0163","file":"PLN-0163-render-hub-suspended-game-kill-control.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0164","file":"PLN-0164-expose-resident-game-termination-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0165","file":"PLN-0165-validate-hub-kill-flow-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]},{"id":"PLN-0166","file":"PLN-0166-publish-hub-kill-contract-spec-and-lesson-hooks.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0040"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}
|
||||
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0163
|
||||
ticket: hub-suspended-game-kill-affordance
|
||||
title: Render Hub Suspended Game Kill Control
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-05
|
||||
ref_decisions: [DEC-0040]
|
||||
tags: [hub, lifecycle, game, ui]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user