@brandon_9527/tcode 1.0.7 → 1.0.9

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 (59) hide show
  1. package/dist/python-src/.env +5 -3
  2. package/dist/python-src/README.md +40 -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 +35 -1
  6. package/dist/python-src/main.py +753 -40
  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/core/deepagents.py +5 -5
  31. package/dist/python-src/src/managers/manager_agent.py +9 -9
  32. package/dist/python-src/src/managers/manager_command.py +62 -0
  33. package/dist/python-src/src/managers/manager_context.py +1 -1
  34. package/dist/python-src/src/managers/manager_instruction.py +7 -7
  35. package/dist/python-src/src/managers/manager_skill.py +3 -3
  36. package/dist/python-src/src/managers/sandbox.py +3 -3
  37. package/dist/python-src/src/middlewares/dynamic_content.py +2 -2
  38. package/dist/python-src/src/middlewares/hitl.py +3 -3
  39. package/dist/python-src/src/middlewares/memory.py +2 -2
  40. package/dist/python-src/src/middlewares/subagents.py +4 -4
  41. package/dist/python-src/src/middlewares/summary.py +37 -37
  42. package/dist/python-src/src/stream/file_write_parser.py +3 -3
  43. package/dist/python-src/src/stream/formatter.py +19 -19
  44. package/dist/python-src/src/stream/handler.py +4 -4
  45. package/dist/python-src/src/stream/handler_with_tracker.py +10 -10
  46. package/dist/python-src/src/trackers/token/pricing.py +2 -2
  47. package/dist/python-src/src/trackers/token/report.py +4 -4
  48. package/dist/python-src/src/trackers/token/tracker.py +8 -8
  49. package/dist/python-src/src/tui/chatui.py +10 -10
  50. package/dist/python-src/src/tui/clawtui.py +224 -0
  51. package/dist/python-src/src/tui/commands/__init__.py +3 -0
  52. package/dist/python-src/src/tui/commands/base.py +6 -0
  53. package/dist/python-src/src/tui/commands/instruction.py +5 -0
  54. package/dist/python-src/src/tui/components/tlist.py +7 -7
  55. package/dist/python-src/src/tui/components/tscroll_panel.py +73 -44
  56. package/dist/python-src/src/tui/components/tscroll_panel_old.py +58 -0
  57. package/dist/python-src/src/tui/utils/trender.py +21 -21
  58. package/dist/python-src/uv.lock +1969 -1958
  59. 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.dv();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 dv(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.da(A);A.clear()
41
+ if A:B.dt(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.da(A);A.clear()
46
- def da(D,records):
45
+ if len(A)>=B.batch_size:B.dt(A);A.clear()
46
+ def dt(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 cz(A):
58
+ def du(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.cz(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.cz(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.du(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.du(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
- a=load_dotenv(find_dotenv())
45
+ u=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(a);A.kb=KeyBindings();A.e();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 d(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 b(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(u);A.kb=KeyBindings();A.s();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 r(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 q(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 e(A):
62
+ def s(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.c('● user',f"● {B}",style='light_salmon3');await asyncio.sleep(.05)
81
+ A.spinner.start();A.t('● 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.c('● bot',K,style=D,markdown=_B);return
85
+ K='\n'.join(E);A.t('● 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.d(spinner='',status='等待输入',tokens=A.token_count);A.app.layout.focus(A.input_box);return L
93
- def c(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.r(spinner='',status='等待输入',tokens=A.token_count);A.app.layout.focus(A.input_box);return L
93
+ def t(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.d(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.r(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,224 @@
1
+ _M='custom'
2
+ _L='updates'
3
+ _K='thread_id'
4
+ _J='class:status'
5
+ _I='messages'
6
+ _H='configurable'
7
+ _G='class:footer'
8
+ _F='auto'
9
+ _E='label'
10
+ _D='description'
11
+ _C=False
12
+ _B=True
13
+ _A=None
14
+ from functools import partial
15
+ from pathlib import Path
16
+ from typing import List
17
+ import traceback,asyncio,json,sys,os,re
18
+ from prompt_toolkit.layout import Layout,HSplit,Window,VSplit,DynamicContainer
19
+ from prompt_toolkit.completion import Completer,Completion,PathCompleter
20
+ from prompt_toolkit.layout.controls import FormattedTextControl
21
+ from prompt_toolkit.widgets import TextArea,Label,Frame
22
+ from prompt_toolkit.application.current import get_app
23
+ from prompt_toolkit.key_binding import KeyBindings
24
+ from prompt_toolkit.layout.dimension import D
25
+ from prompt_toolkit.document import Document
26
+ from prompt_toolkit.styles import Style
27
+ from prompt_toolkit import Application
28
+ from rich.markdown import Markdown
29
+ from rich.columns import Columns
30
+ from rich.console import Group
31
+ from rich.markup import render
32
+ from rich.panel import Panel
33
+ from rich.text import Text
34
+ from rich.tree import Tree
35
+ from rich.box import Box
36
+ from src.claw.bus.queue import MessageBus,InboundMessage,OutboundMessage
37
+ from src.claw.cron.service import CronService
38
+ from src.tui.components.tscroll_panel import ScrollableFormattedLogControl
39
+ from src.tui.components.tlist import InterruptSelector
40
+ from src.stream.handler import astream_handler
41
+ from src.tui.config import LOGO,COMMANDS,USERS,COMMAND_META
42
+ from src.tui.components.live_spinner import Spinner
43
+ from src.tui.utils.render import markdown_to_wrapped_text,extract_msg_info,render_panel,render_info
44
+ from src.tui.utils.trender import display_tool_call,display_tool_result,display_tool_error
45
+ from src.claw.tools.base import AgentContext
46
+ from src.managers.manager_command import CommandManager
47
+ from src.managers.manager_agent import AgentManager
48
+ from langchain_core.messages import HumanMessage
49
+ from langgraph.types import Command
50
+ from dotenv import find_dotenv,load_dotenv
51
+ p=load_dotenv(find_dotenv())
52
+ class CommandCompleter(Completer):
53
+ def __init__(A,commands,agents):A.path_completer=PathCompleter(expanduser=_B);A.commands=commands;A.agents=agents
54
+ def get_completions(G,document,complete_event):
55
+ D=document;M=D.text_before_cursor;A=D.get_word_before_cursor(WORD=_B)
56
+ if D.text.startswith('@file '):
57
+ E=A
58
+ if E.startswith('~/'):B=Path(os.path.expanduser(E)).expanduser()
59
+ else:B=Path(E).expanduser()
60
+ H=B.parent if B.name else B;I=B.name
61
+ try:
62
+ for F in H.iterdir():
63
+ C=F.name
64
+ 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')
65
+ except Exception:pass
66
+ elif A.startswith('/'):
67
+ for K in G.commands:yield Completion(f"{K}",start_position=-len(A))
68
+ elif A.startswith('@'):
69
+ for L in G.agents:yield Completion(f"{L}",start_position=-len(A))
70
+ class LiveChatUI:
71
+ 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 退出)')]);J=FormattedTextControl(text=[('class:spinner',f"{A.spinner.current_frame()}")],show_cursor=_C);K=FormattedTextControl(text=[(_G,f"mode: {A.mode}")]);A.status_bar=VSplit([Window(J,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(K,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(p);A.kb=KeyBindings();A.n();L=Frame(body=A.input_box,style='class:frame');A.input_items=[A.status_bar,L];A.input_area=HSplit([*A.input_items],padding=0);A.interact_items=[];A.interact_area=HSplit([*A.interact_items],padding=1);M=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: {M} ")]),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'
72
+ def l(A,role='user',spinner='●',status='',tokens=0):A.status_label.text=[(_J,f" 状态: {status} | ({role}) | Tokens: {tokens} (esc + ⏎ 换行 按两次 esc 中断 ctrl + c 退出)")];A.app.invalidate()
73
+ def k(A,workspace=_A,mcp_status=_A,sandbox_status=_A,model_status=_A):
74
+ F=model_status;E=sandbox_status;D=mcp_status;C=workspace;B=_G
75
+ if C is not _A:A._footer_workspace.text=[(B,f"{C}(main) ")]
76
+ if D is not _A:A._footer_context.text=[(B,f"{D} ")]
77
+ if E is not _A:A._footer_env.text=[(B,f"Env: {E} ")]
78
+ if F is not _A:A._footer_model.text=[(B,f"{F}")]
79
+ A.app.invalidate()
80
+ def update_input_area_height(A,_):
81
+ C=A.input_box.buffer.text.count('\n')+1;B=min(C,A.max_input_lines)
82
+ if B!=A.input_box.window.height:A.input_box.window.height=B
83
+ def get_suggestions(B,text):
84
+ A=text.strip().split()[-1]if text.strip()else''
85
+ def C(word,lst):
86
+ A=lst
87
+ if word in A:E=A.index(word);B=max(0,E-4);C=min(len(A),B+5)
88
+ else:B,C=0,5
89
+ D=A[B:C];return D+['']*(5-len(D))
90
+ if A.startswith('/'):return C(A,B.COMMANDS)
91
+ elif A.startswith('@'):D=B.AGENTS;return C(A,D)
92
+ return[]
93
+ def update_suggestions(A):
94
+ C=A.input_box.text;D=A.get_suggestions(C);A.suggestions=[]
95
+ if D:
96
+ for(E,B)in enumerate(D):
97
+ A.suggestions.append({_E:B,_D:A.COMMAND_META.get(B,'')})
98
+ if B==C:A.selected_index=E
99
+ A.suggestions_box.children=A.m()
100
+ if A.suggestions_box not in A.suggest_area.children:A.suggest_area.children.insert(0,A.suggestions_box)
101
+ else:A.clear_suggestions()
102
+ A.app.invalidate()
103
+ def clear_suggestions(A):
104
+ A.suggestions=[];A.selected_index=0;A.suggestions_box.children=[]
105
+ if A.suggestions_box in A.suggest_area.children:A.suggest_area.children.remove(A.suggestions_box)
106
+ A.app.invalidate()
107
+ def m(A):
108
+ F='reverse';C=[];G=len(A.suggestions)
109
+ if not G:return[]
110
+ H=0;I=len(A.suggestions)
111
+ 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)
112
+ return C
113
+ 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)
114
+ async def updater(A):await A.spinner.run(A.app)
115
+ async def run_async(A):await asyncio.gather(A.app.run_async(),A.updater())
116
+ def n(A):
117
+ D='enter';C='escape'
118
+ @A.kb.add('@')
119
+ def B(event):A=event;A.app.current_buffer.insert_text('@');A.app.current_buffer.start_completion(select_first=_B)
120
+ @A.kb.add('/')
121
+ def B(event):A=event;A.app.current_buffer.insert_text('/');A.app.current_buffer.start_completion(select_first=_B)
122
+ @A.kb.add(D)
123
+ def B(event):
124
+ 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()
125
+ else:
126
+ C=A.input_box.text.strip()
127
+ if not C:return
128
+ A.input_box.text='';asyncio.ensure_future(A._handle_submit(C))
129
+ @A.kb.add(C,D)
130
+ def B(event):A.input_box.buffer.insert_text('\n')
131
+ @A.kb.add('c-c')
132
+ def B(event):
133
+ if A.log_control._selection_start and A.log_control._selection_end:A.log_control.copy_selection_to_clipboard()
134
+ else:event.app.exit();sys.exit(0)
135
+ @A.kb.add(C,C)
136
+ def B(event):
137
+ if A.cancel_event:A.cancel_event.set()
138
+ async def alisten(A):
139
+ B=A.kwargs.get('bus',_A);C=A.kwargs.get('cron',_A);D=A.kwargs.get('heartbeat',_A);E=A.kwargs.get('channels',_A);B.subscribe_inbound('feishu',partial(A._process_message,bus=B,cron=C))
140
+ try:await C.start();await D.start();await asyncio.gather(B.dispatch_inbound(),E.start_all(),A.app.run_async(),A.updater())
141
+ except KeyboardInterrupt:print('\nShutting down ...');D.stop();await C.stop();await E.stop_call();B.stop()
142
+ 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)
143
+ async def _handle_submit(A,text,context=_A):
144
+ D='light_steel_blue';C=context;B=text
145
+ 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()
146
+ if B.strip()in['quit','exit','q']:get_app().exit();return
147
+ if B.strip()in['/clear','clear']:A.clear();return
148
+ A.spinner.start();A.o('● user',f"● {B}",style='light_salmon3');await asyncio.sleep(.05)
149
+ if B.strip()in['/commands']and A.instruction_manager:
150
+ E=[]
151
+ for F in A.instruction_manager.list_instructions():E.append(f"/{F.name}: - {F.settings[_D]}")
152
+ K='\n'.join(E);A.o('● bot',K,style=D,markdown=_B);return
153
+ if A.instruction_manager:G=A.instruction_manager.parse(B);H,I=G['executed_instruction'],G['message'];B=f"""
154
+ [注意]: 执行用户请求必须严格遵循如下准则:
155
+ {H}
156
+
157
+ 用户请求:
158
+ {I}"""if H else I
159
+ 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.l(spinner='',status='等待输入',tokens=A.token_count);A.app.layout.focus(A.input_box);return L
160
+ def o(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()
161
+ async def _stream_generate(A,prompt,context=_A):
162
+ B=A.agent.astream({_I:[HumanMessage(content=prompt)]},config={_H:{_K:A.thread_id}},stream_mode=[_I,_L,_M],context=context);A.cancel_event=asyncio.Event()
163
+ async for C in astream_handler(B,interrupt_tools=A.interrupt_tools,tool_mode=A.toolcall_mode):
164
+ if A.cancel_event and A.cancel_event.is_set():A.cancel_event.clear();A.cancel_event=_A;return
165
+ yield C
166
+ async def _resume_generate(A,interrupt_id,decisions,context=_A):
167
+ B=A.agent.astream(Command(resume={interrupt_id:{'decisions':decisions}}),config={_H:{_K:A.thread_id}},stream_mode=[_I,_L,_M],context=context);A.cancel_event=asyncio.Event()
168
+ async for C in astream_handler(B,interrupt_tools=A.interrupt_tools,tool_mode=A.toolcall_mode):
169
+ if A.cancel_event and A.cancel_event.is_set():A.cancel_event.clear();A.cancel_event=_A
170
+ yield C
171
+ async def _handle_stream(D,sender,stream,style='green',markdown=_C,items=_A,context=_A):
172
+ 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
173
+ if A is _A:D.log_control.append_text(render_panel(a,'',b,c))
174
+ A=A if A else[]
175
+ async for B in stream:
176
+ if B[I]==e:
177
+ if len(A)>0 and A[-1][1]==e:U+=B[G];J=markdown_to_wrapped_text(U);A[-1][0]=J
178
+ else:U=B[G];J=markdown_to_wrapped_text(U);A.append([J,e,_A,B,[]])
179
+ elif B[I]==f:
180
+ if len(A)>0 and A[-1][1]==f:R+=B[G];J=markdown_to_wrapped_text(f"{R}");A[-1][0]=J
181
+ else:R=B[G];J=markdown_to_wrapped_text(f"{R}");A.append([J,f,_A,B,[]])
182
+ elif B[I]=='token_usage':t=B['input_toks'];u=B['output_toks'];m=B['total_toks'];D.token_count=m
183
+ elif B[I]==Y:
184
+ V=B[P];W=B[Q];H=B['id'];K=B[k]
185
+ if K is _A:F=display_tool_call(V,W,[]);A.append([F,Y,H,B,[]]);A.append([Text('',''),'margin',_A,_A,[]])
186
+ else:
187
+ C=index_(A,lambda x:x[2]==K and x[1]==Y)
188
+ if C is _A:continue
189
+ 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
190
+ elif B[I]==g:
191
+ V=B[P];H=B['id'];K=B[k];X=B[Z]
192
+ if X==Z:
193
+ 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]]
194
+ else:
195
+ C=index_(A,lambda x:x[2]==K)
196
+ if C is _A:continue
197
+ E=A[C];S=E[3][P];T=E[3][Q];M=index_(E[-1],lambda x:x[2]==H)
198
+ if M is _A:continue
199
+ 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
200
+ elif X=='error':
201
+ 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]]
202
+ else:
203
+ C=index_(A,lambda x:x[2]==K)
204
+ if C is _A:continue
205
+ E=A[C];S=E[3][P];T=E[3][Q];M=index_(E[-1],lambda x:x[2]==H)
206
+ if M is _A:continue
207
+ 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
208
+ elif B[I]=='done':0
209
+ elif B[I]=='interrupt':
210
+ D.spinner.stop();i=[];o=B['interrupt_id']
211
+ 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})
212
+ D.spinner.start();await D._handle_stream(a,D._resume_generate(o,i,h),style=b,markdown=c,items=A,context=h);break
213
+ 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.l(spinner=r,status='正在生成 ...',tokens=D.token_count)
214
+ return R
215
+ async def _handle_human_interrupt(A,message,options):
216
+ E=asyncio.get_event_loop();C=E.create_future();D=A.app.key_bindings
217
+ def F(index):
218
+ if B.container in A.interact_items:A.interact_items.remove(B.container);A.interact_area.children=list(A.interact_items);A.app.invalidate()
219
+ A.app.key_bindings=D;A.app.invalidate()
220
+ if not C.done():C.set_result(index)
221
+ 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
222
+ def index_(lst,condition):
223
+ try:return next(A for(A,B)in enumerate(lst)if condition(B))
224
+ 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.p();A.list_container=HSplit(A.rows,padding=0);A.markdown=A.r(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.v()
23
- def r(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 p(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.bk();A.list_container=HSplit(A.rows,padding=0);A.markdown=A.bl(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.bn()
23
+ def bl(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 bk(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 s(A):A.rows=A.p();A.list_container.children=A.rows;get_app().invalidate()
29
- def v(A):
28
+ def bo(A):A.rows=A.bk();A.list_container.children=A.rows;get_app().invalidate()
29
+ def bn(A):
30
30
  @A.kb.add('up')
31
31
  def B(event):
32
- if A.selected_index>0:A.selected_index-=1;A.s()
32
+ if A.selected_index>0:A.selected_index-=1;A.bo()
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.s()
35
+ if A.selected_index<len(A.options)-1:A.selected_index+=1;A.bo()
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.z,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.bq,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.x()
14
+ def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.bt()
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.x()
19
+ A.last_count=len(B);A.bt()
17
20
  def refresh_scroll(A):
18
21
  if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
19
- def x(A):
22
+ def bt(A):
20
23
  if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
21
- def y(A,amount):
24
+ def br(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 z(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 bq(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.br(-1);return
52
+ elif B.event_type==MouseEventType.SCROLL_DOWN:A.br(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.x()
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.x()
43
- def refresh_scroll(A):
44
- if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
45
- def x(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 bs(event):log_control.copy_selection_to_clipboard()
80
+ @kb.add('q')
81
+ def bs(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.bf,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 bg(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 bf(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.bg(-C);return
32
+ elif B.event_type==MouseEventType.SCROLL_DOWN:A.bg(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 bg(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.bg(-1);return
56
+ elif B.event_type==MouseEventType.SCROLL_DOWN:A.bg(1);return
57
+ return NotImplemented
58
+ def sanitize_ansi_text(text):return re.sub('[¹²³⁰⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉]',' ',text)