crabcode 0.1.6__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Keyboard-driven history browser using the CLI's terminal-native palette."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from prompt_toolkit.application import Application
|
|
12
|
+
from prompt_toolkit.buffer import Buffer
|
|
13
|
+
from prompt_toolkit.data_structures import Point
|
|
14
|
+
from prompt_toolkit.filters import Condition
|
|
15
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
16
|
+
from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
|
|
17
|
+
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
|
18
|
+
from prompt_toolkit.styles import Style
|
|
19
|
+
|
|
20
|
+
from crabcode_core.io_worker import run_io
|
|
21
|
+
|
|
22
|
+
PICKER_STYLE = Style.from_dict({
|
|
23
|
+
"accent": "ansicyan bold", "selected": "ansicyan bold",
|
|
24
|
+
"hint": "ansibrightblack", "focused": "ansicyan bold underline",
|
|
25
|
+
"error": "ansired",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _timestamp(value: Any) -> float:
|
|
30
|
+
try:
|
|
31
|
+
return float(value or 0)
|
|
32
|
+
except (ValueError, TypeError):
|
|
33
|
+
try:
|
|
34
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
35
|
+
return parsed.replace(tzinfo=parsed.tzinfo or timezone.utc).timestamp()
|
|
36
|
+
except (ValueError, TypeError, OverflowError):
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _plain(value: Any) -> str:
|
|
41
|
+
# Transcript text is literal, single-line terminal content, never markup.
|
|
42
|
+
return " ".join("".join(c for c in str(value or "") if c.isprintable() or c.isspace()).split())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _age(timestamp: float) -> str:
|
|
46
|
+
if not timestamp:
|
|
47
|
+
return "unknown"
|
|
48
|
+
seconds = max(0, int(time.time() - timestamp))
|
|
49
|
+
for unit, size in (("d", 86400), ("h", 3600), ("m", 60)):
|
|
50
|
+
if seconds >= size:
|
|
51
|
+
return f"{seconds // size}{unit} ago"
|
|
52
|
+
return "just now"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_sessions(cwd: str) -> list[dict[str, Any]]:
|
|
56
|
+
"""Reconcile local JSONL history and include the global metadata index."""
|
|
57
|
+
from crabcode_core.session.meta_db import SessionMetaStore
|
|
58
|
+
from crabcode_core.session.storage import SessionStorage
|
|
59
|
+
|
|
60
|
+
local = SessionStorage.list_sessions(cwd)
|
|
61
|
+
store = SessionMetaStore()
|
|
62
|
+
try:
|
|
63
|
+
# SQLite's -1 means no limit: older history must remain searchable.
|
|
64
|
+
indexed = store.list_recent(limit=-1)
|
|
65
|
+
finally:
|
|
66
|
+
store.close()
|
|
67
|
+
# Local storage also checks durable archive markers; do not reintroduce
|
|
68
|
+
# rows excluded by that reconciliation from the metadata index.
|
|
69
|
+
rows = {r["id"]: r for r in indexed if os.path.normcase(r["cwd"]) != os.path.normcase(cwd)}
|
|
70
|
+
by_id = {r["id"]: r for r in indexed}
|
|
71
|
+
for row in local:
|
|
72
|
+
sid = row["session_id"]
|
|
73
|
+
rows[sid] = {
|
|
74
|
+
**by_id.get(sid, {}), **row, "id": sid, "cwd": cwd,
|
|
75
|
+
"updated_at": _timestamp(row.get("modified")),
|
|
76
|
+
"created_at": _timestamp(row.get("created_at")),
|
|
77
|
+
}
|
|
78
|
+
return list(rows.values())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SessionPicker:
|
|
82
|
+
def __init__(self, cwd: str, current_session_id: str = "") -> None:
|
|
83
|
+
self.cwd = os.path.abspath(cwd)
|
|
84
|
+
self.current_session_id = current_session_id
|
|
85
|
+
self.rows: list[dict[str, Any]] = []
|
|
86
|
+
self.matches: list[dict[str, Any]] = []
|
|
87
|
+
self.selected = 0
|
|
88
|
+
self.all_projects = False
|
|
89
|
+
self.sort_key = "updated_at"
|
|
90
|
+
self.loading = True
|
|
91
|
+
self.error = ""
|
|
92
|
+
self.query = Buffer(on_text_changed=lambda _: self.refresh())
|
|
93
|
+
self.search = BufferControl(buffer=self.query)
|
|
94
|
+
self.options = FormattedTextControl(self._options, focusable=True)
|
|
95
|
+
self.list_control = FormattedTextControl(
|
|
96
|
+
self._list, get_cursor_position=lambda: Point(x=0, y=self.selected),
|
|
97
|
+
)
|
|
98
|
+
self.list_window = Window(
|
|
99
|
+
self.list_control, wrap_lines=False, always_hide_cursor=True,
|
|
100
|
+
)
|
|
101
|
+
self.option_index = 0
|
|
102
|
+
bindings = KeyBindings()
|
|
103
|
+
|
|
104
|
+
@bindings.add("escape", eager=True)
|
|
105
|
+
@bindings.add("c-c", eager=True)
|
|
106
|
+
@bindings.add("c-d", eager=True)
|
|
107
|
+
def cancel(event: Any) -> None:
|
|
108
|
+
event.app.exit(result=None)
|
|
109
|
+
|
|
110
|
+
@bindings.add("enter", eager=True)
|
|
111
|
+
def confirm(event: Any) -> None:
|
|
112
|
+
if self.matches:
|
|
113
|
+
event.app.exit(result=self.matches[self.selected]["id"])
|
|
114
|
+
|
|
115
|
+
@bindings.add("up")
|
|
116
|
+
@bindings.add("down")
|
|
117
|
+
@bindings.add("pageup")
|
|
118
|
+
@bindings.add("pagedown")
|
|
119
|
+
def navigate(event: Any) -> None:
|
|
120
|
+
key = event.key_sequence[0].key
|
|
121
|
+
page = max(1, self.list_window.render_info.window_height - 1) if self.list_window.render_info else 10
|
|
122
|
+
delta = {"up": -1, "down": 1, "pageup": -page, "pagedown": page}[key]
|
|
123
|
+
self.selected = max(0, min(len(self.matches) - 1, self.selected + delta))
|
|
124
|
+
|
|
125
|
+
@bindings.add("tab")
|
|
126
|
+
@bindings.add("s-tab")
|
|
127
|
+
def focus(event: Any) -> None:
|
|
128
|
+
current = self.option_index + 1 if event.app.layout.has_focus(self.options) else 0
|
|
129
|
+
current = (current + (-1 if event.key_sequence[0].key == "s-tab" else 1)) % 3
|
|
130
|
+
if current == 0:
|
|
131
|
+
event.app.layout.focus(self.search)
|
|
132
|
+
else:
|
|
133
|
+
self.option_index = current - 1
|
|
134
|
+
event.app.layout.focus(self.options)
|
|
135
|
+
|
|
136
|
+
@bindings.add("left", filter=Condition(lambda: self.app.layout.has_focus(self.options)))
|
|
137
|
+
@bindings.add("right", filter=Condition(lambda: self.app.layout.has_focus(self.options)))
|
|
138
|
+
@bindings.add("space", filter=Condition(lambda: self.app.layout.has_focus(self.options)))
|
|
139
|
+
def change_option(event: Any) -> None:
|
|
140
|
+
if self.option_index == 0:
|
|
141
|
+
self.all_projects = not self.all_projects
|
|
142
|
+
else:
|
|
143
|
+
self.sort_key = "created_at" if self.sort_key == "updated_at" else "updated_at"
|
|
144
|
+
self.refresh()
|
|
145
|
+
|
|
146
|
+
self.app: Application[str | None] = Application(
|
|
147
|
+
layout=Layout(HSplit([
|
|
148
|
+
Window(FormattedTextControl([("class:accent", " ╭─ CrabCode · Resume session")]), height=1),
|
|
149
|
+
Window(height=1),
|
|
150
|
+
VSplit([
|
|
151
|
+
Window(FormattedTextControl([("class:accent", " Search ❯ ")]), width=11),
|
|
152
|
+
Window(self.search, height=1, wrap_lines=False),
|
|
153
|
+
], height=1),
|
|
154
|
+
Window(self.options, height=1),
|
|
155
|
+
Window(height=1),
|
|
156
|
+
self.list_window,
|
|
157
|
+
Window(FormattedTextControl(self._details), height=2, wrap_lines=False),
|
|
158
|
+
Window(char="─", height=1, style="class:hint"),
|
|
159
|
+
Window(FormattedTextControl(self._footer), height=2, wrap_lines=True),
|
|
160
|
+
]), focused_element=self.search),
|
|
161
|
+
key_bindings=bindings, full_screen=True, erase_when_done=True,
|
|
162
|
+
style=PICKER_STYLE,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def refresh(self) -> None:
|
|
166
|
+
terms = self.query.text.casefold().split()
|
|
167
|
+
self.matches = [r for r in self.rows if r["id"] != self.current_session_id
|
|
168
|
+
and (self.all_projects or os.path.normcase(r["cwd"]) == os.path.normcase(self.cwd))
|
|
169
|
+
and all(term in " ".join(str(r.get(k, "")) for k in
|
|
170
|
+
("title", "first_user_message", "preview", "id", "cwd")).casefold() for term in terms)]
|
|
171
|
+
self.matches.sort(key=lambda r: (_timestamp(r.get(self.sort_key)), r["id"]), reverse=True)
|
|
172
|
+
self.selected = 0
|
|
173
|
+
self.app.invalidate()
|
|
174
|
+
|
|
175
|
+
def _options(self) -> list[tuple[str, str]]:
|
|
176
|
+
focused = self.app.layout.has_focus(self.options)
|
|
177
|
+
return [
|
|
178
|
+
("class:focused" if focused and self.option_index == 0 else "class:hint",
|
|
179
|
+
f" Project: {'All' if self.all_projects else 'Cwd'}"),
|
|
180
|
+
("class:focused" if focused and self.option_index == 1 else "class:hint",
|
|
181
|
+
f" Sort: {'Updated' if self.sort_key == 'updated_at' else 'Created'}"),
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
def _list(self) -> list[tuple[str, str]]:
|
|
185
|
+
if self.loading:
|
|
186
|
+
return [("class:hint", " Loading history…")]
|
|
187
|
+
if self.error:
|
|
188
|
+
return [("class:error", f" Could not load history: {_plain(self.error)}")]
|
|
189
|
+
if not self.matches:
|
|
190
|
+
return [("class:hint", " No matching sessions. Clear search or switch Project to All.")]
|
|
191
|
+
fragments = []
|
|
192
|
+
for i, row in enumerate(self.matches):
|
|
193
|
+
style = "class:selected" if i == self.selected else ""
|
|
194
|
+
title = _plain(row.get("title") or row.get("first_user_message") or row.get("preview") or "Untitled session")
|
|
195
|
+
fragments.extend([
|
|
196
|
+
(style, " ❯ " if i == self.selected else " "),
|
|
197
|
+
(style if i == self.selected else "class:hint", f"{_age(_timestamp(row.get(self.sort_key))):>9} "),
|
|
198
|
+
(style, title + ("\n" if i < len(self.matches) - 1 else "")),
|
|
199
|
+
])
|
|
200
|
+
return fragments
|
|
201
|
+
|
|
202
|
+
def _details(self) -> list[tuple[str, str]]:
|
|
203
|
+
if not self.matches:
|
|
204
|
+
return []
|
|
205
|
+
row = self.matches[self.selected]
|
|
206
|
+
return [("class:hint", f" {_plain(row.get('title') or row.get('preview') or row.get('first_user_message'))}\n"
|
|
207
|
+
f" {_plain(row['cwd'])} · {row['id'][:8]} · {_plain(row.get('model'))}")]
|
|
208
|
+
|
|
209
|
+
def _footer(self) -> list[tuple[str, str]]:
|
|
210
|
+
count = len(self.matches)
|
|
211
|
+
return [("class:hint", f" ↑↓ browse · PgUp/PgDn page · Enter resume · Esc/Ctrl+C cancel {self.selected + 1 if count else 0}/{count}\n"
|
|
212
|
+
" Type to search · Tab focus project/sort · ←→ change option")]
|
|
213
|
+
|
|
214
|
+
async def run(self) -> str | None:
|
|
215
|
+
async def load() -> None:
|
|
216
|
+
try:
|
|
217
|
+
self.rows = await run_io(__name__, "_load_sessions", self.cwd)
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
self.error = str(exc)
|
|
220
|
+
finally:
|
|
221
|
+
self.loading = False
|
|
222
|
+
self.refresh()
|
|
223
|
+
|
|
224
|
+
task: asyncio.Task[None] | None = None
|
|
225
|
+
|
|
226
|
+
def start() -> None:
|
|
227
|
+
nonlocal task
|
|
228
|
+
task = asyncio.create_task(load())
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
return await self.app.run_async(pre_run=start)
|
|
232
|
+
finally:
|
|
233
|
+
if task is not None:
|
|
234
|
+
task.cancel()
|
|
235
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def select_session(cwd: str, current_session_id: str = "") -> str | None:
|
|
239
|
+
return await SessionPicker(cwd, current_session_id).run()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Private stdio supervisor: keep a Windows process tree in a kill-on-close job.
|
|
2
|
+
|
|
3
|
+
The supervisor joins the job before starting the command, avoiding the race
|
|
4
|
+
where a short-lived launcher spawns children before it can be assigned a job.
|
|
5
|
+
Only stdlib imports are used; this file also runs with an isolated environment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ctypes
|
|
11
|
+
from ctypes import wintypes
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def attach_process_tree_job() -> int:
|
|
17
|
+
"""Own descendants until this process exits; keep the job handle open."""
|
|
18
|
+
class BasicLimit(ctypes.Structure):
|
|
19
|
+
_fields_ = [
|
|
20
|
+
("process_time", ctypes.c_int64), ("job_time", ctypes.c_int64),
|
|
21
|
+
("flags", wintypes.DWORD), ("min_working_set", ctypes.c_size_t),
|
|
22
|
+
("max_working_set", ctypes.c_size_t), ("active_processes", wintypes.DWORD),
|
|
23
|
+
("affinity", ctypes.c_size_t), ("priority", wintypes.DWORD),
|
|
24
|
+
("scheduling", wintypes.DWORD),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
class ExtendedLimit(ctypes.Structure):
|
|
28
|
+
_fields_ = [
|
|
29
|
+
("basic", BasicLimit), ("io", ctypes.c_uint64 * 6),
|
|
30
|
+
("process_memory", ctypes.c_size_t), ("job_memory", ctypes.c_size_t),
|
|
31
|
+
("peak_process_memory", ctypes.c_size_t), ("peak_job_memory", ctypes.c_size_t),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
35
|
+
kernel.CreateJobObjectW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR]
|
|
36
|
+
kernel.CreateJobObjectW.restype = wintypes.HANDLE
|
|
37
|
+
kernel.SetInformationJobObject.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]
|
|
38
|
+
kernel.SetInformationJobObject.restype = wintypes.BOOL
|
|
39
|
+
kernel.GetCurrentProcess.restype = wintypes.HANDLE
|
|
40
|
+
kernel.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]
|
|
41
|
+
kernel.AssignProcessToJobObject.restype = wintypes.BOOL
|
|
42
|
+
job = kernel.CreateJobObjectW(None, None) # Non-inheritable handle.
|
|
43
|
+
if not job:
|
|
44
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
45
|
+
limits = ExtendedLimit()
|
|
46
|
+
limits.basic.flags = 0x2000 # JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
47
|
+
if not kernel.SetInformationJobObject(job, 9, ctypes.byref(limits), ctypes.sizeof(limits)):
|
|
48
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
49
|
+
if not kernel.AssignProcessToJobObject(job, kernel.GetCurrentProcess()):
|
|
50
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
51
|
+
return job
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main() -> None:
|
|
55
|
+
attach_process_tree_job()
|
|
56
|
+
code = subprocess.call(
|
|
57
|
+
sys.argv[1:], stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr,
|
|
58
|
+
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
59
|
+
)
|
|
60
|
+
# Process exit closes the sole job handle and reaps any surviving children.
|
|
61
|
+
# Do not explicitly CloseHandle(job) while still running inside the job.
|
|
62
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
63
|
+
kernel.ExitProcess.argtypes = [wintypes.UINT]
|
|
64
|
+
kernel.ExitProcess(code & 0xFFFFFFFF)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|