@brandon_9527/tcode 1.0.8 → 1.0.10
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/dist/python-src/.env +1 -1
- package/dist/python-src/README.md +48 -1
- package/dist/python-src/_workspace/.autodev/config.json +12 -0
- package/dist/python-src/_workspace/.autodev/cron/jobs.json +4 -0
- package/dist/python-src/entry.py +21 -1
- package/dist/python-src/main.py +763 -42
- package/dist/python-src/pyproject.toml +1 -0
- package/dist/python-src/run.sh +9 -0
- package/dist/python-src/src/agents/token_tracker.py +4 -4
- package/dist/python-src/src/claw/bus/queue.py +1 -1
- package/dist/python-src/src/claw/channels/__init__.py +2 -2
- package/dist/python-src/src/claw/channels/base.py +2 -2
- package/dist/python-src/src/claw/channels/feishu.py +57 -16
- package/dist/python-src/src/claw/channels/manager.py +2 -2
- package/dist/python-src/src/claw/config/__init__.py +3 -0
- package/dist/python-src/src/claw/config/loader.py +38 -0
- package/dist/python-src/src/claw/config/schema.py +14 -29
- package/dist/python-src/src/claw/cron/__init__.py +3 -0
- package/dist/python-src/src/claw/cron/service.py +171 -0
- package/dist/python-src/src/claw/cron/types_.py +14 -0
- package/dist/python-src/src/claw/heartbeat/__init__.py +2 -0
- package/dist/python-src/src/claw/heartbeat/service.py +55 -0
- package/dist/python-src/src/claw/run.py +82 -0
- package/dist/python-src/src/claw/tools/base.py +23 -0
- package/dist/python-src/src/claw/tools/channel.py +0 -0
- package/dist/python-src/src/claw/tools/cron.py +138 -0
- package/dist/python-src/src/claw/utils/__init__.py +2 -0
- package/dist/python-src/src/claw/utils/helpers.py +27 -0
- package/dist/python-src/src/core/context.py +158 -0
- package/dist/python-src/src/managers/manager_agent.py +9 -9
- package/dist/python-src/src/managers/manager_command.py +62 -0
- package/dist/python-src/src/managers/manager_context.py +1 -1
- package/dist/python-src/src/managers/manager_instruction.py +7 -7
- package/dist/python-src/src/managers/manager_skill.py +3 -3
- package/dist/python-src/src/managers/sandbox.py +3 -3
- package/dist/python-src/src/middlewares/dynamic_content.py +2 -2
- package/dist/python-src/src/middlewares/hitl.py +3 -3
- package/dist/python-src/src/middlewares/memory.py +2 -2
- package/dist/python-src/src/middlewares/subagents.py +4 -4
- package/dist/python-src/src/middlewares/summary.py +37 -37
- package/dist/python-src/src/stream/file_write_parser.py +3 -3
- package/dist/python-src/src/stream/formatter.py +19 -19
- package/dist/python-src/src/stream/handler.py +4 -4
- package/dist/python-src/src/stream/handler_with_tracker.py +10 -10
- package/dist/python-src/src/trackers/token/pricing.py +2 -2
- package/dist/python-src/src/trackers/token/report.py +4 -4
- package/dist/python-src/src/trackers/token/tracker.py +8 -8
- package/dist/python-src/src/tui/chatui.py +10 -10
- package/dist/python-src/src/tui/clawtui.py +230 -0
- package/dist/python-src/src/tui/commands/__init__.py +3 -0
- package/dist/python-src/src/tui/commands/base.py +6 -0
- package/dist/python-src/src/tui/commands/instruction.py +5 -0
- package/dist/python-src/src/tui/components/tlist.py +7 -7
- package/dist/python-src/src/tui/components/tscroll_panel.py +73 -44
- package/dist/python-src/src/tui/components/tscroll_panel_old.py +58 -0
- package/dist/python-src/src/tui/utils/trender.py +21 -21
- package/dist/python-src/uv.lock +1969 -1958
- package/package.json +1 -1
|
@@ -20,12 +20,12 @@ class BudgetGuard:tag:Optional[str]=_A;user_id:Optional[str]=_A;session_id:Optio
|
|
|
20
20
|
class CostTracker:
|
|
21
21
|
DEFAULT_DB=Path.home()/'.config'/'llm-cost-tracker'/'usage.db'
|
|
22
22
|
def __init__(A,db_path=_A,pricing=_A,budgets=_A,batch_size=50,flush_interval=1.):A.db_path=Path(db_path or os.environ.get('LLM_COST_DB',A.DEFAULT_DB));A.db_path.parent.mkdir(parents=_D,exist_ok=_D);A.pricing=pricing;A.budgets={A.tag or f"{A.user_id or _C}_{A.session_id or _B}":A for A in budgets or[]};A.batch_size=batch_size;A.flush_interval=flush_interval;A._queue=asyncio.Queue();A._task=_A;A._conn=_A
|
|
23
|
-
async def start(A):A._conn=sqlite3.connect(A.db_path,isolation_level=_A,check_same_thread=False);A._conn.execute('PRAGMA journal_mode=WAL;');A._conn.execute('PRAGMA synchronous=NORMAL;');A.
|
|
23
|
+
async def start(A):A._conn=sqlite3.connect(A.db_path,isolation_level=_A,check_same_thread=False);A._conn.execute('PRAGMA journal_mode=WAL;');A._conn.execute('PRAGMA synchronous=NORMAL;');A.dt();A._task=asyncio.create_task(A._writer_loop())
|
|
24
24
|
async def stop(A):
|
|
25
25
|
await A._queue.put(_A)
|
|
26
26
|
if A._task:await A._task
|
|
27
27
|
if A._conn:A._conn.close()
|
|
28
|
-
def
|
|
28
|
+
def dt(A):A._conn.execute(' \n CREATE TABLE IF NOT EXISTS usage (\n id TEXT PRIMARY KEY,\n model TEXT,\n input_tokens INTEGER,\n output_tokens INTEGER,\n cost REAL,\n \n tag TEXT,\n user_id TEXT,\n session_id TEXT,\n \n timestamp TEXT\n )\n ');A._conn.execute('CREATE INDEX IF NOT EXISTS idx_tag ON usage(tag)');A._conn.execute('CREATE INDEX IF NOT EXISTS idx_user_id ON usage(user_id)');A._conn.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON usage(session_id)');A._conn.execute('CREATE INDEX IF NOT EXISTS idx_user_session ON usage(user_id, session_id)')
|
|
29
29
|
async def add_call(A,model,input_tokens,output_tokens,tag=_B,user_id=_C,session_id=_C):
|
|
30
30
|
D=output_tokens;C=input_tokens;B=model;E=.0
|
|
31
31
|
if A.pricing:
|
|
@@ -38,12 +38,12 @@ class CostTracker:
|
|
|
38
38
|
try:C=await asyncio.wait_for(B._queue.get(),timeout=B.flush_interval)
|
|
39
39
|
except asyncio.TimeoutError:C=_A
|
|
40
40
|
if C is _A:
|
|
41
|
-
if A:B.
|
|
41
|
+
if A:B.du(A);A.clear()
|
|
42
42
|
if C is _A and B._queue.empty():break
|
|
43
43
|
continue
|
|
44
44
|
A.append(C)
|
|
45
|
-
if len(A)>=B.batch_size:B.
|
|
46
|
-
def
|
|
45
|
+
if len(A)>=B.batch_size:B.du(A);A.clear()
|
|
46
|
+
def du(D,records):
|
|
47
47
|
F=records;A=D._conn.cursor()
|
|
48
48
|
try:
|
|
49
49
|
A.execute('BEGIN');A.executemany(' \n INSERT INTO usage\n (id, model, input_tokens, output_tokens, cost, tag, user_id, session_id, timestamp)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ',[A.to_tuple()for A in F]);H={A.tag for A in F}
|
|
@@ -55,11 +55,11 @@ class CostTracker:
|
|
|
55
55
|
elif E>=C.alert_at:print(f"[WARN] {B} budget {E:.0%}")
|
|
56
56
|
A.execute('COMMIT')
|
|
57
57
|
except Exception:A.execute('ROLLBACK');raise
|
|
58
|
-
def
|
|
58
|
+
def dv(A):
|
|
59
59
|
B=[];C=[]
|
|
60
60
|
if A.tag:B.append('tag=?');C.append(A.tag)
|
|
61
61
|
if A.user_id:B.append('user_id=?');C.append(A.user_id)
|
|
62
62
|
if A.session_id:B.append('session_id=?');C.append(A.session_id)
|
|
63
63
|
D=' AND '.join(B)if B else'1=1';return D,C
|
|
64
|
-
def total_cost(A,tag=_A,user_id=_A,session_id=_A):B=A._conn.cursor();C=BudgetGuard(tag,user_id,session_id);D,E=A.
|
|
65
|
-
def total_tokens(C,tag=_A,user_id=_A,session_id=_A):E=BudgetGuard(tag,user_id,session_id);D=C._conn.cursor();F,G=C.
|
|
64
|
+
def total_cost(A,tag=_A,user_id=_A,session_id=_A):B=A._conn.cursor();C=BudgetGuard(tag,user_id,session_id);D,E=A.dv(C);B.execute(f"SELECT SUM(cost) FROM usage WHERE {D}",E);return B.fetchone()[0]or .0
|
|
65
|
+
def total_tokens(C,tag=_A,user_id=_A,session_id=_A):E=BudgetGuard(tag,user_id,session_id);D=C._conn.cursor();F,G=C.dv(E);D.execute(f"SELECT SUM(input_tokens), SUM(output_tokens) FROM usage WHERE {F}",G);A,B=D.fetchone();A=A or 0;B=B or 0;return{'input':A,'output':B,'total':A+B}
|
|
@@ -42,11 +42,11 @@ from src.tui.utils.trender import display_tool_call,display_tool_result,display_
|
|
|
42
42
|
from langchain_core.messages import HumanMessage
|
|
43
43
|
from langgraph.types import Command
|
|
44
44
|
from dotenv import find_dotenv,load_dotenv
|
|
45
|
-
|
|
45
|
+
q=load_dotenv(find_dotenv())
|
|
46
46
|
class LiveChatUI:
|
|
47
|
-
def __init__(A,agent=_A,saver=_A,workspace=_A,**C):B='#afafff';A.agent=agent;A.saver=saver;A.kwargs=C;A.workspace=re.sub('^(\\/Users\\/[^/]+|\\/home\\/[^/]+)','~',workspace);A.thread_id=1;A.token_count=0;A.mode=_E;A.context=C.get('context',_A);A.instruction_manager=C.get('instruction_manager',_A);A.spinner=Spinner('block');A.max_input_lines=10;A.cancel_event=_A;A.logo_label=Label(LOGO.format(A.workspace),style='class:logo');A.begin_items=[A.logo_label];A.begin_area=HSplit([*A.begin_items],padding=1);A.log_control=ScrollableFormattedLogControl();A.output_area=Window(content=A.log_control,wrap_lines=_B,always_hide_cursor=_C,height=D(weight=1));A.status_label=FormattedTextControl(text=[(_H,' 状态: 等待输入 | Tokens: 0 (⌥ + ⏎ 换行 Esc 中断 ctrl + c 退出)')]);F=FormattedTextControl(text=[('class:spinner',f"{A.spinner.current_frame()}")],show_cursor=_C);G=FormattedTextControl(text=[(_F,f"mode: {A.mode}")]);A.status_bar=VSplit([Window(F,width=D(weight=5),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1)),Window(A.status_label,width=D(weight=85),height=D(weight=1)),Window(G,width=D(weight=10),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1))],width=D(weight=100));A.input_box=TextArea(height=1,prompt='> ',multiline=_B,wrap_lines=_B,scrollbar=_B,style='class:input_box');A.input_box.buffer.on_text_changed+=lambda _:A.update_input_area_height(
|
|
48
|
-
def
|
|
49
|
-
def
|
|
47
|
+
def __init__(A,agent=_A,saver=_A,workspace=_A,**C):B='#afafff';A.agent=agent;A.saver=saver;A.kwargs=C;A.workspace=re.sub('^(\\/Users\\/[^/]+|\\/home\\/[^/]+)','~',workspace);A.thread_id=1;A.token_count=0;A.mode=_E;A.context=C.get('context',_A);A.instruction_manager=C.get('instruction_manager',_A);A.spinner=Spinner('block');A.max_input_lines=10;A.cancel_event=_A;A.logo_label=Label(LOGO.format(A.workspace),style='class:logo');A.begin_items=[A.logo_label];A.begin_area=HSplit([*A.begin_items],padding=1);A.log_control=ScrollableFormattedLogControl();A.output_area=Window(content=A.log_control,wrap_lines=_B,always_hide_cursor=_C,height=D(weight=1));A.status_label=FormattedTextControl(text=[(_H,' 状态: 等待输入 | Tokens: 0 (⌥ + ⏎ 换行 Esc 中断 ctrl + c 退出)')]);F=FormattedTextControl(text=[('class:spinner',f"{A.spinner.current_frame()}")],show_cursor=_C);G=FormattedTextControl(text=[(_F,f"mode: {A.mode}")]);A.status_bar=VSplit([Window(F,width=D(weight=5),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1)),Window(A.status_label,width=D(weight=85),height=D(weight=1)),Window(G,width=D(weight=10),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1))],width=D(weight=100));A.input_box=TextArea(height=1,prompt='> ',multiline=_B,wrap_lines=_B,scrollbar=_B,style='class:input_box');A.input_box.buffer.on_text_changed+=lambda _:A.update_input_area_height(q);A.kb=KeyBindings();A.t();H=Frame(body=A.input_box,style='class:frame');A.input_items=[A.status_bar,H];A.input_area=HSplit([*A.input_items],padding=0);A.interact_items=[];A.interact_area=HSplit([*A.interact_items],padding=1);E=_F;A.footer=VSplit([Window(FormattedTextControl([(E,f"{A.workspace} (main) ")]),width=D(weight=50)),Window(FormattedTextControl([(E,f"MCP: (0/0) ")]),width=D(weight=20)),Window(FormattedTextControl([(E,'Env: (local) ')]),width=D(weight=20)),Window(FormattedTextControl([(E,f"Model: kimi-k2-0711-preview ")]),wrap_lines=_B,dont_extend_width=_C,always_hide_cursor=_B,width=D(weight=10))],width=D(weight=100),height=1);A.suggest_items=[A.footer];A.suggest_area=HSplit([*A.suggest_items],padding=0);A.logo_area=DynamicContainer(lambda:A.begin_area);A.display_container=DynamicContainer(lambda:A.output_area);A.input_container=DynamicContainer(lambda:A.input_area);A.status_area=DynamicContainer(lambda:A.suggest_area);A.layout=Layout(HSplit([A.logo_area,A.output_area,A.interact_area,A.input_container,A.status_area],padding=0),focused_element=A.input_box);A.style=Style.from_dict({'logo':B,'output':B,'input_box':B,'status':B,'frame.border':B,'suggestions':B,'footer':B,'suggestion.label':B,'suggestion.desc':'#5f5f5f','spinner':B,'suggestion.selected':'bold #00afff'});A.app=Application(layout=A.layout,key_bindings=A.kb,style=A.style,full_screen=_B,mouse_support=_B);A.app.input_area=A.input_box;A.app.kb=A.kb;A.interrupt_tools=C.get('interrupt_tools',[]);A.toolcall_mode='manul'
|
|
48
|
+
def u(A,role='user',spinner='●',status='',tokens=0):A.status_label.text=[(_H,f" 状态: {status} | ({role}) | Tokens: {tokens} (esc + ⏎ 换行 按两次 esc 中断 ctrl + c 退出)")];A.app.invalidate()
|
|
49
|
+
def r(A,workspace=_A,mcp_status=_A,sandbox_status=_A,model_status=_A):
|
|
50
50
|
F=model_status;E=sandbox_status;D=mcp_status;C=workspace;B=_F
|
|
51
51
|
if C is not _A:A._footer_workspace.text=[(B,f"{C}(main) ")]
|
|
52
52
|
if D is not _A:A._footer_context.text=[(B,f"{D} ")]
|
|
@@ -59,7 +59,7 @@ class LiveChatUI:
|
|
|
59
59
|
def clear(A):A.log_control.clear();A.input_box.text='';B=render_info(LOGO.format(A.workspace),style='light_stell_blue',markdown=_C);A.log_control.append_text(B);A.app.invalidate();A.app.layout.focus(A.input_box)
|
|
60
60
|
async def updater(A):await A.spinner.run(A.app)
|
|
61
61
|
async def run_async(A):await asyncio.gather(A.app.run_async(),A.updater())
|
|
62
|
-
def
|
|
62
|
+
def t(A):
|
|
63
63
|
D='enter';C='escape'
|
|
64
64
|
@A.kb.add(D)
|
|
65
65
|
def B(event):
|
|
@@ -78,19 +78,19 @@ class LiveChatUI:
|
|
|
78
78
|
if A.logo_label in A.begin_items:A.begin_items.remove(A.logo_label);A.begin_area.children=list(A.begin_items);J=render_info(LOGO.format(A.workspace),style=D,markdown=_C);A.log_control.append_text(J);A.app.invalidate()
|
|
79
79
|
if B.strip()in['quit','exit','q']:get_app().exit();return
|
|
80
80
|
if B.strip()in['/clear','clear']:A.clear();return
|
|
81
|
-
A.spinner.start();A.
|
|
81
|
+
A.spinner.start();A.s('● user',f"● {B}",style='light_salmon3');await asyncio.sleep(.05)
|
|
82
82
|
if B.strip()in['/commands']and A.instruction_manager:
|
|
83
83
|
E=[]
|
|
84
84
|
for F in A.instruction_manager.list_instructions():E.append(f"/{F.name}: - {F.settings[_D]}")
|
|
85
|
-
K='\n'.join(E);A.
|
|
85
|
+
K='\n'.join(E);A.s('● bot',K,style=D,markdown=_B);return
|
|
86
86
|
if A.instruction_manager:G=A.instruction_manager.parse(B);H,I=G['executed_instruction'],G['message'];B=f"""
|
|
87
87
|
[注意]: 执行用户请求必须严格遵循如下准则:
|
|
88
88
|
{H}
|
|
89
89
|
|
|
90
90
|
用户请求:
|
|
91
91
|
{I}"""if H else I
|
|
92
|
-
C=A.context if C is _A else C;L=await A._handle_stream('○ bot',A._stream_generate(B,C),style=D,markdown=_B,context=C);A.spinner.stop();A.
|
|
93
|
-
def
|
|
92
|
+
C=A.context if C is _A else C;L=await A._handle_stream('○ bot',A._stream_generate(B,C),style=D,markdown=_B,context=C);A.spinner.stop();A.u(spinner='',status='等待输入',tokens=A.token_count);A.app.layout.focus(A.input_box);return L
|
|
93
|
+
def s(A,sender,message,style='green',markdown=_C):D=markdown;C=style;B=message;E=Markdown(B)if D else Text(B,style=C);F=render_panel(sender,E,C,D);A.log_control.append_text(F);A.app.invalidate()
|
|
94
94
|
async def _stream_generate(A,prompt,context=_A):
|
|
95
95
|
B=A.agent.astream({_G:[HumanMessage(content=prompt)]},config={_I:{_J:A.thread_id}},stream_mode=[_G,_K,_L],context=context);A.cancel_event=asyncio.Event()
|
|
96
96
|
async for C in astream_handler(B,interrupt_tools=A.interrupt_tools,tool_mode=A.toolcall_mode):
|
|
@@ -143,7 +143,7 @@ class LiveChatUI:
|
|
|
143
143
|
D.spinner.stop();j=[];p=B['interrupt_id']
|
|
144
144
|
for d in B[m][m]['action_requests']:w=d[P];x=d[Q];y=d[_D];q=await D._handle_human_interrupt(message=f" 允许执行当前函数么? ",options=[{h:'是的,允许当前函数执行',_D:''},{h:'是的,总是允许执行,当前对话过程中不再提示',_D:''},{h:'不, 不允许当前函数执行',_D:''}]);k=['approve',_E,'reject'][q];D.toolcall_mode=_E if k==_E else'manual';j.append({I:k})
|
|
145
145
|
D.spinner.start();await D._handle_stream(a,D._resume_generate(p,j,i),style=b,markdown=c,items=A,context=i);break
|
|
146
|
-
r=Group(*[A[0]for A in A]);D.log_control.update_last(render_panel(a,r,b,c));D.app.invalidate();await asyncio.sleep(.03);s='';D.
|
|
146
|
+
r=Group(*[A[0]for A in A]);D.log_control.update_last(render_panel(a,r,b,c));D.app.invalidate();await asyncio.sleep(.03);s='';D.u(spinner=s,status='正在生成 ...',tokens=D.token_count)
|
|
147
147
|
return R
|
|
148
148
|
async def _handle_human_interrupt(A,message,options):
|
|
149
149
|
E=asyncio.get_event_loop();C=E.create_future();D=A.app.key_bindings
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
_P='custom'
|
|
2
|
+
_O='updates'
|
|
3
|
+
_N='thread_id'
|
|
4
|
+
_M='channels'
|
|
5
|
+
_L='heartbeat'
|
|
6
|
+
_K='class:spinner'
|
|
7
|
+
_J='class:status'
|
|
8
|
+
_I='messages'
|
|
9
|
+
_H='configurable'
|
|
10
|
+
_G='class:footer'
|
|
11
|
+
_F='auto'
|
|
12
|
+
_E='label'
|
|
13
|
+
_D='description'
|
|
14
|
+
_C=False
|
|
15
|
+
_B=True
|
|
16
|
+
_A=None
|
|
17
|
+
from functools import partial
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List
|
|
20
|
+
import traceback,asyncio,json,sys,os,re
|
|
21
|
+
from prompt_toolkit.layout import Layout,HSplit,Window,VSplit,DynamicContainer
|
|
22
|
+
from prompt_toolkit.completion import Completer,Completion,PathCompleter
|
|
23
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
24
|
+
from prompt_toolkit.widgets import TextArea,Label,Frame
|
|
25
|
+
from prompt_toolkit.application.current import get_app
|
|
26
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
27
|
+
from prompt_toolkit.layout.dimension import D
|
|
28
|
+
from prompt_toolkit.document import Document
|
|
29
|
+
from prompt_toolkit.styles import Style
|
|
30
|
+
from prompt_toolkit import Application
|
|
31
|
+
from rich.markdown import Markdown
|
|
32
|
+
from rich.columns import Columns
|
|
33
|
+
from rich.console import Group
|
|
34
|
+
from rich.markup import render
|
|
35
|
+
from rich.panel import Panel
|
|
36
|
+
from rich.text import Text
|
|
37
|
+
from rich.tree import Tree
|
|
38
|
+
from rich.box import Box
|
|
39
|
+
from src.claw.bus.queue import MessageBus,InboundMessage,OutboundMessage
|
|
40
|
+
from src.claw.cron.service import CronService
|
|
41
|
+
from src.tui.components.tscroll_panel import ScrollableFormattedLogControl
|
|
42
|
+
from src.tui.components.tlist import InterruptSelector
|
|
43
|
+
from src.stream.handler import astream_handler
|
|
44
|
+
from src.tui.config import LOGO,COMMANDS,USERS,COMMAND_META
|
|
45
|
+
from src.tui.components.live_spinner import Spinner
|
|
46
|
+
from src.tui.utils.render import markdown_to_wrapped_text,extract_msg_info,render_panel,render_info
|
|
47
|
+
from src.tui.utils.trender import display_tool_call,display_tool_result,display_tool_error
|
|
48
|
+
from src.claw.tools.base import AgentContext
|
|
49
|
+
from src.managers.manager_command import CommandManager
|
|
50
|
+
from src.managers.manager_agent import AgentManager
|
|
51
|
+
from langchain_core.messages import HumanMessage
|
|
52
|
+
from langgraph.types import Command
|
|
53
|
+
from dotenv import find_dotenv,load_dotenv
|
|
54
|
+
k=load_dotenv(find_dotenv())
|
|
55
|
+
class CommandCompleter(Completer):
|
|
56
|
+
def __init__(A,commands,agents):A.path_completer=PathCompleter(expanduser=_B);A.commands=commands;A.agents=agents
|
|
57
|
+
def get_completions(G,document,complete_event):
|
|
58
|
+
D=document;M=D.text_before_cursor;A=D.get_word_before_cursor(WORD=_B)
|
|
59
|
+
if D.text.startswith('@file '):
|
|
60
|
+
E=A
|
|
61
|
+
if E.startswith('~/'):B=Path(os.path.expanduser(E)).expanduser()
|
|
62
|
+
else:B=Path(E).expanduser()
|
|
63
|
+
H=B.parent if B.name else B;I=B.name
|
|
64
|
+
try:
|
|
65
|
+
for F in H.iterdir():
|
|
66
|
+
C=F.name
|
|
67
|
+
if C.startswith(I):J=f"{C}/"if F.is_dir()else C;N=str((H/C).resolve());yield Completion(J,start_position=-len(I),display_meta='dir'if F.is_dir()else'file')
|
|
68
|
+
except Exception:pass
|
|
69
|
+
elif A.startswith('/'):
|
|
70
|
+
for K in G.commands:yield Completion(f"{K}",start_position=-len(A))
|
|
71
|
+
elif A.startswith('@'):
|
|
72
|
+
for L in G.agents:yield Completion(f"{L}",start_position=-len(A))
|
|
73
|
+
class LiveChatUI:
|
|
74
|
+
def __init__(A,agent=_A,saver=_A,workspace=_A,**C):G='agents';F='.autodev';B='#afafff';A.agent=agent;A.saver=saver;A.kwargs=C;A.workspace=re.sub('^(\\/Users\\/[^/]+|\\/home\\/[^/]+)','~',workspace);A.thread_id=1;A.token_count=0;A.mode=_F;A.context=C.get('context',_A);A.instruction_manager=C.get('instruction_manager',_A);A.spinner=Spinner('ball');A.max_input_lines=10;A.command_manager=CommandManager(A,workspace=A.workspace);A.command_descriptions=A.command_manager.description_();H=Path.home()/F/G;I=Path(A.workspace).expanduser()/F/G;A.user_agent_manager=AgentManager(H);A.proj_agent_manager=AgentManager(I);A.agent_descriptions={**A.user_agent_manager.descriptions_(user=_B),**A.proj_agent_manager.descriptions_(user=_C)};A.COMMANDS=list(A.command_descriptions.keys());A.AGENTS=list(A.agent_descriptions.keys());A.COMMAND_META={**A.command_descriptions,**A.agent_descriptions};A.suggestions=[];A.selected_index=0;A.suggestions_box=HSplit(children=[],height=5);A.max_input_lines=10;A.cancel_event=_A;A.logo_label=Label(LOGO.format(A.workspace),style='class:logo');A.begin_items=[A.logo_label];A.begin_area=HSplit([*A.begin_items],padding=1);A.log_control=ScrollableFormattedLogControl();A.output_area=Window(content=A.log_control,wrap_lines=_B,always_hide_cursor=_C,height=D(weight=1));A.status_label=FormattedTextControl(text=[(_J,' 状态: 等待输入 | Tokens: 0 (⌥ + ⏎ 换行 Esc 中断 ctrl + c 退出)')]);A.spinner_control=FormattedTextControl(text=[(_K,f"{A.spinner.current_frame()}")],show_cursor=_C);J=FormattedTextControl(text=[(_G,f"mode: {A.mode}")]);A.status_bar=VSplit([Window(A.spinner_control,width=D(weight=5),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1)),Window(A.status_label,width=D(weight=85),height=D(weight=1)),Window(J,width=D(weight=10),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1))],width=D(weight=100));A.input_box=TextArea(height=1,prompt='> ',multiline=_B,wrap_lines=_B,scrollbar=_B,completer=CommandCompleter(A.COMMANDS,A.AGENTS),complete_while_typing=_B,style='class:input_box');A.input_box.buffer.on_text_changed+=lambda _:A.update_suggestions();A.input_box.buffer.on_text_changed+=lambda _:A.update_input_area_height(k);A.kb=KeyBindings();A.o();K=Frame(body=A.input_box,style='class:frame');A.input_items=[A.status_bar,K];A.input_area=HSplit([*A.input_items],padding=0);A.interact_items=[];A.interact_area=HSplit([*A.interact_items],padding=1);L=os.getenv('DEFAULT_MODEL','kimi-k2-0711-preview');E=_G;A.footer=VSplit([Window(FormattedTextControl([(E,f"{A.workspace} (main) ")]),width=D(weight=50)),Window(FormattedTextControl([(E,f"MCP: (0/0) ")]),width=D(weight=20)),Window(FormattedTextControl([(E,'Env: (local) ')]),width=D(weight=20)),Window(FormattedTextControl([(E,f"Model: {L} ")]),wrap_lines=_B,dont_extend_width=_C,always_hide_cursor=_B,width=D(weight=10))],width=D(weight=100),height=1);A.suggest_items=[A.footer];A.suggest_area=HSplit([*A.suggest_items],padding=0);A.logo_area=DynamicContainer(lambda:A.begin_area);A.display_container=DynamicContainer(lambda:A.output_area);A.input_container=DynamicContainer(lambda:A.input_area);A.status_area=DynamicContainer(lambda:A.suggest_area);A.layout=Layout(HSplit([A.logo_area,A.output_area,A.interact_area,A.input_container,A.status_area],padding=0),focused_element=A.input_box);A.style=Style.from_dict({'logo':B,'output':B,'input_box':B,'status':B,'frame.border':B,'suggestions':B,'footer':B,'suggestion.label':B,'suggestion.desc':'#5f5f5f','spinner':B,'suggestion.selected':'bold #00afff'});A.app=Application(layout=A.layout,key_bindings=A.kb,style=A.style,full_screen=_B,mouse_support=_B);A.app.input_area=A.input_box;A.app.kb=A.kb;A.interrupt_tools=C.get('interrupt_tools',[]);A.toolcall_mode='manul'
|
|
75
|
+
def p(A,role='user',spinner='●',status='',tokens=0):A.status_label.text=[(_J,f" 状态: {status} | ({role}) | Tokens: {tokens} (esc + ⏎ 换行 按两次 esc 中断 ctrl + c 退出)")];A.spinner_control.text=[(_K,f"{A.spinner.current_frame()}")];A.app.invalidate()
|
|
76
|
+
def m(A,workspace=_A,mcp_status=_A,sandbox_status=_A,model_status=_A):
|
|
77
|
+
F=model_status;E=sandbox_status;D=mcp_status;C=workspace;B=_G
|
|
78
|
+
if C is not _A:A._footer_workspace.text=[(B,f"{C}(main) ")]
|
|
79
|
+
if D is not _A:A._footer_context.text=[(B,f"{D} ")]
|
|
80
|
+
if E is not _A:A._footer_env.text=[(B,f"Env: {E} ")]
|
|
81
|
+
if F is not _A:A._footer_model.text=[(B,f"{F}")]
|
|
82
|
+
A.app.invalidate()
|
|
83
|
+
def update_input_area_height(A,_):
|
|
84
|
+
C=A.input_box.buffer.text.count('\n')+1;B=min(C,A.max_input_lines)
|
|
85
|
+
if B!=A.input_box.window.height:A.input_box.window.height=B
|
|
86
|
+
def get_suggestions(B,text):
|
|
87
|
+
A=text.strip().split()[-1]if text.strip()else''
|
|
88
|
+
def C(word,lst):
|
|
89
|
+
A=lst
|
|
90
|
+
if word in A:E=A.index(word);B=max(0,E-4);C=min(len(A),B+5)
|
|
91
|
+
else:B,C=0,5
|
|
92
|
+
D=A[B:C];return D+['']*(5-len(D))
|
|
93
|
+
if A.startswith('/'):return C(A,B.COMMANDS)
|
|
94
|
+
elif A.startswith('@'):D=B.AGENTS;return C(A,D)
|
|
95
|
+
return[]
|
|
96
|
+
def update_suggestions(A):
|
|
97
|
+
C=A.input_box.text;D=A.get_suggestions(C);A.suggestions=[]
|
|
98
|
+
if D:
|
|
99
|
+
for(E,B)in enumerate(D):
|
|
100
|
+
A.suggestions.append({_E:B,_D:A.COMMAND_META.get(B,'')})
|
|
101
|
+
if B==C:A.selected_index=E
|
|
102
|
+
A.suggestions_box.children=A.l()
|
|
103
|
+
if A.suggestions_box not in A.suggest_area.children:A.suggest_area.children.insert(0,A.suggestions_box)
|
|
104
|
+
else:A.clear_suggestions()
|
|
105
|
+
A.app.invalidate()
|
|
106
|
+
def clear_suggestions(A):
|
|
107
|
+
A.suggestions=[];A.selected_index=0;A.suggestions_box.children=[]
|
|
108
|
+
if A.suggestions_box in A.suggest_area.children:A.suggest_area.children.remove(A.suggestions_box)
|
|
109
|
+
A.app.invalidate()
|
|
110
|
+
def l(A):
|
|
111
|
+
F='reverse';C=[];G=len(A.suggestions)
|
|
112
|
+
if not G:return[]
|
|
113
|
+
H=0;I=len(A.suggestions)
|
|
114
|
+
for D in range(H,I):E=A.suggestions[D];B=D==A.selected_index;J=F if B else'class:suggestion.label';K=F if B else'class:suggestion.desc';L='➤ 'if B else' ';M=VSplit([Window(FormattedTextControl(L),width=2),Window(FormattedTextControl([(J,E[_E])]),width=30),Window(FormattedTextControl([(K,E[_D])]),wrap_lines=_B,always_hide_cursor=_B)],height=1);C.append(M)
|
|
115
|
+
return C
|
|
116
|
+
def clear(A):A.log_control.clear();A.input_box.text='';B=render_info(LOGO.format(A.workspace),style='light_stell_blue',markdown=_C);A.log_control.append_text(B);A.app.invalidate();A.app.layout.focus(A.input_box)
|
|
117
|
+
async def updater(A):await A.spinner.run(A.app)
|
|
118
|
+
async def run_async(A):await asyncio.gather(A.app.run_async(),A.updater())
|
|
119
|
+
def o(A):
|
|
120
|
+
D='enter';C='escape'
|
|
121
|
+
@A.kb.add('@')
|
|
122
|
+
def B(event):A=event;A.app.current_buffer.insert_text('@');A.app.current_buffer.start_completion(select_first=_B)
|
|
123
|
+
@A.kb.add('/')
|
|
124
|
+
def B(event):A=event;A.app.current_buffer.insert_text('/');A.app.current_buffer.start_completion(select_first=_B)
|
|
125
|
+
@A.kb.add(D)
|
|
126
|
+
def B(event):
|
|
127
|
+
if A.suggestions:E=A.suggestions[A.selected_index][_E];B=A.input_box.buffer;D=B.document.get_word_before_cursor(WORD=_B);B.delete_before_cursor(count=len(D));A.clear_suggestions()
|
|
128
|
+
else:
|
|
129
|
+
C=A.input_box.text.strip()
|
|
130
|
+
if not C:return
|
|
131
|
+
A.input_box.text='';asyncio.ensure_future(A._handle_submit(C))
|
|
132
|
+
@A.kb.add(C,D)
|
|
133
|
+
def B(event):A.input_box.buffer.insert_text('\n')
|
|
134
|
+
@A.kb.add('c-c')
|
|
135
|
+
def B(event):
|
|
136
|
+
if A.log_control._selection_start and A.log_control._selection_end:A.log_control.copy_selection_to_clipboard()
|
|
137
|
+
else:event.app.exit();sys.exit(0)
|
|
138
|
+
@A.kb.add(C,C)
|
|
139
|
+
def B(event):
|
|
140
|
+
if A.cancel_event:A.cancel_event.set()
|
|
141
|
+
async def shutdown(A):
|
|
142
|
+
print('\nShutting down all services...');B=A.kwargs.get('bus',_A);C=A.kwargs.get('cron',_A);D=A.kwargs.get(_L,_A);E=A.kwargs.get(_M,_A);D.stop();await C.stop();await E.stop_all();B.stop();F=asyncio.get_running_loop()
|
|
143
|
+
for G in asyncio.all_tasks(F):G.cancel()
|
|
144
|
+
async def alisten(A):
|
|
145
|
+
B=A.kwargs.get('bus',_A);C=A.kwargs.get('cron',_A);D=A.kwargs.get(_L,_A);E=A.kwargs.get(_M,_A);B.subscribe_inbound('feishu',partial(A._process_message,bus=B,cron=C))
|
|
146
|
+
try:await C.start();await D.start();await asyncio.gather(B.dispatch_inbound(),E.start_all(),A.app.run_async(),A.updater())
|
|
147
|
+
except KeyboardInterrupt:await A.shutdown()
|
|
148
|
+
async def _process_message(B,msg,bus,cron):A=msg;D=f"{A.channel}:{A.chat_id}";E={_H:{'session_id':D}};B.context=AgentContext(working_directory=os.getcwd(),sandbox=_A,channel=A.channel,chat_id=A.chat_id,cron_service=cron,workspace=os.getcwd());C=await B._handle_submit(A.content,B.context);C=OutboundMessage(channel=A.channel,chat_id=A.chat_id,content=C,metadata=A.metadata or{});await bus.publish_outbound(C)
|
|
149
|
+
async def _handle_submit(A,text,context=_A):
|
|
150
|
+
D='light_steel_blue';C=context;B=text
|
|
151
|
+
if A.logo_label in A.begin_items:A.begin_items.remove(A.logo_label);A.begin_area.children=list(A.begin_items);J=render_info(LOGO.format(A.workspace),style=D,markdown=_C);A.log_control.append_text(J);A.app.invalidate()
|
|
152
|
+
if B.strip()in['quit','exit','q']:get_app().exit();await A.shutdown();return
|
|
153
|
+
if B.strip()in['/clear','clear']:A.clear();return
|
|
154
|
+
A.spinner.start();A.n('● user',f"● {B}",style='light_salmon3');await asyncio.sleep(.05)
|
|
155
|
+
if B.strip()in['/commands']and A.instruction_manager:
|
|
156
|
+
E=[]
|
|
157
|
+
for F in A.instruction_manager.list_instructions():E.append(f"/{F.name}: - {F.settings[_D]}")
|
|
158
|
+
K='\n'.join(E);A.n('● bot',K,style=D,markdown=_B);return
|
|
159
|
+
if A.instruction_manager:G=A.instruction_manager.parse(B);H,I=G['executed_instruction'],G['message'];B=f"""
|
|
160
|
+
[注意]: 执行用户请求必须严格遵循如下准则:
|
|
161
|
+
{H}
|
|
162
|
+
|
|
163
|
+
用户请求:
|
|
164
|
+
{I}"""if H else I
|
|
165
|
+
C=A.context if C is _A else C;L=await A._handle_stream('○ bot',A._stream_generate(B,C),style=D,markdown=_B,context=C);A.spinner.stop();A.p(spinner='',status='等待输入',tokens=A.token_count);A.app.layout.focus(A.input_box);return L
|
|
166
|
+
def n(A,sender,message,style='green',markdown=_C):D=markdown;C=style;B=message;E=Markdown(B)if D else Text(B,style=C);F=render_panel(sender,E,C,D);A.log_control.append_text(F);A.app.invalidate()
|
|
167
|
+
async def _stream_generate(A,prompt,context=_A):
|
|
168
|
+
B=A.agent.astream({_I:[HumanMessage(content=prompt)]},config={_H:{_N:A.thread_id}},stream_mode=[_I,_O,_P],context=context);A.cancel_event=asyncio.Event()
|
|
169
|
+
async for C in astream_handler(B,interrupt_tools=A.interrupt_tools,tool_mode=A.toolcall_mode):
|
|
170
|
+
if A.cancel_event and A.cancel_event.is_set():A.cancel_event.clear();A.cancel_event=_A;return
|
|
171
|
+
yield C
|
|
172
|
+
async def _resume_generate(A,interrupt_id,decisions,context=_A):
|
|
173
|
+
B=A.agent.astream(Command(resume={interrupt_id:{'decisions':decisions}}),config={_H:{_N:A.thread_id}},stream_mode=[_I,_O,_P],context=context);A.cancel_event=asyncio.Event()
|
|
174
|
+
async for C in astream_handler(B,interrupt_tools=A.interrupt_tools,tool_mode=A.toolcall_mode):
|
|
175
|
+
if A.cancel_event and A.cancel_event.is_set():A.cancel_event.clear();A.cancel_event=_A
|
|
176
|
+
yield C
|
|
177
|
+
async def _handle_stream(D,sender,stream,style='green',markdown=_C,items=_A,context=_A):
|
|
178
|
+
l='value';k='parent_id';h=context;g='tool_result';f='text';e='thinking';c=markdown;b=style;a=sender;Z='success';Y='tool_call';Q='args';P='name';I='type';G='content';A=items;R,U='','';s=A is not _A
|
|
179
|
+
if A is _A:D.log_control.append_text(render_panel(a,'',b,c))
|
|
180
|
+
A=A if A else[]
|
|
181
|
+
async for B in stream:
|
|
182
|
+
if B[I]==e:
|
|
183
|
+
if len(A)>0 and A[-1][1]==e:U+=B[G];J=markdown_to_wrapped_text(U);A[-1][0]=J
|
|
184
|
+
else:U=B[G];J=markdown_to_wrapped_text(U);A.append([J,e,_A,B,[]])
|
|
185
|
+
elif B[I]==f:
|
|
186
|
+
if len(A)>0 and A[-1][1]==f:R+=B[G];J=markdown_to_wrapped_text(f"{R}");A[-1][0]=J
|
|
187
|
+
else:R=B[G];J=markdown_to_wrapped_text(f"{R}");A.append([J,f,_A,B,[]])
|
|
188
|
+
elif B[I]=='token_usage':t=B['input_toks'];u=B['output_toks'];m=B['total_toks'];D.token_count=m
|
|
189
|
+
elif B[I]==Y:
|
|
190
|
+
V=B[P];W=B[Q];H=B['id'];K=B[k]
|
|
191
|
+
if K is _A:F=display_tool_call(V,W,[]);A.append([F,Y,H,B,[]]);A.append([Text('',''),'margin',_A,_A,[]])
|
|
192
|
+
else:
|
|
193
|
+
C=index_(A,lambda x:x[2]==K and x[1]==Y)
|
|
194
|
+
if C is _A:continue
|
|
195
|
+
E=A[C];S=E[3][P];T=E[3][Q];A[C][-1].append([_A,Y,H,B,[]]);F=display_tool_call(S,T,A[C][-1]);A[C][0]=F
|
|
196
|
+
elif B[I]==g:
|
|
197
|
+
V=B[P];H=B['id'];K=B[k];X=B[Z]
|
|
198
|
+
if X==Z:
|
|
199
|
+
if K is _A:O=B[G];L=index_(A,lambda x:x[2]==H);n=A[L][3];W=n[Q];F=display_tool_result(V,W,O,A[L][-1]);A[L]=[F,g,H,B,A[L][-1]]
|
|
200
|
+
else:
|
|
201
|
+
C=index_(A,lambda x:x[2]==K)
|
|
202
|
+
if C is _A:continue
|
|
203
|
+
E=A[C];S=E[3][P];T=E[3][Q];M=index_(E[-1],lambda x:x[2]==H)
|
|
204
|
+
if M is _A:continue
|
|
205
|
+
N=E[-1][M];N[3][Z]=X;N[3][G]=B[G];A[C][-1][M]=N;O='';F=display_tool_result(S,T,O,A[C][-1]);A[C][0]=F
|
|
206
|
+
elif X=='error':
|
|
207
|
+
if K is _A:O=B[G];F=display_tool_error(V,W,O,A[L][-1]);A[L]=[F,g,H,B,A[L][-1]]
|
|
208
|
+
else:
|
|
209
|
+
C=index_(A,lambda x:x[2]==K)
|
|
210
|
+
if C is _A:continue
|
|
211
|
+
E=A[C];S=E[3][P];T=E[3][Q];M=index_(E[-1],lambda x:x[2]==H)
|
|
212
|
+
if M is _A:continue
|
|
213
|
+
N=E[-1][M];N[Z]=X;N[G]=B[G];A[C][-1][M]=N;F=display_tool_error(S,T,O,A[C][-1]);A[C][0]=F
|
|
214
|
+
elif B[I]=='done':0
|
|
215
|
+
elif B[I]=='interrupt':
|
|
216
|
+
D.spinner.stop();i=[];o=B['interrupt_id']
|
|
217
|
+
for d in B[l][l]['action_requests']:v=d[P];w=d[Q];x=d[_D];p=await D._handle_human_interrupt(message=f" 允许执行当前函数么? ",options=[{_E:'是的,允许当前函数执行',_D:''},{_E:'是的,总是允许执行,当前对话过程中不再提示',_D:''},{_E:'不, 不允许当前函数执行',_D:''}]);j=['approve',_F,'reject'][p];D.toolcall_mode=_F if j==_F else'manual';i.append({I:j})
|
|
218
|
+
D.spinner.start();await D._handle_stream(a,D._resume_generate(o,i,h),style=b,markdown=c,items=A,context=h);break
|
|
219
|
+
q=Group(*[A[0]for A in A]);D.log_control.update_last(render_panel(a,q,b,c));D.app.invalidate();await asyncio.sleep(.03);r='';D.p(spinner=r,status='正在生成 ...',tokens=D.token_count)
|
|
220
|
+
return R
|
|
221
|
+
async def _handle_human_interrupt(A,message,options):
|
|
222
|
+
E=asyncio.get_event_loop();C=E.create_future();D=A.app.key_bindings
|
|
223
|
+
def F(index):
|
|
224
|
+
if B.container in A.interact_items:A.interact_items.remove(B.container);A.interact_area.children=list(A.interact_items);A.app.invalidate()
|
|
225
|
+
A.app.key_bindings=D;A.app.invalidate()
|
|
226
|
+
if not C.done():C.set_result(index)
|
|
227
|
+
B=InterruptSelector(message,options,callback=F);A.interact_items.append(B.container);A.interact_area.children=list(A.interact_items);A.app.key_bindings=B.kb;A.app.invalidate();await asyncio.sleep(.01);A.log_control.refresh_scroll();A.app.invalidate();G=await C;A.app.layout.focus(A.input_box);A.app.key_bindings=D;A.app.invalidate();return G
|
|
228
|
+
def index_(lst,condition):
|
|
229
|
+
try:return next(A for(A,B)in enumerate(lst)if condition(B))
|
|
230
|
+
except StopIteration:return
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from abc import ABC,abstractmethod
|
|
2
|
+
from typing import Dict,Any,Optional
|
|
3
|
+
class BaseCommand(ABC):
|
|
4
|
+
def __init__(A,name,description,alt_name=None,app=None):A.name=name;A.description=description;A.alt_name=alt_name
|
|
5
|
+
@abstractmethod
|
|
6
|
+
async def execute(self,args,context):0
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from typing import Dict,Any,Optional
|
|
2
|
+
from src.tui.commands.base import BaseCommand
|
|
3
|
+
class InstructionCommand(BaseCommand):
|
|
4
|
+
def __init__(A,name,description,instruction,app):super().__init__(name,description,app=app);A.instruction=instruction
|
|
5
|
+
async def execute(A,context={},args={}):0
|
|
@@ -19,20 +19,20 @@ from prompt_toolkit.layout import Layout,HSplit
|
|
|
19
19
|
from prompt_toolkit.styles import Style
|
|
20
20
|
from prompt_toolkit import Application
|
|
21
21
|
class InterruptSelector:
|
|
22
|
-
def __init__(A,description,options,callback):B=description;A.options=options;A.description=B;A.selected_index=0;A.callback=callback;A.rows=A.
|
|
23
|
-
def
|
|
24
|
-
def
|
|
22
|
+
def __init__(A,description,options,callback):B=description;A.options=options;A.description=B;A.selected_index=0;A.callback=callback;A.rows=A.bm();A.list_container=HSplit(A.rows,padding=0);A.markdown=A.bi(B);C=Window(content=A.markdown.content,height=A.markdown.height,dont_extend_height=_B,style='class:desc');D=Frame(body=C);A.container=HSplit([C,A.list_container]);A.kb=KeyBindings();A.bk()
|
|
23
|
+
def bi(C,content):A=StringIO();B=Console(file=A,width=80,force_terminal=_B,color_system='truecolor');B.print(Align.left(Markdown(content)),justify='left');return Window(content=FormattedTextControl(ANSI(A.getvalue())),height=D(min=1))
|
|
24
|
+
def bm(A):
|
|
25
25
|
E='class:suggestion.selected';B=[]
|
|
26
26
|
for(F,C)in enumerate(A.options):D=F==A.selected_index;G='> 'if D else' ';H=E if D else'class:suggestion.label';I='class:suggestion.desc';J=VSplit([Window(FormattedTextControl([(E,G)]),width=2),Window(FormattedTextControl([(H,C[_A])]),width=60),Window(FormattedTextControl([(I,C[_C])]),wrap_lines=_B,dont_extend_width=False,always_hide_cursor=_B)],height=1);B.append(J)
|
|
27
27
|
return B
|
|
28
|
-
def
|
|
29
|
-
def
|
|
28
|
+
def bl(A):A.rows=A.bm();A.list_container.children=A.rows;get_app().invalidate()
|
|
29
|
+
def bk(A):
|
|
30
30
|
@A.kb.add('up')
|
|
31
31
|
def B(event):
|
|
32
|
-
if A.selected_index>0:A.selected_index-=1;A.
|
|
32
|
+
if A.selected_index>0:A.selected_index-=1;A.bl()
|
|
33
33
|
@A.kb.add('down')
|
|
34
34
|
def C(event):
|
|
35
|
-
if A.selected_index<len(A.options)-1:A.selected_index+=1;A.
|
|
35
|
+
if A.selected_index<len(A.options)-1:A.selected_index+=1;A.bl()
|
|
36
36
|
@A.kb.add('enter')
|
|
37
37
|
def D(event):A.callback(A.selected_index)
|
|
38
38
|
async def demo():
|
|
@@ -1,58 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
_C=True
|
|
2
|
+
_B=None
|
|
3
|
+
_A=False
|
|
4
|
+
from typing import List,Optional,Tuple
|
|
5
|
+
import pyperclip
|
|
6
|
+
from prompt_toolkit.formatted_text import to_formatted_text,fragment_list_to_text
|
|
7
|
+
from prompt_toolkit.mouse_events import MouseEvent,MouseEventType,MouseButton
|
|
4
8
|
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
5
|
-
from prompt_toolkit.
|
|
6
|
-
from prompt_toolkit.
|
|
7
|
-
from prompt_toolkit.formatted_text import ANSI
|
|
9
|
+
from prompt_toolkit.application.current import get_app
|
|
10
|
+
from prompt_toolkit.formatted_text.ansi import ANSI
|
|
8
11
|
class ScrollableFormattedLogControl(FormattedTextControl):
|
|
9
|
-
def __init__(A):A.lines=[];A.scroll_offset=0;A._height=0;A.last_count=0;super().__init__(A.
|
|
12
|
+
def __init__(A):A.lines=[];A.scroll_offset=0;A._height=0;A.last_count=0;A._selecting=_A;A._selection_start=_B;A._selection_end=_B;super().__init__(A.br,focusable=_C,show_cursor=_A)
|
|
10
13
|
def clear(A):A.lines=[];A.scroll_offset=0;A.last_count=0;A._height=0
|
|
11
|
-
def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.
|
|
14
|
+
def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.bs()
|
|
12
15
|
def update_last(A,ansi_text):
|
|
13
16
|
B=ansi_text.splitlines()
|
|
14
17
|
if A.lines:A.lines=A.lines[:-A.last_count]+B
|
|
15
18
|
else:A.lines=B
|
|
16
|
-
A.last_count=len(B);A.
|
|
19
|
+
A.last_count=len(B);A.bs()
|
|
17
20
|
def refresh_scroll(A):
|
|
18
21
|
if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
19
|
-
def
|
|
22
|
+
def bs(A):
|
|
20
23
|
if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
21
|
-
def
|
|
24
|
+
def bq(A,amount):
|
|
22
25
|
if A._height:B=max(0,len(A.lines)-A._height);A.scroll_offset=max(0,min(A.scroll_offset+amount,B))
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
def br(A):
|
|
27
|
+
R=A._height or 100;S=A.lines[A.scroll_offset:A.scroll_offset+R];C=[]
|
|
28
|
+
for(T,U)in enumerate(S):
|
|
29
|
+
L=A.scroll_offset+T;M=ANSI(U).__pt_formatted_text__()
|
|
30
|
+
if A._selection_start and A._selection_end:
|
|
31
|
+
D,G=A._selection_start;E,H=A._selection_end
|
|
32
|
+
if(D,G)>(E,H):D,G,E,H=E,H,D,G
|
|
33
|
+
if D<=L<=E:
|
|
34
|
+
P=fragment_list_to_text(M);I=G if L==D else 0;J=H if L==E else len(P);I,J=max(0,I),min(len(P),J);F=[];K=0
|
|
35
|
+
for(N,B)in M:
|
|
36
|
+
Q=len(B)
|
|
37
|
+
if K+Q<=I:F.append((N,B))
|
|
38
|
+
elif K>=J:F.append((N,B))
|
|
39
|
+
else:
|
|
40
|
+
for O in range(len(B)):
|
|
41
|
+
V=K+O
|
|
42
|
+
if I<=V<J:F.append(('class:selection',B[O]))
|
|
43
|
+
else:F.append((N,B[O]))
|
|
44
|
+
K+=Q
|
|
45
|
+
C.extend(F);C.append(('','\n'));continue
|
|
46
|
+
C.extend(M);C.append(('','\n'))
|
|
47
|
+
return C
|
|
28
48
|
def create_content(B,width,height):A=height;B._height=A or 100;return super().create_content(width,A)
|
|
29
49
|
def mouse_handler(A,mouse_event):
|
|
30
|
-
B=mouse_event;C=
|
|
31
|
-
if B.event_type==MouseEventType.SCROLL_UP:A.
|
|
32
|
-
elif B.event_type==MouseEventType.SCROLL_DOWN:A.
|
|
50
|
+
B=mouse_event;E=B.position;C,D=E.y,E.x
|
|
51
|
+
if B.event_type==MouseEventType.SCROLL_UP:A.bq(-1);return
|
|
52
|
+
elif B.event_type==MouseEventType.SCROLL_DOWN:A.bq(1);return
|
|
53
|
+
elif B.event_type==MouseEventType.MOUSE_DOWN and B.button==MouseButton.LEFT:A._selecting=_C;A._selection_start=A.scroll_offset+C,D;A._selection_end=A._selection_start;get_app().invalidate();return
|
|
54
|
+
elif B.event_type==MouseEventType.MOUSE_MOVE and A._selecting:A._selection_end=A.scroll_offset+C,D;get_app().invalidate();return
|
|
55
|
+
elif B.event_type==MouseEventType.MOUSE_UP and A._selecting:A._selection_end=A.scroll_offset+C,D;A._selecting=_A;get_app().invalidate();return
|
|
33
56
|
return NotImplemented
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def
|
|
57
|
+
def copy_selection_to_clipboard(A):
|
|
58
|
+
if A._selection_start and A._selection_end:
|
|
59
|
+
E,B=A._selection_start;F,C=A._selection_end
|
|
60
|
+
if(E,B)>(F,C):E,B,F,C=F,C,E,B
|
|
61
|
+
I=A.lines[E:F+1];D=[]
|
|
62
|
+
for(G,J)in enumerate(I):
|
|
63
|
+
K=ANSI(J).__pt_formatted_text__();H=fragment_list_to_text(K)
|
|
64
|
+
if G==0 and G==len(I)-1:D.append(H[B:C])
|
|
65
|
+
elif G==0:D.append(H[B:])
|
|
66
|
+
elif G==len(I)-1:D.append(H[:C])
|
|
67
|
+
else:D.append(H)
|
|
68
|
+
pyperclip.copy('\n'.join(D));A._selection_start=_B;A._selection_end=_B;get_app().invalidate()
|
|
69
|
+
from prompt_toolkit.widgets import Frame
|
|
70
|
+
from prompt_toolkit.layout import Layout,HSplit,Window
|
|
71
|
+
from prompt_toolkit.styles import Style
|
|
72
|
+
from prompt_toolkit.application import Application
|
|
73
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
74
|
+
log_control=ScrollableFormattedLogControl()
|
|
75
|
+
log_window=Window(content=log_control,wrap_lines=_A)
|
|
76
|
+
frame=log_window
|
|
77
|
+
kb=KeyBindings()
|
|
78
|
+
@kb.add('c-c')
|
|
79
|
+
def bt(event):log_control.copy_selection_to_clipboard()
|
|
80
|
+
@kb.add('q')
|
|
81
|
+
def bt(event):event.app.exit()
|
|
82
|
+
style=Style.from_dict({'frame.border':'#888888','frame.title':'bold','log':'#ffffff','selection':'reverse'})
|
|
83
|
+
layout=Layout(HSplit([frame]))
|
|
84
|
+
app=Application(layout=layout,key_bindings=kb,mouse_support=_C,full_screen=_C,style=style)
|
|
85
|
+
if __name__=='__main__':
|
|
86
|
+
for i in range(50):log_control.append_text(f"[32m[INFO][0m Line {i} - This is a sample log message.")
|
|
87
|
+
app.run()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import re
|
|
3
|
+
from prompt_toolkit.mouse_events import MouseEvent,MouseEventType
|
|
4
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
5
|
+
from prompt_toolkit.layout.controls import UIControl,UIContent
|
|
6
|
+
from prompt_toolkit.data_structures import Point
|
|
7
|
+
from prompt_toolkit.formatted_text import ANSI
|
|
8
|
+
class ScrollableFormattedLogControl(FormattedTextControl):
|
|
9
|
+
def __init__(A):A.lines=[];A.scroll_offset=0;A._height=0;A.last_count=0;super().__init__(A.bg,focusable=True,show_cursor=False)
|
|
10
|
+
def clear(A):A.lines=[];A.scroll_offset=0;A.last_count=0;A._height=0
|
|
11
|
+
def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.bh()
|
|
12
|
+
def update_last(A,ansi_text):
|
|
13
|
+
B=ansi_text.splitlines()
|
|
14
|
+
if A.lines:A.lines=A.lines[:-A.last_count]+B
|
|
15
|
+
else:A.lines=B
|
|
16
|
+
A.last_count=len(B);A.bh()
|
|
17
|
+
def refresh_scroll(A):
|
|
18
|
+
if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
19
|
+
def bh(A):
|
|
20
|
+
if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
21
|
+
def bf(A,amount):
|
|
22
|
+
if A._height:B=max(0,len(A.lines)-A._height);A.scroll_offset=max(0,min(A.scroll_offset+amount,B))
|
|
23
|
+
def is_focusable(A):return True
|
|
24
|
+
def bg(A):
|
|
25
|
+
C=A._height or 100;D=A.lines[A.scroll_offset:A.scroll_offset+C];B=[]
|
|
26
|
+
for E in D:F=sanitize_ansi_text(E);B.extend(ANSI(F).__pt_formatted_text__());B.append(('','\n'))
|
|
27
|
+
return B
|
|
28
|
+
def create_content(B,width,height):A=height;B._height=A or 100;return super().create_content(width,A)
|
|
29
|
+
def mouse_handler(A,mouse_event):
|
|
30
|
+
B=mouse_event;C=5
|
|
31
|
+
if B.event_type==MouseEventType.SCROLL_UP:A.bf(-C);return
|
|
32
|
+
elif B.event_type==MouseEventType.SCROLL_DOWN:A.bf(C);return
|
|
33
|
+
return NotImplemented
|
|
34
|
+
class ScrollableLogControl(UIControl):
|
|
35
|
+
def __init__(A):A.lines=[];A.scroll_offset=0;A.visible_lines=[];A._height=0;A.last_count=0
|
|
36
|
+
def clear(A):A.visible_lines=[];A.lines=[];A.scroll_offset=0;A.last_count=0;A._height=0
|
|
37
|
+
def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.bh()
|
|
38
|
+
def update_last(A,ansi_text):
|
|
39
|
+
B=ansi_text.splitlines()
|
|
40
|
+
if A.lines:A.lines=A.lines[:-A.last_count]+B
|
|
41
|
+
else:A.lines=B
|
|
42
|
+
A.last_count=len(B);A.bh()
|
|
43
|
+
def refresh_scroll(A):
|
|
44
|
+
if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
45
|
+
def bh(A):A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
46
|
+
def bf(A,amount):A.scroll_offset=max(0,min(A.scroll_offset+amount,max(0,len(A.lines)-A._height)))
|
|
47
|
+
def is_focusable(A):return True
|
|
48
|
+
def create_content(A,width,height):B=height;A._height=B;A.visible_lines=A.lines[A.scroll_offset:A.scroll_offset+B];return UIContent(get_line=A.get_line,line_count=len(A.visible_lines),cursor_position=Point(0,len(A.visible_lines)-1))
|
|
49
|
+
def get_line(B,lineno):
|
|
50
|
+
A=lineno
|
|
51
|
+
if A<0 or A>=len(B.visible_lines):return[]
|
|
52
|
+
C=sanitize_ansi_text(B.visible_lines[A]);return ANSI(C).__pt_formatted_text__()
|
|
53
|
+
def mouse_handler(A,mouse_event):
|
|
54
|
+
B=mouse_event
|
|
55
|
+
if B.event_type==MouseEventType.SCROLL_UP:A.bf(-1);return
|
|
56
|
+
elif B.event_type==MouseEventType.SCROLL_DOWN:A.bf(1);return
|
|
57
|
+
return NotImplemented
|
|
58
|
+
def sanitize_ansi_text(text):return re.sub('[¹²³⁰⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉]',' ',text)
|