Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
C
coderai
Project
Project
Details
Activity
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
nexlab
coderai
Commits
e91046c6
Commit
e91046c6
authored
May 06, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: restore minimal studio pipeline editor
parent
1971ce1d
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
144 additions
and
45 deletions
+144
-45
chat.html
codai/admin/templates/chat.html
+127
-45
test_studio_composed_surfaces.py
tests/test_studio_composed_surfaces.py
+17
-0
No files found.
codai/admin/templates/chat.html
View file @
e91046c6
...
...
@@ -148,6 +148,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
@media
(
max-width
:
720px
)
{
.gen-wrap
{
flex-direction
:
column
;
}
.gen-ctrl
{
width
:
100%
;
min-width
:
0
;
max-width
:
none
;
border-right
:
none
;
border-bottom
:
1px
solid
var
(
--border
);
}
.pipe-shell
{
grid-template-columns
:
1
fr
;
}
}
/* ── Capability cards ─────────────────────────────────────────── */
...
...
@@ -217,6 +218,12 @@ a.dl { display:inline-block; margin-top:.4rem; }
/* ── Pipeline ─────────────────────────────────────────────────── */
.pipe-panel
{
flex
:
1
;
overflow-y
:
auto
;
padding
:
1.2rem
;
display
:
flex
;
flex-direction
:
column
;
gap
:
.6rem
;
}
.pipe-toolbar
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
.75rem
;
flex-wrap
:
wrap
;
}
.pipe-shell
{
display
:
grid
;
grid-template-columns
:
minmax
(
280px
,
360px
)
minmax
(
0
,
1
fr
);
gap
:
.9rem
;
align-items
:
start
;
}
.pipe-list
,
.pipe-editor
{
display
:
flex
;
flex-direction
:
column
;
gap
:
.6rem
;
}
.pipe-empty-state
{
border
:
1px
dashed
var
(
--border-2
);
border-radius
:
8px
;
padding
:
1rem
;
color
:
var
(
--text-2
);
background
:
var
(
--surface-1
);
}
.pipe-list-card
{
background
:
var
(
--surface-1
);
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
padding
:
.8rem
.9rem
;
display
:
flex
;
flex-direction
:
column
;
gap
:
.45rem
;
cursor
:
pointer
;
}
.pipe-list-card.active
{
border-color
:
var
(
--accent
);
box-shadow
:
0
0
0
1px
rgba
(
99
,
102
,
241
,
.2
);
}
.pipe-card
{
background
:
var
(
--surface-1
);
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
overflow
:
hidden
;
}
.pipe-card
summary
{
list-style
:
none
;
padding
:
.8rem
1rem
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
flex-start
;
...
...
@@ -1358,9 +1365,14 @@ a.dl { display:inline-block; margin-top:.4rem; }
</div>
</details>
<!-- Custom pipeline builder + saved custom pipelines -->
<div
id=
"custom-pipe-cards"
></div>
<div
class=
"pipe-toolbar"
>
<button
class=
"btn btn-primary"
onclick=
"createPipeline()"
>
Create pipeline
</button>
<div
class=
"muted small"
id=
"pipe-status"
>
Loading pipelines…
</div>
</div>
<div
class=
"pipe-shell"
>
<div
class=
"pipe-list"
id=
"pipe-list"
></div>
<div
class=
"pipe-editor"
id=
"pipe-editor"
>
<div
class=
"pipe-empty-state"
id=
"pipe-empty-state"
>
No pipeline selected.
</div>
<!-- Builder card (always open) -->
<details
class=
"pipe-card"
id=
"pipe-builder-card"
open
>
<summary>
+ Build New Pipeline
...
...
@@ -1388,6 +1400,9 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div
id=
"pb-out"
></div>
</div>
</details>
<div
id=
"custom-pipe-cards"
></div>
</div>
</div>
</div>
<!-- pipe-panel -->
</div>
<!-- panel-pipe -->
...
...
@@ -3792,6 +3807,7 @@ let _stepTypes = []; // [{type, label, params:[...]}]
let
_pbSteps
=
[];
// current builder steps [{type, label, params:{}}]
let
_customPipelines
=
[];
// saved custom pipelines
let
_editingPipelineId
=
null
;
const
pipelineState
=
{
items
:
[],
selectedId
:
null
};
async
function
initPipelineBuilder
()
{
try
{
...
...
@@ -3801,43 +3817,100 @@ async function initPipelineBuilder() {
]);
_stepTypes
=
typesRes
.
step_types
||
[];
_customPipelines
=
pipesRes
.
pipelines
||
[];
pipelineState
.
items
=
_customPipelines
;
if
(
pipelineState
.
selectedId
&&
!
pipelineState
.
items
.
some
(
p
=>
p
.
id
===
pipelineState
.
selectedId
))
{
pipelineState
.
selectedId
=
null
;
}
// Populate step type selector
const
sel
=
$
(
'pb-add-type'
);
if
(
sel
)
sel
.
innerHTML
=
_stepTypes
.
map
(
t
=>
`<option value="
${
t
.
type
}
">
${
t
.
label
}
</option>`
).
join
(
''
);
renderPipelineList
();
renderCustomPipelineCards
();
}
catch
(
e
)
{
console
.
warn
(
'Pipeline builder init failed:'
,
e
);
}
}
catch
(
e
)
{
const
status
=
$
(
'pipe-status'
);
if
(
status
)
status
.
textContent
=
'Pipeline loading failed.'
;
console
.
warn
(
'Pipeline builder init failed:'
,
e
);
}
}
function
renderPipelineList
()
{
const
container
=
$
(
'pipe-list'
);
const
status
=
$
(
'pipe-status'
);
if
(
!
container
)
return
;
const
items
=
pipelineState
.
items
||
[];
if
(
!
items
.
length
)
{
container
.
innerHTML
=
'<div class="pipe-empty-state">No saved pipelines yet. Create pipeline to start building one.</div>'
;
if
(
status
)
status
.
textContent
=
'No saved pipelines yet.'
;
return
;
}
if
(
status
)
status
.
textContent
=
`
${
items
.
length
}
pipeline
${
items
.
length
===
1
?
''
:
's'
}
available`
;
container
.
innerHTML
=
items
.
map
(
p
=>
`
<button type="button" class="pipe-list-card
${
pipelineState
.
selectedId
===
p
.
id
?
'active'
:
''
}
" onclick="openPipeline('
${
p
.
id
}
')">
<div class="pipe-title">
${
escapeHtml
(
p
.
name
||
p
.
id
)}
</div>
<div class="pipe-summary">
${
escapeHtml
(
p
.
description
||
'Custom multi-step pipeline for combining model and utility actions.'
)}
</div>
<div class="pipe-tags"><span class="pipe-tag">
${(
p
.
steps
||
[]).
length
}
steps</span><span class="pipe-tag">custom</span></div>
</button>
`
).
join
(
''
);
}
function
renderCustomPipelineCards
()
{
const
container
=
$
(
'custom-pipe-cards'
);
const
empty
=
$
(
'pipe-empty-state'
);
if
(
!
container
)
return
;
container
.
innerHTML
=
_customPipelines
.
map
(
p
=>
`
<details class="pipe-card">
const
selected
=
_customPipelines
.
find
(
p
=>
p
.
id
===
pipelineState
.
selectedId
);
if
(
!
selected
)
{
container
.
innerHTML
=
''
;
if
(
empty
)
empty
.
style
.
display
=
''
;
return
;
}
if
(
empty
)
empty
.
style
.
display
=
'none'
;
container
.
innerHTML
=
`
<details class="pipe-card" open>
<summary>
<div class="pipe-head">
<div class="pipe-title">
${
p
.
name
||
p
.
id
}
</div>
<div class="pipe-summary">
${
p
.
description
||
'Custom multi-step pipeline for combining model and utility actions.'
}
</div>
<div class="pipe-title">
${
escapeHtml
(
selected
.
name
||
selected
.
id
)
}
</div>
<div class="pipe-summary">
${
escapeHtml
(
selected
.
description
||
'Custom multi-step pipeline for combining model and utility actions.'
)
}
</div>
<div class="pipe-steps">
${
p
.
steps
.
map
(
s
=>
`<span class="pipe-step">
${
s
.
label
||
s
.
type
}
</span>`
).
join
(
'<span class="pipe-arrow">→</span>'
)}
${
(
selected
.
steps
||
[]).
map
(
s
=>
`<span class="pipe-step">
${
escapeHtml
(
s
.
label
||
s
.
type
)
}
</span>`
).
join
(
'<span class="pipe-arrow">→</span>'
)}
</div>
<div class="pipe-tags"><span class="pipe-tag">custom</span><span class="pipe-tag">pipeline</span><span class="pipe-tag">
${
p
.
steps
.
length
}
steps</span></div>
<div class="pipe-tags"><span class="pipe-tag">custom</span><span class="pipe-tag">pipeline</span><span class="pipe-tag">
${
(
selected
.
steps
||
[])
.
length
}
steps</span></div>
</div>
</summary>
<div class="pipe-card-body">
${
p
.
description
?
`<p style="font-size:12px;color:var(--text-2);margin:0 0 .4rem">
${
p
.
description
}
</p>`
:
''
}
<div class="frow"><label class="fl">Input</label><input id="cpr-input-
${
p
.
id
}
" class="fi" placeholder="{{ '{{' }}input{{ '}}' }} value"></div>
${
selected
.
description
?
`<p style="font-size:12px;color:var(--text-2);margin:0 0 .4rem">
${
escapeHtml
(
selected
.
description
)
}
</p>`
:
''
}
<div class="frow"><label class="fl">Input</label><input id="cpr-input-
${
selected
.
id
}
" class="fi" placeholder="{{ '{{' }}input{{ '}}' }} value"></div>
<div style="display:flex;gap:.4rem;margin-top:.4rem;flex-wrap:wrap">
<button class="btn btn-primary btn-sm" onclick="runCustomPipeline('
${
p
.
id
}
')">▶ Run</button>
<button class="btn btn-ghost btn-sm" onclick="editCustomPipeline('
${
p
.
id
}
')">✎ Edit</button>
<button class="btn btn-ghost btn-sm" style="color:var(--red)" onclick="deleteCustomPipeline('
${
p
.
id
}
')">✕ Delete</button>
<button class="btn btn-primary btn-sm" onclick="runCustomPipeline('
${
selected
.
id
}
')">▶ Run</button>
<button class="btn btn-ghost btn-sm" onclick="editCustomPipeline('
${
selected
.
id
}
')">✎ Edit</button>
<button class="btn btn-ghost btn-sm" style="color:var(--red)" onclick="deleteCustomPipeline('
${
selected
.
id
}
')">✕ Delete</button>
</div>
<div class="progress" id="cpr-prog-
${
p
.
id
}
"></div>
<div id="cpr-out-
${
p
.
id
}
"></div>
<div class="progress" id="cpr-prog-
${
selected
.
id
}
"></div>
<div id="cpr-out-
${
selected
.
id
}
"></div>
</div>
</details>`
).
join
(
''
);
</details>`
;
}
function
createPipeline
()
{
pipelineState
.
selectedId
=
null
;
_editingPipelineId
=
null
;
_pbSteps
=
[];
$
(
'pb-name'
).
value
=
''
;
$
(
'pb-desc'
).
value
=
''
;
$
(
'pb-input'
).
value
=
''
;
$
(
'pb-prog'
).
textContent
=
'Creating a new pipeline draft.'
;
renderBuilderSteps
();
renderPipelineList
();
renderCustomPipelineCards
();
$
(
'pipe-builder-card'
).
open
=
true
;
$
(
'pipe-builder-card'
).
scrollIntoView
({
behavior
:
'smooth'
,
block
:
'start'
});
}
function
openPipeline
(
id
)
{
pipelineState
.
selectedId
=
id
;
renderPipelineList
();
renderCustomPipelineCards
();
}
function
pbAddStep
()
{
...
...
@@ -3920,6 +3993,9 @@ async function pbSave() {
_editingPipelineId
=
res
.
pipeline
?.
id
;
}
_customPipelines
=
(
await
fetch
(
'/v1/pipelines/custom'
).
then
(
r
=>
r
.
json
())).
pipelines
||
[];
pipelineState
.
items
=
_customPipelines
;
pipelineState
.
selectedId
=
_editingPipelineId
;
renderPipelineList
();
renderCustomPipelineCards
();
$
(
'pb-prog'
).
textContent
=
'Saved ✓'
;
}
catch
(
e
)
{
$
(
'pb-prog'
).
textContent
=
'Error: '
+
e
.
message
;
}
...
...
@@ -3951,11 +4027,14 @@ async function runCustomPipeline(id) {
function
editCustomPipeline
(
id
)
{
const
p
=
_customPipelines
.
find
(
x
=>
x
.
id
===
id
);
if
(
!
p
)
return
;
pipelineState
.
selectedId
=
id
;
_editingPipelineId
=
id
;
$
(
'pb-name'
).
value
=
p
.
name
||
''
;
$
(
'pb-desc'
).
value
=
p
.
description
||
''
;
_pbSteps
=
p
.
steps
.
map
(
s
=>
({...
s
,
params
:{...
s
.
params
}}));
renderBuilderSteps
();
renderPipelineList
();
renderCustomPipelineCards
();
$
(
'pipe-builder-card'
).
open
=
true
;
$
(
'pipe-builder-card'
).
scrollIntoView
({
behavior
:
'smooth'
});
$
(
'pb-prog'
).
textContent
=
`Editing "
${
p
.
name
}
"`
;
...
...
@@ -3966,7 +4045,10 @@ async function deleteCustomPipeline(id) {
try
{
await
fetch
(
`/v1/pipelines/custom/
${
id
}
`
,
{
method
:
'DELETE'
});
_customPipelines
=
_customPipelines
.
filter
(
p
=>
p
.
id
!==
id
);
pipelineState
.
items
=
_customPipelines
;
if
(
pipelineState
.
selectedId
===
id
)
pipelineState
.
selectedId
=
null
;
if
(
_editingPipelineId
===
id
)
{
_editingPipelineId
=
null
;
_pbSteps
=
[];
renderBuilderSteps
();
}
renderPipelineList
();
renderCustomPipelineCards
();
}
catch
(
e
)
{
alert
(
'Delete failed: '
+
e
.
message
);
}
}
...
...
tests/test_studio_composed_surfaces.py
View file @
e91046c6
...
...
@@ -366,3 +366,20 @@ def test_studio_output_surfaces_capability_warnings():
assert
"cap-output-note"
in
text
assert
"renderCapabilityOutputNote"
in
text
assert
"renderOutputCapabilityNotes"
in
text
def
test_pipeline_tab_exposes_create_action_and_empty_state
():
template_path
=
"/storage/coderai/.worktrees/web-admin-polish/codai/admin/templates/chat.html"
text
=
open
(
template_path
,
"r"
,
encoding
=
"utf-8"
)
.
read
()
assert
"Create pipeline"
in
text
assert
"pipe-empty-state"
in
text
assert
"renderPipelineList"
in
text
def
test_pipeline_tab_exposes_editor_shell
():
template_path
=
"/storage/coderai/.worktrees/web-admin-polish/codai/admin/templates/chat.html"
text
=
open
(
template_path
,
"r"
,
encoding
=
"utf-8"
)
.
read
()
assert
"pipe-editor"
in
text
assert
"openPipeline"
in
text
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