Skip to content

Commit 66e7f5a

Browse files
committed
support /fork command
Fixes #366
1 parent 0d981f1 commit 66e7f5a

File tree

9 files changed

+73
-0
lines changed

9 files changed

+73
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add `/fork` command to clone current chat into a new chat with the same history and settings, and `chat/opened` server notification.
6+
57
## 0.118.1
68

79
- Fix ECA stop timing out.

docs/protocol.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,31 @@ interface ChatClearedParams {
15111511
}
15121512
```
15131513

1514+
### Chat opened (⬅️)
1515+
1516+
A server notification indicating a new chat was created server-side (e.g. via `/fork`).
1517+
Clients should create a new chat entry in the UI. The chat messages will follow as `chat/contentReceived` notifications.
1518+
1519+
_Notification:_
1520+
1521+
* method: `chat/opened`
1522+
* params: `ChatOpenedParams` defined as follows:
1523+
1524+
```typescript
1525+
interface ChatOpenedParams {
1526+
1527+
/**
1528+
* The new chat session identifier.
1529+
*/
1530+
chatId: string;
1531+
1532+
/**
1533+
* The title of the new chat.
1534+
*/
1535+
title?: string;
1536+
}
1537+
```
1538+
15141539
### Chat delete (↩️)
15151540

15161541
A client request to delete a existing chat, removing all previous messages and used tokens/costs from memory, good for reduce context or start a new clean chat.

integration-test/integration/chat/commands_test.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
{:name "skill-create" :arguments [{:name "name"} {:name "prompt"}]}
2929
{:name "costs" :arguments []}
3030
{:name "compact" :arguments [{:name "additional-input"}]}
31+
{:name "fork" :arguments []}
3132
{:name "resume" :arguments [{:name "chat-id"}]}
3233
{:name "remote" :arguments []}
3334
{:name "config" :arguments []}

src/eca/features/chat.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,10 @@
928928
(lifecycle/send-content! chat-ctx :user {:type :text
929929
:content-id (:user-content-id chat-ctx)
930930
:text (str message "\n")})
931+
;; Clear prompt-finished? so finish-chat-prompt! can properly terminate
932+
;; this prompt cycle. prompt-messages! already does this for regular
933+
;; prompts, but commands and mcp-prompts go through different paths.
934+
(swap! db* update-in [:chats chat-id] dissoc :prompt-finished?)
931935
(case (:type decision)
932936
:mcp-prompt (send-mcp-prompt! decision chat-ctx)
933937
:eca-command (handle-command! decision chat-ctx)

src/eca/features/commands.clj

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525

2626
(set! *warn-on-reflection* true)
2727

28+
(defn ^:private fork-title [title]
29+
(let [title (or title "Untitled")]
30+
(if-let [[_ base n] (re-matches #"(.+?)\s*\((\d+)\)\s*$" title)]
31+
(str base " (" (inc (parse-long n)) ")")
32+
(str title " (2)"))))
33+
2834
(defn ^:private normalize-command-name [f]
2935
(string/lower-case (fs/strip-ext (fs/file-name f))))
3036

@@ -113,6 +119,10 @@
113119
:type :native
114120
:description "Summarize the chat so far cleaning previous chat history to reduce context."
115121
:arguments [{:name "additional-input"}]}
122+
{:name "fork"
123+
:type :native
124+
:description "Fork current chat into a new chat with the same history and settings."
125+
:arguments []}
116126
{:name "resume"
117127
:type :native
118128
:description "Resume the specified chat-id. Blank to list chats or 'latest'."
@@ -326,6 +336,28 @@
326336
metrics)
327337
{:type :new-chat-status
328338
:status :login})
339+
"fork" (let [chat (get-in db [:chats chat-id])
340+
new-id (str (random-uuid))
341+
now (System/currentTimeMillis)
342+
new-title (fork-title (:title chat))
343+
new-chat {:id new-id
344+
:title new-title
345+
:status :idle
346+
:created-at now
347+
:updated-at now
348+
:model (:model chat)
349+
:last-api (:last-api chat)
350+
:messages (vec (:messages chat))
351+
:prompt-finished? true}]
352+
(swap! db* assoc-in [:chats new-id] new-chat)
353+
(db/update-workspaces-cache! @db* metrics)
354+
(messenger/chat-opened messenger {:chat-id new-id :title new-title})
355+
{:type :chat-messages
356+
:chats {new-id {:messages (:messages chat)
357+
:title new-title}
358+
chat-id {:messages [{:role "system"
359+
:content [{:type :text
360+
:text (str "Chat forked to: " new-title)}]}]}}})
329361
"resume" (let [chats (into {}
330362
(filter #(and (not= chat-id (first %))
331363
(not (:subagent (second %)))))

src/eca/messenger.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
(chat-cleared [this params])
1010
(chat-status-changed [this params])
1111
(chat-deleted [this params])
12+
(chat-opened [this params])
1213
(rewrite-content-received [this data])
1314
(tool-server-updated [this params])
1415
(config-updated [this params])

src/eca/remote/messenger.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
(messenger/chat-deleted inner params)
3131
(sse/broadcast! sse-connections* "chat:deleted" (->camel params)))
3232

33+
(chat-opened [_this params]
34+
(messenger/chat-opened inner params)
35+
(sse/broadcast! sse-connections* "chat:opened" (->camel params)))
36+
3337
(rewrite-content-received [_this data]
3438
(messenger/rewrite-content-received inner data))
3539

src/eca/server.clj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@
153153
(chat-deleted [_this params]
154154
(jsonrpc.server/discarding-stdout
155155
(jsonrpc.server/send-notification server "chat/deleted" params)))
156+
(chat-opened [_this params]
157+
(jsonrpc.server/discarding-stdout
158+
(jsonrpc.server/send-notification server "chat/opened" params)))
156159
(rewrite-content-received [_this content]
157160
(jsonrpc.server/discarding-stdout
158161
(jsonrpc.server/send-notification server "rewrite/contentReceived" content)))

test/eca/test_helper.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
(chat-cleared [_ params] (swap! messages* update :chat-clear (fnil conj []) params))
3333
(chat-status-changed [_ params] (swap! messages* update :chat-status-changed (fnil conj []) params))
3434
(chat-deleted [_ params] (swap! messages* update :chat-deleted (fnil conj []) params))
35+
(chat-opened [_ params] (swap! messages* update :chat-opened (fnil conj []) params))
3536
(rewrite-content-received [_ data] (swap! messages* update :rewrite-content-received (fnil conj []) data))
3637
(config-updated [_ data] (swap! messages* update :config-updated (fnil conj []) data))
3738
(tool-server-updated [_ data] (swap! messages* update :tool-server-update (fnil conj []) data))

0 commit comments

Comments
 (0)