Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c5aed5f
Read admitted field declarations and share native editor lifecycle
trissim Oct 15, 2026
698006a
Finish bound field migration in sidebar consumers
trissim Oct 15, 2026
907840a
Use native geometry and paint throughout settings and sort forms
trissim Oct 15, 2026
bff49a4
Bind forms and saved settings to admitted ObjectState paths
trissim Oct 15, 2026
fa1d602
Merge commit '8c264fb0478354def9089ff8234eaef0f4cec79f' into refactor…
trissim Oct 15, 2026
d19c492
Pin the paired native forms cohort and retain admitted addresses dire…
trissim Oct 15, 2026
9fbec1d
Use field-owned parent addresses and pin the corrected cohort
trissim Oct 15, 2026
6c1748b
Pin the complete field leaf-name correction
trissim Oct 15, 2026
6761172
Pin bridge conversion to the admitted declaration owner
trissim Oct 15, 2026
952188a
Pin the three-argument shared conversion API
trissim Oct 15, 2026
f76f4d5
Pin shared root and nested field-path behavior
trissim Oct 15, 2026
3579e21
Merge remote-tracking branch 'origin/main' into refactor/native-formb…
trissim Oct 15, 2026
ea33717
Use native screen sizing modes for Settings
trissim Oct 15, 2026
eca1578
Delete CSS category markers and unused native-block policy
trissim Oct 16, 2026
80d2197
Checkpoint forms-migration before compaction
trissim Oct 16, 2026
da8254f
Resolve dependency pins after merging current main
trissim Oct 16, 2026
3f7d597
Finish lockfile merge with native form dependencies
trissim Oct 16, 2026
1b9f893
Prepare command fields from their declared configuration catalogs
trissim Oct 16, 2026
2a67605
Render ordinary fields through the admitted ParameterInfo root
trissim Oct 16, 2026
b5ea8b9
Checkpoint forms-migration before compaction
trissim Oct 16, 2026
78e0e6a
Own resolved Comms services and runtime in existing Toad lifetimes
trissim Oct 16, 2026
321390e
Merge current Toad with native form and Core catalog dependencies
trissim Oct 16, 2026
72ced63
Reuse model search and provider connection for declared creation cata…
trissim Oct 16, 2026
fd48dff
Pair generated forms with normalized native creation defaults
trissim Oct 16, 2026
322b906
Checkpoint forms-migration before compaction
trissim Oct 16, 2026
1551844
Keep reconnect failure with its session and complete application cleanup
trissim Oct 16, 2026
408b4c6
Record P68 source-family search terms
trissim Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .scratch/forms-source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""NRA source evidence for the existing forms and category-filter families."""
import ast
import json
import subprocess
import sys
from pathlib import Path
from nominal_refactor_advisor.ast_tools import SourceModule, module_syntax_index
from nominal_refactor_advisor.class_index import build_class_family_index

roots = [Path('/home/ts/wt/toad-forms-migration/src/toad'),
Path('/home/ts/wt/textual-native-forms/src/textual'),
Path('/home/ts/wt/objectstate-native-forms/src/objectstate'),
Path('/home/ts/wt/formbridge-native-forms/src/formbridge'),
Path('/home/ts/wt/pyqt-native-formbridge/src/pyqt_reactive'),
Path('/home/ts/wt/core-forms-migration/src/agent_comms'),
Path('/home/ts/code/projects/openhcs/openhcs')]
terms = {'message_category', 'apply_block_filter', 'keep_live_block', 'block_category',
'selected_categories', 'visible_categories', 'set_display_constraint',
'update_classes', 'set_class', 'has_class', 'parameters_for_state',
'is_optional', 'WidgetCreationConfig', 'widget_creation_type',
'shown_in', 'FieldView', 'DeclaredField', 'FormView', 'SortDialog',
'NewThreadCliCommand', 'ForkCliCommand', 'NewThreadSpec', 'ForkSpec',
'CommsForkTool', 'initial_prompt', 'pi_bin', 'prompt', 'TargetField',
'TargetAction', 'ModelConfigOption', 'ThinkingLevelConfigOption',
'ConfigOptions', 'CommandDialog', 'NativeThreadDefaults',
'wire', 'Comms', 'CommsRoute', 'resolve_comms_route', 'LaunchRuntime',
'CoordinationAccess', 'CoordinationTranscriptReader', 'WireRelationshipSource',
'DetachedTranscriptReader', 'ObservedTranscriptReader', 'accepts_launch',
'sync_identity', 'on_comms_session_named', 'rename_coordination', 'coordinated_title',
'ModelSwitcher', 'ModelSelection', 'ConnectProvider', 'filter_models', 'select_model',
'provider_login', 'ConfigChoice', 'SelectChoices', 'configuration_changed',
'admission', 'admit', 'custody', 'receipt', '__setattr__',
'ImportSnapshot', 'ImportBuffer', 'ImportAdapter', 'CodexImportScan',
'SessionMetaCodexRecord', 'import_thread', 'title', 'stem',
'AgentSession', 'SessionDisposition', 'reconnect', 'record_failure',
'retirement', 'settled', 'ObservationProcess', '_close_all',
'AsyncResourceRelease', 'stop_observing',
'LayoutKind', 'LayoutStrategy', 'title_components', 'is_nested',
'unwrapped_type', 'WidgetCreationPipeline', 'WidgetCreationRuntime'}
modules, classes, sites, errors = [], [], [], []
for root in roots:
for path in root.rglob('*.py'):
try:
source = path.read_text()
qt = Path('/home/ts/wt/pyqt-native-formbridge')
if '--qt-before' in sys.argv and path in {
qt / 'src/pyqt_reactive/forms/widget_creation_config.py',
qt / 'src/pyqt_reactive/forms/widget_creation_types.py'}:
source = subprocess.check_output(
['git', '-C', str(qt), 'show', 'HEAD:' + str(path.relative_to(qt))], text=True)
parsed = SourceModule.from_source_path(path, source).parse()
except Exception as error:
errors.append((str(path), str(error)))
continue
modules.append(parsed)
syntax = module_syntax_index(parsed.module)
for _, node in syntax.indexed_nodes_of_type(ast.ClassDef):
classes.append((str(path), node.lineno, node.name, [ast.unparse(base) for base in node.bases]))
for _, node in syntax.indexed_nodes_of_type(ast.ImportFrom):
if any(name.name in terms for name in node.names):
sites.append((str(path), node.lineno, 'import', ast.unparse(node)))
for kind in (ast.Call, ast.Attribute, ast.Name, ast.FunctionDef, ast.AnnAssign, ast.Assign):
for _, node in syntax.indexed_nodes_of_type(kind):
text = ast.unparse(node)
if any(term in text for term in terms) and len(text) < 2500:
sites.append((str(path), node.lineno, kind.__name__, text))
index = build_class_family_index(modules)
print(json.dumps({'roots': list(map(str, roots)), 'modules': len(modules),
'classes': classes, 'indexed_classes': len(index.classes_by_symbol),
'sites': sites, 'parse_errors': errors}, indent=2))
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,19 @@ dependencies = [
"agent-comms[acp]",
"agent-client-protocol==0.12.1",
"ui-theme",
"formbridge==0.1.0",
"objectstate==2.1.1",
"formbridge==0.1.5",
"objectstate==4.0.1",
]

[tool.uv.workspace]
members = ["toad"]

[tool.uv.sources]
ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "2f19daf21f9fb6f22aafb0e1f470a9bb949a3633" }
agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "688c0c5c23c71973abe823041f6c4d530b4c8ef4" }
textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" }
agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "e56c4004aacef25f763a36f525227e1dd642db4a" }
textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "e2fd8a336c49db0f16f22fc52618b378e30968cf" }
objectstate = { git = "https://github.com/OpenHCSDev/ObjectState.git", rev = "82a6213263c9532f1b7cd4ebb2e9e368fdc5e2bd" }
formbridge = { git = "https://github.com/OpenHCSDev/formbridge.git", rev = "dc5d21e482d086c02b07dbfa5916f409487894d4" }
textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" }
textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", rev = "8fa7d4d0db993ea3b761c2760ca9b6a56a6251e9" }

Expand Down
21 changes: 9 additions & 12 deletions src/toad/acp/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,19 +296,16 @@ async def observe_thread_presentation(self, presentation) -> None:
await self.session.observe_owner(presentation)

async def update_project(self, path: str) -> str:
if (self.coordination.wire_root if self.coordination else None) is None or (
self.coordination.thread.name if self.coordination else None
) is None:
coordination = self.coordination
if coordination is None or coordination.wire_root is None or coordination.thread.name is None:
raise ValueError("Project changes require an agent-comms thread.")
from agent_comms.comms import wire

result = await asyncio.to_thread(
wire(
(self.coordination.wire_root if self.coordination else None)
).threads.set_project,
(self.coordination.thread.name if self.coordination else None),
path,
)
authority = ClientSessionRequest(self, self.session_id)
async with self.controller.transcripts.bind(coordination.wire_root) as comms:
authority.require()
result = await asyncio.to_thread(
comms.threads.set_project, coordination.thread.name, path,
)
authority.require()
self.coordination = replace(self.coordination, worktree=result.current)
self.project_root_path = Path(result.current)
self.events.publish(core_events.CommsUpdated(self.coordination, self.session_id))
Expand Down
3 changes: 2 additions & 1 deletion src/toad/acp/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from functools import partial

from agent_comms.declared_family import DeclaredFamily
from agent_comms.launch_runtime import LaunchRuntime
from toad import surface_binding
from toad.acp.sdk_boundary import ValidateSessionUpdateTask
from toad.plan import PlanItem
Expand Down Expand Up @@ -69,7 +70,7 @@ def __init__(self, agent):
self.validation: ValidationOwner = HeadlessValidationOwner()
self._deferred_submissions: set[asyncio.Task] = set()
self.prompt_in_flight = 0
self.transcripts = DetachedTranscriptReader()
self.transcripts = DetachedTranscriptReader(LaunchRuntime.running())
self.coordination = None
self.session = SessionBinding(None)
self.mode_state: SessionModeState | None = None
Expand Down
84 changes: 45 additions & 39 deletions src/toad/acp/agent_session.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""ACP session handshake, durable metadata and reconnection custody."""
"""ACP session handshake, durable metadata and reconnection."""
from __future__ import annotations
from agent_comms.declared_family import DeclaredFamily
from agent_comms.child_process import join_retirement
import asyncio
import os
from pathlib import Path
from dataclasses import dataclass, replace
from agent_comms.acp_ingress import AcpIngress
from agent_comms.route_selection import RouteSelection
import toad
from toad import constants, jsonrpc
from toad.core import events as core_events
Expand Down Expand Up @@ -72,6 +75,9 @@ class RefusedSessionDisposition(FailedSessionDisposition):

class AgentSession:
"""Own readiness and durable session effects; binding identity stays on the controller."""

reconnect_timeout_seconds = 30

def __init__(self, agent, pk):
self.agent = agent
self.task = None
Expand Down Expand Up @@ -118,11 +124,15 @@ def start(self):
self.task = asyncio.create_task(self.run())
self.task.add_done_callback(self.session_finished)

async def retire(self):
def begin_retirement(self):
self.close()
if self.retirement is None:
self.retirement = asyncio.create_task(self._retire())
await join_retirement(self.retirement)
self.retirement.add_done_callback(self.retirement_finished)
return self.retirement

async def retire(self):
await join_retirement(self.begin_retirement())

async def _retire(self):
pending = tuple(task for task in (self.task, *self.responses)
Expand All @@ -141,8 +151,6 @@ def session_finished(self, task):
error = task.exception()
if error is not None and self.accepts_updates:
self.startup_failed(f"{type(error).__name__}: {error}")
self.retirement = asyncio.create_task(self._retire())
self.retirement.add_done_callback(self.retirement_finished)

def retirement_finished(self, task):
if task.cancelled():
Expand All @@ -161,6 +169,7 @@ def record_failure(
self.failed()
self.disposition = state(event)
self.agent.events.publish(event)
self.begin_retirement()

def session_failed(self, failure):
self.record_failure(
Expand Down Expand Up @@ -189,7 +198,7 @@ def supports_load(self):
return self.capabilities.load_session

def starting(self):
"""Start admission clears readiness before any asynchronous work."""
"""Starting clears readiness before any asynchronous work."""
self.connected = False
self.settled.clear()
self.agent.controller.reset_configuration()
Expand All @@ -205,7 +214,7 @@ async def touch(self):
if self.pk is not None:
await DB().session_update_last_used(self.pk)

def coordinated_title(self, title):
async def coordinated_title(self, title):
"""A coordination update presents the thread's title.

A name the user chose wins: it is announced as the session's title
Expand All @@ -217,7 +226,7 @@ def coordinated_title(self, title):
if self.chosen_name is not None:
coordination = self.agent.coordination
if coordination is not None and coordination.title != self.chosen_name:
self.rename_coordination(self.chosen_name)
await self.rename_coordination(self.chosen_name)
return
if title:
self.agent.events.publish(SessionThreadTitle(title))
Expand Down Expand Up @@ -277,31 +286,27 @@ async def reconnect(self, admission: SessionLoadAdmission | None = None) -> None
"""Reattach the existing view after login or an explicit owner start."""
if self.reconnecting:
return
if self.agent.controller.session.bound and not self.supports_load:
raise ValueError("This agent cannot resume its session.")
from pathlib import Path
from agent_comms.acp_ingress import AcpIngress
from agent_comms.route_selection import RouteSelection

requested_cwd = str(self.agent.project_root_path.resolve())
self.reconnecting = True
try:
try:
selection = await asyncio.to_thread(
RouteSelection.for_child, os.environ.copy(), requested_cwd)
await AcpIngress(selection, Path(requested_cwd),
self.agent.process.attached_root(self.agent)).preflight()
except Exception as error:
raise ValueError(
f"Reconnect not attempted: maintenance admission denied: {error}"
) from error
selection = await asyncio.to_thread(
RouteSelection.for_child, os.environ.copy(), requested_cwd)
await AcpIngress(selection, Path(requested_cwd),
self.agent.process.attached_root(self.agent)).preflight()
await self.agent.stop()
self.load_admission = admission or EnsuringSessionLoadAdmission()
self.settled.clear()
await self.agent.start((self.agent.process,))
await asyncio.wait_for(self.settled.wait(), timeout=30)
if not self.connected:
raise ValueError("The agent could not reconnect.")
try:
await asyncio.wait_for(
self.settled.wait(), timeout=self.reconnect_timeout_seconds)
except TimeoutError:
self.startup_failed(
f"Reconnect timed out after {self.reconnect_timeout_seconds} seconds "
"waiting for the session handshake to settle."
)
except (OSError, ValueError, RuntimeError, jsonrpc.APIError) as error:
self.startup_failed(f"Reconnect failed: {type(error).__name__}: {error}")
finally:
self.reconnecting = False

Expand Down Expand Up @@ -440,21 +445,22 @@ async def load(self) -> None:
self.publish_configuration(response)


def rename_coordination(self, display_name: str) -> None:
async def rename_coordination(self, display_name: str) -> None:
if not self.accepts_session(self.agent.session_id):
return
thread = self.agent.coordination.thread.name if self.agent.coordination else None
wire_root = self.agent.coordination.wire_root if self.agent.coordination else None
if thread is None or wire_root is None:
coordination = self.agent.coordination
if coordination is None or coordination.thread.name is None or coordination.wire_root is None:
return

from agent_comms.comms import wire

result = wire(wire_root).threads.rename_managed_thread(
thread,
display_name,
owner_pid=self.agent.coordination.owner_pid,
)
authority = ClientSessionRequest(self.agent, self.agent.session_id)
async with self.agent.controller.transcripts.bind(coordination.wire_root) as comms:
authority.require()
result = await asyncio.to_thread(
comms.threads.rename_managed_thread,
coordination.thread.name,
display_name,
owner_pid=coordination.owner_pid,
)
authority.require()
self.agent.coordination = replace(
self.agent.coordination,
thread=replace(self.agent.coordination.thread, name=result.current),
Expand Down Expand Up @@ -488,7 +494,7 @@ async def set_name(self, name: str) -> None:
if self.chosen_name == name:
return
self.chosen_name = name or None
self.rename_coordination(name)
await self.rename_coordination(name)
if self.pk is None:
return
db = DB()
Expand Down
4 changes: 2 additions & 2 deletions src/toad/acp/comms_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def input_started(self, update: InputStartedUpdate) -> None:
self.agent._post_queue_view(starts)

@handles(CoordinationChangedUpdate)
def coordination_changed(self, update: CoordinationChangedUpdate) -> None:
async def coordination_changed(self, update: CoordinationChangedUpdate) -> None:
from dataclasses import replace
from pathlib import Path

Expand All @@ -136,7 +136,7 @@ def coordination_changed(self, update: CoordinationChangedUpdate) -> None:
agent.context_measurement = ContextMeasurement.saved(update.context_usage)
agent.update_status_line()
agent.events.publish(core_events.CommsUpdated(agent.coordination, self.session_id))
agent.session.coordinated_title(update.title)
await agent.session.coordinated_title(update.title)

@handles(GoalChangedUpdate)
def goal_changed(self, update: GoalChangedUpdate) -> None:
Expand Down
Loading