Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions TMWebDriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def __init__(self, session_id, info, client=None):
@property
def url(self): return self.info.get('url', '')
def is_active(self):
if self.type == 'http' and time.time() - self.connect_at > 60: self.mark_disconnected()
if self.type == 'http' and self.disconnect_at is None and time.time() - self.connect_at > 60:
self.disconnect_at = time.time()
return self.disconnect_at is None
def reconnect(self, client, info):
self.info = info
Expand All @@ -29,7 +30,7 @@ def reconnect(self, client, info):
self.connect_at = time.time()
self.disconnect_at = None
def mark_disconnected(self):
if self.is_active(): print(f"Tab disconnected: {self.url} (Session: {self.id})")
if self.disconnect_at is None: print(f"Tab disconnected: {self.url} (Session: {self.id})")
self.disconnect_at = time.time()


Expand Down Expand Up @@ -150,7 +151,7 @@ def handle(self) -> None:
except Exception as e:
print(f"Error handling message: {e}")
if hasattr(self, 'data'): print(self.data)
def connected(self): (f"New connection from {self.address}")
def connected(self): print(f"New WS connection from {self.address}")
def handle_close(self):
print(f"WS Connection closed: {self.address}")
driver._unregister_client(self)
Expand Down
4 changes: 3 additions & 1 deletion assets/tmwd_cdp_bridge/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ async function handleBatch(msg, sender) {
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
R.push({ ok: true, data: tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })) });
} else if (c.cmd === 'cdp') {
if (!chrome.debugger) { R.push({ ok: false, error: 'debugger API not available in this browser (Firefox)' }); continue; }
const tabId = c.tabId || msg.tabId || sender.tab?.id;
if (attached !== tabId) {
if (attached) { await chrome.debugger.detach({ tabId: attached }); attached = null; }
Expand All @@ -139,6 +140,7 @@ async function handleBatch(msg, sender) {
async function handleCDP(msg, sender) {
const tabId = msg.tabId || sender.tab?.id;
if (!tabId) return { ok: false, error: 'no tabId' };
if (!chrome.debugger) return { ok: false, error: 'debugger API not available in this browser (Firefox)' };
try {
await chrome.debugger.attach({ tabId }, '1.3');
const result = await chrome.debugger.sendCommand({ tabId }, msg.method, msg.params || {});
Expand Down Expand Up @@ -292,7 +294,7 @@ async function handleWsExec(data) {
res = { ok: false, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' }, csp: true };
}
// CDP fallback for CSP-restricted pages
if (res && !res.ok && res.csp) {
if (res && !res.ok && res.csp && chrome.debugger) {
console.log('[TMWD-WS] CDP fallback for tab', tabId);
const wrappedCode = buildCdpScript(data.code);
try {
Expand Down
41 changes: 41 additions & 0 deletions assets/tmwd_cdp_bridge/manifest.chrome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"manifest_version": 3,
"name": "TMWD CDP Bridge",
"version": "2.0",
"description": "Cookie viewer + CDP bridge",
"permissions": [
"cookies",
"tabs",
"activeTab",
"debugger",
"scripting",
"alarms",
"declarativeNetRequest",
"management",
"contentSettings"
],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "background.js",
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["disable_dialogs.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
},
{
"matches": ["<all_urls>"],
"js": ["config.js", "content.js"],
"run_at": "document_idle",
"all_frames": true
}
],
"action": {
"default_popup": "popup.html",
"default_title": "TMWD CDP Bridge"
}
}
43 changes: 43 additions & 0 deletions assets/tmwd_cdp_bridge/manifest.firefox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"manifest_version": 3,
"name": "TMWD CDP Bridge (Firefox)",
"version": "2.0",
"description": "Cookie viewer + Bridge for Firefox",
"permissions": [
"cookies",
"tabs",
"activeTab",
"scripting",
"alarms",
"declarativeNetRequest",
"management"
],
"host_permissions": ["<all_urls>"],
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["disable_dialogs.js"],
"run_at": "document_start",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"js": ["config.js", "content.js"],
"run_at": "document_idle",
"all_frames": true
}
],
"browser_specific_settings": {
"gecko": {
"id": "tmwd-bridge@genericagent.local",
"strict_min_version": "128.0"
}
},
"action": {
"default_popup": "popup.html",
"default_title": "TMWD Bridge"
}
}
26 changes: 16 additions & 10 deletions assets/tmwd_cdp_bridge/manifest.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
{
"manifest_version": 3,
"name": "TMWD CDP Bridge",
"name": "TMWD CDP Bridge (Firefox)",
"version": "2.0",
"description": "Cookie viewer + CDP bridge",
"description": "Cookie viewer + Bridge for Firefox",
"permissions": [
"cookies",
"tabs",
"activeTab",
"debugger",
"scripting",
"alarms",
"declarativeNetRequest",
"management",
"contentSettings"
"management"
],
"host_permissions": ["<all_urls>"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' ws://127.0.0.1:18765 http://127.0.0.1:18765"
},
"background": {
"service_worker": "background.js"
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["disable_dialogs.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
"all_frames": true
},
{
"matches": ["<all_urls>"],
Expand All @@ -33,8 +33,14 @@
"all_frames": true
}
],
"browser_specific_settings": {
"gecko": {
"id": "tmwd-bridge@genericagent.local",
"strict_min_version": "128.0"
}
},
"action": {
"default_popup": "popup.html",
"default_title": "TMWD CDP Bridge"
"default_title": "TMWD Bridge"
}
}
}
71 changes: 71 additions & 0 deletions frontends/todo_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Shared /todo command — persistent user TODO list.

Data file: temp/user_todo.json (list of str, creation time tracked for display)
API: load(), save(), add(text), remove(idx), list_all()
"""

from __future__ import annotations
import json, os, time

_TODO_PATH = os.path.join(os.path.dirname(__file__), "..", "temp", "user_todo.json")


def _path() -> str:
return os.path.abspath(_TODO_PATH)


def load() -> list[dict]:
"""Return [{id, text, created}, …] or empty list."""
p = _path()
if not os.path.isfile(p):
return []
try:
with open(p, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
return data
except (json.JSONDecodeError, OSError):
pass
return []


def save(items: list[dict]) -> None:
p = _path()
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, "w", encoding="utf-8") as f:
json.dump(items, f, ensure_ascii=False, indent=2)


def add(text: str) -> int:
"""Append a new TODO item. Returns its 1-based index."""
items = load()
item = {
"id": int(time.time() * 1000) % 1000000,
"text": text.strip(),
"created": time.strftime("%m-%d %H:%M"),
}
items.append(item)
save(items)
return len(items)


def remove(idx: int) -> str | None:
"""Remove by 0-based index. Returns removed text or None."""
items = load()
if not (0 <= idx < len(items)):
return None
removed = items.pop(idx)
save(items)
return removed["text"]


def list_all() -> list[dict]:
"""Return items with 'text' and 'created'."""
return load()


def format_list(items: list[dict]) -> list[str]:
"""Format items for terminal display, one str per item, 1-based."""
if not items:
return ["(empty)"]
return [f"{i+1}. {it['text']} ({it['created']})" for i, it in enumerate(items)]
39 changes: 39 additions & 0 deletions frontends/tui_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
'cmd.rename.desc': 'Rename the current session',
'cmd.clear.desc': 'Clear display (LLM history untouched)',
'cmd.cost.desc': 'Token usage for the current session',
'cmd.todo.desc': 'Personal TODO list (add / ls / run / del)',
'cmd.verbose.desc': 'Tool-call audit',
'cmd.export.desc': 'Export the last reply (clip/file/all)',
'cmd.stop.desc': 'Abort current task',
Expand Down Expand Up @@ -430,6 +431,7 @@
'cmd.rename.desc': '重命名当前会话',
'cmd.clear.desc': '清空显示(不动 LLM 历史)',
'cmd.cost.desc': '显示当前会话 token 用量',
'cmd.todo.desc': '个人 TODO 列表 (add / ls / run / del)',
'cmd.verbose.desc': '工具调用审计',
'cmd.export.desc': '导出最后回复(剪贴板/文件/日志路径)',
'cmd.stop.desc': '中止当前任务',
Expand Down Expand Up @@ -1821,6 +1823,7 @@ def _cmds() -> list[tuple[str, str, str]]:
('/conductor', _t('cmd.conductor.arg'), _t('cmd.conductor.desc')),
('/scheduler', '', _t('cmd.scheduler.desc')),
('/rewind', _t('cmd.rewind.arg'), _t('cmd.rewind.desc')),
('/todo', 'add|ls|run|del', _t('cmd.todo.desc')),
('/continue', _t('cmd.continue.arg'), _t('cmd.continue.desc')),
('/new', _t('cmd.new.arg'), _t('cmd.new.desc')),
('/rename', _t('cmd.rename.arg'), _t('cmd.rename.desc')),
Expand Down Expand Up @@ -4161,6 +4164,42 @@ def _pick_session(idx: int) -> None:
_do_restore(sess[idx][0])

self._show_menu(_t('continue.title'), options, _pick_session)
elif name == 'todo':
from frontends import todo_cmd as _tc
sub = (arg.split(None, 1)[0] if arg else '').lower()
if sub == 'add':
rest = (arg[len('add'):] if arg.lower().startswith('add') else '').strip()
if not rest:
self.commit([_t('err.todo_add_usage', fallback='Usage: /todo add <text>')]); return
n = _tc.add(rest)
self.commit([f'✓ TODO #{n} added'])
elif sub == 'ls':
items = _tc.list_all()
lines = _tc.format_list(items)
self.commit(lines)
elif sub == 'run':
items = _tc.list_all()
if not items:
self.commit([_DIM + '(TODO list is empty)' + _RST]); return
opts = _tc.format_list(items)
def _run_todo(idx):
text = items[idx]['text']
_tc.remove(idx)
# Submit as normal user message to execute immediately
self._commit_user(text)
self._submit(text, [])
self._show_menu('Select TODO to execute:', opts, _run_todo)
elif sub == 'del':
items = _tc.list_all()
if not items:
self.commit([_DIM + '(TODO list is empty)' + _RST]); return
opts = _tc.format_list(items)
def _del_todo(idx):
removed = _tc.remove(idx)
self.commit([f'🗑 Deleted: {removed}'])
self._show_menu('Select TODO to delete:', opts, _del_todo)
else:
self.commit([_t('err.todo_usage', fallback='Usage: /todo add|ls|run|del')])
elif name == 'clear':
self._reset_session(ag)
self.commit([_DIM + _t('msg.cleared') + _RST])
Expand Down
Loading