@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.
Files changed (58) hide show
  1. package/dist/python-src/.env +1 -1
  2. package/dist/python-src/README.md +48 -1
  3. package/dist/python-src/_workspace/.autodev/config.json +12 -0
  4. package/dist/python-src/_workspace/.autodev/cron/jobs.json +4 -0
  5. package/dist/python-src/entry.py +21 -1
  6. package/dist/python-src/main.py +763 -42
  7. package/dist/python-src/pyproject.toml +1 -0
  8. package/dist/python-src/run.sh +9 -0
  9. package/dist/python-src/src/agents/token_tracker.py +4 -4
  10. package/dist/python-src/src/claw/bus/queue.py +1 -1
  11. package/dist/python-src/src/claw/channels/__init__.py +2 -2
  12. package/dist/python-src/src/claw/channels/base.py +2 -2
  13. package/dist/python-src/src/claw/channels/feishu.py +57 -16
  14. package/dist/python-src/src/claw/channels/manager.py +2 -2
  15. package/dist/python-src/src/claw/config/__init__.py +3 -0
  16. package/dist/python-src/src/claw/config/loader.py +38 -0
  17. package/dist/python-src/src/claw/config/schema.py +14 -29
  18. package/dist/python-src/src/claw/cron/__init__.py +3 -0
  19. package/dist/python-src/src/claw/cron/service.py +171 -0
  20. package/dist/python-src/src/claw/cron/types_.py +14 -0
  21. package/dist/python-src/src/claw/heartbeat/__init__.py +2 -0
  22. package/dist/python-src/src/claw/heartbeat/service.py +55 -0
  23. package/dist/python-src/src/claw/run.py +82 -0
  24. package/dist/python-src/src/claw/tools/base.py +23 -0
  25. package/dist/python-src/src/claw/tools/channel.py +0 -0
  26. package/dist/python-src/src/claw/tools/cron.py +138 -0
  27. package/dist/python-src/src/claw/utils/__init__.py +2 -0
  28. package/dist/python-src/src/claw/utils/helpers.py +27 -0
  29. package/dist/python-src/src/core/context.py +158 -0
  30. package/dist/python-src/src/managers/manager_agent.py +9 -9
  31. package/dist/python-src/src/managers/manager_command.py +62 -0
  32. package/dist/python-src/src/managers/manager_context.py +1 -1
  33. package/dist/python-src/src/managers/manager_instruction.py +7 -7
  34. package/dist/python-src/src/managers/manager_skill.py +3 -3
  35. package/dist/python-src/src/managers/sandbox.py +3 -3
  36. package/dist/python-src/src/middlewares/dynamic_content.py +2 -2
  37. package/dist/python-src/src/middlewares/hitl.py +3 -3
  38. package/dist/python-src/src/middlewares/memory.py +2 -2
  39. package/dist/python-src/src/middlewares/subagents.py +4 -4
  40. package/dist/python-src/src/middlewares/summary.py +37 -37
  41. package/dist/python-src/src/stream/file_write_parser.py +3 -3
  42. package/dist/python-src/src/stream/formatter.py +19 -19
  43. package/dist/python-src/src/stream/handler.py +4 -4
  44. package/dist/python-src/src/stream/handler_with_tracker.py +10 -10
  45. package/dist/python-src/src/trackers/token/pricing.py +2 -2
  46. package/dist/python-src/src/trackers/token/report.py +4 -4
  47. package/dist/python-src/src/trackers/token/tracker.py +8 -8
  48. package/dist/python-src/src/tui/chatui.py +10 -10
  49. package/dist/python-src/src/tui/clawtui.py +230 -0
  50. package/dist/python-src/src/tui/commands/__init__.py +3 -0
  51. package/dist/python-src/src/tui/commands/base.py +6 -0
  52. package/dist/python-src/src/tui/commands/instruction.py +5 -0
  53. package/dist/python-src/src/tui/components/tlist.py +7 -7
  54. package/dist/python-src/src/tui/components/tscroll_panel.py +73 -44
  55. package/dist/python-src/src/tui/components/tscroll_panel_old.py +58 -0
  56. package/dist/python-src/src/tui/utils/trender.py +21 -21
  57. package/dist/python-src/uv.lock +1969 -1958
  58. 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.db();A._task=asyncio.create_task(A._writer_loop())
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 db(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)')
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.cz(A);A.clear()
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.cz(A);A.clear()
46
- def cz(D,records):
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 da(A):
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.da(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.da(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}
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
- d=load_dotenv(find_dotenv())
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(d);A.kb=KeyBindings();A.b();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 e(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 c(A,workspace=_A,mcp_status=_A,sandbox_status=_A,model_status=_A):
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 b(A):
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.a('● user',f"● {B}",style='light_salmon3');await asyncio.sleep(.05)
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.a('● bot',K,style=D,markdown=_B);return
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.e(spinner='',status='等待输入',tokens=A.token_count);A.app.layout.focus(A.input_box);return L
93
- def a(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()
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.e(spinner=s,status='正在生成 ...',tokens=D.token_count)
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,3 @@
1
+ from src.tui.commands.instruction import InstructionCommand
2
+ __all__=['InstructionCommand']
3
+ commands=[]
@@ -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.q();A.list_container=HSplit(A.rows,padding=0);A.markdown=A.t(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.s()
23
- def t(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 q(A):
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 u(A):A.rows=A.q();A.list_container.children=A.rows;get_app().invalidate()
29
- def s(A):
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.u()
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.u()
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
- from typing import List
2
- import re
3
- from prompt_toolkit.mouse_events import MouseEvent,MouseEventType
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.layout.controls import UIControl,UIContent
6
- from prompt_toolkit.data_structures import Point
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.x,focusable=True,show_cursor=False)
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.z()
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.z()
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 z(A):
22
+ def bs(A):
20
23
  if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
21
- def y(A,amount):
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 is_focusable(A):return True
24
- def x(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
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=5
31
- if B.event_type==MouseEventType.SCROLL_UP:A.y(-C);return
32
- elif B.event_type==MouseEventType.SCROLL_DOWN:A.y(C);return
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
- 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.z()
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.z()
43
- def refresh_scroll(A):
44
- if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
45
- def z(A):A.scroll_offset=max(0,len(A.lines)-A._height)
46
- def y(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.y(-1);return
56
- elif B.event_type==MouseEventType.SCROLL_DOWN:A.y(1);return
57
- return NotImplemented
58
- def sanitize_ansi_text(text):return re.sub('[¹²³⁰⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉]',' ',text)
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"[INFO] 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)