git.lucas.co / hou-control
SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git

commit1e533ecebb94dca19f08ddbfe6032f1ff90fec9d
parent918d3db363
authorLucas Galante <[email protected]>
date2026-09-10 10:02
hc: wire package to Houdini 22

Houdini 22 runs Python 3.13 and only reads python3.13libs, so the package
directory is renamed from python3.11libs and the docs naming the old path
follow it. Houdini 21 is already uninstalled, so nothing needs the old
name. The manifest itself is unchanged: HOUDINI_PATH and HC_PATH still
point at this repo, installed as ~/houdini22.0/packages/.

Guard the startup last-file handling. Houdini reports $HOME/untitled.hip
as the path of a scene that was never saved, 456.py recorded that phantom
path, and 123.py fed it straight to hou.hipFile.load, so Houdini opened
with a traceback and no scene. Moving a project to a new folder produced
the same failure. 456.py now records only files that exist, and 123.py
skips missing paths and reports a failed load instead of aborting the
rest of startup.

Delete hclistener.py. It has been dead since the HC* rename and is not
importable at all: a syntax error on line 43, and imports of .session,
.pane and .tab modules that no longer exist. Nothing references it.

Repair this repo's copy of houdini-studio-utils.json, which was truncated
mid-structure, carried CRLF endings and pointed at an old path. Houdini
never read it, since only a packages/ subdirectory is scanned, but it now
matches the manifest that is actually installed.

Verified on 22.0.429: all 33 modules import under Python 3.13, and a
throwaway GUI instance starts clean with all 102 hotkey assignments
applied and no traceback.

Co-Authored-By: Claude Opus 5 <[email protected]>

 CLAUDE.md                                          |   9 +-
 GEMINI.md                                          |  79 ++++++----
 houdini-studio-utils.json                          |  14 +-
 python3.11libs/hc/hclistener.py                    | 100 -------------
 .../fuzzyfinder/__init__.py                        |   0
 .../fuzzyfinder/main.py                            |   0
 {python3.11libs => python3.13libs}/hc/__init__.py  |   0
 .../hc/hcbindings.py                               |   0
 {python3.11libs => python3.13libs}/hc/hccam.py     |   0
 .../hc/hcconfigloader.py                           |   0
 .../hc/hcdefaultcam.py                             |   0
 {python3.11libs => python3.13libs}/hc/hcgeo.py     |   0
 {python3.11libs => python3.13libs}/hc/hcguides.py  |   0
 {python3.11libs => python3.13libs}/hc/hclayout.py  |   0
 {python3.11libs => python3.13libs}/hc/hcmaps.py    |   0
 .../hc/hcmenuutils.py                              |   0
 .../hc/hcnetworkeditor.py                          |   0
 {python3.11libs => python3.13libs}/hc/hcnode.py    |   0
 {python3.11libs => python3.13libs}/hc/hcpane.py    |   0
 .../hc/hcparametertab.py                           |   0
 {python3.11libs => python3.13libs}/hc/hcpathtab.py |   0
 {python3.11libs => python3.13libs}/hc/hcradial.py  |   0
 .../hc/hcsceneviewer.py                            |   0
 {python3.11libs => python3.13libs}/hc/hcsession.py |   0
 .../hc/hcsettings.py                               |   0
 .../hc/hcsplithandles.py                           |   0
 {python3.11libs => python3.13libs}/hc/hcstatus.py  |   0
 .../hc/hcstatusbar.py                              |   0
 {python3.11libs => python3.13libs}/hc/hctab.py     |   0
 .../hc/hcviewport.py                               |   0
 .../hc/hcvisualizers.py                            |   0
 {python3.11libs => python3.13libs}/hc/hcwidgets.py |   0
 .../hc/hcwindowwatcher.py                          |   0
 .../nodegraphhooks.py                              |   0
 {python3.11libs => python3.13libs}/uiready.py      |   0
 scripts/123.py                                     | 166 ++++++++++++++++++---
 scripts/456.py                                     |  19 ++-
 37 files changed, 220 insertions(+), 167 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 9e83b60..e250d03 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,14 +6,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 `hou-control` (hc) is a SideFX Houdini customization package — not a standalone application. There is no build, no test suite, and no package manager. Code here is loaded by Houdini at runtime via the `houdini-studio-utils.json` package manifest (which adds this directory to `HOUDINI_PATH`) and the `HC_PATH` env var, which the code reads with `hou.getenv("HC_PATH")` to locate config/scripts on disk.
 
-Everything runs inside Houdini's embedded Python 3.11. To exercise changes, reload from within Houdini:
+Everything runs inside Houdini's embedded Python 3.13. To exercise changes, reload from within Houdini:
 - From the `hc` main menu: **Reload Hotkeys**, **Reload Colors**, **Reload Keycam**.
 - Full package reload: `hou.ui.reloadPackage(...)` (see `HCSession.reloadHC`).
 - Hotkey JSON changes take effect via `HCSession.reloadHotkeys()` which calls `HCBindings().load()` (assigns from `hc_hotkeys.json`, auto-clearing any conflicts via `hou.hotkeys.findConflicts`).
 
 ## Architecture
 
-### The `hc` Python package (`python3.11libs/hc/`)
+### The `hc` Python package (`python3.13libs/hc/`)
 
 The core abstraction is a set of wrapper classes around Houdini's `hou` API objects. Each wrapper holds the underlying `hou_*` object and adds higher-level behavior. Classes are re-exported from `hc/__init__.py`.
 
@@ -27,7 +27,7 @@ Key hierarchy:
 
 When adding a new command: implement it on the appropriate wrapper (`HCSession`/`HCPane`/`HC*Tab`), then register it in the matching `HCMaps.tab_map_*` so it appears in the HC Panel. If it should be hotkey-bound, add a Houdini symbol entry to `hc_hotkeys.json`.
 
-### Non-package Python (`python3.11libs/`)
+### Non-package Python (`python3.13libs/`)
 
 These are recognized by Houdini's startup/event system by filename convention:
 - `uiready.py` — runs once when the UI is ready; instantiates `HCSession` and calls `reloadHotkeys` + `toggleStowbars`.
@@ -67,3 +67,6 @@ These are reloaded via `HCSession.reloadColorSchemes()` (`hou.ui.reloadColorSche
 - `.type()` methods return string discriminators (`'HCNetworkEditor'`, `'HCSceneViewer'`, `'HCParameterTab'`, `'HCPathTab'`) — this is the idiomatic way to branch on tab kind in this codebase, not `isinstance`. When branching, remember that `'HCParameterTab'` is a sibling of `'HCPathTab'` (Parm tabs return the former, DetailsView returns the latter) — code that wants both should check `tab.type() in ('HCPathTab', 'HCParameterTab')`.
 - Toggle-style methods often use a small string map (e.g. `{'0': '1', '1': '0'}`) because Houdini prefs are stored as strings.
 - Paths to bundled files are built from `hou.getenv("HC_PATH")` — don't hardcode absolute paths.
+- **`HCWidgets.SelectionDialog.execute()` has no try/except**: any exception in the callable stored in `self.list_dict` is silently swallowed — the user just sees "nothing happens." When debugging a new HC Panel command that doesn't work, connect via `hrun` and test the callable directly, or add a temporary try/except with `traceback.print_exc()` in `execute()` to surface the error.
+- **`HCSession.reloadHC()` destroys HC-owned Qt widgets** (`hc_status_overlay`, `hc_status_dialog`) and must re-create any that should persist (e.g. status bar). Forgetting to restore them causes them to disappear until Houdini restarts.
+- **PySide6 widget API mismatches**: `setOpenExternalLinks()` belongs to `QTextBrowser`, not `QTextEdit`. Using the wrong base class causes a silent `AttributeError` inside SelectionDialog callables.
diff --git a/GEMINI.md b/GEMINI.md
index 80ff637..54943bf 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -1,39 +1,62 @@
-# Project Overview: hou-control
+# GEMINI.md - Project Context for hou-control (hc)
 
-`hou-control` is a specialized Houdini environment and toolset designed to enhance the user interface, automate session management, and provide custom modeling and simulation operators. It bridges Houdini with external AI agents via a Model Context Protocol (MCP) server.
+## Project Overview
+`hou-control` (hc) is a specialized SideFX Houdini customization package. It enhances Houdini's workflow through custom Python wrapper classes, hotkey-driven navigation, UI enhancements (HC Panel), and specialized viewer states.
 
-## Architecture & Technology Stack
+### Core Technologies
+- **Python 3.13:** The primary language for logic, running within Houdini's embedded environment.
+- **PySide6:** Used for custom UI elements like the `SelectionDialog` (HC Panel).
+- **VEX:** Used for geometry processing and custom SOP/COP logic in HDAs.
+- **XML:** Defines Houdini's main menus and network view menus.
+- **JSON:** Manages hotkey assignments (`hc_hotkeys.json`) and project settings (`hc_settings.json`).
 
-- **Houdini Python API (hou):** Core integration for UI manipulation and node graph control.
-- **PySide6:** Used for custom Qt-based UI elements (HC Status, Floating Layouts).
-- **RPyC:** Enables remote procedure calls between the external MCP server and the internal Houdini session.
-- **FastMCP:** Powers the Model Context Protocol server for AI-driven interaction.
-- **Operator Sets:** 
-  - **GEM (General Export Methods):** Manufacturing and export-focused HDAs.
-  - **Developer (Shapeshifter):** Simulation and surface development tools.
+## Architecture & Key Components
 
-## Key Components
+### Wrapper Hierarchy (`python3.13libs/hc/`)
+The project uses a "wrapper" pattern rather than inheritance for `hou` API objects:
+- **`HCSession`**: Central entry point. Manages desktop-wide state, visibility, and launches the HC Panel.
+- **`HCPane`**: Wraps `hou.Pane`. Handles splitting and pane-level logic.
+- **`HCTab` / `HCNetworkEditor` / `HCSceneViewer`**: Specialized wrappers for different Houdini pane tabs.
 
-- **`python3.11libs/hc/`**: Core logic for session management, hotkey binding, and UI overlays.
-- **`mcpserver/`**: Contains the `houdini_mcp.py` server which allows external agents to evaluate code in Houdini.
-- **`viewer_states/keycam.py`**: A custom viewer state for keyboard-driven camera navigation using the official `PluginHotkeyDefinitions` system.
-- **`config/`**: Contains UI theme files, custom cursors, and node shape definitions.
+### Dispatch & Commands
+- **`HCMaps` (`hcmaps.py`)**: A central registry that maps human-readable command names to bound methods. This is used to populate the HC Panel selection dialog.
+- **`HCWidgets.SelectionDialog`**: The UI backend for the HC Panel (Alt+Shift+D).
 
-## Building and Running
+### Integration Scripts
+- **`uiready.py`**: Runs once Houdini's UI is initialized.
+- **`nodegraphhooks.py`**: Hooks into the network editor for custom event handling.
+- **`123.py` / `456.py`**: Houdini's startup and scene-load scripts.
 
-Houdini automatically loads the package if the directory is added to the `HOUDINI_PATH`.
+## Workflows: Building & Running
+Since this is a runtime package, there is no traditional "build" step. 
 
-- **Start Houdini with HC:** Ensure `uiready.py` is in your Houdini search path; it initializes the session and starts the RPyC server.
-- **Start MCP Server:**
-  ```bash
-  source mcpserver/.venv/bin/activate
-  python mcpserver/houdini_mcp.py
-  ```
-- **Tests:** Currently, verification is performed manually within the Houdini session or via RPyC evaluation.
+### Applying Changes
+To exercise code changes without restarting Houdini:
+1.  **Reload Python Logic**: Use the **"Reload HC"** command from the HC Panel or HC Menu. This clears `hc.*` from `sys.modules` and re-imports.
+2.  **Reload Hotkeys**: Use **"Reload Hotkeys"**. This re-reads `hc_hotkeys.json` and updates Houdini's internal binding table.
+3.  **Reload Colors/Keycam**: Use specific "Reload" commands for these assets.
 
 ## Development Conventions
 
-- **Surgical Updates:** (MANDATORY) Never remove or refactor unrelated code. Maintain all existing parameters, menu items, and logic.
-- **Hotkey Management:** Use the official `hou.PluginHotkeyDefinitions` for viewer states to ensure proper context scoping.
-- **Node Tagging:** Use `hc_custom_color: "1"` user data to identify nodes that should be affected by global style updates.
-- **Communication:** Use the RPyC server (port 18811) for speculative research and debugging within the live environment.
+### General Engineering Standards
+- **Surgical Updates**: Prioritize minimal, precise changes that follow local patterns.
+- **No Direct Subclassing**: Never subclass `hou.*` types. Use the established wrapper pattern.
+- **Type Discrimination**: Use `.type()` string checks (e.g., `tab.type() == 'HCNetworkEditor'`) instead of `isinstance`.
+- **Environment Paths**: Build file paths using `hou.getenv("HC_PATH")` to remain portable.
+
+### Node Graph Logic
+- **Snapping**: Nodes should snap to grid intersections. Note that in recent versions, `node.position()` refers to the **bottom-left corner**, while the standard visual center is offset by approximately `(0.5, 0.15)` for typical nodes.
+- **Hierarchical Movement**: Keyboard shifting (Alt+HJKL) supports hierarchy modifiers:
+    - `Ctrl`: Downstream (descendants).
+    - `Shift`: Upstream (ancestors).
+
+### Testing
+- **Agent Tests**: Use the `agent_tests/` directory for investigative or verification scripts (e.g., `test_hierarchy.py`).
+- **RPC Support**: The project supports RPyC on port `18811`, allowing external tools to execute code directly in the running Houdini session.
+
+## Key Files
+- `CLAUDE.md`: Highly detailed technical documentation and architectural reference.
+- `hc_hotkeys.json`: Source of truth for all custom keyboard shortcuts.
+- `python3.13libs/hc/hcsession.py`: Main session logic and entry points.
+- `python3.13libs/hc/hcnetworkeditor.py`: Node graph navigation and manipulation logic.
+- `agent_tests/`: Persistent test suite for agent-implemented features.
diff --git a/houdini-studio-utils.json b/houdini-studio-utils.json
index 159f4c7..250eb1c 100755
--- a/houdini-studio-utils.json
+++ b/houdini-studio-utils.json
@@ -1,8 +1,6 @@
-{

-    "hpath": [

-        "~/src/houdini-studio-utils"

-    ],

-    "env": [

-        "

-    [

-}

+{
+    "env": [
+        {"HOUDINI_PATH": "~/Dropbox/src/hou-control"},
+        {"HC_PATH": "~/Dropbox/src/hou-control"}
+    ]
+}
diff --git a/python3.11libs/hc/hclistener.py b/python3.11libs/hc/hclistener.py
deleted file mode 100644
index c8d4c5a..0000000
--- a/python3.11libs/hc/hclistener.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import hou
-from .session import Session
-from .pane import Pane
-from .tab import Tab
-
-
-class Listener():
-    def __init__(self):
-        self.report_tab = 1
-        self.tab = None
-        self.pane = None
-        self.session = None
-        self.pane = None
-        self.tab = None
-        self.sceneviewer = None
-        self.networkeditor = None
-        self.project_path = hou.hipFile.path()
-        self.network_path = None
-        self.tab_type = None
-        self.update_objects()
-        return
-
-    def start(self):
-        hou.ui.addEventLoopCallback(self.listener)
-
-    def stop(self):
-        hou.ui.removeEventLoopCallback(self.listener)
-
-    def update_objects(self):
-        self.tab = hou.ui.paneTabUnderCursor()
-        self.pane = hou.ui.paneUnderCursor()
-
-        self.session = Session()
-        self.pane = Pane(hou.ui.paneUnderCursor())
-        self.tab = Tab(self.tab)
-
-        if self.tab != None:
-
-            if self.tab.hasNetworkControls():
-                self.network_path = self.tab.path()
-            self.tab_type = self.tab.type()
-
-            if self.tab.type() == "SceneViewer:"
-                self.sceneviewer = SceneViewer(self.tab)
-            else:
-                self.sceneviewer = None
-
-            if self.tab.type() == "NetworkEditor":
-                self.networkeditor = NetworkEditor(self.tab)
-            else:
-                self.networkeditor = None
-
-    def listener(self):
-        tab = hou.ui.paneTabUnderCursor()
-        if tab == None:
-            hou.session.tab = None
-        elif tab != hou.session.tab:
-            print(tab)
-            self.update_objects()
-
-    def lists(self):
-        # Arrays for navigating pane tabs
-        self.tab_types = (
-            hou.paneTabType.ApexEditor,
-            hou.paneTabType.CompositorViewer,
-            hou.paneTabType.DetailsView,
-            hou.paneTabType.NetworkEditor,
-            hou.paneTabType.Parm,
-            hou.paneTabType.PythonPanel,
-            hou.paneTabType.PythonShell,
-            hou.paneTabType.SceneViewer,
-            hou.paneTabType.Textport
-        )
-        self.tab_type_names = (
-            "ApexEditor",
-            "CompositorViewer",
-            "DetailsView",
-            "NetworkEditor",
-            "Parm",
-            "PythonPanel",
-            "PythonShell",
-            "SceneViewer",
-            "Textport"
-        )
-        self.tab_names = [tab.name() for tab in hou.session.session.tabs()]
-
-        # Populate pane tab labels array
-        self.tab_labels = []
-        for tab in self.session.tabs():
-            index = self.tab_types.index(tab.type())
-            label = self.tab_type_names[index]
-            self.tab_labels.append(label)
-
-    def networkPath(self):
-        return str(hou.session.tab.pwd())
-
-    def projectPath(self):
-        return hou.hipFile.name()
-        # ct = self.project_path.count("/")
-        # self.project_path = self.project_path.split("/", ct - 2)[-1]
diff --git a/python3.11libs/fuzzyfinder/__init__.py b/python3.13libs/fuzzyfinder/__init__.py
similarity index 100%
rename from python3.11libs/fuzzyfinder/__init__.py
rename to python3.13libs/fuzzyfinder/__init__.py
diff --git a/python3.11libs/fuzzyfinder/main.py b/python3.13libs/fuzzyfinder/main.py
similarity index 100%
rename from python3.11libs/fuzzyfinder/main.py
rename to python3.13libs/fuzzyfinder/main.py
diff --git a/python3.11libs/hc/__init__.py b/python3.13libs/hc/__init__.py
similarity index 100%
rename from python3.11libs/hc/__init__.py
rename to python3.13libs/hc/__init__.py
diff --git a/python3.11libs/hc/hcbindings.py b/python3.13libs/hc/hcbindings.py
similarity index 100%
rename from python3.11libs/hc/hcbindings.py
rename to python3.13libs/hc/hcbindings.py
diff --git a/python3.11libs/hc/hccam.py b/python3.13libs/hc/hccam.py
similarity index 100%
rename from python3.11libs/hc/hccam.py
rename to python3.13libs/hc/hccam.py
diff --git a/python3.11libs/hc/hcconfigloader.py b/python3.13libs/hc/hcconfigloader.py
similarity index 100%
rename from python3.11libs/hc/hcconfigloader.py
rename to python3.13libs/hc/hcconfigloader.py
diff --git a/python3.11libs/hc/hcdefaultcam.py b/python3.13libs/hc/hcdefaultcam.py
similarity index 100%
rename from python3.11libs/hc/hcdefaultcam.py
rename to python3.13libs/hc/hcdefaultcam.py
diff --git a/python3.11libs/hc/hcgeo.py b/python3.13libs/hc/hcgeo.py
similarity index 100%
rename from python3.11libs/hc/hcgeo.py
rename to python3.13libs/hc/hcgeo.py
diff --git a/python3.11libs/hc/hcguides.py b/python3.13libs/hc/hcguides.py
similarity index 100%
rename from python3.11libs/hc/hcguides.py
rename to python3.13libs/hc/hcguides.py
diff --git a/python3.11libs/hc/hclayout.py b/python3.13libs/hc/hclayout.py
similarity index 100%
rename from python3.11libs/hc/hclayout.py
rename to python3.13libs/hc/hclayout.py
diff --git a/python3.11libs/hc/hcmaps.py b/python3.13libs/hc/hcmaps.py
similarity index 100%
rename from python3.11libs/hc/hcmaps.py
rename to python3.13libs/hc/hcmaps.py
diff --git a/python3.11libs/hc/hcmenuutils.py b/python3.13libs/hc/hcmenuutils.py
similarity index 100%
rename from python3.11libs/hc/hcmenuutils.py
rename to python3.13libs/hc/hcmenuutils.py
diff --git a/python3.11libs/hc/hcnetworkeditor.py b/python3.13libs/hc/hcnetworkeditor.py
similarity index 100%
rename from python3.11libs/hc/hcnetworkeditor.py
rename to python3.13libs/hc/hcnetworkeditor.py
diff --git a/python3.11libs/hc/hcnode.py b/python3.13libs/hc/hcnode.py
similarity index 100%
rename from python3.11libs/hc/hcnode.py
rename to python3.13libs/hc/hcnode.py
diff --git a/python3.11libs/hc/hcpane.py b/python3.13libs/hc/hcpane.py
similarity index 100%
rename from python3.11libs/hc/hcpane.py
rename to python3.13libs/hc/hcpane.py
diff --git a/python3.11libs/hc/hcparametertab.py b/python3.13libs/hc/hcparametertab.py
similarity index 100%
rename from python3.11libs/hc/hcparametertab.py
rename to python3.13libs/hc/hcparametertab.py
diff --git a/python3.11libs/hc/hcpathtab.py b/python3.13libs/hc/hcpathtab.py
similarity index 100%
rename from python3.11libs/hc/hcpathtab.py
rename to python3.13libs/hc/hcpathtab.py
diff --git a/python3.11libs/hc/hcradial.py b/python3.13libs/hc/hcradial.py
similarity index 100%
rename from python3.11libs/hc/hcradial.py
rename to python3.13libs/hc/hcradial.py
diff --git a/python3.11libs/hc/hcsceneviewer.py b/python3.13libs/hc/hcsceneviewer.py
similarity index 100%
rename from python3.11libs/hc/hcsceneviewer.py
rename to python3.13libs/hc/hcsceneviewer.py
diff --git a/python3.11libs/hc/hcsession.py b/python3.13libs/hc/hcsession.py
similarity index 100%
rename from python3.11libs/hc/hcsession.py
rename to python3.13libs/hc/hcsession.py
diff --git a/python3.11libs/hc/hcsettings.py b/python3.13libs/hc/hcsettings.py
similarity index 100%
rename from python3.11libs/hc/hcsettings.py
rename to python3.13libs/hc/hcsettings.py
diff --git a/python3.11libs/hc/hcsplithandles.py b/python3.13libs/hc/hcsplithandles.py
similarity index 100%
rename from python3.11libs/hc/hcsplithandles.py
rename to python3.13libs/hc/hcsplithandles.py
diff --git a/python3.11libs/hc/hcstatus.py b/python3.13libs/hc/hcstatus.py
similarity index 100%
rename from python3.11libs/hc/hcstatus.py
rename to python3.13libs/hc/hcstatus.py
diff --git a/python3.11libs/hc/hcstatusbar.py b/python3.13libs/hc/hcstatusbar.py
similarity index 100%
rename from python3.11libs/hc/hcstatusbar.py
rename to python3.13libs/hc/hcstatusbar.py
diff --git a/python3.11libs/hc/hctab.py b/python3.13libs/hc/hctab.py
similarity index 100%
rename from python3.11libs/hc/hctab.py
rename to python3.13libs/hc/hctab.py
diff --git a/python3.11libs/hc/hcviewport.py b/python3.13libs/hc/hcviewport.py
similarity index 100%
rename from python3.11libs/hc/hcviewport.py
rename to python3.13libs/hc/hcviewport.py
diff --git a/python3.11libs/hc/hcvisualizers.py b/python3.13libs/hc/hcvisualizers.py
similarity index 100%
rename from python3.11libs/hc/hcvisualizers.py
rename to python3.13libs/hc/hcvisualizers.py
diff --git a/python3.11libs/hc/hcwidgets.py b/python3.13libs/hc/hcwidgets.py
similarity index 100%
rename from python3.11libs/hc/hcwidgets.py
rename to python3.13libs/hc/hcwidgets.py
diff --git a/python3.11libs/hc/hcwindowwatcher.py b/python3.13libs/hc/hcwindowwatcher.py
similarity index 100%
rename from python3.11libs/hc/hcwindowwatcher.py
rename to python3.13libs/hc/hcwindowwatcher.py
diff --git a/python3.11libs/nodegraphhooks.py b/python3.13libs/nodegraphhooks.py
similarity index 100%
rename from python3.11libs/nodegraphhooks.py
rename to python3.13libs/nodegraphhooks.py
diff --git a/python3.11libs/uiready.py b/python3.13libs/uiready.py
similarity index 100%
rename from python3.11libs/uiready.py
rename to python3.13libs/uiready.py
diff --git a/scripts/123.py b/scripts/123.py
index 901f57e..521ccfc 100644
--- a/scripts/123.py
+++ b/scripts/123.py
@@ -1,34 +1,156 @@
 from pathlib import Path
-import hou
 import json
 
-# This file runs when houdini is opened without a .hip file
+import hou
+from PySide6 import QtWidgets
+from PySide6.QtCore import Qt, QTimer
+
+
+def _houdini_font():
+    main_window = hou.qt.mainWindow()
+    if main_window is not None:
+        return main_window.font()
+
+    app = QtWidgets.QApplication.instance()
+    return app.font() if app is not None else None
+
+
+def _read_settings():
+    hc_path = hou.getenv("HC_PATH")
+    if not hc_path:
+        return {}
+    settings_path = Path(hc_path) / "hc_settings.json"
+    try:
+        return json.loads(settings_path.read_text())
+    except Exception:
+        return {}
+
+
+def _write_settings(data):
+    hc_path = hou.getenv("HC_PATH")
+    if not hc_path:
+        return
+    settings_path = Path(hc_path) / "hc_settings.json"
+    settings_path.write_text(json.dumps(data, indent=4) + "\n")
+
+
+def _default_autosave_state():
+    return bool(_read_settings().get("startup", {}).get("default_autosave_state", False))
+
+
+def _current_desktop_mode():
+    return _read_settings().get("desktop_mode", "attached")
+
+
+class StartupPrompt(QtWidgets.QDialog):
+    def __init__(self, last_file, parent=None):
+        super().__init__(parent or hou.qt.mainWindow())
+        self._open_last_file = False
+
+        self.setWindowTitle("Startup")
+        self.setWindowFlags(self.windowFlags() | Qt.Tool)
+        self.resize(820, 240)
+
+        font = _houdini_font()
+        if font is not None:
+            self.setFont(font)
+
+        layout = QtWidgets.QVBoxLayout(self)
+
+        label = QtWidgets.QLabel(f"Open last file?\n{last_file}")
+        label.setWordWrap(True)
+        label.setTextInteractionFlags(Qt.TextSelectableByMouse)
+        label.setFocusPolicy(Qt.NoFocus)
+        layout.addWidget(label)
+
+        self.autosave_checkbox = QtWidgets.QCheckBox("Enable autosave")
+        self.autosave_checkbox.setChecked(_default_autosave_state())
+        self.autosave_checkbox.setFocusPolicy(Qt.NoFocus)
+        layout.addWidget(self.autosave_checkbox)
+
+        mode_layout = QtWidgets.QHBoxLayout()
+        mode_label = QtWidgets.QLabel("Layout mode:")
+        mode_label.setFocusPolicy(Qt.NoFocus)
+        self.mode_combo = QtWidgets.QComboBox()
+        self.mode_combo.addItem("Attached", "attached")
+        self.mode_combo.addItem("Detached", "detached")
+        current = _current_desktop_mode()
+        idx = self.mode_combo.findData(current)
+        if idx >= 0:
+            self.mode_combo.setCurrentIndex(idx)
+        mode_layout.addWidget(mode_label)
+        mode_layout.addWidget(self.mode_combo)
+        mode_layout.addStretch()
+        layout.addLayout(mode_layout)
+
+        buttons = QtWidgets.QDialogButtonBox()
+        yes_button = buttons.addButton("Yes", QtWidgets.QDialogButtonBox.AcceptRole)
+        no_button = buttons.addButton("No", QtWidgets.QDialogButtonBox.RejectRole)
+        yes_button.clicked.connect(self._acceptOpen)
+        no_button.clicked.connect(self.reject)
+        layout.addWidget(buttons)
+
+        QTimer.singleShot(0, self._clearInitialFocus)
+
+    def _clearInitialFocus(self):
+        widget = QtWidgets.QApplication.focusWidget()
+        if widget is not None and self.isAncestorOf(widget):
+            widget.clearFocus()
+
+    def _acceptOpen(self):
+        self._open_last_file = True
+        self.accept()
+
+    @property
+    def open_last_file(self):
+        return self._open_last_file
+
+    @property
+    def desktop_mode(self):
+        data = self.mode_combo.currentData()
+        return data if data is not None else "attached"
+
+
+# This file runs when Houdini is opened without a .hip file.
 
-# Check/create st_data directory
 data_dir = Path(hou.getenv("HOUDINI_USER_PREF_DIR")) / "st_data"
 data_dir.mkdir(parents=True, exist_ok=True)
 
-# Check/create state.json file
 state_file = data_dir / "state.json"
-
 if not state_file.exists():
-    defaults = {
-        "last_file": ""
-    }
+    state_file.write_text(json.dumps({"last_file": ""}, indent=4))
+else:
+    try:
+        loaded_state = json.loads(state_file.read_text())
+    except (OSError, ValueError):
+        loaded_state = {}
+    last_file = loaded_state.get("last_file", "")
 
-    with open(state_file, 'w') as f:
-        json.dump(defaults, f, indent=4)
+    # A recorded path goes stale whenever the scene is moved, renamed or
+    # deleted, and a never-saved scene reports a phantom $HOME/untitled.hip
+    # that was never written to disk. Offering either one made hou.hipFile.load
+    # raise hou.OperationFailed out of this startup script. Only offer a file
+    # that is really there.
+    if last_file and not Path(last_file).is_file():
+        last_file = ""
 
-else:
-    with open(state_file, 'r') as f:
-        loaded_state = json.load(f)
-        last_file = loaded_state['last_file']
-        if last_file != "":
-            choice = hou.ui.displayCustomConfirmation(
-                text=f"Open last file {last_file}?",
-                buttons=("Yes", "No"),
-                suppress=hou.confirmType.NoConfirmType
-            )
-            if choice == 0:
-                hou.hipFile.load(last_file)
+    if last_file:
+        dialog = StartupPrompt(last_file)
+        dialog.exec()
+
+        hou.setPreference("autoSave", "1" if dialog.autosave_checkbox.isChecked() else "0")
+
+        settings = _read_settings()
+        settings["desktop_mode"] = dialog.desktop_mode
+        _write_settings(settings)
 
+        if dialog.open_last_file:
+            # Still guard the load: the file can be unreadable or corrupt even
+            # when it exists, and an exception here aborts the rest of startup.
+            try:
+                hou.hipFile.load(last_file, suppress_save_prompt=True)
+            except hou.OperationFailed as exc:
+                hou.ui.displayMessage(
+                    "Could not open last file:\n%s\n\n%s" % (last_file, exc),
+                    severity=hou.severityType.Warning,
+                )
diff --git a/scripts/456.py b/scripts/456.py
index 21eb097..192ab03 100644
--- a/scripts/456.py
+++ b/scripts/456.py
@@ -1,14 +1,21 @@
 from pathlib import Path
 import hou
 import json
+from hc import HCSession
 
 # This file runs when a .hip file is loaded
 
-current_file = hou.hipFile.path() 
+current_file = hou.hipFile.path()
 state_file = Path(hou.getenv("HOUDINI_USER_PREF_DIR")) / "st_data" / "state.json"
-data = {"last_opened_file": current_file}
 
-# Write to JSON
-updates = {"last_file": current_file}
-with open(state_file, 'w') as f:
-    json.dump(updates, f, indent=4)
+# Houdini reports $HOME/untitled.hip for a scene that has never been saved.
+# Recording that phantom path made the next startup offer to open a file that
+# was never written, which raised hou.OperationFailed in 123.py. Only remember
+# a scene that actually exists on disk.
+if Path(current_file).is_file():
+    state_file.parent.mkdir(parents=True, exist_ok=True)
+    updates = {"last_file": current_file}
+    with open(state_file, 'w') as f:
+        json.dump(updates, f, indent=4)
+
+HCSession().updateNodeColors()