Unified Agent + reusable Go agent core.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: preserve chat topic and draft state

Lyric 5d8845ec 324778af

+181 -22
+25 -2
web/console/src/composables/useAppShell.js
··· 1 1 import { computed, onMounted, onUnmounted, ref, watch } from "vue"; 2 2 import { useRoute, useRouter } from "vue-router"; 3 3 4 + import { lastTopicID } from "../core/chat-topic-memory"; 4 5 import { endpointDisplayItem, visibleEndpoints } from "../core/endpoints"; 5 6 import { 6 7 authValid, 7 8 endpointState, 8 9 ensureEndpointSelection, 9 10 loadEndpoints, 11 + runtimeEndpointByRef, 10 12 setSelectedEndpointRef, 11 13 translate, 12 14 } from "../core/context"; 13 15 import { NAV_ITEMS_META } from "../router"; 16 + 17 + function chatRoutePath(topicID = "") { 18 + const normalizedTopicID = String(topicID || "").trim(); 19 + return normalizedTopicID ? `/chat/${encodeURIComponent(normalizedTopicID)}` : "/chat"; 20 + } 21 + 22 + function chatSubmitEndpointRef(endpointRef) { 23 + const selected = runtimeEndpointByRef(endpointRef); 24 + if (!selected) { 25 + return ""; 26 + } 27 + const mapped = String(selected.submit_endpoint_ref || "").trim(); 28 + if (mapped) { 29 + return mapped; 30 + } 31 + return selected.can_submit ? String(selected.endpoint_ref || "").trim() : ""; 32 + } 14 33 15 34 function useAppShell() { 16 35 const t = translate; ··· 93 112 return; 94 113 } 95 114 mobileNavOpen.value = false; 96 - if (route.path !== item.id) { 97 - router.push(item.id); 115 + let nextPath = item.id; 116 + if (item.id === "/chat") { 117 + nextPath = chatRoutePath(lastTopicID(chatSubmitEndpointRef(endpointState.selectedRef))); 118 + } 119 + if (route.path !== nextPath) { 120 + router.push(nextPath); 98 121 } 99 122 } 100 123
+54
web/console/src/core/chat-draft-memory.js
··· 1 + const draftsByScope = new Map(); 2 + const NEW_TOPIC_SCOPE_ID = "__new__"; 3 + 4 + function normalizeEndpointRef(value) { 5 + return typeof value === "string" ? value.trim() : ""; 6 + } 7 + 8 + function normalizeTopicID(value) { 9 + return String(value || "").trim(); 10 + } 11 + 12 + function normalizeDraftText(value) { 13 + return typeof value === "string" ? value : String(value || ""); 14 + } 15 + 16 + function scopeKey(endpointRef, topicID) { 17 + const normalizedEndpointRef = normalizeEndpointRef(endpointRef); 18 + if (!normalizedEndpointRef) { 19 + return ""; 20 + } 21 + const normalizedTopicID = normalizeTopicID(topicID) || NEW_TOPIC_SCOPE_ID; 22 + return `${normalizedEndpointRef}\n${normalizedTopicID}`; 23 + } 24 + 25 + function rememberChatDraft(endpointRef, topicID, text) { 26 + const key = scopeKey(endpointRef, topicID); 27 + if (!key) { 28 + return; 29 + } 30 + const normalizedText = normalizeDraftText(text); 31 + if (!normalizedText) { 32 + draftsByScope.delete(key); 33 + return; 34 + } 35 + draftsByScope.set(key, normalizedText); 36 + } 37 + 38 + function chatDraft(endpointRef, topicID) { 39 + const key = scopeKey(endpointRef, topicID); 40 + if (!key) { 41 + return ""; 42 + } 43 + return normalizeDraftText(draftsByScope.get(key)); 44 + } 45 + 46 + function clearChatDraft(endpointRef, topicID) { 47 + const key = scopeKey(endpointRef, topicID); 48 + if (!key) { 49 + return; 50 + } 51 + draftsByScope.delete(key); 52 + } 53 + 54 + export { chatDraft, clearChatDraft, rememberChatDraft };
+28
web/console/src/core/chat-topic-memory.js
··· 1 + const lastTopicIDsByEndpoint = new Map(); 2 + 3 + function normalizeEndpointRef(value) { 4 + return typeof value === "string" ? value.trim() : ""; 5 + } 6 + 7 + function normalizeTopicID(value) { 8 + return String(value || "").trim(); 9 + } 10 + 11 + function rememberLastTopicID(endpointRef, topicID) { 12 + const normalizedEndpointRef = normalizeEndpointRef(endpointRef); 13 + const normalizedTopicID = normalizeTopicID(topicID); 14 + if (!normalizedEndpointRef || !normalizedTopicID) { 15 + return; 16 + } 17 + lastTopicIDsByEndpoint.set(normalizedEndpointRef, normalizedTopicID); 18 + } 19 + 20 + function lastTopicID(endpointRef) { 21 + const normalizedEndpointRef = normalizeEndpointRef(endpointRef); 22 + if (!normalizedEndpointRef) { 23 + return ""; 24 + } 25 + return normalizeTopicID(lastTopicIDsByEndpoint.get(normalizedEndpointRef)); 26 + } 27 + 28 + export { lastTopicID, rememberLastTopicID };
-9
web/console/src/i18n/index.js
··· 160 160 runtime_field_location: "Location", 161 161 runtime_field_instance: "Instance", 162 162 runtime_field_last_poke: "Last Heartbeat", 163 - runtime_field_submit: "Submit", 164 - runtime_submit_ready: "ready", 165 - runtime_submit_blocked: "Blocked", 166 163 runtime_status_configured: "Configured", 167 164 runtime_status_not_configured: "Not Configured", 168 165 runtime_status_running: "Running", ··· 804 801 runtime_field_location: "位置", 805 802 runtime_field_instance: "实例", 806 803 runtime_field_last_poke: "最近心跳", 807 - runtime_field_submit: "提交", 808 - runtime_submit_ready: "待命中", 809 - runtime_submit_blocked: "已阻塞", 810 804 runtime_status_configured: "已配置", 811 805 runtime_status_not_configured: "未配置", 812 806 runtime_status_running: "运行中", ··· 1448 1442 runtime_field_location: "場所", 1449 1443 runtime_field_instance: "インスタンス", 1450 1444 runtime_field_last_poke: "最後の Heartbeat", 1451 - runtime_field_submit: "送信", 1452 - runtime_submit_ready: "可能", 1453 - runtime_submit_blocked: "未準備", 1454 1445 runtime_status_configured: "設定済み", 1455 1446 runtime_status_not_configured: "未設定", 1456 1447 runtime_status_running: "稼働中",
+74
web/console/src/views/ChatView.js
··· 6 6 import AppPage from "../components/AppPage"; 7 7 import MarkdownContent from "../components/MarkdownContent"; 8 8 import RawJsonDialog from "../components/RawJsonDialog"; 9 + import { chatDraft, clearChatDraft, rememberChatDraft } from "../core/chat-draft-memory"; 10 + import { rememberLastTopicID } from "../core/chat-topic-memory"; 9 11 import { endpointChannelLabel } from "../core/endpoints"; 10 12 import { workspaceTreeIcon } from "../core/workspace-icons"; 11 13 import { ··· 62 64 return String(raw || "").trim(); 63 65 } 64 66 67 + function rememberTopicSelection(endpointRef, topicID) { 68 + const normalizedTopicID = normalizeTopicID(topicID); 69 + if (!normalizedTopicID || normalizedTopicID === HEARTBEAT_TOPIC_ID) { 70 + return; 71 + } 72 + rememberLastTopicID(endpointRef, normalizedTopicID); 73 + } 74 + 75 + function composerDraftTopicID(consoleTopicsEnabled, creatingTopic, selectedTopicID, routeTopicID) { 76 + if (!consoleTopicsEnabled || creatingTopic) { 77 + return ""; 78 + } 79 + const normalizedSelectedTopicID = normalizeTopicID(selectedTopicID); 80 + if (normalizedSelectedTopicID) { 81 + return normalizedSelectedTopicID; 82 + } 83 + return normalizeTopicID(routeTopicID); 84 + } 85 + 65 86 function isWorkspaceCommandText(raw) { 66 87 return String(raw || "").trim().toLowerCase().startsWith("/workspace"); 67 88 } ··· 538 559 const pollTimers = new Set(); 539 560 const streamSockets = new Map(); 540 561 const composerField = ref(null); 562 + const suppressDraftPersistence = ref(false); 541 563 const rawDialogOpen = ref(false); 542 564 const rawDialogJSON = ref(""); 543 565 const rawRevealItemID = ref(""); ··· 561 583 return selected.can_submit ? String(selected.endpoint_ref || "").trim() : ""; 562 584 }); 563 585 const submitEndpoint = computed(() => runtimeEndpointByRef(submitEndpointRef.value)); 586 + const composerDraftScope = computed(() => ({ 587 + endpointRef: String(submitEndpointRef.value || "").trim(), 588 + topicID: composerDraftTopicID( 589 + consoleTopicsEnabled.value, 590 + creatingTopic.value, 591 + selectedTopicID.value, 592 + routeTopicID.value 593 + ), 594 + })); 564 595 const activeAgentName = computed(() => { 565 596 const submitName = String(submitEndpoint.value?.agent_name || "").trim(); 566 597 if (submitName) { ··· 926 957 return root.querySelector("textarea"); 927 958 } 928 959 960 + function persistComposerDraft(scope = composerDraftScope.value, text = taskInput.value) { 961 + const endpointRef = String(scope?.endpointRef || "").trim(); 962 + if (!endpointRef) { 963 + return; 964 + } 965 + rememberChatDraft(endpointRef, normalizeTopicID(scope?.topicID), text); 966 + } 967 + 968 + function restoreComposerDraft(scope = composerDraftScope.value) { 969 + const endpointRef = String(scope?.endpointRef || "").trim(); 970 + const nextText = endpointRef ? chatDraft(endpointRef, normalizeTopicID(scope?.topicID)) : ""; 971 + suppressDraftPersistence.value = true; 972 + taskInput.value = nextText; 973 + syncComposerHeight(); 974 + void nextTick(() => { 975 + suppressDraftPersistence.value = false; 976 + }); 977 + } 978 + 929 979 function focusComposer() { 930 980 if (chatReadonly.value || (mobileTopicSplitEnabled.value && !showChatPane.value)) { 931 981 return; ··· 1851 1901 1852 1902 if (preferredTopicID && items.some((topic) => normalizeTopicID(topic?.id) === preferredTopicID)) { 1853 1903 selectedTopicID.value = preferredTopicID; 1904 + rememberTopicSelection(submitEndpointRef.value, preferredTopicID); 1854 1905 creatingTopic.value = false; 1855 1906 syncMobileTopicView({ preferChat: true }); 1856 1907 return true; ··· 1861 1912 } 1862 1913 const currentID = normalizeTopicID(selectedTopicID.value); 1863 1914 if (currentID && items.some((topic) => normalizeTopicID(topic?.id) === currentID)) { 1915 + rememberTopicSelection(submitEndpointRef.value, currentID); 1864 1916 creatingTopic.value = false; 1865 1917 syncMobileTopicView({ preferChat: true }); 1866 1918 return true; ··· 2083 2135 } 2084 2136 creatingTopic.value = false; 2085 2137 selectedTopicID.value = normalized; 2138 + rememberTopicSelection(submitEndpointRef.value, normalized); 2086 2139 syncMobileTopicView({ preferChat: true }); 2087 2140 void loadHistory().finally(() => { 2088 2141 focusComposer(); ··· 2107 2160 if (!task || sending.value) { 2108 2161 return; 2109 2162 } 2163 + const submittedDraftScope = composerDraftScope.value; 2110 2164 const endpointRef = submitEndpointRef.value; 2111 2165 if (!endpointRef) { 2112 2166 err.value = submitBlockedMessage.value || t("msg_select_endpoint"); ··· 2122 2176 2123 2177 sending.value = true; 2124 2178 err.value = ""; 2179 + suppressDraftPersistence.value = true; 2125 2180 taskInput.value = ""; 2126 2181 if (consoleTopicsEnabled.value && !normalizeTopicID(selectedTopicID.value)) { 2127 2182 creatingTopic.value = true; ··· 2152 2207 if (!taskID) { 2153 2208 throw new Error(t("chat_missing_task_id")); 2154 2209 } 2210 + clearChatDraft(submittedDraftScope.endpointRef, submittedDraftScope.topicID); 2155 2211 const existingAgentItem = chatHistoryItems.value.find((item) => item.id === agentHistoryID) || null; 2156 2212 patchHistoryItem(agentHistoryID, { 2157 2213 taskId: taskID, ··· 2168 2224 } 2169 2225 creatingTopic.value = false; 2170 2226 selectedTopicID.value = topicID; 2227 + rememberTopicSelection(submitEndpointRef.value, topicID); 2171 2228 await loadTopics({ 2172 2229 preferredTopicID: topicID, 2173 2230 preserveSelection: true, ··· 2181 2238 } catch (e) { 2182 2239 const message = e?.message || t("msg_load_failed"); 2183 2240 err.value = message; 2241 + rememberChatDraft(submittedDraftScope.endpointRef, submittedDraftScope.topicID, task); 2242 + taskInput.value = task; 2184 2243 patchHistoryItem(agentHistoryID, { 2185 2244 status: "failed", 2186 2245 text: message, 2187 2246 rawJSON: "", 2188 2247 }); 2189 2248 } finally { 2249 + suppressDraftPersistence.value = false; 2190 2250 sending.value = false; 2191 2251 syncComposerHeight(); 2192 2252 focusComposer(); ··· 2206 2266 syncComposerHeight(); 2207 2267 }); 2208 2268 onUnmounted(() => { 2269 + persistComposerDraft(); 2209 2270 window.removeEventListener("resize", refreshMobileMode); 2210 2271 clearPollTimers(); 2211 2272 clearStreamSockets(); ··· 2256 2317 } 2257 2318 } 2258 2319 ); 2320 + watch( 2321 + () => composerDraftScope.value, 2322 + (nextScope, prevScope) => { 2323 + if (prevScope?.endpointRef) { 2324 + persistComposerDraft(prevScope); 2325 + } 2326 + restoreComposerDraft(nextScope); 2327 + }, 2328 + { immediate: true } 2329 + ); 2259 2330 watch(taskInput, () => { 2331 + if (!suppressDraftPersistence.value) { 2332 + persistComposerDraft(); 2333 + } 2260 2334 syncComposerHeight(); 2261 2335 }); 2262 2336
-11
web/console/src/views/RuntimeView.js
··· 125 125 health: "ok", 126 126 mode: "", 127 127 agent_name: "", 128 - submit_enabled: false, 129 128 poke_enabled: false, 130 129 heartbeat_running: false, 131 130 instance_id: "", ··· 203 202 const routeRows = computed(() => [ 204 203 { key: "provider", label: t("stat_llm_provider"), value: stringValue(overview.llm_provider) }, 205 204 { key: "model", label: t("stat_llm_model"), value: stringValue(overview.llm_model) }, 206 - { 207 - key: "submit", 208 - label: t("runtime_field_submit"), 209 - value: overview.submit_enabled ? t("runtime_submit_ready") : t("runtime_submit_blocked"), 210 - tone: overview.submit_enabled ? "success" : "warning", 211 - }, 212 205 ]); 213 206 const channelRows = computed(() => [ 214 207 { ··· 244 237 overview.health = data.health || "ok"; 245 238 overview.mode = data.mode || ""; 246 239 overview.agent_name = data.agent_name || ""; 247 - overview.submit_enabled = toBool(data.submit_enabled, false); 248 240 overview.poke_enabled = toBool(data.poke_enabled, false); 249 241 overview.heartbeat_running = toBool(data.heartbeat_running, false); 250 242 overview.instance_id = data.instance_id || ""; ··· 345 337 <div class="runtime-hero-status"> 346 338 <div class="runtime-hero-badges"> 347 339 <QBadge :type="healthBadgeType(overview.health)" size="sm">{{ overview.health || "ok" }}</QBadge> 348 - <QBadge :type="overview.submit_enabled ? 'success' : 'warning'" size="sm"> 349 - {{ overview.submit_enabled ? t("runtime_submit_ready") : t("runtime_submit_blocked") }} 350 - </QBadge> 351 340 </div> 352 341 </div> 353 342 </div>