Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion docs/open-collection-gap-analysis/AGENT_PROGRESS.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ This task also owns the dark-mode contrast polish for WebSocket lifecycle button

| Area | Expected Work |
| --- | --- |
| Dark mode connect | In dark mode, use background `#2aa32a` and white text for the connected/connect action state. |
| Dark mode disconnect | In dark mode, use background `#a33e2a` and white text for the disconnect/destructive action state. |
| Dark mode connect | Use the same no-fill treatment as the collection Add Environment control: theme success text, normal input border, and theme success border on hover while preserving the current lifecycle button size and shape. |
| Dark mode disconnect | Use the same no-fill treatment as the collection Remove Environment control: theme error text, normal input border, and theme error border on hover while preserving the current lifecycle button size and shape. |
| States | Preserve disabled, hover, active, and focus-visible states with accessible contrast. |
| Theme safety | Keep light/high-contrast themes readable. Use scoped CSS and VS Code theme selectors where appropriate. |

Expand All @@ -87,7 +87,7 @@ This task also owns the dark-mode contrast polish for WebSocket lifecycle button
| Clear unresolved behavior | Missing variables in assertion fields are detected before execution or reported through deterministic assertion diagnostics. |
| WebSocket result visibility | WebSocket users can see runtime script/test/assertion/action output in the editor after the relevant lifecycle response or disconnect event. |
| Stable WebSocket layout | Result visibility does not bring back an empty HTTP response pane or stale HTTP controls for WebSocket requests. |
| Button contrast | Dark-mode WebSocket lifecycle buttons use white text, connect green `#2aa32a`, and disconnect red `#a33e2a` without black text. |
| Button contrast | WebSocket lifecycle buttons use theme success/error text and hover borders with no filled background, matching the collection environment toolbar style without black text or hardcoded OC-150 color fills. |
| Tests | Complete automated tests cover editor field behavior, runtime interpolation, unresolved-variable detection, WebSocket result visibility, button contrast, and shared runtime/protocol regressions. |

## Suggested Tests
Expand Down
22 changes: 11 additions & 11 deletions src/panels/requestPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1162,17 +1162,6 @@ window.missioPdfJsReady = import('${pdfJsUri}')

<!-- Response Section -->
<div class="response-section" id="responseSection">
<div class="websocket-session-panel" id="webSocketSessionPanel" style="display:none;">
<div class="websocket-session-header">
<span class="websocket-state-badge" id="webSocketStateBadge">Disconnected</span>
<span class="websocket-session-meta" id="webSocketSessionMeta"></span>
<div class="websocket-session-actions">
<button class="btn btn-secondary websocket-history-btn" id="wsCopyHistoryBtn" type="button" title="Copy WebSocket history">Copy</button>
<button class="btn btn-secondary websocket-history-btn" id="wsClearHistoryBtn" type="button" title="Clear visible WebSocket history">Clear</button>
</div>
</div>
<div class="websocket-history" id="webSocketHistory"></div>
</div>
<div class="loading-overlay" id="respLoading" style="display:none;">
<div class="spinner"></div>
<span>Sending request…</span>
Expand Down Expand Up @@ -1201,6 +1190,17 @@ window.missioPdfJsReady = import('${pdfJsUri}')
</div>
<div class="response-body">
<div class="tab-panel active" id="panel-resp-body">
<div class="websocket-session-panel" id="webSocketSessionPanel" style="display:none;">
<div class="websocket-session-header">
<span class="websocket-state-badge" id="webSocketStateBadge">Disconnected</span>
<span class="websocket-session-meta" id="webSocketSessionMeta"></span>
<div class="websocket-session-actions">
<button class="btn btn-secondary websocket-history-btn" id="wsCopyHistoryBtn" type="button" title="Copy WebSocket history">Copy</button>
<button class="btn btn-secondary websocket-history-btn" id="wsClearHistoryBtn" type="button" title="Clear visible WebSocket history">Clear</button>
</div>
</div>
<div class="websocket-history" id="webSocketHistory"></div>
</div>
<div class="empty-state" id="respEmpty">Send a request to see the response</div>
<div id="respBinaryOverlay" style="display:none;padding:32px;text-align:center;color:var(--vscode-foreground);font-family:var(--vscode-font-family,system-ui);">
<div style="font-size:14px;margin-bottom:8px;">Response body contains binary data</div>
Expand Down
58 changes: 50 additions & 8 deletions src/services/runtimeExecutionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,28 +316,48 @@ export class RuntimeExecutionService {

private _runAssertions(state: RuntimeState, assertions: Assertion[]): void {
for (const assertion of assertions) {
const visibleVariables = buildVisibleVariables(state.variables);
const expression = interpolateRuntimeTemplate(assertion.expression, visibleVariables);
const expected = assertion.value === undefined
? undefined
: interpolateRuntimeTemplate(String(assertion.value), visibleVariables);
const description = interpolateOptionalTemplate(descriptionToText(assertion.description), visibleVariables);
if (assertion.disabled) {
state.result.assertions.push({
expression: assertion.expression,
expression,
operator: assertion.operator,
expected: assertion.value,
expected,
actual: undefined,
passed: true,
skipped: true,
description: descriptionToText(assertion.description),
description,
});
continue;
}

const actual = evaluateExpression(assertion.expression, state);
const comparison = compareValues(actual, assertion.value, assertion.operator);
const unresolved = unresolvedTemplateNames(expression, expected, description);
if (unresolved.length > 0) {
state.result.assertions.push({
expression,
operator: assertion.operator,
expected,
actual: undefined,
passed: false,
description,
message: unresolvedAssertionMessage(unresolved),
});
continue;
}

const actual = evaluateExpression(expression, state);
const comparison = compareValues(actual, expected, assertion.operator);
state.result.assertions.push({
expression: assertion.expression,
expression,
operator: assertion.operator,
expected: assertion.value,
expected,
actual,
passed: comparison.passed,
description: descriptionToText(assertion.description),
description,
message: comparison.message,
});
}
Expand Down Expand Up @@ -535,6 +555,28 @@ function interpolateRuntimeTemplate(template: string, variables: Map<string, str
});
}

function interpolateOptionalTemplate(template: string | undefined, variables: Map<string, string>): string | undefined {
return template === undefined ? undefined : interpolateRuntimeTemplate(template, variables);
}

function unresolvedTemplateNames(...values: Array<string | undefined>): string[] {
const names = new Set<string>();
for (const value of values) {
if (!value) continue;
const re = varPatternGlobal();
let match: RegExpExecArray | null;
while ((match = re.exec(value)) !== null) {
names.add(match[1].trim());
}
}
return [...names];
}

function unresolvedAssertionMessage(names: string[]): string {
const label = names.length === 1 ? 'variable' : 'variables';
return `Unresolved assertion ${label}: ${names.map(name => `{{${name}}}`).join(', ')}`;
}

function resolveRuntimeBuiltin(name: string): string | undefined {
switch (name) {
case '$guid': return randomUUID();
Expand Down
4 changes: 4 additions & 0 deletions src/services/unresolvedVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export async function detectUnresolvedVars(
scanAllStrings(auth, varNames);
}

// Assertion templates are evaluated after after-response scripts/actions, so
// they may depend on variables produced during runtime. Leave unresolved
// assertion placeholders to deterministic runtime assertion diagnostics.

if (varNames.size === 0) return [];

// Resolve variables, then find which referenced names remain unresolved
Expand Down
72 changes: 56 additions & 16 deletions src/webview/requestPanel.css
Original file line number Diff line number Diff line change
Expand Up @@ -355,22 +355,47 @@ body {
.btn-primary { background: var(--btn-bg); color: var(--btn-fg); }
#sendBtn { min-width: 70px; text-align: center; }
#exportCopyBtn, #exportSaveBtn { width: 70px; text-align: center; }
#sendBtn:not(.btn-cancel), #wsSendBtn {
background: transparent;
border: 1px solid var(--input-border);
color: var(--btn-bg, #0078d4);
}
#sendBtn:not(.btn-cancel):hover:not(:disabled), #wsSendBtn:hover:not(:disabled) {
background: transparent;
border-color: var(--btn-bg, #0078d4);
}
.btn-primary:hover { background: var(--btn-hover); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary.sending { animation: pulse 1s infinite; }
.btn-primary.btn-cancel { background: var(--vscode-errorForeground, #f14c4c); animation: none; }
.btn-primary.btn-cancel {
background: transparent;
border: 1px solid var(--input-border);
color: var(--badge-error, #ef4444);
animation: none;
}
.btn-primary.btn-cancel:hover:not(:disabled) {
background: transparent;
border-color: var(--badge-error, #ef4444);
}
.request-editor-shell[data-protocol="websocket"] #sendBtn {
flex: 0 0 90px;
width: 90px;
min-width: 90px;
background: var(--badge-success);
color: var(--m-status-badge-text);
background: transparent;
border: 1px solid var(--input-border);
color: var(--badge-success, #22c55e);
}
.request-editor-shell[data-protocol="websocket"] #sendBtn:hover:not(:disabled) {
filter: brightness(1.08);
background: transparent;
border-color: var(--badge-success, #22c55e);
}
.request-editor-shell[data-protocol="websocket"] #sendBtn.ws-disconnect-state {
background: var(--badge-error);
background: transparent;
color: var(--badge-error, #ef4444);
}
.request-editor-shell[data-protocol="websocket"] #sendBtn.ws-disconnect-state:hover:not(:disabled) {
background: transparent;
border-color: var(--badge-error, #ef4444);
}
.request-editor-shell[data-protocol="websocket"] #sendBtn:disabled {
opacity: 0.5;
Expand Down Expand Up @@ -642,10 +667,32 @@ body {
.runtime-description {
flex-basis: 100%;
}
.runtime-var-field {
display: inline-flex;
min-width: 120px;
flex: 1 1 160px;
}
.runtime-var-field .runtime-input {
width: 100%;
min-width: 0;
flex: 1 1 auto;
}
.runtime-var-field .var-overlay {
padding: 5px 8px;
font-size: 12px;
line-height: 1.4;
border-radius: 4px;
}
.rt-assertion-expression,
.rt-action-selector {
flex-basis: 220px;
}
.rt-assertion-expression-field {
flex-basis: 220px;
}
.rt-assertion-description-field {
flex-basis: 100%;
}
.rt-action-name {
flex-basis: 140px;
}
Expand Down Expand Up @@ -730,21 +777,14 @@ body {
.websocket-session-panel {
display: flex;
flex-direction: column;
min-height: 112px;
max-height: 190px;
border-bottom: 1px solid var(--border);
background: var(--bg);
}
.response-section.websocket-response-ledger-only .websocket-session-panel {
flex: 1 1 auto;
min-height: 0;
max-height: none;
border-bottom: none;
background: var(--bg);
}
.response-section.websocket-response-ledger-only .response-bar,
.response-section.websocket-response-ledger-only #respTabs,
.response-section.websocket-response-ledger-only .resp-search-bar,
.response-section.websocket-response-ledger-only .response-body {
.response-section.websocket-response-tabs #respEmpty,
.response-section.websocket-response-tabs #respBodyWrap,
.response-section.websocket-response-tabs #respBinaryOverlay {
display: none !important;
}
.websocket-session-header {
Expand Down
Loading
Loading