Skip to content
Merged
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
888 changes: 846 additions & 42 deletions crates/ironclaw_gateway/static/app.js

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions crates/ironclaw_gateway/static/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,30 @@ I18n.register('en', {
'missions.cadence': 'Cadence',
'missions.threads': 'Threads',
'missions.status': 'Status',
'missions.progress': 'Progress',
'missions.actions': 'Actions',
'missions.noConfigured': 'No missions found. Ask the assistant to create one.',
'missions.summary.total': 'Total',
'missions.summary.active': 'Active',
'missions.summary.paused': 'Paused',
'missions.summary.completed': 'Completed',
'missions.summary.failed': 'Failed',
'activity.empty': 'No recent jobs or missions',
'activity.kind.job': 'Job',
'activity.kind.mission': 'Mission',
'activity.waitingApproval': 'Waiting for approval',
'activity.waitingAuth': 'Waiting for auth',
'activity.resuming': 'Resuming',
'activity.starting': 'Starting',
'activity.processing': 'Processing...',
'activity.thinking': 'Thinking',
'activity.usingTool': 'Using {name}',
'activity.failedTool': 'Failed {name}',
'activity.streamingResponse': 'Streaming response',
'activity.working': 'Working',
'activity.runningTool': 'Running {name}',
'activity.finishedTool': 'Finished {name}',
'activity.tool': 'tool',

// Routines Tab
'routines.summary': 'Routines Summary',
Expand Down
17 changes: 17 additions & 0 deletions crates/ironclaw_gateway/static/i18n/ko.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,30 @@ I18n.register('ko', {
'missions.cadence': '주기',
'missions.threads': '스레드',
'missions.status': '상태',
'missions.progress': '진행',
'missions.actions': '작업',
'missions.noConfigured': '미션이 없습니다. 어시스턴트에게 미션 생성을 요청하세요.',
'missions.summary.total': '전체',
'missions.summary.active': '활성',
'missions.summary.paused': '일시정지',
'missions.summary.completed': '완료됨',
'missions.summary.failed': '실패',
'activity.empty': '최근 작업 또는 미션이 없습니다',
'activity.kind.job': '작업',
'activity.kind.mission': '미션',
'activity.waitingApproval': '승인 대기 중',
'activity.waitingAuth': '인증 대기 중',
'activity.resuming': '재개 중',
'activity.starting': '시작 중',
'activity.processing': '처리 중...',
'activity.thinking': '생각 중',
'activity.usingTool': '{name} 사용 중',
'activity.failedTool': '{name} 실패',
'activity.streamingResponse': '응답 스트리밍 중',
'activity.working': '작업 중',
'activity.runningTool': '{name} 실행 중',
'activity.finishedTool': '{name} 완료',
'activity.tool': '도구',

// 루틴 탭
'routines.summary': '루틴 요약',
Expand Down
17 changes: 17 additions & 0 deletions crates/ironclaw_gateway/static/i18n/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,30 @@ I18n.register('zh-CN', {
'missions.cadence': '节奏',
'missions.threads': '线程',
'missions.status': '状态',
'missions.progress': '进度',
'missions.actions': '操作',
'missions.noConfigured': '暂无使命。请让助手创建一个。',
'missions.summary.total': '总计',
'missions.summary.active': '活跃',
'missions.summary.paused': '暂停',
'missions.summary.completed': '已完成',
'missions.summary.failed': '失败',
'activity.empty': '暂无最近的任务或使命',
'activity.kind.job': '任务',
'activity.kind.mission': '使命',
'activity.waitingApproval': '等待批准',
'activity.waitingAuth': '等待认证',
'activity.resuming': '恢复中',
'activity.starting': '启动中',
'activity.processing': '处理中...',
'activity.thinking': '思考中',
'activity.usingTool': '正在使用 {name}',
'activity.failedTool': '{name} 失败',
'activity.streamingResponse': '响应传输中',
'activity.working': '处理中',
'activity.runningTool': '正在运行 {name}',
'activity.finishedTool': '{name} 已完成',
'activity.tool': '工具',

// 定时任务标签页
'routines.summary': '定时任务摘要',
Expand Down
4 changes: 3 additions & 1 deletion crates/ironclaw_gateway/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ <h2 data-i18n="restart.title">Restart IronClaw Instance</h2>
<button data-tab="memory" data-i18n="tab.memory">Memory</button>
<button data-tab="jobs" data-i18n="tab.jobs">Jobs</button>
<button data-tab="missions" data-i18n="tab.missions">Missions</button>
<button data-tab="routines" data-i18n="tab.routines">Routines</button>
<button data-tab="routines" data-tab-role="routines" data-i18n="tab.routines">Routines</button>
<button data-tab="settings" data-i18n="tab.settings">Settings</button>
<div class="spacer"></div>

Expand Down Expand Up @@ -224,6 +224,7 @@ <h2 data-i18n="restart.title">Restart IronClaw Instance</h2>
<span data-i18n="status.restart">Restart</span>
</button>
</div>
<div class="active-work-strip" id="active-work-strip" hidden></div>

<!-- Chat Tab -->
<div class="tab-panel active" id="tab-chat">
Expand Down Expand Up @@ -352,6 +353,7 @@ <h2 data-i18n="restart.title">Restart IronClaw Instance</h2>
<th data-i18n="missions.cadence">Cadence</th>
<th data-i18n="missions.threads">Threads</th>
<th data-i18n="missions.status">Status</th>
<th data-i18n="missions.progress">Progress</th>
<th data-i18n="missions.actions">Actions</th>
</tr>
</thead>
Expand Down
114 changes: 114 additions & 0 deletions crates/ironclaw_gateway/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,28 @@ body {
cursor: pointer;
font-size: var(--text-base);
font-weight: 500;
position: relative;
transition: color 0.2s, border-color 0.2s;
}

.tab-bar button[data-active-count]:not([data-active-count="0"])::after {
content: attr(data-active-count);
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
margin-left: 8px;
padding: 0 5px;
border-radius: 999px;
background: var(--accent);
color: var(--bg);
font-size: 11px;
font-weight: 700;
line-height: 1;
vertical-align: middle;
}

.tab-bar button:not(.status-logs-btn):not(.restart-btn):hover {
color: var(--text);
}
Expand Down Expand Up @@ -260,6 +279,80 @@ body {
background: var(--accent-tee-bg);
}

.active-work-strip {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border-bottom: 1px solid var(--border);
background: color-mix(in srgb, var(--surface) 90%, var(--accent-subtle) 10%);
overflow-x: auto;
flex-shrink: 0;
min-height: 34px;
}

.active-work-item {
min-width: 0;
max-width: 320px;
padding: 4px 9px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--panel);
color: var(--text);
display: inline-flex;
align-items: center;
gap: 6px;
text-align: left;
cursor: pointer;
white-space: nowrap;
}

.active-work-item:hover {
border-color: var(--accent);
background: var(--accent-subtle);
}

.active-work-item[data-state="running"] {
border-color: color-mix(in srgb, var(--accent) 35%, var(--border) 65%);
background: color-mix(in srgb, var(--panel) 82%, var(--accent-subtle) 18%);
}

.active-work-item[data-state="done"] {
border-color: color-mix(in srgb, var(--success) 28%, var(--border) 72%);
}

.active-work-item[data-state="failed"] {
border-color: color-mix(in srgb, var(--error) 32%, var(--border) 68%);
background: color-mix(in srgb, var(--panel) 88%, var(--error) 12%);
}

.active-work-kind {
font-size: var(--text-xs);
font-weight: 600;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.04em;
}

.active-work-title {
font-size: var(--text-xs);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
}

.active-work-status {
font-size: var(--text-xs);
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
}

.active-work-empty {
font-size: var(--text-xs);
color: var(--text-muted);
}

/* ── User account avatar + dropdown ─────────────────────────────── */

.user-account {
Expand Down Expand Up @@ -2961,6 +3054,19 @@ body {
padding: 16px 0;
}

.mission-progress-live {
color: var(--accent);
font-weight: 500;
}

.mission-progress-idle {
color: var(--text-muted);
}

.mission-thread-progress {
border-color: var(--accent);
}

.thread-message {
border-left: 3px solid var(--border);
padding: 8px 12px;
Expand Down Expand Up @@ -4768,6 +4874,14 @@ mark {
white-space: nowrap;
}

.tab-bar button[data-active-count]:not([data-active-count="0"])::after {
min-width: 16px;
height: 16px;
margin-left: 6px;
padding: 0 4px;
font-size: 10px;
}

/* Chat messages: wider */
.message {
max-width: 95%;
Expand Down
2 changes: 2 additions & 0 deletions src/channels/web/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4290,6 +4290,7 @@ async fn gateway_status_handler(
ws_connections,
total_connections: sse_connections + ws_connections,
uptime_secs,
engine_v2: crate::bridge::is_engine_v2_enabled(),
restart_enabled,
daily_cost,
actions_this_hour,
Expand All @@ -4315,6 +4316,7 @@ struct GatewayStatusResponse {
ws_connections: u64,
total_connections: u64,
uptime_secs: u64,
engine_v2: bool,
restart_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
daily_cost: Option<String>,
Expand Down
58 changes: 58 additions & 0 deletions tests/e2e/scenarios/test_v2_activity_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Playwright coverage for the v2 activity shell."""

import pytest
from playwright.async_api import expect

from helpers import AUTH_TOKEN, api_post

from .test_v2_engine_approval_flow import _wait_for_approval, v2_approval_server


@pytest.fixture
async def v2_approval_page(v2_approval_server, browser):
"""Fresh Playwright page bound to the v2 approval server fixture."""
context = await browser.new_context(viewport={"width": 1280, "height": 720})
page = await context.new_page()
await page.goto(f"{v2_approval_server}/?token={AUTH_TOKEN}")
await page.wait_for_selector("#auth-screen", state="hidden", timeout=15000)
await page.wait_for_function(
"() => typeof sseHasConnectedBefore !== 'undefined' && sseHasConnectedBefore === true",
timeout=10000,
)
yield page
await context.close()


@pytest.mark.asyncio
async def test_v2_hides_routines_tab(v2_approval_page):
"""The legacy Routines tab should not be shown when ENGINE_V2 is enabled."""
routines_tab = v2_approval_page.locator('.tab-bar button[data-tab="routines"]')
await expect(routines_tab).to_be_hidden()
await expect(v2_approval_page.locator('.tab-bar button[data-tab="missions"]')).to_be_visible()


@pytest.mark.asyncio
async def test_active_work_strip_survives_tab_switch(
v2_approval_server,
v2_approval_page,
):
"""Background v2 work stays visible after leaving the Chat tab."""
r = await api_post(v2_approval_server, "/api/chat/thread/new")
r.raise_for_status()
thread_id = r.json()["id"]

r = await api_post(
v2_approval_server,
"/api/chat/send",
json={"content": "make approval post active-shell", "thread_id": thread_id},
timeout=15,
)
r.raise_for_status()
await _wait_for_approval(v2_approval_server, thread_id)

strip = v2_approval_page.locator("#active-work-strip")
await expect(strip).to_be_visible()

await v2_approval_page.locator('.tab-bar button[data-tab="settings"]').click()
await v2_approval_page.locator("#tab-settings.active").wait_for(state="visible", timeout=5000)
await expect(strip).to_be_visible()
Loading