Skip to content

Commit 23c24f0

Browse files
Merge pull request #74 from srijanAtGithub/feature/navigator
Feature/navigator
2 parents aa4f74c + 4f53f56 commit 23c24f0

16 files changed

Lines changed: 592 additions & 130 deletions

File tree

Cowork/cowork_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def _load_settings():
5757
log = structlog.get_logger()
5858

5959
BANNER = """
60-
╔═════════ Sicily Cowork v2.7.1 ═══════════╦════════════════ What Sicily Can Do ════════════════╗
60+
╔═════════ Sicily Cowork v2.7.2 ═══════════╦════════════════ What Sicily Can Do ════════════════╗
6161
║ ║ ║
6262
║ ║ Sicily can search, inspect, read, organize, ║
6363
║ Files are sandboxed to this directory. ║ and safely modify the contents of your ║

Navigator/Task_Files/ChatStore.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
ChatStore.py
33
------------
4-
Persistent, tab-scoped conversation history for the Navigator chat.
4+
Persistent conversation history for the Navigator chat, keyed by a
5+
stable session key.
56
67
Replaces the old in-memory SessionStore in navigator_bridge.py. Same
78
public shape (get / set / clear / __len__) so the WebSocket handler
@@ -10,10 +11,18 @@
1011
Chat" summaries, @-mentioned tab content) survive:
1112
1213
- a backend restart (uvicorn reload, machine reboot, crash)
13-
- closing and reopening the exact same tab (Ctrl+Shift+T), since
14-
Chrome's session-restore reassigns a NEW tab id in that case, but
15-
the popup's currentTab.id at load time is whatever Chrome now
16-
reports — see the note on tab-id stability below.
14+
- closing and reopening the exact same tab (Ctrl+Shift+T)
15+
16+
The `tab_id` column name is kept for backward compatibility (schema,
17+
route params, and this class's method signatures all still say
18+
"tab_id"), but the frontend now sends a stable hash of the page URL
19+
here (see api.js:getSessionKey), not Chrome's actual tabId. Chrome
20+
reassigns tabId on every browsing session — including a plain
21+
Ctrl+Shift+T reopen of a just-closed tab — so keying by it made history
22+
for that "same" tab unreachable the moment Chrome handed out a new id.
23+
This class itself needed zero changes for that fix: it always treated
24+
this column as an opaque TEXT key, never cast to int or compared
25+
numerically, so any string works equally well.
1726
1827
Storage lives at: ~/.sicily/Navigator/ChatsData/chats.db
1928
(SICILY_HOME / "Navigator" / "ChatsData" / "chats.db")

Navigator/Task_Files/Edit_Selection.py

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class EditSelectionResponse(BaseModel):
2727
# For Pydantic purposes
2828
class EditResult(BaseModel):
2929
edited_text: str = Field(
30-
description="The final output to return to the user, either a rewritten text or a direct answer to their question."
30+
description="The final output. MUST contain ONLY the rewritten target text. Never include the context before or after."
3131
)
3232

3333

@@ -44,7 +44,7 @@ async def process_edit_selection(req: EditSelectionRequest) -> EditSelectionResp
4444

4545

4646
async def call_edit_model(selected_text: str, instruction: str, action_type: str = "edit", surrounding_context: str = "") -> str:
47-
llm = configuration.navigator_general_llm(EditResult)
47+
llm = configuration.navigator_general_llm(EditResult)
4848

4949
# Branch the persona based on the button clicked
5050
if action_type == "ask":
@@ -59,16 +59,16 @@ async def call_edit_model(selected_text: str, instruction: str, action_type: str
5959
"'Hope this helps!', or 'Shall I do anything else?'). "
6060
"Provide a clean, self-contained final response with absolutely no open-ended transitions."
6161
)
62-
)
62+
)
6363
elif action_type == "rewrite":
6464
system_msg = SystemMessage(
6565
content=(
6666
"You are an automated, programmatic text-replacement engine. "
6767
"Rewrite the user's selected text in a highly professional manner, completely free of jargon. "
6868
"The tone should be polished and appropriate for professional emails or personal documents. "
69-
"CRITICAL CONSTRAINT: Output EXCLUSIVELY the final revised text. "
70-
"Do NOT include any introductions, explanations, pleasantries, meta-commentary, "
71-
"or follow-up questions. Your entire output will be injected directly into the user's document."
69+
"CRITICAL CONSTRAINTS: "
70+
"1. Output EXCLUSIVELY the final revised text. Do NOT include any introductions, explanations, or meta-commentary. "
71+
"2. If surrounding context is provided, use it ONLY to understand the flow and tone. DO NOT rewrite, include, or repeat the surrounding context in your final output. ONLY replace the selected text."
7272
)
7373
)
7474
elif action_type == "summarise":
@@ -80,36 +80,56 @@ async def call_edit_model(selected_text: str, instruction: str, action_type: str
8080
"Do NOT include any introductions like 'Here is the summary', pleasantries, or meta-commentary. "
8181
"Your entire output will be injected directly into the user's document."
8282
)
83-
)
83+
)
8484
else:
8585
system_msg = SystemMessage(
8686
content=(
8787
"You are an automated, programmatic text-replacement engine. "
8888
"Rewrite the user's selected text exactly according to their instruction. "
89-
"CRITICAL CONSTRAINT: Output EXCLUSIVELY the final revised text. "
90-
"Do NOT include any introductions, explanations, pleasantries, meta-commentary, or follow-up questions."
89+
"CRITICAL CONSTRAINTS: "
90+
"1. Output EXCLUSIVELY the final revised text. Do NOT include any introductions, explanations, or meta-commentary. "
91+
"2. If surrounding context is provided, use it ONLY as a background reference. DO NOT rewrite, include, or repeat the surrounding context in your final output. ONLY replace the selected text."
9192
)
9293
)
9394

94-
prompt_text = f"Instruction/Question: {instruction}\n\nSelected Text:\n{selected_text}"
95-
if surrounding_context:
96-
prompt_text += f"\n\nSurrounding Context:\n{surrounding_context}"
95+
prompt_text = f"Instruction: {instruction}\n\n"
96+
97+
# If the selection exists perfectly inside the context, split it apart to remove overlap
98+
if surrounding_context and selected_text in surrounding_context:
99+
before, after = surrounding_context.split(selected_text, 1)
100+
if before.strip():
101+
prompt_text += f"--- CONTEXT BEFORE ---\n{before.strip()}\n\n"
102+
103+
prompt_text += f"--- TEXT TO EDIT (REWRITE ONLY THIS) ---\n{selected_text}\n\n"
104+
105+
if after.strip():
106+
prompt_text += f"--- CONTEXT AFTER ---\n{after.strip()}\n"
107+
108+
# Fallback if the string formatting doesn't perfectly match
109+
elif surrounding_context:
110+
prompt_text += f"--- BACKGROUND CONTEXT ---\n{surrounding_context.strip()}\n\n"
111+
prompt_text += f"--- TEXT TO EDIT (REWRITE ONLY THIS) ---\n{selected_text}\n"
112+
113+
else:
114+
prompt_text += f"--- TEXT TO EDIT (REWRITE ONLY THIS) ---\n{selected_text}\n"
115+
116+
prompt_text += f"--- SELECTED TEXT (ONLY REWRITE THIS) ---\n{selected_text}"
97117

98-
from usage_tracker import record_usage
118+
from usage_tracker import record_usage
99119

100-
edited_text = ""
101-
session_id = f"edit_{uuid.uuid4().hex[:8]}"
120+
edited_text = ""
121+
session_id = f"edit_{uuid.uuid4().hex[:8]}"
102122

103123
try:
104-
# Use astream_events to catch the AIMessage tokens before Pydantic parsing
105-
async for event in llm.astream_events([system_msg, HumanMessage(content=prompt_text)], version="v2"):
124+
# Use astream_events to catch the AIMessage tokens before Pydantic parsing
125+
async for event in llm.astream_events([system_msg, HumanMessage(content=prompt_text)], version="v2"):
106126

107-
# 1. Catch the raw LLM usage stats
108-
if event["event"] == "on_chat_model_end":
109-
output = event.get("data", {}).get("output")
110-
if output and hasattr(output, "usage_metadata") and output.usage_metadata:
111-
usage = output.usage_metadata
112-
model_name = output.response_metadata.get("model_name", "unknown")
127+
# 1. Catch the raw LLM usage stats
128+
if event["event"] == "on_chat_model_end":
129+
output = event.get("data", {}).get("output")
130+
if output and hasattr(output, "usage_metadata") and output.usage_metadata:
131+
usage = output.usage_metadata
132+
model_name = output.response_metadata.get("model_name", "unknown")
113133

114134
try:
115135
record_usage(
@@ -119,22 +139,22 @@ async def call_edit_model(selected_text: str, instruction: str, action_type: str
119139
input_tokens=usage.get("input_tokens", 0),
120140
output_tokens=usage.get("output_tokens", 0),
121141
cached_input_tokens=usage.get("input_token_details", {}).get("cache_read_tokens", 0)
122-
)
142+
)
123143
except Exception as rec_err:
124-
log.warning("record_usage failed for edit", error=str(rec_err))
144+
log.warning("record_usage failed for edit", error=str(rec_err))
125145

126-
# 2. Catch the final structured output
127-
elif event["event"] == "on_chain_end":
128-
data_out = event.get("data", {}).get("output")
129-
if isinstance(data_out, EditResult):
130-
edited_text = data_out.edited_text
146+
# 2. Catch the final structured output
147+
elif event["event"] == "on_chain_end":
148+
data_out = event.get("data", {}).get("output")
149+
if isinstance(data_out, EditResult):
150+
edited_text = data_out.edited_text
131151

132152
except Exception as e:
133-
log.warning("Failed during edit event stream tracking", error=str(e))
153+
log.warning("Failed during edit event stream tracking", error=str(e))
134154

135-
# Fallback in case the event stream didn't resolve the text correctly
136-
if not edited_text:
137-
response = await llm.ainvoke([system_msg, HumanMessage(content=prompt_text)])
138-
edited_text = response.edited_text
155+
# Fallback in case the event stream didn't resolve the text correctly
156+
if not edited_text:
157+
response = await llm.ainvoke([system_msg, HumanMessage(content=prompt_text)])
158+
edited_text = response.edited_text
139159

140160
return edited_text

Navigator/extension/background.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
8383
return true; // Important: keeps the channel open for async response
8484
});
8585

86-
chrome.tabs.onRemoved.addListener((tabId) => {
87-
// Best-effort: if the backend isn't running, there's nothing to clean up
88-
// anyway (in-memory sessions die with the server), so just log and move on.
89-
fetch(`http://${BACKEND_HOST}/session/${tabId}`, { method: "DELETE" }).catch((err) => {
90-
console.log("[Sicily Navigator] couldn't clear session for closed tab", tabId, err);
91-
});
92-
});
86+
// NOTE: we deliberately do NOT delete the backend session when a tab
87+
// closes. Sessions are keyed by a hash of the page URL (see
88+
// api.js:getSessionKey), not by Chrome's tabId — tabId is reassigned by
89+
// Chrome on every browsing session, so it can't identify "the same tab"
90+
// across a close/reopen anyway. Deleting on close used to silently wipe
91+
// history the moment a tab was closed, including the extremely common
92+
// "accidentally closed it, Ctrl+Shift+T to bring it right back" case.
93+
// Clearing history is now only ever the user's explicit "Clear" button.

0 commit comments

Comments
 (0)