@nosserb/prmt 0.0.0-stage → 2.0.0
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.
- package/LICENSE +21 -0
- package/README.md +191 -2
- package/bin/prmt.mjs +174 -0
- package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
- package/dist/assets/Audits-yUBm0EpW.js +1 -0
- package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
- package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
- package/dist/assets/Carte-CPuiR1Ha.css +1 -0
- package/dist/assets/Carte-xycu1mQn.js +1 -0
- package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
- package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
- package/dist/assets/Field-C7c2mCbb.js +2 -0
- package/dist/assets/Journal-BLIIlh80.css +1 -0
- package/dist/assets/Journal-BaelFjwV.js +1 -0
- package/dist/assets/Landing-B2jxV3zs.css +1 -0
- package/dist/assets/Landing-Dr612Zdu.js +26 -0
- package/dist/assets/Register-DA_Aq5f8.js +1 -0
- package/dist/assets/Reports-8wfDNSxH.js +1 -0
- package/dist/assets/Reports-B8GXN95N.css +1 -0
- package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
- package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
- package/dist/assets/SettingsPage-BElIP46c.css +1 -0
- package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
- package/dist/assets/Shell-BAyHVljp.css +1 -0
- package/dist/assets/Shell-CQRlx9-V.js +2 -0
- package/dist/assets/Table-DUmGel5e.js +1 -0
- package/dist/assets/Tabs-CfhbELZk.js +1 -0
- package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
- package/dist/assets/Terminal-CEfj38NV.css +1 -0
- package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
- package/dist/assets/auditViews-BMza2Sdz.js +1 -0
- package/dist/assets/auditViews-BePWj26h.css +1 -0
- package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
- package/dist/assets/charts-BcijSJ4J.js +1 -0
- package/dist/assets/circle-stop-C33BAcKW.js +1 -0
- package/dist/assets/command-ChA49Xk7.js +1 -0
- package/dist/assets/command-Ck4pdQCv.css +1 -0
- package/dist/assets/download-CeuKOenD.js +1 -0
- package/dist/assets/eye-CXXHBBpr.js +1 -0
- package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/assets/habillage-B5mMBadn.js +1 -0
- package/dist/assets/index-BH1etemR.css +1 -0
- package/dist/assets/index-COSiM-R9.js +14 -0
- package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
- package/dist/assets/layers-BWBAp2CZ.png +0 -0
- package/dist/assets/markdown-DF87LQh1.css +1 -0
- package/dist/assets/markdown-o5sMhPHN.js +4 -0
- package/dist/assets/marker-icon-hN30_KVU.png +0 -0
- package/dist/assets/play-Daw4K_73.js +1 -0
- package/dist/assets/plus-D_QHm8ud.js +1 -0
- package/dist/assets/trash-DLpHM-s8.js +1 -0
- package/dist/assets/web-6j1-aY67.js +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +18 -0
- package/docs/ANALYSE.md +87 -0
- package/docs/SECURITE.md +188 -0
- package/lib/scripts/lier.js +15 -0
- package/lib/scripts/searxng.js +91 -0
- package/lib/server/app.js +444 -0
- package/lib/server/audits/modules/local.js +429 -0
- package/lib/server/audits/modules/network.js +331 -0
- package/lib/server/audits/modules/web.js +302 -0
- package/lib/server/audits/net.js +141 -0
- package/lib/server/audits/registry.js +202 -0
- package/lib/server/audits/types.js +58 -0
- package/lib/server/brain/brain.js +624 -0
- package/lib/server/brain/conversations.js +82 -0
- package/lib/server/brain/ollama.js +197 -0
- package/lib/server/brain/prompt.js +188 -0
- package/lib/server/brain/provider.js +1 -0
- package/lib/server/cli/bridge.js +292 -0
- package/lib/server/cli/pont.js +62 -0
- package/lib/server/config.js +68 -0
- package/lib/server/engine/analyzer.js +205 -0
- package/lib/server/engine/catalog.js +229 -0
- package/lib/server/engine/decoder.js +91 -0
- package/lib/server/engine/engine.js +194 -0
- package/lib/server/engine/paths.js +129 -0
- package/lib/server/engine/policy.js +397 -0
- package/lib/server/engine/relax.js +10 -0
- package/lib/server/engine/runner.js +193 -0
- package/lib/server/errors.js +23 -0
- package/lib/server/http/guard.js +75 -0
- package/lib/server/http/router.js +154 -0
- package/lib/server/http/static.js +76 -0
- package/lib/server/index.js +40 -0
- package/lib/server/journal.js +139 -0
- package/lib/server/lien.js +33 -0
- package/lib/server/savoir/base.js +284 -0
- package/lib/server/savoir/lecture.js +406 -0
- package/lib/server/savoir/memoire.js +115 -0
- package/lib/server/search/categories.js +179 -0
- package/lib/server/search/fiche.js +326 -0
- package/lib/server/search/lieux.js +134 -0
- package/lib/server/search/overpass.js +216 -0
- package/lib/server/search/searxng.js +223 -0
- package/lib/server/search/tiles.js +103 -0
- package/lib/server/status.js +157 -0
- package/lib/server/stores.js +141 -0
- package/lib/server/telephone/acces.js +457 -0
- package/lib/server/telephone/certificat.js +79 -0
- package/lib/server/telephone/qr.js +308 -0
- package/lib/shared/analysis.js +40 -0
- package/lib/shared/schemas.js +105 -0
- package/lib/shared/types.js +10 -0
- package/package.json +81 -4
- package/searxng/settings.yml +35 -0
- package/tui/Modelfile +23 -0
- package/tui/README.md +103 -0
- package/tui/prmt_tui.py +65 -0
- package/tui/promethee/__init__.py +1 -0
- package/tui/promethee/__main__.py +4 -0
- package/tui/promethee/accueil.py +66 -0
- package/tui/promethee/agents.py +220 -0
- package/tui/promethee/app/__init__.py +4 -0
- package/tui/promethee/app/application.py +299 -0
- package/tui/promethee/app/clavier.py +99 -0
- package/tui/promethee/app/commandes_slash.py +164 -0
- package/tui/promethee/app/contexte.py +188 -0
- package/tui/promethee/app/definitions.py +518 -0
- package/tui/promethee/app/ecran.py +177 -0
- package/tui/promethee/app/envoi.py +141 -0
- package/tui/promethee/app/historique.py +373 -0
- package/tui/promethee/app/modele.py +108 -0
- package/tui/promethee/app/notes.py +63 -0
- package/tui/promethee/app/outils.py +589 -0
- package/tui/promethee/app/plateforme.py +409 -0
- package/tui/promethee/app/saisie.py +100 -0
- package/tui/promethee/app/taches.py +92 -0
- package/tui/promethee/app/tour.py +539 -0
- package/tui/promethee/app/validation.py +210 -0
- package/tui/promethee/appels.py +35 -0
- package/tui/promethee/commandes.py +154 -0
- package/tui/promethee/completion.py +140 -0
- package/tui/promethee/config.py +12 -0
- package/tui/promethee/consignes.py +278 -0
- package/tui/promethee/conversation.py +94 -0
- package/tui/promethee/etat.py +253 -0
- package/tui/promethee/fichiers.py +354 -0
- package/tui/promethee/journal.py +116 -0
- package/tui/promethee/logo.py +132 -0
- package/tui/promethee/memoire.py +50 -0
- package/tui/promethee/ollama.py +52 -0
- package/tui/promethee/plan.py +91 -0
- package/tui/promethee/politique.py +429 -0
- package/tui/promethee/preferences.py +65 -0
- package/tui/promethee/presse_papiers.py +44 -0
- package/tui/promethee/recherche.py +581 -0
- package/tui/promethee/securite.py +204 -0
- package/tui/promethee/shell.py +190 -0
- package/tui/promethee/textes.py +190 -0
- package/tui/promethee/theme.py +92 -0
- package/tui/promethee/widgets.py +493 -0
- package/tui/requirements.txt +1 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Couleurs de l'interface : la palette de Prométhée, les rôles qui s'en servent et les thèmes Textual.
|
|
2
|
+
|
|
3
|
+
Les valeurs sont les mêmes que celles de la console web (src/styles/base.css) : les deux interfaces
|
|
4
|
+
partagent la nuit, l'or antique et le feu. Aucune couleur n'est écrite en dur ailleurs que dans ce
|
|
5
|
+
fichier et dans logo.py (les pixels du logo).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from textual.theme import Theme
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------- identité
|
|
12
|
+
NOIR = "#0a0908" # fond
|
|
13
|
+
NOIR_2 = "#12100e" # panneaux : saisie, validation, accueil
|
|
14
|
+
OR = "#d99a21"
|
|
15
|
+
AMBRE = "#ff9900"
|
|
16
|
+
FEU = "#ff6500"
|
|
17
|
+
BLANC = "#f5f0e8" # texte
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------- surfaces, traits, texte
|
|
20
|
+
PANNEAU = "#181410" # bloc de code, citation
|
|
21
|
+
TRAIT = "#3a2f23" # bordure au repos, ascenseur
|
|
22
|
+
SELECTION = "#5a3a10" # texte sélectionné
|
|
23
|
+
OR_CLAIR = "#f3d27a" # titre de premier niveau
|
|
24
|
+
TEXTE_2 = "#b8ac9c"
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------- rôles (noms utilisés partout dans le code)
|
|
27
|
+
ORANGE = AMBRE # accent principal : invite, puces, bordure active
|
|
28
|
+
DIM = "#9c9183" # texte discret : aides, messages de l'utilisateur, détails (contraste 6,4 sur le fond)
|
|
29
|
+
GREEN = "#58b97a" # réussite
|
|
30
|
+
RED = "#e5484d" # danger, échec
|
|
31
|
+
YELLOW = "#f2c230" # avertissement, refus
|
|
32
|
+
|
|
33
|
+
# Modes de saisie : conservés tels quels. Ils codent un état que tu connais déjà, et chacun est doublé
|
|
34
|
+
# d'un caractère d'invite et d'une ligne d'aide : la couleur n'est jamais le seul repère.
|
|
35
|
+
SHELL_COLOR = "#ff5fd7" # mode « ! » (comme le mode bash de Claude Code)
|
|
36
|
+
MEMORY_COLOR = "#5fafff" # mode « # » (note pour la mémoire)
|
|
37
|
+
PLAN_COLOR = "#5fd7d7" # mode plan (Shift+Tab)
|
|
38
|
+
AGENT_COLOR = "#af87ff" # agents
|
|
39
|
+
CODE_COLOR = "#5fd787" # IA de code
|
|
40
|
+
|
|
41
|
+
DIFF_DEL = "#ffd7d7 on #5f1f1f"
|
|
42
|
+
DIFF_ADD = "#d7ffd7 on #1f4f1f"
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------- politique : un signe et un mot, jamais la couleur seule
|
|
45
|
+
# issue rendue par la politique → (signe, libellé, couleur)
|
|
46
|
+
NIVEAUX = {
|
|
47
|
+
"read": ("●", "lecture", GREEN),
|
|
48
|
+
"sensitive": ("▲", "sensible", YELLOW),
|
|
49
|
+
"outside": ("◆", "hors politique", FEU),
|
|
50
|
+
"refused": ("✕", "refusée", RED),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------- thèmes Textual
|
|
54
|
+
# Les widgets de Textual (Markdown, zone de saisie, notifications, ascenseurs) prennent leurs couleurs
|
|
55
|
+
# dans le thème : sans lui, les titres, les liens et les tableaux des réponses sortaient en bleu.
|
|
56
|
+
_VARIABLES = {
|
|
57
|
+
"markdown-h1-color": OR_CLAIR, "markdown-h1-background": "transparent", "markdown-h1-text-style": "bold",
|
|
58
|
+
"markdown-h2-color": OR, "markdown-h2-background": "transparent", "markdown-h2-text-style": "bold",
|
|
59
|
+
"markdown-h3-color": AMBRE, "markdown-h3-background": "transparent", "markdown-h3-text-style": "bold",
|
|
60
|
+
"markdown-h4-color": TEXTE_2, "markdown-h4-background": "transparent", "markdown-h4-text-style": "bold",
|
|
61
|
+
"markdown-h5-color": TEXTE_2, "markdown-h5-background": "transparent", "markdown-h5-text-style": "italic",
|
|
62
|
+
"markdown-h6-color": DIM, "markdown-h6-background": "transparent", "markdown-h6-text-style": "italic",
|
|
63
|
+
"link-color": AMBRE, "link-style": "underline", "link-background": "transparent",
|
|
64
|
+
"link-color-hover": BLANC, "link-background-hover": SELECTION, "link-style-hover": "bold",
|
|
65
|
+
"text-primary": AMBRE, "text-secondary": OR, "text-accent": FEU,
|
|
66
|
+
"text-warning": YELLOW, "text-error": RED, "text-success": GREEN,
|
|
67
|
+
"scrollbar": TRAIT, "scrollbar-hover": AMBRE, "scrollbar-active": AMBRE,
|
|
68
|
+
"border": AMBRE, "border-blurred": TRAIT,
|
|
69
|
+
"input-selection-background": SELECTION,
|
|
70
|
+
"screen-selection-background": SELECTION, "screen-selection-foreground": BLANC,
|
|
71
|
+
"block-cursor-background": AMBRE, "block-cursor-foreground": NOIR, "block-cursor-text-style": "bold",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
THEME = "promethee"
|
|
75
|
+
THEME_TERMINAL = "promethee-terminal"
|
|
76
|
+
# Préférence « fond » → thème
|
|
77
|
+
FONDS = {"promethee": THEME, "terminal": THEME_TERMINAL}
|
|
78
|
+
|
|
79
|
+
THEMES = (
|
|
80
|
+
# Le fond de Prométhée : la nuit, peinte par l'application.
|
|
81
|
+
Theme(name=THEME, dark=True, primary=AMBRE, secondary=OR, accent=FEU, foreground=BLANC, background=NOIR,
|
|
82
|
+
surface=NOIR_2, panel=PANNEAU, success=GREEN, warning=YELLOW, error=RED,
|
|
83
|
+
variables={**_VARIABLES, "scrollbar-background": NOIR, "scrollbar-background-hover": NOIR,
|
|
84
|
+
"scrollbar-background-active": NOIR, "scrollbar-corner-color": NOIR}),
|
|
85
|
+
# Le fond de ton terminal (transparence, image de fond) : rien n'est peint derrière le texte.
|
|
86
|
+
Theme(name=THEME_TERMINAL, dark=True, ansi=True, primary=AMBRE, secondary=OR, accent=FEU, success=GREEN,
|
|
87
|
+
warning=YELLOW, error=RED, foreground="ansi_default", background="ansi_default", surface="ansi_default",
|
|
88
|
+
panel="ansi_default", boost="ansi_default",
|
|
89
|
+
variables={**_VARIABLES, "ansi-background": "ansi_black", "ansi-foreground": "ansi_white",
|
|
90
|
+
"scrollbar-background": "ansi_default", "scrollbar-background-hover": "ansi_default",
|
|
91
|
+
"scrollbar-background-active": "ansi_default", "scrollbar-corner-color": "ansi_default"}),
|
|
92
|
+
)
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Widgets de la conversation : saisie, réponse, appel d'outil, agent, choix et validation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
|
|
6
|
+
from rich.style import Style
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.containers import Horizontal, Vertical
|
|
11
|
+
from textual.message import Message
|
|
12
|
+
from textual.reactive import reactive
|
|
13
|
+
from textual.widget import Widget
|
|
14
|
+
from textual.widgets import Markdown, Static, TextArea
|
|
15
|
+
|
|
16
|
+
from .recherche import js_number, osm_link
|
|
17
|
+
from .theme import AGENT_COLOR, DIM, FEU, GREEN, NIVEAUX, ORANGE, RED, SHELL_COLOR, YELLOW
|
|
18
|
+
|
|
19
|
+
# --------------------------------------------------------------------------- Widgets
|
|
20
|
+
class PromptInput(TextArea):
|
|
21
|
+
"""Zone de saisie : Entrée envoie, Ctrl+J (ou \\ + Entrée) ajoute une ligne, ↑/↓ = historique."""
|
|
22
|
+
|
|
23
|
+
class Submitted(Message):
|
|
24
|
+
def __init__(self, value: str) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.value = value
|
|
27
|
+
|
|
28
|
+
def __init__(self, **kwargs) -> None:
|
|
29
|
+
super().__init__(
|
|
30
|
+
soft_wrap=True,
|
|
31
|
+
show_line_numbers=False,
|
|
32
|
+
highlight_cursor_line=False,
|
|
33
|
+
compact=True,
|
|
34
|
+
tab_behavior="focus",
|
|
35
|
+
**kwargs,
|
|
36
|
+
)
|
|
37
|
+
self.prompt_history: list[str] = []
|
|
38
|
+
self._hist_idx: int | None = None
|
|
39
|
+
self._draft = ""
|
|
40
|
+
|
|
41
|
+
async def _on_key(self, event) -> None:
|
|
42
|
+
key = event.key
|
|
43
|
+
if key == "enter":
|
|
44
|
+
event.stop()
|
|
45
|
+
event.prevent_default()
|
|
46
|
+
app = self.app
|
|
47
|
+
if getattr(app, "_comp_items", None):
|
|
48
|
+
# Menu d'autocomplétion ouvert : Entrée choisit (et lance la commande « / » choisie)
|
|
49
|
+
app.accept_completion(submit=app._comp_kind == "/")
|
|
50
|
+
return
|
|
51
|
+
row, col = self.cursor_location
|
|
52
|
+
line = self.document.get_line(row)
|
|
53
|
+
if col > 0 and line[:col].endswith("\\"):
|
|
54
|
+
# "\" + Entrée => nouvelle ligne (comme dans Claude Code)
|
|
55
|
+
self.replace("\n", (row, col - 1), (row, col))
|
|
56
|
+
return
|
|
57
|
+
if self.text.strip():
|
|
58
|
+
self.post_message(self.Submitted(self.text))
|
|
59
|
+
return
|
|
60
|
+
if key in ("ctrl+j", "shift+enter", "alt+enter"):
|
|
61
|
+
event.stop()
|
|
62
|
+
event.prevent_default()
|
|
63
|
+
self.insert("\n")
|
|
64
|
+
return
|
|
65
|
+
await super()._on_key(event)
|
|
66
|
+
|
|
67
|
+
# Historique des messages avec ↑ / ↓
|
|
68
|
+
def action_cursor_up(self, select: bool = False) -> None:
|
|
69
|
+
if not select and getattr(self.app, "_comp_items", None):
|
|
70
|
+
self.app.move_completion(-1)
|
|
71
|
+
return
|
|
72
|
+
if not select and self.cursor_location[0] == 0 and self.prompt_history:
|
|
73
|
+
if self._hist_idx is None:
|
|
74
|
+
self._draft = self.text
|
|
75
|
+
self._hist_idx = len(self.prompt_history)
|
|
76
|
+
if self._hist_idx > 0:
|
|
77
|
+
self._hist_idx -= 1
|
|
78
|
+
self._load(self.prompt_history[self._hist_idx])
|
|
79
|
+
return
|
|
80
|
+
super().action_cursor_up(select)
|
|
81
|
+
|
|
82
|
+
def action_cursor_down(self, select: bool = False) -> None:
|
|
83
|
+
if not select and getattr(self.app, "_comp_items", None):
|
|
84
|
+
self.app.move_completion(1)
|
|
85
|
+
return
|
|
86
|
+
last_row = self.document.line_count - 1
|
|
87
|
+
if not select and self._hist_idx is not None and self.cursor_location[0] == last_row:
|
|
88
|
+
self._hist_idx += 1
|
|
89
|
+
if self._hist_idx >= len(self.prompt_history):
|
|
90
|
+
self._hist_idx = None
|
|
91
|
+
self._load(self._draft)
|
|
92
|
+
else:
|
|
93
|
+
self._load(self.prompt_history[self._hist_idx])
|
|
94
|
+
return
|
|
95
|
+
super().action_cursor_down(select)
|
|
96
|
+
|
|
97
|
+
def _load(self, text: str) -> None:
|
|
98
|
+
self.app._comp_dismissed = text # un « /commande » de l'historique n'ouvre pas le menu
|
|
99
|
+
self.load_text(text)
|
|
100
|
+
self.move_cursor(self.document.end)
|
|
101
|
+
|
|
102
|
+
def remember(self, text: str) -> None:
|
|
103
|
+
if not self.prompt_history or self.prompt_history[-1] != text:
|
|
104
|
+
self.prompt_history.append(text)
|
|
105
|
+
self._hist_idx = None
|
|
106
|
+
self._draft = ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AssistantMessage(Horizontal):
|
|
110
|
+
def __init__(self, text: str = "") -> None:
|
|
111
|
+
super().__init__()
|
|
112
|
+
self._initial = text
|
|
113
|
+
|
|
114
|
+
def compose(self) -> ComposeResult:
|
|
115
|
+
yield Static("⏺", classes="bullet")
|
|
116
|
+
with Vertical(classes="body"):
|
|
117
|
+
yield Markdown(self._initial, classes="answer")
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def markdown(self) -> Markdown:
|
|
121
|
+
return self.query_one(".answer", Markdown)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ToolCallView(Vertical):
|
|
125
|
+
"""Affichage d'un outil : « ⏺ PowerShell(commande) » (ou « ! commande », « Modifier(fichier »…) puis « ⎿ sortie »."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, command: str, label: str = "PowerShell", waiting: str = "En attente de ta validation…",
|
|
128
|
+
initial=None) -> None:
|
|
129
|
+
super().__init__()
|
|
130
|
+
self.command = command
|
|
131
|
+
self.label = label
|
|
132
|
+
self._waiting = waiting
|
|
133
|
+
self._initial = initial # fonction appelée une fois affiché (réaffichage d'une session)
|
|
134
|
+
|
|
135
|
+
def compose(self) -> ComposeResult:
|
|
136
|
+
yield Static(self._head(DIM), classes="tool-head")
|
|
137
|
+
yield Static(Text.assemble(("⎿ ", DIM), (self._waiting, DIM)), classes="tool-out")
|
|
138
|
+
|
|
139
|
+
def on_mount(self) -> None:
|
|
140
|
+
if self._initial is not None:
|
|
141
|
+
self._initial(self)
|
|
142
|
+
self._initial = None
|
|
143
|
+
|
|
144
|
+
def _head(self, color: str) -> Text:
|
|
145
|
+
cmd = self.command if len(self.command) <= 300 else self.command[:300] + "…"
|
|
146
|
+
if self.label == "!":
|
|
147
|
+
return Text.assemble(("! ", f"bold {SHELL_COLOR}"), (cmd, "bold"))
|
|
148
|
+
return Text.assemble(("⏺ ", color), (self.label, "bold"), ("(", DIM), (cmd, ""), (")", DIM))
|
|
149
|
+
|
|
150
|
+
def _out(self, text: Text) -> None:
|
|
151
|
+
self.query_one(".tool-head", Static).update(self._head(self._color))
|
|
152
|
+
self.query_one(".tool-out", Static).update(text)
|
|
153
|
+
|
|
154
|
+
def set_running(self, auto: bool = False) -> None:
|
|
155
|
+
self._color = ORANGE
|
|
156
|
+
note = "Exécution…" + (" (autorisée pour la session)" if auto else "")
|
|
157
|
+
self._out(Text.assemble(("⎿ ", DIM), (note, DIM)))
|
|
158
|
+
|
|
159
|
+
def set_denied(self) -> None:
|
|
160
|
+
self._color = DIM
|
|
161
|
+
self._out(Text.assemble(("⎿ ", DIM), ("Refusée", YELLOW)))
|
|
162
|
+
|
|
163
|
+
def set_skipped(self, reason: str) -> None:
|
|
164
|
+
self._color = DIM
|
|
165
|
+
self._out(Text.assemble(("⎿ ", DIM), (reason, YELLOW)))
|
|
166
|
+
|
|
167
|
+
def set_refused(self, reason: str, advice: list[str] | None = None) -> None:
|
|
168
|
+
"""Refus de la politique : la commande ne t'a pas été proposée et rien n'a été exécuté."""
|
|
169
|
+
self._color = RED
|
|
170
|
+
text = Text.assemble(("⎿ ", DIM), ("✕ Refusée par la politique", f"bold {RED}"))
|
|
171
|
+
if reason:
|
|
172
|
+
text.append(f" : {reason}")
|
|
173
|
+
if advice:
|
|
174
|
+
text.append("\n Pour la permettre : " + " · ".join(advice), style=DIM)
|
|
175
|
+
self._out(text)
|
|
176
|
+
|
|
177
|
+
def set_text(self, summary: str, detail: Text | None = None, color: str = GREEN) -> None:
|
|
178
|
+
"""Résultat d'un outil fichier : une ligne de résumé, puis éventuellement le diff."""
|
|
179
|
+
self._color = color
|
|
180
|
+
text = Text.assemble(("⎿ ", DIM), (summary, ""))
|
|
181
|
+
if detail is not None and detail.plain.strip():
|
|
182
|
+
text.append("\n")
|
|
183
|
+
text.append_text(detail)
|
|
184
|
+
self._out(text)
|
|
185
|
+
|
|
186
|
+
def set_result(self, code: int | None, output: str, note: str = "") -> None:
|
|
187
|
+
self._color = GREEN if code == 0 else RED
|
|
188
|
+
lines = output.strip("\n").split("\n") if output.strip() else []
|
|
189
|
+
shown = lines[:12]
|
|
190
|
+
text = Text()
|
|
191
|
+
if not shown:
|
|
192
|
+
text.append("⎿ ", style=DIM)
|
|
193
|
+
text.append("(aucune sortie)", style=DIM)
|
|
194
|
+
for i, line in enumerate(shown):
|
|
195
|
+
text.append("⎿ " if i == 0 else " ", style=DIM)
|
|
196
|
+
text.append(line[:400] + ("\n" if i < len(shown) - 1 else ""))
|
|
197
|
+
if len(lines) > len(shown):
|
|
198
|
+
text.append(f"\n … +{len(lines) - len(shown)} lignes (/sortie pour tout voir)", style=f"italic {DIM}")
|
|
199
|
+
extra = []
|
|
200
|
+
if note:
|
|
201
|
+
extra.append(note)
|
|
202
|
+
if code not in (0, None):
|
|
203
|
+
extra.append(f"code de sortie {code}")
|
|
204
|
+
if extra:
|
|
205
|
+
text.append("\n " + " · ".join(extra), style=RED if code else YELLOW)
|
|
206
|
+
self._out(text)
|
|
207
|
+
|
|
208
|
+
_color = DIM
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _domaine(url: str) -> str:
|
|
212
|
+
try:
|
|
213
|
+
host = urlsplit(url).hostname or ""
|
|
214
|
+
except ValueError:
|
|
215
|
+
return ""
|
|
216
|
+
return host[4:] if host.startswith("www.") else host
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _lien(url: str) -> Style:
|
|
220
|
+
"""Adresse cliquable dans les terminaux qui le permettent (lien OSC 8, comme Windows Terminal)."""
|
|
221
|
+
return Style(link=url, underline=True)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def search_lines(search: dict) -> Text:
|
|
225
|
+
"""Ce que montre une recherche web terminée : sources numérotées (titre, domaine, adresse complète) puis, pour un
|
|
226
|
+
lieu, son nom, son adresse, ses coordonnées et le lien OpenStreetMap. Pas de carte dans un terminal."""
|
|
227
|
+
text = Text()
|
|
228
|
+
|
|
229
|
+
def line() -> None:
|
|
230
|
+
text.append("\n " if text.plain else "⎿ ", style=DIM)
|
|
231
|
+
|
|
232
|
+
if search.get("state") == "failed":
|
|
233
|
+
line()
|
|
234
|
+
text.append(search.get("error") or "La recherche a échoué.", style=RED)
|
|
235
|
+
else:
|
|
236
|
+
sources = search.get("sources") or []
|
|
237
|
+
if not sources:
|
|
238
|
+
line()
|
|
239
|
+
text.append("Aucun résultat", style=YELLOW)
|
|
240
|
+
for s in sources:
|
|
241
|
+
line()
|
|
242
|
+
text.append(f"[{s['n']}] ", style=f"bold {ORANGE}")
|
|
243
|
+
text.append(s["title"], style="bold")
|
|
244
|
+
domain = _domaine(s["url"])
|
|
245
|
+
if domain:
|
|
246
|
+
text.append(f" · {domain}", style=DIM)
|
|
247
|
+
text.append("\n ")
|
|
248
|
+
text.append(s["url"], style=_lien(s["url"]))
|
|
249
|
+
place = search.get("place")
|
|
250
|
+
if place:
|
|
251
|
+
lat, lon = js_number(place["lat"]), js_number(place["lon"])
|
|
252
|
+
link = osm_link(place["lat"], place["lon"])
|
|
253
|
+
line()
|
|
254
|
+
text.append("Lieu : ", style=f"bold {ORANGE}")
|
|
255
|
+
text.append(place["name"], style="bold")
|
|
256
|
+
if place.get("address"):
|
|
257
|
+
text.append(f"\n {place['address']}")
|
|
258
|
+
text.append(f"\n coordonnées : {lat}, {lon}", style=DIM)
|
|
259
|
+
text.append("\n ")
|
|
260
|
+
text.append(link, style=_lien(link))
|
|
261
|
+
elif search.get("placeQuery") and search.get("state") != "failed":
|
|
262
|
+
line()
|
|
263
|
+
text.append(f"Lieu « {search['placeQuery']} » introuvable sur la carte.", style=YELLOW)
|
|
264
|
+
return text
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class SearchView(Vertical):
|
|
268
|
+
"""Recherche web : « ⏺ Recherche web : « requête » » pendant la recherche, puis ses sources et son lieu."""
|
|
269
|
+
|
|
270
|
+
def __init__(self, requete: str, search: dict | None = None, replay: bool = False) -> None:
|
|
271
|
+
super().__init__()
|
|
272
|
+
self.requete = requete
|
|
273
|
+
self._search = search
|
|
274
|
+
self._replay = replay # réaffichage d'une session : la recherche est finie, même sans détail enregistré
|
|
275
|
+
|
|
276
|
+
def compose(self) -> ComposeResult:
|
|
277
|
+
yield Static(self._head(ORANGE), classes="tool-head")
|
|
278
|
+
yield Static(Text.assemble(("⎿ ", DIM), ("Recherche en cours…", DIM)), classes="tool-out")
|
|
279
|
+
|
|
280
|
+
def on_mount(self) -> None:
|
|
281
|
+
if self._search is not None:
|
|
282
|
+
self.set_result(self._search)
|
|
283
|
+
elif self._replay:
|
|
284
|
+
self._out(DIM, Text.assemble(("⎿ ", DIM), ("Résultats non enregistrés", DIM)))
|
|
285
|
+
|
|
286
|
+
def _head(self, color: str) -> Text:
|
|
287
|
+
return Text.assemble(("⏺ ", color), ("Recherche web : ", "bold"), (f"« {self.requete} »", ""))
|
|
288
|
+
|
|
289
|
+
def _out(self, color: str, text: Text) -> None:
|
|
290
|
+
self.query_one(".tool-head", Static).update(self._head(color))
|
|
291
|
+
self.query_one(".tool-out", Static).update(text)
|
|
292
|
+
|
|
293
|
+
def set_result(self, search: dict) -> None:
|
|
294
|
+
self._search = search
|
|
295
|
+
self._out(RED if search.get("state") == "failed" else GREEN, search_lines(search))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def short_count(n: int) -> str:
|
|
299
|
+
return f"{n / 1000:.1f} k".replace(".", ",") if n >= 1000 else str(n)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class AgentView(Vertical):
|
|
303
|
+
"""Travail d'un agent : « ⏺ Agent nom(tâche) », ses outils en retrait, puis « ⎿ Terminé (… outils · … s) »."""
|
|
304
|
+
|
|
305
|
+
def __init__(self, name: str, task: str, initial=None) -> None:
|
|
306
|
+
super().__init__()
|
|
307
|
+
self.agent_name = name
|
|
308
|
+
self.agent_task = task
|
|
309
|
+
self._initial = initial
|
|
310
|
+
self.done = False
|
|
311
|
+
self.uses = 0
|
|
312
|
+
|
|
313
|
+
def compose(self) -> ComposeResult:
|
|
314
|
+
yield Static(self._head(AGENT_COLOR), classes="agent-head")
|
|
315
|
+
yield Vertical(classes="agent-body")
|
|
316
|
+
yield Static(Text.assemble(("⎿ ", DIM), ("Démarrage…", DIM)), classes="agent-foot")
|
|
317
|
+
|
|
318
|
+
def on_mount(self) -> None:
|
|
319
|
+
if self._initial is not None:
|
|
320
|
+
self._initial(self)
|
|
321
|
+
self._initial = None
|
|
322
|
+
|
|
323
|
+
def _head(self, color: str) -> Text:
|
|
324
|
+
task = " ".join(self.agent_task.split())
|
|
325
|
+
task = task if len(task) <= 90 else task[:90] + "…"
|
|
326
|
+
return Text.assemble(("⏺ ", color), ("Agent ", "bold"), (self.agent_name, f"bold {AGENT_COLOR}"),
|
|
327
|
+
("(", DIM), (task, ""), (")", DIM))
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def body(self) -> Vertical:
|
|
331
|
+
return self.query_one(".agent-body", Vertical)
|
|
332
|
+
|
|
333
|
+
def _foot(self, text: Text) -> None:
|
|
334
|
+
self.query_one(".agent-foot", Static).update(text)
|
|
335
|
+
|
|
336
|
+
def set_progress(self, uses: int) -> None:
|
|
337
|
+
self.uses = uses
|
|
338
|
+
self._foot(Text.assemble(("⎿ ", DIM), (f"Au travail… {uses} outil{'s' if uses > 1 else ''} utilisé"
|
|
339
|
+
f"{'s' if uses > 1 else ''}", DIM)))
|
|
340
|
+
|
|
341
|
+
def set_done(self, uses: int, tokens: int, seconds: float, note: str = "", color: str = GREEN,
|
|
342
|
+
expanded: bool = False) -> None:
|
|
343
|
+
self.done, self.uses = True, uses
|
|
344
|
+
self._result = (tokens, seconds, note, color)
|
|
345
|
+
self.query_one(".agent-head", Static).update(self._head(color))
|
|
346
|
+
self.set_expanded(expanded)
|
|
347
|
+
|
|
348
|
+
def set_expanded(self, expanded: bool) -> None:
|
|
349
|
+
if not self.done:
|
|
350
|
+
return # pendant le travail, le détail reste visible
|
|
351
|
+
tokens, seconds, note, color = self._result
|
|
352
|
+
stats = [f"{self.uses} outil{'s' if self.uses > 1 else ''}"] if self.uses >= 0 else []
|
|
353
|
+
if tokens:
|
|
354
|
+
stats.append(f"{short_count(tokens)} tokens")
|
|
355
|
+
if seconds:
|
|
356
|
+
stats.append(f"{seconds:.0f} s")
|
|
357
|
+
text = Text.assemble(("⎿ ", DIM), (note or "Terminé", color if note else ""),
|
|
358
|
+
(f" ({' · '.join(stats)})" if stats else "", DIM))
|
|
359
|
+
if self.body.children:
|
|
360
|
+
text.append(" · Ctrl+O : " + ("replier" if expanded else "voir le détail"), style=f"italic {DIM}")
|
|
361
|
+
self._foot(text)
|
|
362
|
+
self.body.display = expanded
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class ChoicePrompt(Widget, can_focus=True):
|
|
366
|
+
"""Choix au clavier sous la conversation (validation d'une commande, d'un fichier, d'une session…)."""
|
|
367
|
+
|
|
368
|
+
BINDINGS = [
|
|
369
|
+
Binding("up", "move(-1)", show=False),
|
|
370
|
+
Binding("down", "move(1)", show=False),
|
|
371
|
+
Binding("enter", "choose", show=False),
|
|
372
|
+
*[Binding(str(i), f"pick({i - 1})", show=False) for i in range(1, 10)],
|
|
373
|
+
]
|
|
374
|
+
selected = reactive(0)
|
|
375
|
+
hint = "↑↓ choisir · Entrée valider (ou tape le numéro) · Esc refuser"
|
|
376
|
+
|
|
377
|
+
def __init__(self, title: str, body: Text | None, question: str, options: list[tuple[str, str]], callback,
|
|
378
|
+
danger: bool = False, command: str = "") -> None:
|
|
379
|
+
super().__init__()
|
|
380
|
+
self.title = title
|
|
381
|
+
self.body = body
|
|
382
|
+
self.question = question
|
|
383
|
+
self.options = options
|
|
384
|
+
self.callback = callback
|
|
385
|
+
self.danger = danger
|
|
386
|
+
self.command = command
|
|
387
|
+
self._done = False
|
|
388
|
+
if danger:
|
|
389
|
+
self.add_class("-danger")
|
|
390
|
+
|
|
391
|
+
def render(self) -> Text:
|
|
392
|
+
t = Text()
|
|
393
|
+
t.append(f"{self.title}\n", style=f"bold {RED if self.danger else ORANGE}")
|
|
394
|
+
if self.body is not None and self.body.plain.strip():
|
|
395
|
+
t.append("\n")
|
|
396
|
+
t.append_text(self.body)
|
|
397
|
+
t.append("\n")
|
|
398
|
+
if self.question:
|
|
399
|
+
t.append(f"\n{self.question}\n", style="bold")
|
|
400
|
+
for i, (_, label) in enumerate(self.options):
|
|
401
|
+
if i == self.selected:
|
|
402
|
+
t.append(f"❯ {i + 1}. {label}", style=f"bold {ORANGE}")
|
|
403
|
+
else:
|
|
404
|
+
t.append(f" {i + 1}. {label}", style=DIM)
|
|
405
|
+
if i < len(self.options) - 1:
|
|
406
|
+
t.append("\n")
|
|
407
|
+
return t
|
|
408
|
+
|
|
409
|
+
def action_move(self, delta: int) -> None:
|
|
410
|
+
self.selected = (self.selected + delta) % len(self.options)
|
|
411
|
+
|
|
412
|
+
def action_pick(self, index: int) -> None:
|
|
413
|
+
if index < len(self.options):
|
|
414
|
+
self.selected = index
|
|
415
|
+
self.action_choose()
|
|
416
|
+
|
|
417
|
+
def action_choose(self) -> None:
|
|
418
|
+
self.resolve(self.options[self.selected][0])
|
|
419
|
+
|
|
420
|
+
def resolve(self, decision: str) -> None:
|
|
421
|
+
if not self._done:
|
|
422
|
+
self._done = True
|
|
423
|
+
self.callback(decision)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def verdict_lines(verdict) -> Text:
|
|
427
|
+
"""Ce que la politique dit d'une commande : son niveau (un signe et un mot), ce qu'elle écrit, qui elle contacte,
|
|
428
|
+
et ce qu'il faut lire avant de valider."""
|
|
429
|
+
sign, label, color = NIVEAUX[verdict.issue]
|
|
430
|
+
text = Text()
|
|
431
|
+
text.append(f" {sign} {label}", style=f"bold {color}")
|
|
432
|
+
text.append(f" · politique {verdict.mode}\n", style=DIM)
|
|
433
|
+
for path in verdict.ecritures[:4]:
|
|
434
|
+
text.append(f" écrit : {path}\n", style=DIM)
|
|
435
|
+
if len(verdict.ecritures) > 4:
|
|
436
|
+
text.append(f" … et {len(verdict.ecritures) - 4} autres fichiers\n", style=DIM)
|
|
437
|
+
if verdict.reseau:
|
|
438
|
+
text.append(f" contacte : {', '.join(verdict.reseau[:6])}\n", style=DIM)
|
|
439
|
+
for reason in verdict.a_lire()[:5]:
|
|
440
|
+
text.append(f" {reason.message}\n", style=YELLOW if reason.gravite == "warn" else FEU)
|
|
441
|
+
if verdict.issue == "outside":
|
|
442
|
+
text.append(" L'analyse ne sait pas vérifier cette commande : toi seul la contrôles.\n", style=f"bold {FEU}")
|
|
443
|
+
return text
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class PermissionPrompt(ChoicePrompt):
|
|
447
|
+
"""Demande de validation d'une commande (comme le mode par défaut de Claude Code).
|
|
448
|
+
|
|
449
|
+
verdict : le verdict de la politique, ou None si elle n'est pas disponible (validation manuelle).
|
|
450
|
+
scope : ce que couvre « ne plus demander » : "noms" (les commandes citées) ou "ligne" (cette commande exacte).
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
def __init__(self, command: str, explanation: str, dangerous: bool, prefixes: list[str], callback,
|
|
454
|
+
verdict=None, scope: str = "noms", title: str = "Exécuter une commande",
|
|
455
|
+
question: str = "Veux-tu exécuter cette commande ?", refusal: str = "Non, et dire à Prométhée quoi faire autrement (Esc)") -> None:
|
|
456
|
+
body = Text()
|
|
457
|
+
for line in command.split("\n"):
|
|
458
|
+
body.append(f" {line}\n", style="bold")
|
|
459
|
+
if explanation:
|
|
460
|
+
body.append(f" {explanation}\n", style=DIM)
|
|
461
|
+
if verdict is not None:
|
|
462
|
+
body.append("\n")
|
|
463
|
+
body.append_text(verdict_lines(verdict))
|
|
464
|
+
if dangerous:
|
|
465
|
+
body.append("\n ⚠ Cette commande peut modifier ou supprimer des choses sur ta machine : lis-la bien.\n",
|
|
466
|
+
style=f"bold {RED}")
|
|
467
|
+
body.rstrip()
|
|
468
|
+
options: list[tuple[str, str]] = [("yes", "Oui")]
|
|
469
|
+
if not dangerous and prefixes and (verdict is None or verdict.lecture):
|
|
470
|
+
names = ", ".join(f"« {p} »" for p in prefixes) if scope == "noms" else "cette commande exacte"
|
|
471
|
+
options.append(("always", f"Oui, et ne plus demander pour {names} pendant cette session"))
|
|
472
|
+
options.append(("no", refusal))
|
|
473
|
+
super().__init__(title, body, question, options, callback,
|
|
474
|
+
danger=dangerous or (verdict is not None and verdict.issue == "outside"), command=command)
|
|
475
|
+
self.verdict = verdict
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class ToolState:
|
|
479
|
+
"""Suivi des outils pendant une réponse (ou pendant le travail d'un agent)."""
|
|
480
|
+
|
|
481
|
+
def __init__(self) -> None:
|
|
482
|
+
self.failed: set[str] = set() # commandes déjà en échec
|
|
483
|
+
self.last_ok: str | None = None # dernière commande réussie (relancée telle quelle juste après = inutile)
|
|
484
|
+
self.ran: list[str] = [] # commandes / fichiers + leurs sorties (pour contrôler la réponse finale)
|
|
485
|
+
self.uses = 0 # nombre d'outils appelés
|
|
486
|
+
self.unverified: str | None = None # code modifié sans commande lancée depuis (pas recompilé, pas testé)
|
|
487
|
+
self.last_fail: tuple[str, int] | None = None # dernière commande lancée, si elle a échoué
|
|
488
|
+
self.compiled_only = False # le code compile mais le programme n'a pas encore été lancé
|
|
489
|
+
self.refused = 0 # commandes refusées par la politique pendant cette réponse
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# clés des messages propres à l'interface, retirées avant l'envoi au modèle (search : sources et lieu, pour l'écran)
|
|
493
|
+
LOCAL_KEYS = ("display", "direct", "intent", "mode", "search")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
textual>=8,<9
|