stcode 0.0.1a0__py3-none-any.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.
- stcode/__init__.py +10 -0
- stcode/cli/__init__.py +9 -0
- stcode/cli/bootstrap.py +70 -0
- stcode/cli/labels.py +621 -0
- stcode/cli/logic/__init__.py +0 -0
- stcode/cli/logic/commands.py +36 -0
- stcode/cli/logic/completion.py +43 -0
- stcode/cli/logic/connection.py +46 -0
- stcode/cli/logic/requests.py +18 -0
- stcode/cli/logic/sessions.py +67 -0
- stcode/cli/logic/settings.py +35 -0
- stcode/cli/logic/state.py +43 -0
- stcode/cli/logic/workspace.py +17 -0
- stcode/cli/main.py +290 -0
- stcode/cli/models.py +61 -0
- stcode/cli/services/__init__.py +0 -0
- stcode/cli/services/client.py +393 -0
- stcode/cli/services/launcher.py +159 -0
- stcode/cli/services/paths.py +18 -0
- stcode/cli/services/preferences.py +107 -0
- stcode/cli/ui/__init__.py +0 -0
- stcode/cli/ui/app.py +136 -0
- stcode/cli/ui/app.tcss +3 -0
- stcode/cli/ui/components/__init__.py +0 -0
- stcode/cli/ui/components/banner.py +78 -0
- stcode/cli/ui/components/cards.py +229 -0
- stcode/cli/ui/components/prompt.py +225 -0
- stcode/cli/ui/components/status.py +95 -0
- stcode/cli/ui/components/transcript/__init__.py +51 -0
- stcode/cli/ui/components/transcript/entries.py +232 -0
- stcode/cli/ui/components/transcript/formatting.py +188 -0
- stcode/cli/ui/components/transcript/view.py +101 -0
- stcode/cli/ui/screens/__init__.py +0 -0
- stcode/cli/ui/screens/chat/__init__.py +0 -0
- stcode/cli/ui/screens/chat/chat.tcss +12 -0
- stcode/cli/ui/screens/chat/presenter.py +187 -0
- stcode/cli/ui/screens/chat/screen.py +682 -0
- stcode/cli/ui/screens/connect.py +126 -0
- stcode/cli/ui/screens/connect.tcss +49 -0
- stcode/cli/ui/screens/dialog.tcss +40 -0
- stcode/cli/ui/screens/sessions.py +105 -0
- stcode/cli/ui/screens/settings.py +137 -0
- stcode/cli/ui/screens/settings.tcss +69 -0
- stcode/cli/ui/screens/trust.py +50 -0
- stcode/cli/ui/theme.py +221 -0
- stcode/core/__main__.py +7 -0
- stcode/core/agent/__init__.py +41 -0
- stcode/core/agent/agent.py +651 -0
- stcode/core/agent/events.py +97 -0
- stcode/core/agent/supervisor.py +226 -0
- stcode/core/agent/task.py +120 -0
- stcode/core/common/__init__.py +21 -0
- stcode/core/common/compat.py +27 -0
- stcode/core/common/ids.py +43 -0
- stcode/core/common/tools.py +50 -0
- stcode/core/common/trace.py +406 -0
- stcode/core/common/truncate.py +76 -0
- stcode/core/configs.py +490 -0
- stcode/core/daemon/__init__.py +32 -0
- stcode/core/daemon/autonomy.py +71 -0
- stcode/core/daemon/main.py +126 -0
- stcode/core/daemon/protocol.py +499 -0
- stcode/core/daemon/runner.py +359 -0
- stcode/core/daemon/server.py +682 -0
- stcode/core/daemon/workspace.py +88 -0
- stcode/core/harness/__init__.py +81 -0
- stcode/core/harness/approvals.py +85 -0
- stcode/core/harness/context.py +260 -0
- stcode/core/harness/errors.py +40 -0
- stcode/core/harness/harness.py +417 -0
- stcode/core/harness/mcp.py +530 -0
- stcode/core/harness/outputs.py +99 -0
- stcode/core/harness/prompts/__init__.py +138 -0
- stcode/core/harness/prompts/execute.py +51 -0
- stcode/core/harness/prompts/plan.py +57 -0
- stcode/core/harness/prompts/sections.py +238 -0
- stcode/core/harness/skills/__init__.py +30 -0
- stcode/core/harness/skills/loader.py +247 -0
- stcode/core/harness/tools/__init__.py +111 -0
- stcode/core/harness/tools/base.py +640 -0
- stcode/core/harness/tools/files.py +276 -0
- stcode/core/harness/tools/interact.py +72 -0
- stcode/core/harness/tools/registry.py +113 -0
- stcode/core/harness/tools/repl.py +97 -0
- stcode/core/harness/tools/schema.py +186 -0
- stcode/core/harness/tools/search.py +356 -0
- stcode/core/harness/tools/shell.py +372 -0
- stcode/core/harness/tools/skill.py +66 -0
- stcode/core/harness/tools/todo.py +62 -0
- stcode/core/harness/tools/web.py +163 -0
- stcode/core/providers/__init__.py +31 -0
- stcode/core/providers/anthropic_claude.py +243 -0
- stcode/core/providers/base.py +73 -0
- stcode/core/providers/gateway.py +267 -0
- stcode/core/providers/google_gemini.py +225 -0
- stcode/core/providers/openai_gpt.py +250 -0
- stcode/core/providers/registry.py +46 -0
- stcode/core/providers/types.py +148 -0
- stcode/core/repl/__init__.py +19 -0
- stcode/core/repl/_worker.py +179 -0
- stcode/core/repl/client.py +374 -0
- stcode/core/session/__init__.py +39 -0
- stcode/core/session/session.py +493 -0
- stcode/core/team/__init__.py +38 -0
- stcode/core/team/mailbox.py +249 -0
- stcode/core/team/tools.py +145 -0
- stcode-0.0.1a0.dist-info/METADATA +229 -0
- stcode-0.0.1a0.dist-info/RECORD +111 -0
- stcode-0.0.1a0.dist-info/WHEEL +4 -0
- stcode-0.0.1a0.dist-info/entry_points.txt +3 -0
- stcode-0.0.1a0.dist-info/licenses/LICENSE +201 -0
stcode/cli/ui/theme.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Themes — the project's green, in the terminal's own light or dark.
|
|
3
|
+
|
|
4
|
+
Two themes, one primary colour (`#78b032`). The default preference is `auto`, and
|
|
5
|
+
`auto` is ours to implement: Textual 8 detects the terminal's background nowhere, so
|
|
6
|
+
this asks.
|
|
7
|
+
|
|
8
|
+
Three sources, cheapest first:
|
|
9
|
+
|
|
10
|
+
1. **`COLORFGBG`** — exported by a handful of terminals, free to read, and an answer
|
|
11
|
+
without a round trip.
|
|
12
|
+
2. **`OSC 11`** — write `ESC ] 11 ; ? BEL`, read back `rgb:rrrr/gggg/bbbb`. Works in
|
|
13
|
+
most modern terminals, and the ones where it does not simply say nothing.
|
|
14
|
+
3. **Dark.** Not a guess about your taste; a guess about terminals, most of which are.
|
|
15
|
+
|
|
16
|
+
The budget is the part that matters. A terminal that ignores the query must cost 200ms
|
|
17
|
+
once, never a UI that will not start — so the read is bounded by `select`, the tty is
|
|
18
|
+
put back exactly as it was, and every failure path returns rather than raises.
|
|
19
|
+
|
|
20
|
+
Detection runs **before** the app starts: Textual owns the tty afterwards, and two
|
|
21
|
+
things reading raw escape sequences off the same terminal is a race with a corrupted
|
|
22
|
+
screen at the end of it.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import select
|
|
30
|
+
import sys
|
|
31
|
+
from typing import Literal
|
|
32
|
+
|
|
33
|
+
from textual.theme import Theme
|
|
34
|
+
|
|
35
|
+
from stcode.cli.models import ThemePreference
|
|
36
|
+
|
|
37
|
+
PRIMARY = "#78b032"
|
|
38
|
+
"""stcode's green. The one colour the UI is built out of."""
|
|
39
|
+
|
|
40
|
+
TerminalMode = Literal["dark", "light"]
|
|
41
|
+
|
|
42
|
+
QUERY = "\x1b]11;?\x07"
|
|
43
|
+
REPLY = re.compile(r"rgb:([0-9a-fA-F]{2,4})/([0-9a-fA-F]{2,4})/([0-9a-fA-F]{2,4})")
|
|
44
|
+
TIMEOUT = 0.2
|
|
45
|
+
"""Long enough for a terminal on the other end of an ssh hop, short enough that a
|
|
46
|
+
terminal which will never answer costs one fifth of a second, once."""
|
|
47
|
+
|
|
48
|
+
BRAND_TEXT_ON_LIGHT = "#3f6618"
|
|
49
|
+
"""The green, darkened enough to be *read* on the light ground.
|
|
50
|
+
|
|
51
|
+
`primary` stays `#78b032` in both themes, as the brand does. But #78b032 on an off-white
|
|
52
|
+
background is about 2.6:1 — fine for a border, unreadable as a sentence — so anything
|
|
53
|
+
writing words asks `brand_text()` instead. In CSS the equivalent is `$text-primary`,
|
|
54
|
+
which Textual derives the same way; this constant exists because a Rich style string is
|
|
55
|
+
not CSS and cannot reference a variable.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
STCODE_DARK = Theme(
|
|
59
|
+
name="stcode-dark",
|
|
60
|
+
primary=PRIMARY,
|
|
61
|
+
secondary="#4f7d1f",
|
|
62
|
+
accent=PRIMARY,
|
|
63
|
+
warning="#ffbe3d",
|
|
64
|
+
error="#ff625a",
|
|
65
|
+
success=PRIMARY,
|
|
66
|
+
foreground="#d6d6d2",
|
|
67
|
+
background="#101210",
|
|
68
|
+
surface="#191c19",
|
|
69
|
+
panel="#20241f",
|
|
70
|
+
dark=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
STCODE_LIGHT = Theme(
|
|
74
|
+
name="stcode-light",
|
|
75
|
+
primary=PRIMARY,
|
|
76
|
+
secondary=BRAND_TEXT_ON_LIGHT,
|
|
77
|
+
accent=PRIMARY,
|
|
78
|
+
warning="#a66300",
|
|
79
|
+
error="#cf3028",
|
|
80
|
+
success=BRAND_TEXT_ON_LIGHT,
|
|
81
|
+
foreground="#1c1f1b",
|
|
82
|
+
background="#fbfbf8",
|
|
83
|
+
surface="#f2f3ee",
|
|
84
|
+
panel="#e8eae2",
|
|
85
|
+
dark=False,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
THEMES = (STCODE_DARK, STCODE_LIGHT)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def brand_text(dark: bool) -> str:
|
|
92
|
+
"""The brand colour, in the version that can be read as text on this ground."""
|
|
93
|
+
return PRIMARY if dark else BRAND_TEXT_ON_LIGHT
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def error_text(dark: bool) -> str:
|
|
97
|
+
"""The theme's red, for the same reason `brand_text` exists: a Rich style string is
|
|
98
|
+
not CSS and cannot say `$error`."""
|
|
99
|
+
return str(STCODE_DARK.error if dark else STCODE_LIGHT.error)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def theme_name_for(preference: ThemePreference, *, detected: TerminalMode) -> str:
|
|
103
|
+
"""Which registered theme to use. An explicit choice always beats detection."""
|
|
104
|
+
if preference == "dark":
|
|
105
|
+
return STCODE_DARK.name
|
|
106
|
+
if preference == "light":
|
|
107
|
+
return STCODE_LIGHT.name
|
|
108
|
+
return STCODE_LIGHT.name if detected == "light" else STCODE_DARK.name
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def background_is_light(reply: str) -> bool | None:
|
|
112
|
+
"""Read an `OSC 11` reply. None when it is not one — which is not a failure.
|
|
113
|
+
|
|
114
|
+
Perceived luminance, not the mean: a saturated blue background is dark with one
|
|
115
|
+
channel at full, and a mid yellow is light with one channel at zero.
|
|
116
|
+
"""
|
|
117
|
+
match = REPLY.search(reply or "")
|
|
118
|
+
if match is None:
|
|
119
|
+
return None
|
|
120
|
+
try:
|
|
121
|
+
channels = [int(value, 16) / (16 ** len(value) - 1) for value in match.groups()]
|
|
122
|
+
except (ValueError, ZeroDivisionError):
|
|
123
|
+
return None
|
|
124
|
+
red, green, blue = channels
|
|
125
|
+
return (0.2126 * red + 0.7152 * green + 0.0722 * blue) > 0.5
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def mode_from_colorfgbg(value: str | None) -> TerminalMode | None:
|
|
129
|
+
"""`COLORFGBG` as some terminals export it: `"<fg>;<bg>"`, ANSI indices.
|
|
130
|
+
|
|
131
|
+
The background is the last field. 0-6 and 8 are the dark half of the base palette;
|
|
132
|
+
`default` means the terminal's own, which for the terminals that set this variable
|
|
133
|
+
at all is light.
|
|
134
|
+
"""
|
|
135
|
+
if not value:
|
|
136
|
+
return None
|
|
137
|
+
background = value.split(";")[-1].strip().lower()
|
|
138
|
+
if background == "default":
|
|
139
|
+
return "light"
|
|
140
|
+
if not background.isdigit():
|
|
141
|
+
return None
|
|
142
|
+
return "dark" if int(background) in (0, 1, 2, 3, 4, 5, 6, 8) else "light"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def detect_terminal_mode(timeout: float = TIMEOUT) -> TerminalMode:
|
|
146
|
+
"""The terminal's background, as far as it will say. Dark when it will not."""
|
|
147
|
+
from_env = mode_from_colorfgbg(os.environ.get("COLORFGBG"))
|
|
148
|
+
if from_env is not None:
|
|
149
|
+
return from_env
|
|
150
|
+
reply = _query_background(timeout)
|
|
151
|
+
is_light = background_is_light(reply) if reply else None
|
|
152
|
+
if is_light is None:
|
|
153
|
+
return "dark"
|
|
154
|
+
return "light" if is_light else "dark"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _query_background(timeout: float) -> str:
|
|
158
|
+
"""Ask the tty directly and read whatever comes back inside `timeout`.
|
|
159
|
+
|
|
160
|
+
Raw mode for the duration and restored in a `finally`: leaving a terminal without
|
|
161
|
+
echo is worse than picking the wrong theme, and this runs before Textual has taken
|
|
162
|
+
the tty over. Every failure — no tty, no termios (Windows), a terminal that writes
|
|
163
|
+
nothing — returns the empty string, which the caller reads as "no answer".
|
|
164
|
+
"""
|
|
165
|
+
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
166
|
+
return ""
|
|
167
|
+
try:
|
|
168
|
+
import termios
|
|
169
|
+
import tty
|
|
170
|
+
except ImportError: # pragma: no cover — Windows
|
|
171
|
+
return ""
|
|
172
|
+
|
|
173
|
+
fd = sys.stdin.fileno()
|
|
174
|
+
try:
|
|
175
|
+
saved = termios.tcgetattr(fd)
|
|
176
|
+
except termios.error:
|
|
177
|
+
return ""
|
|
178
|
+
try:
|
|
179
|
+
tty.setraw(fd, termios.TCSANOW)
|
|
180
|
+
sys.stdout.write(QUERY)
|
|
181
|
+
sys.stdout.flush()
|
|
182
|
+
chunks: list[str] = []
|
|
183
|
+
deadline_left = timeout
|
|
184
|
+
while deadline_left > 0:
|
|
185
|
+
ready, _, _ = select.select([fd], [], [], deadline_left)
|
|
186
|
+
if not ready:
|
|
187
|
+
break
|
|
188
|
+
chunks.append(os.read(fd, 64).decode("utf-8", errors="replace"))
|
|
189
|
+
text = "".join(chunks)
|
|
190
|
+
# Stop at the terminator the reply actually used — BEL or ST — rather than
|
|
191
|
+
# waiting out the whole budget on a terminal that already answered.
|
|
192
|
+
if text.endswith("\x07") or text.endswith("\x1b\\") or REPLY.search(text):
|
|
193
|
+
break
|
|
194
|
+
deadline_left = timeout / 4
|
|
195
|
+
return "".join(chunks)
|
|
196
|
+
except (OSError, ValueError):
|
|
197
|
+
return ""
|
|
198
|
+
finally:
|
|
199
|
+
with_suppressed_error = getattr(termios, "error", Exception)
|
|
200
|
+
try:
|
|
201
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, saved)
|
|
202
|
+
except with_suppressed_error: # pragma: no cover
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
__all__ = [
|
|
207
|
+
"BRAND_TEXT_ON_LIGHT",
|
|
208
|
+
"PRIMARY",
|
|
209
|
+
"QUERY",
|
|
210
|
+
"STCODE_DARK",
|
|
211
|
+
"STCODE_LIGHT",
|
|
212
|
+
"THEMES",
|
|
213
|
+
"TIMEOUT",
|
|
214
|
+
"TerminalMode",
|
|
215
|
+
"background_is_light",
|
|
216
|
+
"brand_text",
|
|
217
|
+
"detect_terminal_mode",
|
|
218
|
+
"error_text",
|
|
219
|
+
"mode_from_colorfgbg",
|
|
220
|
+
"theme_name_for",
|
|
221
|
+
]
|
stcode/core/__main__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent — the turn loop, and the sub-agent tool that spawns another one.
|
|
3
|
+
|
|
4
|
+
agent = await Agent.create(config, cwd=workspace, role="backend-dev")
|
|
5
|
+
|
|
6
|
+
async for ev in agent.run("Add rate limiting to the API"):
|
|
7
|
+
match ev:
|
|
8
|
+
case TextDelta(): out(ev.text)
|
|
9
|
+
case ToolStarted(): spinner(ev.name)
|
|
10
|
+
case ToolFinished(): done(ev.name)
|
|
11
|
+
case TurnFinished(): idle(ev.usage)
|
|
12
|
+
case AgentFailed(): error(ev.message)
|
|
13
|
+
|
|
14
|
+
`push()` + `events()` is the same machine with the door left open — see `agent.py`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from stcode.core.agent.agent import DEFAULT_MAX_TURNS, Agent
|
|
18
|
+
from stcode.core.agent.events import (
|
|
19
|
+
AgentEvent,
|
|
20
|
+
AgentFailed,
|
|
21
|
+
ReasoningDelta,
|
|
22
|
+
TextDelta,
|
|
23
|
+
ToolFinished,
|
|
24
|
+
ToolStarted,
|
|
25
|
+
TurnFinished,
|
|
26
|
+
)
|
|
27
|
+
from stcode.core.agent.task import MAX_DEPTH, make_task_tool
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DEFAULT_MAX_TURNS",
|
|
31
|
+
"MAX_DEPTH",
|
|
32
|
+
"Agent",
|
|
33
|
+
"AgentEvent",
|
|
34
|
+
"AgentFailed",
|
|
35
|
+
"ReasoningDelta",
|
|
36
|
+
"TextDelta",
|
|
37
|
+
"ToolFinished",
|
|
38
|
+
"ToolStarted",
|
|
39
|
+
"TurnFinished",
|
|
40
|
+
"make_task_tool",
|
|
41
|
+
]
|