diff --git a/packages/app-ai-native/src/i18n/en.ts b/packages/app-ai-native/src/i18n/en.ts index b855e9840..197104453 100644 --- a/packages/app-ai-native/src/i18n/en.ts +++ b/packages/app-ai-native/src/i18n/en.ts @@ -2105,7 +2105,7 @@ export const dict = { "workspace.running": "Running", "workspace.idle": "Idle", "workspace.running.empty": "No running workspaces", - "workspace.running.emptyHint": "Click an idle workspace below to activate", + "workspace.running.emptyHint": "Click an online idle workspace below to activate", "workspace.empty": "No workspaces", "workspace.emptyHint": "Click the + button above to create one", "workspace.close": "Close", @@ -2128,7 +2128,7 @@ export const dict = { "workspace.content.closeAll": "Close All", "workspace.createFromDevice": "Create from", - "workspace.createFromDevice.empty": "Please register a device first", + "workspace.createFromDevice.empty": "Follow the guide on the home page to connect your device and complete registration", "workspace.device.list": "Device List", "workspace.device.search": "Search devices...", "workspace.device.notFound": "No devices found", diff --git a/packages/app-ai-native/src/i18n/zh.ts b/packages/app-ai-native/src/i18n/zh.ts index e0c79b7f7..eabc0307b 100644 --- a/packages/app-ai-native/src/i18n/zh.ts +++ b/packages/app-ai-native/src/i18n/zh.ts @@ -1850,7 +1850,7 @@ export const dict = { "workspace.running": "运行中", "workspace.idle": "空闲", "workspace.running.empty": "暂无运行中工作空间", - "workspace.running.emptyHint": "点击下方空闲工作空间以激活", + "workspace.running.emptyHint": "点击下方在线的空闲工作空间以激活", "workspace.empty": "暂无工作空间", "workspace.emptyHint": "点击上方的 + 按钮创建", "workspace.close": "关闭", @@ -1873,7 +1873,7 @@ export const dict = { "workspace.content.closeAll": "关闭全部", "workspace.createFromDevice": "创建自", - "workspace.createFromDevice.empty": "请先完成设备注册", + "workspace.createFromDevice.empty": "请根据主页的引导连接开发设备,完成设备注册", "workspace.device.list": "设备列表", "workspace.device.search": "搜索设备...", "workspace.device.notFound": "未找到设备", diff --git a/packages/app-ai-native/src/pages/session/message-timeline.tsx b/packages/app-ai-native/src/pages/session/message-timeline.tsx index 98cca9275..7ff7e9984 100644 --- a/packages/app-ai-native/src/pages/session/message-timeline.tsx +++ b/packages/app-ai-native/src/pages/session/message-timeline.tsx @@ -20,7 +20,6 @@ import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { useSessionChat } from "@/context/session-chat" import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" -import { MessageTransition } from "./message-transition" type MessageComment = { path: string @@ -511,7 +510,7 @@ export function MessageTimeline(props: {
{ - if (active()) return false - const activeID = activeMessageID() - if (activeID) return messageID > activeID - return false - }) const comments = createMemo(() => messageComments(chat.messageParts(messageID)), [], { equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), }) @@ -606,22 +599,19 @@ export function MessageTimeline(props: {
- - - + ) }} diff --git a/packages/app-ai-native/src/pages/workspace/components/workspace-sidebar.tsx b/packages/app-ai-native/src/pages/workspace/components/workspace-sidebar.tsx index 71e9f5000..f0459fe19 100644 --- a/packages/app-ai-native/src/pages/workspace/components/workspace-sidebar.tsx +++ b/packages/app-ai-native/src/pages/workspace/components/workspace-sidebar.tsx @@ -2,6 +2,7 @@ import { createSignal, createMemo, For, Show } from "solid-js" import { useNavigate, useParams } from "@solidjs/router" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" +import { Button } from "@opencode-ai/ui/button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -13,7 +14,7 @@ import { useWorkspace } from "../context" import { useWorkspaceNavigate } from "@/hooks/use-workspace-navigate" import { useActiveWorkspace } from "../active-workspace" import { useLanguage } from "@/context/language" -import { WorkspaceCard, getPrimaryDirectory } from "./workspace-card" +import { WorkspaceCard } from "./workspace-card" export function WorkspaceSidebar(props: { hide?: () => void } = {}) { const language = useLanguage() @@ -56,27 +57,13 @@ export function WorkspaceSidebar(props: { hide?: () => void } = {}) { if (active.id === workspace.id) active.clear() } - const [workspaceSearchQuery, setWorkspaceSearchQuery] = createSignal("") - - const filteredWorkspaces = createMemo(() => { - const query = workspaceSearchQuery().toLowerCase() - const all = workspaces() - if (!query) return all - return all.filter( - (workspace) => - workspace.name.toLowerCase().includes(query) || - workspace.description?.toLowerCase().includes(query) || - getPrimaryDirectory(workspace)?.path.toLowerCase().includes(query), - ) - }) - const runningIds = createMemo(() => - filteredWorkspaces() + workspaces() .filter((w) => isEnabled(w)) .map((w) => w.id), ) const idleIds = createMemo(() => - filteredWorkspaces() + workspaces() .filter((w) => !isEnabled(w)) .map((w) => w.id), ) @@ -151,103 +138,84 @@ export function WorkspaceSidebar(props: { hide?: () => void } = {}) {
-
-
- - setWorkspaceSearchQuery((e.target as HTMLInputElement).value)} - class="flex-1 min-w-0 h-full px-2 text-sm bg-transparent text-sidebar-foreground placeholder:text-sidebar-foreground/50 focus:outline-none" - /> - - + +
+ + {t("workspace.device.createWorkspace")} + + +
- -
- - - - + +
+ {t("workspace.createFromDevice")} +
+ + + + + + +
- -
- {t("workspace.createFromDevice")} -
- - - - - - + 0} + fallback={ +
+ + {t("workspace.createFromDevice.empty")}
-
- 0} - fallback={ -
- - {t("workspace.createFromDevice.empty")} -
- } - > -
    - - {(device) => { - const state = () => upgrade.upgradeMap()[device.deviceId] - const hasUpgrade = () => - !!device.canUpdate && - device.status === "online" && - !state() && - !upgrade.isRecentlyUpgraded(device.deviceId) - return ( - - ) - }} - -
-
- - -
+ } + > +
    + + {(device) => { + const state = () => upgrade.upgradeMap()[device.deviceId] + const hasUpgrade = () => + !!device.canUpdate && + device.status === "online" && + !state() && + !upgrade.isRecentlyUpgraded(device.deviceId) + return ( + + ) + }} + +
+ +
+
@@ -302,7 +270,7 @@ export function WorkspaceSidebar(props: { hide?: () => void } = {}) {
{(id) => } - +
{t("workspace.empty")} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index b87810fb5..3b1c62f3d 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -151,7 +151,7 @@ export interface MessagePartProps { defaultOpen?: boolean copy?: boolean showAssistantCopyPartID?: string | null - turnDurationMs?: number + durationMs?: number } export type PartComponent = Component @@ -552,7 +552,7 @@ function partDefaultOpen(part: PartType, shell = false, edit = false) { export function AssistantParts(props: { messages: AssistantMessage[] showAssistantCopyPartID?: string | null - turnDurationMs?: number + durationMs?: number working?: boolean showReasoningSummaries?: boolean shellToolDefaultOpen?: boolean @@ -643,7 +643,7 @@ export function AssistantParts(props: { part={item()!} message={message()!} showAssistantCopyPartID={props.showAssistantCopyPartID} - turnDurationMs={props.turnDurationMs} + durationMs={props.durationMs} defaultOpen={partDefaultOpen(item()!, props.shellToolDefaultOpen, props.editToolDefaultOpen)} copy={copy()} /> @@ -1232,7 +1232,7 @@ export function Part(props: MessagePartProps) { hideDetails={props.hideDetails} defaultOpen={props.defaultOpen} showAssistantCopyPartID={props.showAssistantCopyPartID} - turnDurationMs={props.turnDurationMs} + durationMs={props.durationMs} copy={props.copy} /> @@ -1456,15 +1456,8 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { const duration = createMemo(() => { if (props.message.role !== "assistant") return "" - const message = props.message as AssistantMessage - const completed = message.time.completed - const ms = - typeof props.turnDurationMs === "number" - ? props.turnDurationMs - : typeof completed === "number" - ? completed - message.time.created - : -1 - if (!(ms >= 0)) return "" + const ms = typeof props.durationMs === "number" ? props.durationMs : -1 + if (!(ms > 0)) return "" const total = Math.round(ms / 1000) if (total < 60) return i18n.t("ui.message.duration.seconds", { count: numfmt().format(total) }) const minutes = Math.floor(total / 60) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 16acedb83..e15cb70b3 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -230,3 +230,7 @@ [data-slot="session-turn-list"] { gap: 12px; } + +[data-message-id]:not(:has(*)) { + display: none; +} diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 6c1050056..9353ba9fb 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -329,21 +329,6 @@ export function SessionTurn( if (working()) return null return showAssistantCopyPartID() ?? null }) - const turnDurationMs = createMemo(() => { - const start = message()?.time.created - if (typeof start !== "number") return undefined - - const end = assistantMessages().reduce((max, item) => { - const completed = item.time.completed - if (typeof completed !== "number") return max - if (max === undefined) return completed - return Math.max(max, completed) - }, undefined) - - if (typeof end !== "number") return undefined - if (end < start) return undefined - return end - start - }) const assistantDerived = createMemo(() => { let visible = 0 let tail: "text" | "other" | undefined @@ -408,7 +393,6 @@ export function SessionTurn( void - classes?: { root?: string; content?: string; container?: string } + classes?: { root?: string; container?: string } }) { const data = useData() const i18n = useI18n() @@ -251,11 +249,14 @@ export function TimelineMessage(props: { }) const showThinking = createMemo(() => { - if (!working()) return false + if (status().type === "idle") return false if (status().type === "retry") return false - if (!isUser()) return false - if (showReasoningSummaries()) return !assistantHasVisible() - return true + const all = list(data.store.message?.[props.sessionID], [] as MessageType[]) + const last = all[all.length - 1] + if (!last || last.id !== props.message.id) return false + if (last.role !== "assistant") return true + const lastParts = list(data.store.part?.[last.id], emptyParts) + return !lastParts.some((p) => partVisible(p, showReasoningSummaries())) }) const error = createMemo(() => { @@ -273,186 +274,212 @@ export function TimelineMessage(props: { () => isAssistant() && (props.message as AssistantMessage).error?.name === "MessageAbortedError", ) - const turnDurationMs = createMemo(() => { - if (!isAssistant()) return undefined - const t = (props.message as AssistantMessage).time - if (typeof t.created !== "number" || typeof t.completed !== "number") return undefined - if (t.completed < t.created) return undefined - return t.completed - t.created + const userHasContent = createMemo(() => { + if (!isUser()) return true + if (compaction() || showThinking() || edited() > 0 || status().type === "retry") return true + for (const p of parts()) { + if (p.type === "text") { + if (!(p as { synthetic?: boolean }).synthetic && (p.text ?? "").trim()) return true + } else if (p.type === "file" || p.type === "agent" || p.type === "compaction") { + return true + } + } + return false }) - const autoScroll = createAutoScroll({ - working, - onUserInteracted: props.onUserInteracted, - overflowAnchor: "dynamic", + const durationMs = createMemo(() => { + if (!isAssistant()) return undefined + const self = props.message as AssistantMessage + const end = self.time.completed + if (typeof end !== "number") return undefined + const all = list(data.store.message?.[props.sessionID], [] as MessageType[]) + const idx = all.findIndex((m) => m.id === props.message.id) + if (idx === -1) return undefined + let start: number | undefined + for (let i = idx - 1; i >= 0; i--) { + const m = all[i] + if (!m) continue + if (m.role === "assistant") { + const c = (m as AssistantMessage).time.completed + if (typeof c === "number") { + start = c + break + } + } else if (m.role === "user") { + const c = m.time.created + if (typeof c === "number") { + start = c + break + } + } + } + if (typeof start !== "number") return undefined + if (end < start) return undefined + return end - start }) return ( -
-
-
- -
-
- -
- -
- -
-
- -
- - - - -
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ + + - - 0 && !working()}> -
- setDiffState("open", value)} - variant="ghost" - > - -
-
- - {i18n.t("ui.sessionReview.change.modified")} - - - {edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} - -
- - -
-
+
+ + + 0 && !working()}> +
+ setDiffState("open", value)} + variant="ghost" + > + +
+
+ + {i18n.t("ui.sessionReview.change.modified")} + + + {edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} + +
+ +
- - - -
- - setDiffState("expanded", Array.isArray(value) ? value : value ? [value] : []) - } - > - - {(diff) => { - const active = createMemo(() => diffState.expanded.includes(diff.file)) - const [visible, setVisible] = createSignal(false) - createEffect( - on( - active, - (value) => { - if (!value) { - setVisible(false) - return - } - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) - }) - }, - { defer: true }, - ), - ) - return ( - - - -
- - - - {`\u202A${getDirectory(diff.file)}\u202C`} - - - - {getFilename(diff.file)} - +
+
+ + + +
+ + setDiffState("expanded", Array.isArray(value) ? value : value ? [value] : []) + } + > + + {(diff) => { + const active = createMemo(() => diffState.expanded.includes(diff.file)) + const [visible, setVisible] = createSignal(false) + createEffect( + on( + active, + (value) => { + if (!value) { + setVisible(false) + return + } + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }, + { defer: true }, + ), + ) + return ( + + + +
+ + + + {`\u202A${getDirectory(diff.file)}\u202C`} -
- - - - - - -
-
-
-
- - -
- -
-
-
-
- ) - }} -
-
-
-
-
- -
- + + + {getFilename(diff.file)} + + +
+ + + + + + +
+
+ + + + +
+ +
+
+
+ + ) + }} + + +
+
+ +
+
+
- -
-
- -
- -
- -
-
- - - {errorText()} - - + +
+ + +
+
+ +
+ +
+
+ + + {errorText()} + +
+
-
+
) }