@@ -1786,21 +1786,50 @@ async def _agentic_callback(
17861786 # Task list commands
17871787 # ------------------------------------------------------------------
17881788
1789+ @staticmethod
1790+ def _parse_task_flags (text : str ) -> tuple :
1791+ """Strip --model and --effort flags from *text*, return (model, effort, clean_text).
1792+
1793+ Accepted forms (case-insensitive, anywhere in the string):
1794+ --model opus --model sonnet --model haiku
1795+ --effort low --effort medium --effort high --effort max
1796+ """
1797+ import re as _re
1798+
1799+ _VALID_MODELS = {"opus" , "sonnet" , "haiku" }
1800+ _VALID_EFFORTS = {"low" , "medium" , "high" , "max" }
1801+
1802+ model : Optional [str ] = None
1803+ effort : Optional [str ] = None
1804+
1805+ def _extract (pattern : str , valid : set , src : str ) -> tuple :
1806+ m = _re .search (pattern , src , _re .IGNORECASE )
1807+ if m :
1808+ val = m .group (1 ).lower ()
1809+ if val in valid :
1810+ return val , src [: m .start ()].rstrip () + " " + src [m .end () :].lstrip ()
1811+ return None , src
1812+
1813+ model , text = _extract (r"--model\s+(\S+)" , _VALID_MODELS , text )
1814+ effort , text = _extract (r"--effort\s+(\S+)" , _VALID_EFFORTS , text )
1815+ return model , effort , text .strip ()
1816+
17891817 async def agentic_tasks (
17901818 self , update : Update , context : ContextTypes .DEFAULT_TYPE
17911819 ) -> None :
17921820 """Manage the task list.
17931821
17941822 Usage:
1795- /tasks — show list
1796- /tasks add <description> — add a task manually
1797- /tasks plan <description> — ask Claude to decompose into tasks
1798- /tasks del <id> — delete a task by id
1799- /tasks clear — clear all tasks
1800- /tasks reset — reset failed/in-progress tasks to pending
1823+ /tasks — show list
1824+ /tasks add [--model M] [--effort E] <desc> — add a task
1825+ /tasks plan [--model M] [--effort E] <desc> — Claude decomposes into tasks
1826+ /tasks del <id> — delete a task by id
1827+ /tasks clear — clear all tasks
1828+ /tasks reset — reset failed/in-progress to pending
1829+
1830+ Model values : opus · sonnet · haiku
1831+ Effort values: low · medium · high · max (model-dependent)
18011832 """
1802- from ..tasklist .runner import TaskRunner # local import, already cached
1803-
18041833 user_id = update .effective_user .id
18051834 storage = context .bot_data .get ("storage" )
18061835 if not storage :
@@ -1814,7 +1843,7 @@ async def agentic_tasks(
18141843 rest = parts [2 ].strip () if len (parts ) > 2 else ""
18151844
18161845 # --- show list ---
1817- if sub == "" or sub == "list" :
1846+ if sub in ( "" , "list" ) :
18181847 tasks = await task_repo .get_tasks (user_id )
18191848 if not tasks :
18201849 await update .message .reply_text (
@@ -1827,14 +1856,23 @@ async def agentic_tasks(
18271856
18281857 counts = await task_repo .count_by_status (user_id )
18291858 running = self ._task_runners .get (user_id )
1830- runner_badge = " · <b>runner active</b>" if (
1831- running and running .is_running ()
1832- ) else ""
1859+ runner_badge = (
1860+ " · <b>runner active</b>"
1861+ if running and running .is_running ()
1862+ else ""
1863+ )
18331864
18341865 lines = [f"📋 <b>Task list</b>{ runner_badge } \n " ]
18351866 for t in tasks :
18361867 desc = escape_html (t .description [:80 ])
1837- lines .append (f"{ t .icon } <code>{ t .id } </code> { desc } " )
1868+ model_badge = (
1869+ f" <code>[{ t .model .capitalize ()} "
1870+ + (f"/{ t .effort } " if t .effort else "" )
1871+ + "]</code>"
1872+ if t .model
1873+ else ""
1874+ )
1875+ lines .append (f"{ t .icon } <code>{ t .id } </code>{ model_badge } { desc } " )
18381876
18391877 summary_parts = []
18401878 for status , label in (
@@ -1848,7 +1886,7 @@ async def agentic_tasks(
18481886 summary_parts .append (f"{ n } { label } " )
18491887 lines .append ("\n " + " · " .join (summary_parts ))
18501888 lines .append (
1851- "\n <i>/taskrun to start · /tasks add <desc> to add more </i>"
1889+ "\n <i>/taskrun to start · /tasks add [--model opus] <desc></i>"
18521890 )
18531891
18541892 await update .message .reply_text ("\n " .join (lines ), parse_mode = "HTML" )
@@ -1858,13 +1896,26 @@ async def agentic_tasks(
18581896 if sub == "add" :
18591897 if not rest :
18601898 await update .message .reply_text (
1861- "Usage: <code>/tasks add <description></code>" ,
1899+ "Usage: <code>/tasks add [--model opus|sonnet|haiku]"
1900+ " [--effort low|medium|high|max] <description></code>" ,
18621901 parse_mode = "HTML" ,
18631902 )
18641903 return
1865- task = await task_repo .add_task (user_id , rest )
1904+ model , effort , desc = self ._parse_task_flags (rest )
1905+ if not desc :
1906+ await update .message .reply_text ("Please include a task description." )
1907+ return
1908+ task = await task_repo .add_task (user_id , desc , model = model , effort = effort )
1909+ badge = (
1910+ f" <code>[{ model .capitalize ()} "
1911+ + (f"/{ effort } " if effort else "" )
1912+ + "]</code>"
1913+ if model
1914+ else ""
1915+ )
18661916 await update .message .reply_text (
1867- f"✅ Task <code>{ task .id } </code> added: <i>{ escape_html (rest [:80 ])} </i>" ,
1917+ f"✅ Task <code>{ task .id } </code> added{ badge } :"
1918+ f" <i>{ escape_html (desc [:80 ])} </i>" ,
18681919 parse_mode = "HTML" ,
18691920 )
18701921 return
@@ -1873,8 +1924,8 @@ async def agentic_tasks(
18731924 if sub == "plan" :
18741925 if not rest :
18751926 await update .message .reply_text (
1876- "Usage: <code>/tasks plan <description></code>\n "
1877- "Example: <code>/tasks plan refactor auth, add dark mode, write tests </code>" ,
1927+ "Usage: <code>/tasks plan [--model M] [--effort E] <description></code>\n "
1928+ "Example: <code>/tasks plan --model opus refactor auth, add dark mode</code>" ,
18781929 parse_mode = "HTML" ,
18791930 )
18801931 return
@@ -1884,14 +1935,20 @@ async def agentic_tasks(
18841935 await update .message .reply_text ("Claude integration unavailable." )
18851936 return
18861937
1938+ # Flags here apply to every created task (acts as a batch default).
1939+ model , effort , description = self ._parse_task_flags (rest )
1940+ if not description :
1941+ await update .message .reply_text ("Please include a description to plan." )
1942+ return
1943+
18871944 thinking_msg = await update .message .reply_text ("🧠 Planning tasks…" )
18881945
18891946 decompose_prompt = (
18901947 "Break the following work description into a concise, ordered task list.\n "
18911948 "Output ONLY the tasks — one per line, no numbering, no bullet points, "
18921949 "no extra commentary.\n "
18931950 "Each line must be a single, self-contained actionable task.\n \n "
1894- f"Work description: { rest } "
1951+ f"Work description: { description } "
18951952 )
18961953
18971954 current_dir = context .user_data .get (
@@ -1930,13 +1987,20 @@ async def agentic_tasks(
19301987 return
19311988
19321989 created = []
1933- for desc in task_lines :
1934- t = await task_repo .add_task (user_id , desc )
1990+ for task_desc in task_lines :
1991+ t = await task_repo .add_task (
1992+ user_id , task_desc , model = model , effort = effort
1993+ )
19351994 created .append (t )
19361995
1937- lines = [
1938- f"📋 <b>{ len (created )} task(s) created:</b>\n "
1939- ]
1996+ model_note = (
1997+ f" (all using <code>{ model .capitalize ()} "
1998+ + (f"/{ effort } " if effort else "" )
1999+ + "</code>)"
2000+ if model
2001+ else ""
2002+ )
2003+ lines = [f"📋 <b>{ len (created )} task(s) created{ model_note } :</b>\n " ]
19402004 for t in created :
19412005 lines .append (
19422006 f"{ t .icon } <code>{ t .id } </code> <i>{ escape_html (t .description [:80 ])} </i>"
@@ -1967,15 +2031,12 @@ async def agentic_tasks(
19672031 # --- clear ---
19682032 if sub == "clear" :
19692033 count = await task_repo .clear_tasks (user_id )
1970- await update .message .reply_text (
1971- f"🗑️ Cleared { count } task(s)."
1972- )
2034+ await update .message .reply_text (f"🗑️ Cleared { count } task(s)." )
19732035 return
19742036
19752037 # --- reset ---
19762038 if sub == "reset" :
19772039 count = await task_repo .reset_stale_in_progress (user_id )
1978- # Also reset explicitly failed tasks to pending
19792040 tasks = await task_repo .get_tasks (user_id )
19802041 reset_failed = 0
19812042 for t in tasks :
@@ -1994,13 +2055,16 @@ async def agentic_tasks(
19942055 await update .message .reply_text (
19952056 "<b>Task list commands:</b>\n "
19962057 "<code>/tasks</code> — show list\n "
1997- "<code>/tasks add <description></code> — add a task\n "
1998- "<code>/tasks plan <description></code> — Claude plans tasks for you\n "
2058+ "<code>/tasks add [--model M] [--effort E] <desc></code> — add a task\n "
2059+ "<code>/tasks plan [--model M] [--effort E] <desc></code>"
2060+ " — Claude plans tasks for you\n "
19992061 "<code>/tasks del <id></code> — delete a task\n "
20002062 "<code>/tasks clear</code> — clear all tasks\n "
20012063 "<code>/tasks reset</code> — reset failed tasks to pending\n "
20022064 "<code>/taskrun</code> — start the runner\n "
2003- "<code>/taskstop</code> — stop the runner" ,
2065+ "<code>/taskstop</code> — stop the runner\n \n "
2066+ "<i>Model: opus · sonnet · haiku"
2067+ " | Effort: low · medium · high · max</i>" ,
20042068 parse_mode = "HTML" ,
20052069 )
20062070
@@ -2041,6 +2105,11 @@ async def agentic_taskrun(
20412105 )
20422106 session_id = context .user_data .get ("claude_session_id" )
20432107
2108+ # Inherit the user's active /model selection as the runner-wide default.
2109+ # Individual tasks can still override this via their own model field.
2110+ default_model = context .user_data .get ("model_override" )
2111+ default_effort = context .user_data .get ("effort_override" )
2112+
20442113 runner = TaskRunner (
20452114 user_id = user_id ,
20462115 chat_id = update .effective_chat .id ,
@@ -2049,12 +2118,23 @@ async def agentic_taskrun(
20492118 claude_integration = claude_integration ,
20502119 working_directory = current_dir ,
20512120 session_id = session_id ,
2121+ default_model = default_model ,
2122+ default_effort = default_effort ,
20522123 )
20532124 self ._task_runners [user_id ] = runner
20542125 runner .start ()
20552126
2127+ model_note = (
2128+ f" using <b>{ default_model .capitalize ()} "
2129+ + (f"/{ default_effort } " if default_effort else "" )
2130+ + "</b> as default"
2131+ if default_model
2132+ else ""
2133+ )
20562134 await update .message .reply_text (
2057- "▶️ Task runner started. I'll work through your list and report back." ,
2135+ f"▶️ Task runner started{ model_note } ."
2136+ " I'll work through your list and report back." ,
2137+ parse_mode = "HTML" ,
20582138 )
20592139
20602140 audit_logger = context .bot_data .get ("audit_logger" )
0 commit comments