Merge remote-tracking branch 'origin/master' into codex/new-task
This commit is contained in:
@@ -284,6 +284,13 @@ export function apply(ctx: Context): void {
|
||||
})
|
||||
},
|
||||
loadOlder: () => { void scoped.loadOlder() },
|
||||
forkAt: (seq) => {
|
||||
sessions.fork({ sessionId, atSeq: seq, increaseTitle: true })
|
||||
.then((childId) => { sessions.open(childId) })
|
||||
.catch(() => {
|
||||
// Fork or child-rename failure keeps the source view untouched.
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}, ChatView)
|
||||
|
||||
@@ -23,6 +23,10 @@ export interface AssistantMarkdownProps {
|
||||
interrupted?: boolean | undefined
|
||||
/** Unix epoch ms for the finalized IconActions clock; omitted while streaming. */
|
||||
time?: number | undefined
|
||||
/** Event sequence used as the fork boundary; omitted while streaming. */
|
||||
seq?: number | undefined
|
||||
/** Fork the session through the turn containing this finalized message. */
|
||||
onFork?: ((seq: number) => void) | undefined
|
||||
}
|
||||
|
||||
function firstLine(text: string): string {
|
||||
@@ -60,7 +64,7 @@ function ThinkRow({ text, running }: { text: string; running: boolean }) {
|
||||
}
|
||||
|
||||
export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
blocks, streaming, interrupted, time,
|
||||
blocks, streaming, interrupted, time, seq, onFork,
|
||||
}: AssistantMarkdownProps) {
|
||||
const last = blocks.length - 1
|
||||
// Tool-call heads render as tool rows in the chat view's grouping pass, so
|
||||
@@ -91,6 +95,7 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
text={copyText(blocks)}
|
||||
time={time}
|
||||
clock="end"
|
||||
onBranch={onFork === undefined || seq === undefined ? undefined : () => { onFork(seq) }}
|
||||
className={css.actions}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -230,7 +230,7 @@ function StreamingTail({ useSession, onGrow }: {
|
||||
* The chat view slot entry: pure component over the composed props (tool rows
|
||||
* render through the declared keyed hole's renderSlot share).
|
||||
*/
|
||||
export function ChatView({ useSession, useSessions, useStore, renderSlot, sessionId, openFile, loadOlder }: ChatViewSlotProps) {
|
||||
export function ChatView({ useSession, useSessions, useStore, renderSlot, sessionId, openFile, loadOlder, forkAt }: ChatViewSlotProps) {
|
||||
const nodes = useSession(s => s.nodes)
|
||||
// Workspace root off the session list row: path summaries display relative to it.
|
||||
const cwd = useSessions(s => s.byId[sessionId]?.cwd)
|
||||
@@ -377,6 +377,8 @@ export function ChatView({ useSession, useSessions, useStore, renderSlot, sessio
|
||||
streaming={false}
|
||||
interrupted={node.interrupted}
|
||||
time={node.time}
|
||||
seq={node.seq}
|
||||
onFork={forkAt}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -385,7 +387,7 @@ export function ChatView({ useSession, useSessions, useStore, renderSlot, sessio
|
||||
}
|
||||
/* v8 ignore next -- tool-result never reaches here: deriveChatFlow folds them into groups. */
|
||||
if (node.kind === 'tool-result') return null
|
||||
return <MessageItem key={item.key} node={node} />
|
||||
return <MessageItem key={item.key} node={node} onFork={forkAt} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Shared IconActions chrome for user and assistant messages: copy / branch
|
||||
// live (branch still a stub), date-aware clock, optional edit stub.
|
||||
// Shared IconActions chrome for user and assistant messages: copy live,
|
||||
// branch wired through onBranch, date-aware clock,
|
||||
// optional edit stub.
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
@@ -18,17 +19,19 @@ export interface MessageIconActionsProps {
|
||||
clock: 'start' | 'end'
|
||||
/** When true, append the stub edit control (user bubble). */
|
||||
edit?: boolean | undefined
|
||||
/** Fork the session at this message. */
|
||||
onBranch?: (() => void) | undefined
|
||||
/** Parent layout class composed onto the actions row. */
|
||||
className?: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy / branch (/ clock) IconActions row shared by user and assistant chrome.
|
||||
* @param props - Copy text, event time, clock side, optional edit, className.
|
||||
* @param props - Copy text, event time, clock side, optional edit, branch callback, className.
|
||||
* @returns The actions row element.
|
||||
*/
|
||||
export function MessageIconActions({
|
||||
text, time, clock, edit, className,
|
||||
text, time, clock, edit, onBranch, className,
|
||||
}: MessageIconActionsProps) {
|
||||
const day = useCalendarDay()
|
||||
const onCopy = useCallback(() => {
|
||||
@@ -48,7 +51,7 @@ export function MessageIconActions({
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label="在新对话中分支" side="bottom">
|
||||
<button type="button" className={css.action} aria-label="在新对话中分支">
|
||||
<button type="button" className={css.action} aria-label="在新对话中分支" onClick={onBranch}>
|
||||
<IconBranchOutline16 />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -16,6 +16,8 @@ import css from './MessageItem.module.css'
|
||||
|
||||
export interface MessageItemProps {
|
||||
node: UserMessageNode | SteeringMessageNode | ContextMessageNode | UnknownSurfaceNode
|
||||
/** Fork the session through the turn containing this message (user-bubble branch action). */
|
||||
onFork?: (seq: number) => void
|
||||
}
|
||||
|
||||
function contentText(content: readonly unknown[]): { text: string; rest: unknown[] } {
|
||||
@@ -61,7 +63,7 @@ function projectUserText(text: string): ReactNode {
|
||||
return <>{parts}</>
|
||||
}
|
||||
|
||||
export const MessageItem = memo(function MessageItem({ node }: MessageItemProps) {
|
||||
export const MessageItem = memo(function MessageItem({ node, onFork }: MessageItemProps) {
|
||||
switch (node.kind) {
|
||||
case 'user': {
|
||||
const { text, rest } = contentText(node.content)
|
||||
@@ -76,6 +78,7 @@ export const MessageItem = memo(function MessageItem({ node }: MessageItemProps)
|
||||
time={node.time}
|
||||
clock="start"
|
||||
edit
|
||||
onBranch={onFork === undefined ? undefined : () => { onFork(node.seq) }}
|
||||
className={css.actions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -421,6 +421,8 @@ export interface ChatViewInjected {
|
||||
*/
|
||||
openFile: (path: string) => void
|
||||
loadOlder: () => void
|
||||
/** Fork the session through the turn containing the message at `seq`, then open the child. */
|
||||
forkAt: (seq: number) => void
|
||||
}
|
||||
|
||||
/** Full chat-view component props: runtime share & the declared toolview/commandview holes' render share & store share & injected share. */
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
padding-top: 2px;
|
||||
border-radius: 14px 14px 0 0;
|
||||
background: var(--dsw-specific-tip);
|
||||
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
||||
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
||||
}
|
||||
|
||||
.panel::after {
|
||||
@@ -32,7 +34,52 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 4px 16px 4px 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header:focus-visible {
|
||||
outline: 2px solid var(--dsw-alias-label-tertiary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.header:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.count {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
font-family: Inter, var(--dsw-font-family);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.list {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
// The 'conversation.input.dock' SlotMap declaration lives in
|
||||
// ../contract/slots.ts beside the other input-region slots.
|
||||
import type { Context } from 'cordis'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useId, useState } from 'react'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
IconCheckOutline16, IconCloseOutline16, IconEditOutline16, IconTrashOutline16,
|
||||
IconCheckOutline16, IconChevronDownOutline14, IconChevronUpOutline14,
|
||||
IconCloseOutline16, IconEditOutline16, IconTrashOutline16,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { QueueAction, QueueItemId } from '../contract/queue.ts'
|
||||
import css from './QueueDock.module.css'
|
||||
@@ -22,18 +23,28 @@ export interface QueueDockInjected {
|
||||
/** Full props of a dock entry: InputZone owner share + session standard kit + global seat. */
|
||||
export type QueueDockProps = PropsRuntime<'conversation.input.dock'> & QueueDockInjected
|
||||
|
||||
/** Queue strip: one preview line per queued message; renders null when the queue is empty. */
|
||||
/**
|
||||
* Queue strip: one item renders directly; multiple items default to a
|
||||
* collapsible count header; an empty queue renders nothing.
|
||||
*/
|
||||
export function QueueDock({ useSession, updateQueue, notify }: QueueDockProps) {
|
||||
const queue = useSession(s => s.queue)
|
||||
const [editing, setEditing] = useState<{ id: QueueItemId; text: string } | null>(null)
|
||||
const [busy, setBusy] = useState<QueueItemId | null>(null)
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
const listId = useId()
|
||||
|
||||
useEffect(() => {
|
||||
if (queue.length === 0 && !collapsed) setCollapsed(true)
|
||||
if (editing !== null && !queue.some(row => row.id === editing.id)) setEditing(null)
|
||||
}, [editing, queue])
|
||||
}, [collapsed, editing, queue])
|
||||
|
||||
if (queue.length === 0) return null
|
||||
|
||||
const interactionActive = editing !== null || busy !== null
|
||||
const expanded = !collapsed || interactionActive
|
||||
const listVisible = queue.length === 1 || expanded
|
||||
|
||||
const applyAction = async (
|
||||
itemId: QueueItemId,
|
||||
action: QueueAction,
|
||||
@@ -63,8 +74,23 @@ export function QueueDock({ useSession, updateQueue, notify }: QueueDockProps) {
|
||||
return (
|
||||
<div className={css.dock}>
|
||||
<div className={css.panel}>
|
||||
<ul className={css.list}>
|
||||
{queue.map(row => (
|
||||
{queue.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.header}
|
||||
aria-controls={listId}
|
||||
aria-expanded={expanded}
|
||||
disabled={interactionActive}
|
||||
onClick={() => { setCollapsed(value => !value) }}
|
||||
>
|
||||
<span className={css.count}>{queue.length} 条排队消息</span>
|
||||
<span className={css.chevron} aria-hidden>
|
||||
{expanded ? <IconChevronDownOutline14 /> : <IconChevronUpOutline14 />}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<ul id={listId} className={css.list} hidden={!listVisible}>
|
||||
{listVisible && queue.map(row => (
|
||||
<li key={row.id} className={css.row}>
|
||||
{editing?.id === row.id
|
||||
? (
|
||||
|
||||
Reference in New Issue
Block a user