synapse-cli-agent 0.1.13__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.
- synapse/__init__.py +13 -0
- synapse/__main__.py +6 -0
- synapse/app/__init__.py +1 -0
- synapse/app/agent.py +492 -0
- synapse/app/agent_md.py +107 -0
- synapse/cli.py +750 -0
- synapse/commands/__init__.py +1 -0
- synapse/commands/compression.py +573 -0
- synapse/commands/helpers.py +22 -0
- synapse/commands/mcp.py +406 -0
- synapse/commands/model.py +173 -0
- synapse/commands/result.py +34 -0
- synapse/commands/sessions.py +443 -0
- synapse/commands/slash_cmds.py +521 -0
- synapse/commands/slash_complete.py +816 -0
- synapse/commands/theme.py +99 -0
- synapse/config.py +27 -0
- synapse/content/__init__.py +1 -0
- synapse/content/input_history.py +122 -0
- synapse/content/multimodal.py +733 -0
- synapse/content/prompts.py +249 -0
- synapse/content/skills_catalog.py +128 -0
- synapse/integrations/__init__.py +1 -0
- synapse/integrations/checkpoint_seed.py +281 -0
- synapse/integrations/codex_history.py +375 -0
- synapse/integrations/codex_import.py +393 -0
- synapse/integrations/codex_sessions.py +629 -0
- synapse/integrations/describe_image.py +370 -0
- synapse/integrations/http_clients.py +199 -0
- synapse/integrations/llm_openai_compat.py +90 -0
- synapse/integrations/llm_openai_websocket.py +187 -0
- synapse/integrations/mcp_client.py +646 -0
- synapse/integrations/vision_middleware.py +62 -0
- synapse/models/__init__.py +5 -0
- synapse/models/config.py +240 -0
- synapse/models/helpers.py +206 -0
- synapse/models/profile.py +59 -0
- synapse/models/registry.py +722 -0
- synapse/models_registry.py +7 -0
- synapse/observability/__init__.py +1 -0
- synapse/observability/startup_trace.py +127 -0
- synapse/runtime/__init__.py +1 -0
- synapse/runtime/async_runtime.py +176 -0
- synapse/runtime/backends.py +458 -0
- synapse/runtime/context_compact.py +249 -0
- synapse/runtime/execute_capture.py +48 -0
- synapse/runtime/fs_permissions.py +79 -0
- synapse/runtime/harness.py +57 -0
- synapse/runtime/hitl.py +197 -0
- synapse/runtime/interaction_ledger.py +82 -0
- synapse/runtime/middleware.py +802 -0
- synapse/runtime/model_request_compression_middleware.py +745 -0
- synapse/runtime/pathing.py +146 -0
- synapse/runtime/safety.py +184 -0
- synapse/runtime/steer.py +240 -0
- synapse/runtime/subagents.py +207 -0
- synapse/runtime/tool_ignore.py +221 -0
- synapse/runtime/tool_output_eval.py +118 -0
- synapse/runtime/tool_output_middleware.py +585 -0
- synapse/runtime/tool_output_usage_middleware.py +60 -0
- synapse/sessions/__init__.py +31 -0
- synapse/sessions/cancel_repair.py +208 -0
- synapse/sessions/session_recap.py +174 -0
- synapse/sessions/store.py +695 -0
- synapse/sessions/transcript.py +754 -0
- synapse/settings/__init__.py +5 -0
- synapse/settings/config_paths.py +184 -0
- synapse/settings/schema.py +464 -0
- synapse/tool_output/__init__.py +59 -0
- synapse/tool_output/detection.py +170 -0
- synapse/tool_output/metrics.py +32 -0
- synapse/tool_output/models.py +173 -0
- synapse/tool_output/pipeline.py +330 -0
- synapse/tool_output/repository.py +721 -0
- synapse/tool_output/transformers.py +648 -0
- synapse/tools/__init__.py +5 -0
- synapse/tools/session_tools.py +204 -0
- synapse/ui/__init__.py +10 -0
- synapse/ui/bottombar/__init__.py +73 -0
- synapse/ui/bottombar/components/__init__.py +143 -0
- synapse/ui/bottombar/components/key_hints.py +30 -0
- synapse/ui/bottombar/components/mcp.py +64 -0
- synapse/ui/bottombar/components/mode.py +24 -0
- synapse/ui/bottombar/components/model.py +28 -0
- synapse/ui/bottombar/components/thread.py +29 -0
- synapse/ui/bottombar/context.py +36 -0
- synapse/ui/bottombar/core.py +74 -0
- synapse/ui/dialogs/__init__.py +25 -0
- synapse/ui/dialogs/base.py +362 -0
- synapse/ui/dialogs/codex_session_list.py +84 -0
- synapse/ui/dialogs/compression_diagnostics.py +210 -0
- synapse/ui/dialogs/git_explore.py +702 -0
- synapse/ui/dialogs/mcp_panel.py +407 -0
- synapse/ui/dialogs/model_picker.py +128 -0
- synapse/ui/dialogs/safety_panel.py +63 -0
- synapse/ui/dialogs/session_list.py +98 -0
- synapse/ui/dialogs/theme_designer.py +863 -0
- synapse/ui/dialogs/theme_picker.py +113 -0
- synapse/ui/git_explore/__init__.py +31 -0
- synapse/ui/git_explore/engine.py +82 -0
- synapse/ui/git_explore/provider.py +242 -0
- synapse/ui/git_explore/unified.py +85 -0
- synapse/ui/rendering.py +350 -0
- synapse/ui/sink.py +70 -0
- synapse/ui/steer_widget.py +367 -0
- synapse/ui/stream.py +1207 -0
- synapse/ui/stream_events.py +421 -0
- synapse/ui/stream_runtime.py +252 -0
- synapse/ui/theme.py +1154 -0
- synapse/ui/timeline.py +621 -0
- synapse/ui/topbar/__init__.py +97 -0
- synapse/ui/topbar/components/__init__.py +150 -0
- synapse/ui/topbar/components/branch.py +41 -0
- synapse/ui/topbar/components/title.py +24 -0
- synapse/ui/topbar/components/tool_output.py +24 -0
- synapse/ui/topbar/components/usage.py +24 -0
- synapse/ui/topbar/components/workspace.py +32 -0
- synapse/ui/topbar/context.py +32 -0
- synapse/ui/topbar/core.py +979 -0
- synapse/ui/topbar/git_changes_popover.py +178 -0
- synapse/ui/topbar/git_chrome.py +475 -0
- synapse/ui/topbar/tool_output_popover.py +84 -0
- synapse/ui/topbar/widget.py +474 -0
- synapse/ui/tui.py +5717 -0
- synapse/ui/turn_rail.py +71 -0
- synapse/ui/user_turn.py +83 -0
- synapse/ui/welcome.py +261 -0
- synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
- synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
- synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
- synapse_cli_agent-0.1.13.dist-info/entry_points.txt +2 -0
synapse/ui/turn_rail.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Pure helpers for the TUI turn-rail minimap."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
_WS_RE = re.compile(r"\s+")
|
|
7
|
+
_RAIL_PREVIEW_MAX = 28
|
|
8
|
+
_RAIL_BAR = "───"
|
|
9
|
+
_RAIL_BAR_DENSE = "━━━"
|
|
10
|
+
_RAIL_BAR_HEAVY = "▓▓▓"
|
|
11
|
+
|
|
12
|
+
def format_turn_rail_preview(
|
|
13
|
+
text: str,
|
|
14
|
+
*,
|
|
15
|
+
max_len: int = _RAIL_PREVIEW_MAX,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Single-line user-turn preview for the right rail (ellipsis when long)."""
|
|
18
|
+
one = _WS_RE.sub(" ", (text or "").strip())
|
|
19
|
+
if not one:
|
|
20
|
+
return "(empty)"
|
|
21
|
+
limit = max(8, int(max_len or _RAIL_PREVIEW_MAX))
|
|
22
|
+
if len(one) > limit:
|
|
23
|
+
return one[: limit - 1].rstrip() + "…"
|
|
24
|
+
return one
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def turn_rail_tick_slots(n: int, height: int) -> list[list[int]]:
|
|
28
|
+
"""Map ``n`` turns onto ``height`` minimap rows.
|
|
29
|
+
|
|
30
|
+
When ``n <= height`` the turns are packed tightly and centered vertically
|
|
31
|
+
so the mouse need not travel far. When ``n > height`` the rows are filled
|
|
32
|
+
proportionally with bucket merging (same as before).
|
|
33
|
+
"""
|
|
34
|
+
h = max(1, int(height or 1))
|
|
35
|
+
n = max(0, int(n or 0))
|
|
36
|
+
slots: list[list[int]] = [[] for _ in range(h)]
|
|
37
|
+
if n <= 0:
|
|
38
|
+
return slots
|
|
39
|
+
if n <= h:
|
|
40
|
+
# Compact, centered placement.
|
|
41
|
+
start = (h - n) // 2
|
|
42
|
+
for i in range(n):
|
|
43
|
+
slots[start + i].append(i)
|
|
44
|
+
return slots
|
|
45
|
+
# n > h — proportional bucket merging.
|
|
46
|
+
for i in range(n):
|
|
47
|
+
y = i * h // n
|
|
48
|
+
y = min(h - 1, max(0, y))
|
|
49
|
+
slots[y].append(i)
|
|
50
|
+
return slots
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def format_turn_rail_bucket_label(
|
|
54
|
+
indices: list[int],
|
|
55
|
+
previews: list[str],
|
|
56
|
+
*,
|
|
57
|
+
max_len: int = _RAIL_PREVIEW_MAX,
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Hover label for a minimap slot (single turn or merged bucket)."""
|
|
60
|
+
if not indices:
|
|
61
|
+
return ""
|
|
62
|
+
if len(indices) == 1:
|
|
63
|
+
return previews[0] if previews else f"#{indices[0] + 1}"
|
|
64
|
+
first = indices[0] + 1
|
|
65
|
+
last = indices[-1] + 1
|
|
66
|
+
head = previews[0] if previews else ""
|
|
67
|
+
prefix = f"#{first}-{last} "
|
|
68
|
+
room = max(6, int(max_len or _RAIL_PREVIEW_MAX) - len(prefix))
|
|
69
|
+
if len(head) > room:
|
|
70
|
+
head = head[: max(0, room - 1)].rstrip() + "…"
|
|
71
|
+
return f"{prefix}{head}" if head else f"#{first}-{last}"
|
synapse/ui/user_turn.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Pure formatting helpers for user transcript turns."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from synapse.ui.topbar import display_width, truncate_to_width
|
|
7
|
+
|
|
8
|
+
_WS_RE = re.compile(r"\s+")
|
|
9
|
+
_USER_PREVIEW_MAX_LINES = 3
|
|
10
|
+
|
|
11
|
+
def wrap_user_turn_text(
|
|
12
|
+
text: str,
|
|
13
|
+
*,
|
|
14
|
+
width: int,
|
|
15
|
+
max_lines: int | None = _USER_PREVIEW_MAX_LINES,
|
|
16
|
+
) -> tuple[list[str], bool]:
|
|
17
|
+
"""Word-wrap user prompt for the transcript bar.
|
|
18
|
+
|
|
19
|
+
Returns ``(lines, truncated)``. When ``max_lines`` is None, never truncates.
|
|
20
|
+
Prefers breaks at spaces; falls back to display-width chunks (CJK-safe).
|
|
21
|
+
"""
|
|
22
|
+
width = max(8, int(width or 8))
|
|
23
|
+
raw = _WS_RE.sub(" ", (text or "").strip())
|
|
24
|
+
if not raw:
|
|
25
|
+
return [""], False
|
|
26
|
+
|
|
27
|
+
lines: list[str] = []
|
|
28
|
+
i = 0
|
|
29
|
+
n = len(raw)
|
|
30
|
+
while i < n:
|
|
31
|
+
acc = ""
|
|
32
|
+
last_space_acc_len = -1
|
|
33
|
+
j = i
|
|
34
|
+
while j < n:
|
|
35
|
+
ch = raw[j]
|
|
36
|
+
trial = acc + ch
|
|
37
|
+
if display_width(trial) > width:
|
|
38
|
+
break
|
|
39
|
+
acc = trial
|
|
40
|
+
if ch == " ":
|
|
41
|
+
last_space_acc_len = len(acc)
|
|
42
|
+
j += 1
|
|
43
|
+
if not acc:
|
|
44
|
+
# Single character wider than width (rare); force one cell.
|
|
45
|
+
acc = raw[i]
|
|
46
|
+
j = i + 1
|
|
47
|
+
elif j < n and last_space_acc_len > 0:
|
|
48
|
+
# Break at last space inside this line.
|
|
49
|
+
acc = acc[:last_space_acc_len].rstrip()
|
|
50
|
+
j = i + last_space_acc_len
|
|
51
|
+
# skip the space
|
|
52
|
+
if j < n and raw[j] == " ":
|
|
53
|
+
j += 1
|
|
54
|
+
lines.append(acc)
|
|
55
|
+
i = j
|
|
56
|
+
|
|
57
|
+
if max_lines is None or len(lines) <= max_lines:
|
|
58
|
+
return lines, False
|
|
59
|
+
kept = list(lines[: max(1, int(max_lines))])
|
|
60
|
+
last = kept[-1]
|
|
61
|
+
kept[-1] = truncate_to_width(last, max(4, width))
|
|
62
|
+
if not kept[-1].endswith("…"):
|
|
63
|
+
kept[-1] = truncate_to_width(kept[-1], max(4, width - 1)).rstrip("…") + "…"
|
|
64
|
+
return kept, True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def format_user_turn_meta(
|
|
68
|
+
*,
|
|
69
|
+
stamp: str,
|
|
70
|
+
turn_index: int | None = None,
|
|
71
|
+
image_count: int = 0,
|
|
72
|
+
expanded: bool = False,
|
|
73
|
+
truncated: bool = False,
|
|
74
|
+
) -> str:
|
|
75
|
+
"""Right-side meta: optional #n, img count, time; expand hint is separate."""
|
|
76
|
+
bits: list[str] = []
|
|
77
|
+
if turn_index is not None and int(turn_index) > 0:
|
|
78
|
+
bits.append(f"#{int(turn_index)}")
|
|
79
|
+
if image_count and int(image_count) > 0:
|
|
80
|
+
bits.append(f"img×{int(image_count)}")
|
|
81
|
+
if stamp:
|
|
82
|
+
bits.append(stamp)
|
|
83
|
+
return " · ".join(bits)
|
synapse/ui/welcome.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Animated welcome screen for the Synapse TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.cells import cell_len
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
|
|
12
|
+
_BRAILLE_BLANK = "\u2800"
|
|
13
|
+
_SHIMMER_FPS = 12.0
|
|
14
|
+
_REVEAL_DURATION = 3.0
|
|
15
|
+
_VISIBLE_DURATION = 3.0
|
|
16
|
+
_ERASE_DURATION = 3.0
|
|
17
|
+
_HIDDEN_DURATION = 0.8
|
|
18
|
+
_CYCLE_SECONDS = (
|
|
19
|
+
_REVEAL_DURATION + _VISIBLE_DURATION + _ERASE_DURATION + _HIDDEN_DURATION
|
|
20
|
+
)
|
|
21
|
+
_LEFT_COL_MASK = 0x4D # braille left-column bits: 1+2+4+64
|
|
22
|
+
_BRAILLE_DOTS = (
|
|
23
|
+
((0, 0, 1), (1, 0, 2), (2, 0, 4), (3, 0, 64)),
|
|
24
|
+
((0, 1, 8), (1, 1, 16), (2, 1, 32), (3, 1, 128)),
|
|
25
|
+
)
|
|
26
|
+
_WORD_BITMAPS = {
|
|
27
|
+
"S": ("11110", "10000", "10000", "11110", "00001", "00001", "11110"),
|
|
28
|
+
"Y": ("10001", "10001", "01010", "00100", "00100", "00100", "00100"),
|
|
29
|
+
"N": ("10001", "11001", "11001", "10101", "10011", "10011", "10001"),
|
|
30
|
+
"A": ("01110", "10001", "10001", "11111", "10001", "10001", "10001"),
|
|
31
|
+
"P": ("11110", "10001", "10001", "11110", "10000", "10000", "10000"),
|
|
32
|
+
"E": ("11111", "10000", "10000", "11110", "10000", "10000", "11111"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _braille_cell(bitmap: list[str], row: int, column: int) -> str:
|
|
37
|
+
value = 0
|
|
38
|
+
for row_offset, column_offset, bit in (
|
|
39
|
+
dot for column_dots in _BRAILLE_DOTS for dot in column_dots
|
|
40
|
+
):
|
|
41
|
+
source_row = row * 4 + row_offset
|
|
42
|
+
source_column = column * 2 + column_offset
|
|
43
|
+
if (
|
|
44
|
+
source_row < len(bitmap)
|
|
45
|
+
and source_column < len(bitmap[source_row])
|
|
46
|
+
and bitmap[source_row][source_column] == "1"
|
|
47
|
+
):
|
|
48
|
+
value |= bit
|
|
49
|
+
return chr(0x2800 + value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _braille_glyph(pattern: tuple[str, ...], x_scale: int, y_scale: int) -> tuple[str, ...]:
|
|
53
|
+
bitmap = [
|
|
54
|
+
"".join(pixel * x_scale for pixel in source_row)
|
|
55
|
+
for source_row in pattern
|
|
56
|
+
for _ in range(y_scale)
|
|
57
|
+
]
|
|
58
|
+
cell_rows = (len(bitmap) + 3) // 4
|
|
59
|
+
cell_columns = (len(bitmap[0]) + 1) // 2
|
|
60
|
+
return tuple(
|
|
61
|
+
"".join(_braille_cell(bitmap, row, column) for column in range(cell_columns))
|
|
62
|
+
for row in range(cell_rows)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_braille_logo(x_scale: int, y_scale: int) -> tuple[str, ...]:
|
|
67
|
+
letters = "SYNAPSE"
|
|
68
|
+
glyphs = {
|
|
69
|
+
letter: _braille_glyph(pattern, x_scale, y_scale)
|
|
70
|
+
for letter, pattern in _WORD_BITMAPS.items()
|
|
71
|
+
}
|
|
72
|
+
return tuple(
|
|
73
|
+
_BRAILLE_BLANK.join(glyphs[letter][letter_row] for letter in letters)
|
|
74
|
+
for letter_row in range(len(next(iter(glyphs.values()))))
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_LOGO = _build_braille_logo(x_scale=3, y_scale=4)
|
|
79
|
+
_COMPACT_LOGO = _build_braille_logo(x_scale=2, y_scale=2)
|
|
80
|
+
|
|
81
|
+
def _workspace_label(workspace: str | Path) -> str:
|
|
82
|
+
value = str(workspace or "workspace").rstrip("/\\")
|
|
83
|
+
return Path(value).name or value or "workspace"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _logo_phase(elapsed: float) -> tuple[str, float]:
|
|
87
|
+
"""Return the cycle phase and its elapsed time in seconds."""
|
|
88
|
+
cycle_elapsed = elapsed % _CYCLE_SECONDS
|
|
89
|
+
reveal_end = _REVEAL_DURATION
|
|
90
|
+
visible_end = reveal_end + _VISIBLE_DURATION
|
|
91
|
+
erase_end = visible_end + _ERASE_DURATION
|
|
92
|
+
if cycle_elapsed < reveal_end:
|
|
93
|
+
return "revealing", cycle_elapsed
|
|
94
|
+
if cycle_elapsed < visible_end:
|
|
95
|
+
return "visible", cycle_elapsed - reveal_end
|
|
96
|
+
if cycle_elapsed < erase_end:
|
|
97
|
+
return "erasing", cycle_elapsed - visible_end
|
|
98
|
+
return "hidden", cycle_elapsed - erase_end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _dot_char(full_value: int, fraction: float) -> str:
|
|
102
|
+
"""Build a Braille glyph with only a subset of dots active.
|
|
103
|
+
|
|
104
|
+
``fraction`` 0→1 reveals left-column first, then right-column.
|
|
105
|
+
``fraction >= 1`` returns the full glyph.
|
|
106
|
+
"""
|
|
107
|
+
if full_value == 0:
|
|
108
|
+
return _BRAILLE_BLANK
|
|
109
|
+
if fraction >= 1.0:
|
|
110
|
+
return chr(0x2800 + full_value)
|
|
111
|
+
if fraction < 0.5:
|
|
112
|
+
partial = full_value & _LEFT_COL_MASK
|
|
113
|
+
else:
|
|
114
|
+
partial = full_value
|
|
115
|
+
return chr(0x2800 + partial) if partial else _BRAILLE_BLANK
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _dot_char_erase(full_value: int, fraction: float) -> str:
|
|
119
|
+
"""Build a Braille glyph erasing right-column then left-column.
|
|
120
|
+
|
|
121
|
+
``fraction`` 0→1 removes right-column first, then everything.
|
|
122
|
+
"""
|
|
123
|
+
if full_value == 0:
|
|
124
|
+
return _BRAILLE_BLANK
|
|
125
|
+
if fraction >= 1.0:
|
|
126
|
+
return _BRAILLE_BLANK
|
|
127
|
+
if fraction < 0.5:
|
|
128
|
+
partial = full_value & _LEFT_COL_MASK
|
|
129
|
+
else:
|
|
130
|
+
partial = 0
|
|
131
|
+
return chr(0x2800 + partial) if partial else _BRAILLE_BLANK
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _logo_style(
|
|
135
|
+
frame: int,
|
|
136
|
+
column: int,
|
|
137
|
+
row: int,
|
|
138
|
+
width: int,
|
|
139
|
+
full_value: int,
|
|
140
|
+
*,
|
|
141
|
+
muted: str,
|
|
142
|
+
fg: str,
|
|
143
|
+
accent: str,
|
|
144
|
+
total_rows: int = 7,
|
|
145
|
+
) -> tuple[str, str]:
|
|
146
|
+
"""Return ``(braille_char, style)`` for one animation frame."""
|
|
147
|
+
elapsed = frame / _SHIMMER_FPS
|
|
148
|
+
phase, phase_elapsed = _logo_phase(elapsed)
|
|
149
|
+
|
|
150
|
+
if phase == "hidden":
|
|
151
|
+
return _dot_char(full_value, 0), muted
|
|
152
|
+
if phase == "revealing":
|
|
153
|
+
reveal_column = (phase_elapsed / _REVEAL_DURATION) * width
|
|
154
|
+
if column > reveal_column:
|
|
155
|
+
return _dot_char(full_value, 0), muted
|
|
156
|
+
# typing cursor: recently-revealed chars flash accent then settle to fg
|
|
157
|
+
distance = reveal_column - column
|
|
158
|
+
if distance < 1.2:
|
|
159
|
+
return _dot_char(full_value, 1.0), f"bold {accent}"
|
|
160
|
+
if distance < 3.5:
|
|
161
|
+
return _dot_char(full_value, 1.0), fg
|
|
162
|
+
return _dot_char(full_value, 1.0), fg
|
|
163
|
+
if phase == "erasing":
|
|
164
|
+
column_norm = column / max(1, width - 1)
|
|
165
|
+
position = row + column_norm * 0.4
|
|
166
|
+
progress = (phase_elapsed / _ERASE_DURATION) * (total_rows + 0.4)
|
|
167
|
+
local = max(0.0, min(1.0, (progress - position) / 0.6))
|
|
168
|
+
return _dot_char_erase(full_value, local), fg
|
|
169
|
+
return _dot_char(full_value, 1.0), fg
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def render_welcome_frame(
|
|
173
|
+
frame: int,
|
|
174
|
+
*,
|
|
175
|
+
workspace: str | Path = "workspace",
|
|
176
|
+
compact: bool = False,
|
|
177
|
+
theme: Any | None = None,
|
|
178
|
+
) -> Text:
|
|
179
|
+
"""Build one animation frame as theme-aware Rich text."""
|
|
180
|
+
if theme is None:
|
|
181
|
+
from synapse.ui.theme import get_theme
|
|
182
|
+
|
|
183
|
+
theme = get_theme()
|
|
184
|
+
|
|
185
|
+
fg = str(getattr(theme, "fg", "#e8eaed"))
|
|
186
|
+
dim = str(getattr(theme, "dim", "#9aa0a6"))
|
|
187
|
+
muted = str(getattr(theme, "muted", "#5f6368"))
|
|
188
|
+
accent = str(getattr(theme, "user", "#8ab4f8"))
|
|
189
|
+
green = str(getattr(theme, "green", "#81c995"))
|
|
190
|
+
|
|
191
|
+
out = Text(justify="center")
|
|
192
|
+
logo = _COMPACT_LOGO if compact else _LOGO
|
|
193
|
+
logo_width = max(cell_len(line) for line in logo)
|
|
194
|
+
logo_rows = len(logo)
|
|
195
|
+
for row, line in enumerate(logo):
|
|
196
|
+
left = max(0, (logo_width - cell_len(line)) // 2)
|
|
197
|
+
for column, char in enumerate(line):
|
|
198
|
+
if char in {" ", _BRAILLE_BLANK}:
|
|
199
|
+
out.append(char, style=muted)
|
|
200
|
+
else:
|
|
201
|
+
anim_char, style = _logo_style(
|
|
202
|
+
frame,
|
|
203
|
+
left + column,
|
|
204
|
+
row,
|
|
205
|
+
logo_width,
|
|
206
|
+
ord(char) - 0x2800,
|
|
207
|
+
muted=muted,
|
|
208
|
+
fg=fg,
|
|
209
|
+
accent=accent,
|
|
210
|
+
total_rows=logo_rows,
|
|
211
|
+
)
|
|
212
|
+
out.append(anim_char, style=style)
|
|
213
|
+
out.append("\n")
|
|
214
|
+
|
|
215
|
+
out.append("\n", style=muted)
|
|
216
|
+
out.append("\nLOCAL CODING INTELLIGENCE\n", style=f"bold {green}")
|
|
217
|
+
out.append("Inspect. Plan. Build. Verify.\n", style=dim)
|
|
218
|
+
out.append(f"\n{_workspace_label(workspace)}\n", style=f"bold {fg}")
|
|
219
|
+
out.append("Describe the outcome you want to create.\n", style=dim)
|
|
220
|
+
out.append("@ files / commands F2 model F3 theme", style=muted)
|
|
221
|
+
return out
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class WelcomeView(Static):
|
|
225
|
+
"""A restrained animated Braille welcome screen for an empty timeline."""
|
|
226
|
+
|
|
227
|
+
def __init__(self, workspace: str | Path, **kwargs: Any) -> None:
|
|
228
|
+
super().__init__(**kwargs)
|
|
229
|
+
self.workspace = workspace
|
|
230
|
+
self._frame = 0
|
|
231
|
+
self._animate = True
|
|
232
|
+
|
|
233
|
+
def on_mount(self) -> None:
|
|
234
|
+
self.refresh_logo()
|
|
235
|
+
self.set_interval(1 / _SHIMMER_FPS, self._advance_frame)
|
|
236
|
+
|
|
237
|
+
def on_resize(self) -> None:
|
|
238
|
+
self.refresh_logo()
|
|
239
|
+
|
|
240
|
+
def start_animation(self) -> None:
|
|
241
|
+
self._animate = True
|
|
242
|
+
self.refresh_logo()
|
|
243
|
+
|
|
244
|
+
def stop_animation(self) -> None:
|
|
245
|
+
self._animate = False
|
|
246
|
+
|
|
247
|
+
def _advance_frame(self) -> None:
|
|
248
|
+
if not self._animate:
|
|
249
|
+
return
|
|
250
|
+
self._frame += 1
|
|
251
|
+
self.refresh_logo()
|
|
252
|
+
|
|
253
|
+
def refresh_logo(self) -> None:
|
|
254
|
+
compact = bool(self.size.width and self.size.width < 66)
|
|
255
|
+
self.update(
|
|
256
|
+
render_welcome_frame(
|
|
257
|
+
self._frame,
|
|
258
|
+
workspace=self.workspace,
|
|
259
|
+
compact=compact,
|
|
260
|
+
)
|
|
261
|
+
)
|