diff --git a/agent/first_task_prompt.py b/agent/first_task_prompt.py new file mode 100644 index 0000000000..d0086d52cd --- /dev/null +++ b/agent/first_task_prompt.py @@ -0,0 +1,29 @@ +"""The ``first-task`` skill for the task chat a setup handoff opens. + +``start_chat`` sends it in that chat's first user message, the way ``/initiate-setup`` carries its skill, so +no system prompt changes. It rides at the tail, after the visible ask and the "What setup learned" block: +session previews read the ask, and the history projection cuts the message at ``MARKER`` so no surface +shows the skill. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from hermes_constants import get_optional_skills_dir + +MARKER = "\n\n[/first-task]\n\n" + + +def first_task_tail(connect: list, install: list) -> str: + """``connect``: connector ids to show on one connect card; ``install``: catalog plugin ids to install.""" + skill_dir = get_optional_skills_dir(Path(__file__).resolve().parent.parent / "optional-skills") + skill = (skill_dir / "productivity" / "first-task" / "SKILL.md").read_text(encoding="utf-8-sig").strip() + facts = json.dumps({"connect": connect, "install": install}, ensure_ascii=False) + return f"{MARKER}{skill}\n\n```json\n{facts}\n```" + + +def visible_text(text: str) -> str: + """The part of a user message a surface shows: everything before the first-task skill.""" + return text.partition(MARKER)[0] diff --git a/agent/initiate_setup_prompt.py b/agent/initiate_setup_prompt.py index aee8fc205e..d07ffdae3a 100644 --- a/agent/initiate_setup_prompt.py +++ b/agent/initiate_setup_prompt.py @@ -72,6 +72,11 @@ def collect_setup_cards() -> dict: return host_facts.setup_cards(_collect(host_facts)) +def fork_card(cards: dict) -> dict: + """The fork card as it is shown, its first rows built from the picks this setup conversation recorded.""" + return _host_facts_module(_skill_dir()).fork_card(cards) + + def build_initiate_setup_prompt(surface: str, tools, primary_profile: str, session_id: str | None = None) -> str: """``session_id``: the desktop session the turn runs in; its ``setup_choose`` cards read these same facts.""" from hermes_cli.anon_auth import free_tier_route diff --git a/apps/desktop/src/components/assistant-ui/thread/setup-learned.tsx b/apps/desktop/src/components/assistant-ui/thread/setup-learned.tsx new file mode 100644 index 0000000000..0c1ef69e02 --- /dev/null +++ b/apps/desktop/src/components/assistant-ui/thread/setup-learned.tsx @@ -0,0 +1,23 @@ +import type { FC } from 'react' + +// tui_gateway/start_chat.py::_setup_learned: the block a setup handoff appends +// under the user's ask. The model reads all of it; the bubble shows only the ask. +const SETUP_LEARNED_MARKER = '\n\nWhat setup learned about me:\n' + +/** `[ask, block]`: the block is null for every message that has none. */ +export function splitSetupLearned(text: string): [string, null | string] { + const index = text.indexOf(SETUP_LEARNED_MARKER) + + return index < 0 ? [text, null] : [text.slice(0, index), text.slice(index + 2)] +} + +export const SetupLearnedNote: FC<{ text: string }> = ({ text }) => { + const [title = '', ...lines] = text.split('\n') + + return ( +
+ {title.replace(/:$/, '')} +
{lines.join('\n')}
+
+ ) +} diff --git a/apps/desktop/src/components/assistant-ui/thread/user-message.tsx b/apps/desktop/src/components/assistant-ui/thread/user-message.tsx index 231b1739e7..f86e873f60 100644 --- a/apps/desktop/src/components/assistant-ui/thread/user-message.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/user-message.tsx @@ -9,6 +9,7 @@ import { PROCESS_NOTIFICATION_RE } from '@/components/assistant-ui/thread/content' import { ReactionBadge, ReactionPicker } from '@/components/assistant-ui/thread/message-reactions' +import { SetupLearnedNote, splitSetupLearned } from '@/components/assistant-ui/thread/setup-learned' import { BackgroundResult } from '@/components/assistant-ui/thread/system-message' import { threadUserOrdinal } from '@/components/assistant-ui/thread/thread-message-index' import { MessageTimelineTimestamp } from '@/components/assistant-ui/thread/timeline-timestamp' @@ -249,7 +250,8 @@ export const UserMessage: FC<{ const copy = t.assistant.thread const messageId = useAuiState(s => s.message.id) const content = useAuiState(s => s.message.content) - const messageText = messageContentText(content) + const fullText = messageContentText(content) + const [messageText, setupLearned] = splitSetupLearned(fullText) const threadRunning = useAuiState(s => s.thread.isRunning) const latestUserId = useAuiState(s => { @@ -411,11 +413,14 @@ export const UserMessage: FC<{ // it. No negative margin: -mt-* pulls the row up into the sticky box, // where the sticky-prompt clip hides its top even at rest. Image refs // render as thumbnails, file refs as chips; no border. - attachmentRefs.length > 0 && !chipOnlyTurn ? ( -
- -
- ) : null + <> + {attachmentRefs.length > 0 && !chipOnlyTurn && ( +
+ +
+ )} + {setupLearned && } + } messageId={messageId} > @@ -527,7 +532,7 @@ export const UserMessage: FC<{ event.stopPropagation() triggerHaptic('selection') onRequestRestoreConfirm?.(messageId, { - text: messageText, + text: fullText, userOrdinal: runtimeUserOrdinal }) }} diff --git a/optional-skills/productivity/first-task/SKILL.md b/optional-skills/productivity/first-task/SKILL.md new file mode 100644 index 0000000000..1c9afc5a7e --- /dev/null +++ b/optional-skills/productivity/first-task/SKILL.md @@ -0,0 +1,114 @@ +--- +name: first-task +description: "Run the first task chat that setup hands off." +version: 0.1.0 +author: "Siddharth Balyan (alt-glitch) + Hermes Agent" +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [onboarding, first-run, desktop, handoff] + category: productivity + related_skills: [initiate-setup] +--- + +# First Task Skill + +Runs the first chat after setup, the one `start_chat` opened. The top of this message is the user's ask, then "What setup learned about me" (their picks and the scan), then these rules and a JSON block. The goal: they see work within one minute and hold a finished, useful result within five. + +## When to Use + +- This message carries the skill under a handoff from setup. +- Until the first result lands. After that it is a normal chat. + +## Prerequisites + +The task chat's own tools: `manage_connections`, `manage_catalog`, `clarify`, `terminal`, the file tools, the browser, `tool_search`, `skill_view`, and `desktop_preview` in the desktop app. Use only tools that are in your tool list. + +## How to Run + +The JSON block at the end holds `connect`, the connector ids of the apps they picked in setup, and `install`, the catalog plugin ids they picked or their first task brings. The ids are exact: skip search and status checks. + +The "What setup learned" lines are your recon. Do not survey the machine first: no system scan, no update search, no inventory of installed tools. Look at one thing only when the next step needs it. + +## Quick Reference + +| Step | What | Call | +|---|---|---| +| 1 | One line on what you start with | none | +| 2 | Connect the picked apps | `manage_connections` connect, every id in `connect`, once | +| 3 | Install the picked plugins | `manage_catalog` install, every id in `install`, once | +| 4 | Specific ask: start it. Vague ask: three options | `clarify`, three choices | +| 5 | The first slice, finished in five minutes | the task's own tools | +| 6 | Show the result, offer the next step | `clarify`: Looks right, Change something, Take it further | + +``` +manage_connections {"action":"connect","connectors":["", ...]} +manage_catalog {"action":"install","items":[{"kind":"plugin","id":""}, ...],"reason":""} +clarify {"questions":[{"question":"","choices":["