Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
b9019396
Commit
b9019396
authored
May 18, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(ui): scope task artifact copy feedback
parent
e4fdf87e
Pipeline
#18473
failed
Changes
4
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
46 additions
and
10 deletions
+46
-10
project-workspace-executor.ts
apps/desktop/src/main/services/project-workspace-executor.ts
+6
-2
projectWorkspaceExecutorArtifacts.test.ts
apps/desktop/test/projectWorkspaceExecutorArtifacts.test.ts
+19
-0
TaskPanelView.tsx
apps/ui/src/features/tasks/TaskPanelView.tsx
+13
-8
taskPanelViewSource.test.ts
apps/ui/test/taskPanelViewSource.test.ts
+8
-0
No files found.
apps/desktop/src/main/services/project-workspace-executor.ts
View file @
b9019396
import
{
randomUUID
}
from
"node:crypto"
;
import
{
createHash
,
randomUUID
}
from
"node:crypto"
;
import
{
spawn
}
from
"node:child_process"
;
import
{
spawn
}
from
"node:child_process"
;
import
{
existsSync
}
from
"node:fs"
;
import
{
existsSync
}
from
"node:fs"
;
import
type
{
Dirent
}
from
"node:fs"
;
import
type
{
Dirent
}
from
"node:fs"
;
...
@@ -179,6 +179,10 @@ function toLocalDateTimeValue(date: Date): string {
...
@@ -179,6 +179,10 @@ function toLocalDateTimeValue(date: Date): string {
return
`
${
year
}
-
${
month
}
-
${
day
}
${
hour
}
:
${
minute
}
:
${
second
}
`
;
return
`
${
year
}
-
${
month
}
-
${
day
}
${
hour
}
:
${
minute
}
:
${
second
}
`
;
}
}
function
createWorkspaceArtifactId
(
relativePath
:
string
):
string
{
return
"artifact-"
+
createHash
(
"sha256"
).
update
(
relativePath
).
digest
(
"hex"
).
slice
(
0
,
32
);
}
function
normalizeSnapshotPath
(
value
:
string
):
string
{
function
normalizeSnapshotPath
(
value
:
string
):
string
{
return
value
.
replace
(
/
\\
/g
,
"/"
);
return
value
.
replace
(
/
\\
/g
,
"/"
);
}
}
...
@@ -285,7 +289,7 @@ export async function collectWorkspaceExecutionArtifacts(
...
@@ -285,7 +289,7 @@ export async function collectWorkspaceExecutionArtifacts(
const
artifacts
:
TaskPanelArtifact
[]
=
[];
const
artifacts
:
TaskPanelArtifact
[]
=
[];
for
(
const
entry
of
changedEntries
)
{
for
(
const
entry
of
changedEntries
)
{
artifacts
.
push
({
artifacts
.
push
({
id
:
"artifact-"
+
Buffer
.
from
(
entry
.
relativePath
).
toString
(
"base64url"
).
slice
(
0
,
32
),
id
:
createWorkspaceArtifactId
(
entry
.
relativePath
),
name
:
path
.
basename
(
entry
.
path
),
name
:
path
.
basename
(
entry
.
path
),
kind
:
inferWorkspaceArtifactKind
(
entry
.
path
),
kind
:
inferWorkspaceArtifactKind
(
entry
.
path
),
summary
:
await
readWorkspaceArtifactSummary
(
entry
.
path
,
input
.
assistantSummary
),
summary
:
await
readWorkspaceArtifactSummary
(
entry
.
path
,
input
.
assistantSummary
),
...
...
apps/desktop/test/projectWorkspaceExecutorArtifacts.test.ts
View file @
b9019396
...
@@ -86,3 +86,22 @@ test("infers image video and spreadsheet artifact kinds", async () => {
...
@@ -86,3 +86,22 @@ test("infers image video and spreadsheet artifact kinds", async () => {
)
)
})
})
})
})
test
(
"uses full relative path identity for artifact ids"
,
async
()
=>
{
await
withProjectRoot
(
async
(
projectRoot
)
=>
{
const
before
=
await
createWorkspaceArtifactSnapshot
(
projectRoot
)
const
sharedPrefix
=
"aaaaaaaaaaaaaaaaaaaaaaaa"
await
mkdir
(
path
.
join
(
projectRoot
,
sharedPrefix
),
{
recursive
:
true
})
await
writeFile
(
path
.
join
(
projectRoot
,
sharedPrefix
,
"one.md"
),
"one"
,
"utf8"
)
await
writeFile
(
path
.
join
(
projectRoot
,
sharedPrefix
,
"two.md"
),
"two"
,
"utf8"
)
const
artifacts
=
await
collectWorkspaceExecutionArtifacts
({
projectRoot
,
beforeSnapshot
:
before
,
assistantSummary
:
"assistant fallback"
})
assert
.
deepEqual
(
artifacts
.
map
((
artifact
)
=>
artifact
.
name
),
[
"one.md"
,
"two.md"
])
assert
.
equal
(
new
Set
(
artifacts
.
map
((
artifact
)
=>
artifact
.
id
)).
size
,
2
)
})
})
apps/ui/src/features/tasks/TaskPanelView.tsx
View file @
b9019396
...
@@ -153,8 +153,12 @@ function formatTaskPanelOutputTime(task: TaskPanelItem, artifact?: TaskPanelArti
...
@@ -153,8 +153,12 @@ function formatTaskPanelOutputTime(task: TaskPanelItem, artifact?: TaskPanelArti
return
`
${
dateText
}
${
hour
}
:
${
minute
}
:
${
second
}
`
return
`
${
dateText
}
${
hour
}
:
${
minute
}
:
${
second
}
`
}
}
function
getTaskPanelOutputRowKey
(
task
:
TaskPanelItem
,
artifact
:
TaskPanelArtifact
)
{
return
`
${
task
.
id
}
:
${
artifact
.
id
}
:
${
artifact
.
path
??
artifact
.
url
??
artifact
.
name
}
`
}
function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
const
[
copied
ArtifactId
,
setCopiedArtifactId
]
=
useState
(
""
)
const [copied
OutputRowKey, setCopiedOutputRowKey
] = useState("")
const copiedTimerRef = useRef<number | null>(null)
const copiedTimerRef = useRef<number | null>(null)
useEffect(() => {
useEffect(() => {
...
@@ -166,18 +170,18 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
...
@@ -166,18 +170,18 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
}, [])
}, [])
useEffect(() => {
useEffect(() => {
setCopied
ArtifactId
(
""
)
setCopied
OutputRowKey
("")
}, [outputs])
}, [outputs])
const
copyArtifactUrl
=
async
(
artifactId
:
string
,
artifactUrl
:
string
)
=>
{
const copyArtifactUrl = async (
outputRowKey
: string, artifactUrl: string) => {
try {
try {
await navigator.clipboard.writeText(artifactUrl)
await navigator.clipboard.writeText(artifactUrl)
setCopied
ArtifactId
(
artifactId
)
setCopied
OutputRowKey(outputRowKey
)
if (copiedTimerRef.current !== null) {
if (copiedTimerRef.current !== null) {
window.clearTimeout(copiedTimerRef.current)
window.clearTimeout(copiedTimerRef.current)
}
}
copiedTimerRef.current = window.setTimeout(() => {
copiedTimerRef.current = window.setTimeout(() => {
setCopied
ArtifactId
(
""
)
setCopied
OutputRowKey
("")
copiedTimerRef.current = null
copiedTimerRef.current = null
}, 1400)
}, 1400)
} catch {
} catch {
...
@@ -198,8 +202,9 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
...
@@ -198,8 +202,9 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
<div className="task-panel-output-list">
<div className="task-panel-output-list">
{outputs.length ? outputs.map(({ artifact, task }) => {
{outputs.length ? outputs.map(({ artifact, task }) => {
const artifactPath = artifact.path ?? artifact.url
const artifactPath = artifact.path ?? artifact.url
const outputRowKey = getTaskPanelOutputRowKey(task, artifact)
return (
return (
<
article
key=
{
task
.
id
+
"-"
+
artifact
.
id
}
className=
"task-panel-output-item"
>
<article key={
outputRowKey
} className="task-panel-output-item">
<span className="task-panel-output-icon" aria-hidden="true">
<span className="task-panel-output-icon" aria-hidden="true">
<TaskPanelOutputIcon artifact={artifact} />
<TaskPanelOutputIcon artifact={artifact} />
</span>
</span>
...
@@ -215,11 +220,11 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
...
@@ -215,11 +220,11 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
type="button"
type="button"
className="task-panel-output-url"
className="task-panel-output-url"
title={artifactPath}
title={artifactPath}
onClick=
{
()
=>
void
copyArtifactUrl
(
artifact
.
id
,
artifactPath
)
}
onClick={() => void copyArtifactUrl(
outputRowKey
, artifactPath)}
>
>
{artifactPath}
{artifactPath}
</button>
</button>
{
copied
ArtifactId
===
artifact
.
id
?
(
{copied
OutputRowKey === outputRowKey
? (
<span className="task-panel-artifact-copied" aria-live="polite">✅已复制</span>
<span className="task-panel-artifact-copied" aria-live="polite">✅已复制</span>
) : null}
) : null}
</div>
</div>
...
...
apps/ui/test/taskPanelViewSource.test.ts
View file @
b9019396
...
@@ -18,6 +18,14 @@ test("task panel output uses real artifact paths and produced timestamps", () =>
...
@@ -18,6 +18,14 @@ test("task panel output uses real artifact paths and produced timestamps", () =>
assert
.
match
(
source
,
/artifact
\.
producedAt
\s
*
\?\?
/
)
assert
.
match
(
source
,
/artifact
\.
producedAt
\s
*
\?\?
/
)
})
})
test
(
"task panel output copy feedback is scoped to the clicked output row"
,
()
=>
{
assert
.
match
(
source
,
/copiedOutputRowKey/
)
assert
.
match
(
source
,
/const outputRowKey = getTaskPanelOutputRowKey
\(
task, artifact
\)
/
)
assert
.
match
(
source
,
/copyArtifactUrl
\(
outputRowKey, artifactPath
\)
/
)
assert
.
match
(
source
,
/copiedOutputRowKey === outputRowKey/
)
assert
.
doesNotMatch
(
source
,
/copiedArtifactId === artifact
\.
id/
)
})
test
(
"task panel output summaries use a single-line visual ellipsis"
,
()
=>
{
test
(
"task panel output summaries use a single-line visual ellipsis"
,
()
=>
{
const
summaryBlock
=
cssBlock
(
taskStylesSource
,
".task-panel-output-main p"
)
const
summaryBlock
=
cssBlock
(
taskStylesSource
,
".task-panel-output-main p"
)
assert
.
match
(
summaryBlock
,
/overflow:
\s
*hidden;/
)
assert
.
match
(
summaryBlock
,
/overflow:
\s
*hidden;/
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment