Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3d4b880
test(web-shell): add browser and lint harness
alijs Jul 7, 2026
ed24723
test(web-shell): harden browser smoke harness
alijs Jul 8, 2026
e718749
Merge remote-tracking branch 'origin/main' into chore/harness
alijs Jul 8, 2026
202cfad
Merge branch 'main' into chore/harness
harbin1053020115 Jul 8, 2026
975ab64
fix(web-shell): guard mock daemon model state
alijs Jul 8, 2026
5debd4a
Merge branch 'main' into chore/harness
harbin1053020115 Jul 8, 2026
42f1e84
Merge branch 'main' into chore/harness
harbin1053020115 Jul 8, 2026
8055415
test(web-shell): remove unused scenario harness
alijs Jul 8, 2026
042dd51
Merge branch 'main' into chore/harness
harbin1053020115 Jul 8, 2026
8c0cb13
fix(web-shell): remove stale lint disables
alijs Jul 8, 2026
0ce3022
test(web-shell): make matchMedia stub writable
alijs Jul 8, 2026
c39bd78
Merge branch 'main' into chore/harness
harbin1053020115 Jul 8, 2026
e952998
Merge branch 'main' into chore/harness
harbin1053020115 Jul 8, 2026
c919817
Merge branch 'main' into chore/harness
harbin1053020115 Jul 8, 2026
680c88f
fix(web-shell): exclude tests from package typecheck
alijs Jul 9, 2026
a7bb632
test(web-shell): tighten mock daemon route contract
alijs Jul 9, 2026
c5e7576
Update packages/web-shell/client/e2e/utils/mockDaemon.ts
harbin1053020115 Jul 9, 2026
a1564a4
Merge branch 'main' into chore/harness
wenshao Jul 9, 2026
35fb7f7
test(web-shell): clear stale SSE connections
alijs Jul 9, 2026
0913034
ci(web-shell): gate smoke on full CI profile
alijs Jul 9, 2026
f5c3b15
Merge branch 'main' into chore/harness
yiliang114 Jul 9, 2026
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
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,56 @@ jobs:
name: 'coverage-reports-22.x-ubuntu-latest'
path: 'packages/*/coverage'

web_shell_e2e_smoke:
name: 'web-shell E2E Smoke (ubuntu-latest, Node 22.x)'
needs: 'classify_pr'
Comment thread
harbin1053020115 marked this conversation as resolved.
Resolved
if: "${{ !cancelled() && github.event_name == 'pull_request' && needs.classify_pr.outputs.skip_ci != 'true' }}"
runs-on: 'ubuntu-latest'
timeout-minutes: 20
permissions:
contents: 'read'
steps:
- name: 'Checkout'
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
with:
ref: "${{ format('refs/pull/{0}/head', github.event.pull_request.number) }}"
fetch-depth: 1

- name: 'Set up Node.js 22.x'
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version: '22.x'
cache: 'npm'
cache-dependency-path: 'package-lock.json'
registry-url: 'https://registry.npmjs.org/'

- name: 'Configure npm for rate limiting'
run: |-
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set fetch-retries 5
npm config set fetch-timeout 300000

- name: 'Install dependencies'
run: |-
npm ci --prefer-offline --no-audit --progress=false

- name: 'Install Playwright Chromium'
run: 'npx playwright install --with-deps chromium'

- name: 'Run web-shell browser smoke'
run: 'npm run test:e2e:smoke --workspace=packages/web-shell'

- name: 'Upload web-shell Playwright artifacts'
if: '${{ always() }}'
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
with:
name: 'web-shell-e2e-smoke'
path: |-
packages/web-shell/client/e2e/test-results
packages/web-shell/client/e2e/playwright-report
if-no-files-found: 'ignore'

# macOS/Windows: slowest/costliest runners, rare platform regressions — run
# only in the merge queue. Skipped on PR (ubuntu is the fast PR signal) and on
# push (the queue already tested the merged tree, so a post-merge re-run is
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,47 @@ jobs:
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
run: 'npm run test:e2e'

web-shell-browser-regression:
name: 'web-shell Browser Regression'
runs-on: 'ubuntu-latest'
if: |-
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
steps:
- name: 'Checkout'
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2

- name: 'Set up Node.js'
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: 'package-lock.json'
registry-url: 'https://registry.npmjs.org/'

- name: 'Configure npm for rate limiting'
run: |-
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set fetch-retries 5
npm config set fetch-timeout 300000

- name: 'Install dependencies'
run: |-
npm ci --prefer-offline --no-audit --progress=false

- name: 'Install Playwright Chromium'
run: 'npx playwright install --with-deps chromium'

- name: 'Run web-shell browser regression'
run: 'npm run test:e2e --workspace=packages/web-shell'

- name: 'Upload web-shell Playwright artifacts'
if: '${{ always() }}'
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
with:
name: 'web-shell-browser-regression'
path: |-
packages/web-shell/client/e2e/test-results
packages/web-shell/client/e2e/playwright-report
if-no-files-found: 'ignore'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ bundle
# Test report files
junit.xml
packages/*/coverage/
packages/web-shell/client/e2e/playwright-report/
packages/web-shell/client/e2e/test-results/

# PR body draft
pr_body.md
Expand Down
76 changes: 76 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,82 @@ export default tseslint.config(
'default-case': 'error',
},
},
{
files: [
'packages/web-shell/client/**/*.{ts,tsx}',
'packages/web-shell/*.config.ts',
],
plugins: {
import: importPlugin,
},
settings: {
'import/resolver': {
node: true,
},
},
languageOptions: {
globals: {
...globals.browser,
...globals.es2021,
...globals.node,
},
},
rules: {
'react/prop-types': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{ disallowTypeAnnotations: false },
],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'object-shorthand': 'error',
'prefer-const': ['error', { destructuring: 'all' }],
},
},
{
files: [
'packages/web-shell/client/**/*.test.{ts,tsx}',
'packages/web-shell/client/test/**/*.{ts,tsx}',
],
plugins: {
vitest,
},
languageOptions: {
globals: {
...globals.browser,
...globals.es2021,
...globals.vitest,
},
},
rules: {
...vitest.configs.recommended.rules,
'vitest/expect-expect': 'off',
'vitest/no-commented-out-tests': 'off',
'no-console': 'off',
},
},
{
files: ['packages/web-shell/client/e2e/**/*.{ts,tsx}'],
languageOptions: {
globals: {
...globals.browser,
...globals.es2021,
...globals.node,
},
},
rules: {
'no-console': 'off',
},
},
{
// Enforce kebab-case filenames
files: ['packages/core/src/**/*.ts', 'packages/cli/src/**/*.ts'],
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 1 addition & 12 deletions packages/web-shell/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -595,12 +595,6 @@ function isAlreadyDispatched(error: unknown): error is AlreadyDispatchedError {
);
}

function logSessionNoticesHook(notices: readonly DaemonSessionNotice[]): void {
if (notices.length > 0) {
console.info('[web-shell] useSessionNotices()', { notices });
}
}

function shouldToastNotice(notice: DaemonSessionNotice): boolean {
return (
notice.category === 'validation' ||
Expand Down Expand Up @@ -1704,11 +1698,9 @@ export function App({
(error: unknown, fallback: string) => {
if (isAbortError(error)) return;
if (isDaemonTurnError(error)) {
console.debug('[web-shell] turn error rendered in transcript', error);
return;
}
if (isAlreadyDispatched(error)) {
console.debug('[web-shell] error already handled by notice', error);
return;
}
const message = formatError(error, fallback);
Expand Down Expand Up @@ -1800,13 +1792,10 @@ export function App({
);

useEffect(() => {
logSessionNoticesHook(notices);
for (const notice of notices) {
if (shouldToastNotice(notice)) {
pushToast(toastToneFromNotice(notice), notice.message);
} else if (notice.category === 'lifecycle') {
console.debug('[web-shell] daemon notice', notice);
} else {
} else if (notice.category !== 'lifecycle') {
console.warn('[web-shell] daemon notice', notice);
}
dismissNotice(notice.id);
Expand Down
15 changes: 12 additions & 3 deletions packages/web-shell/client/components/ChatEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,12 @@ function SlashCommandPanel({
} as CSSProperties;

return createPortal(
<div ref={panelRef} className={styles.slashPortalLayer} style={themeVars}>
<div
ref={panelRef}
className={styles.slashPortalLayer}
style={themeVars}
data-web-shell-slash-menu
>
<div
className={styles.slashPanel}
style={positionedPanelStyle}
Expand Down Expand Up @@ -1299,10 +1304,11 @@ export const ChatEditor = memo(
const showCancelButton = isRunning && !core.hasContent;

return (
<div className={styles.editorShell} data-composer>
<div className={styles.editorShell} data-composer data-web-shell-composer>
<div
ref={containerRef}
className={styles.container}
data-web-shell-composer-surface
data-dac-glow
onClick={() => {
setModeDropdownOpen(false);
Expand Down Expand Up @@ -1457,7 +1463,7 @@ export const ChatEditor = memo(
!
</span>
)}
<div ref={core.containerRef} />
<div ref={core.containerRef} data-web-shell-composer-editor />
</div>
<div className={styles.toolbar}>
<div className={styles.toolbarLeading}>
Expand Down Expand Up @@ -1486,6 +1492,7 @@ export const ChatEditor = memo(
<button
ref={modeBtnRef}
className={styles.toolBtn}
data-web-shell-mode-button
onClick={(e) => {
e.stopPropagation();
core.closeSlashMenu();
Expand Down Expand Up @@ -1521,6 +1528,7 @@ export const ChatEditor = memo(
<button
ref={modelBtnRef}
className={styles.toolBtn}
data-web-shell-model-button
onClick={(e) => {
e.stopPropagation();
core.closeSlashMenu();
Expand Down Expand Up @@ -1646,6 +1654,7 @@ export const ChatEditor = memo(
? !onCancel
: core.disabled || !core.hasContent
}
data-web-shell-composer-submit
onClick={(e) => {
e.stopPropagation();
if (isPreparing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ const userShellMsg = (
id,
role: 'user_shell',
command: 'npm test',
output: '',
});
const toolMsg = (id: string): ToolGroupMessage => ({
id,
Expand Down
20 changes: 14 additions & 6 deletions packages/web-shell/client/components/MessageList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ function makeBackgroundAgentToolGroup(id: string): Message {
};
}

function makeMultiToolGroup(id: string): Message {
function makeMultiToolGroup(
id: string,
): Extract<Message, { role: 'tool_group' }> {
return {
id,
role: 'tool_group',
Expand All @@ -126,15 +128,19 @@ function makeMultiToolGroup(id: string): Message {
};
}

function makeUserMessage(id: string): Message {
function makeUserMessage(id: string): Extract<Message, { role: 'user' }> {
return { id, role: 'user', content: 'hello' };
}

function makeUserShellMessage(id: string): Message {
return { id, role: 'user_shell', command: 'npm test' };
function makeUserShellMessage(
id: string,
): Extract<Message, { role: 'user_shell' }> {
return { id, role: 'user_shell', command: 'npm test', output: '' };
}

function makeAssistantMessage(id: string): Message {
function makeAssistantMessage(
id: string,
): Extract<Message, { role: 'assistant' }> {
return { id, role: 'assistant', content: 'response' };
}

Expand Down Expand Up @@ -310,7 +316,9 @@ describe('groupParallelAgents', () => {
});

describe('getTurnTimelineNode', () => {
const item = (message: Message): DisplayItem => ({
const item = (
message: Message,
): Extract<DisplayItem, { type: 'message' }> => ({
type: 'message',
key: message.id,
message,
Expand Down
Loading
Loading