Skip to content
Draft
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
149 changes: 149 additions & 0 deletions design/jcode-desktop-redesign-v1-floating.html

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions design/jcode-desktop-redesign-v2-dock.html

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions design/jcode-desktop-redesign-v3-focus.html

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions design/jcode-desktop-redesign.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>jcode Desktop redesign · 方案总览</title>
<style>
:root{--bg:#f1f2ee;--surface:#fff;--ink:#171816;--muted:#747770;--line:#d9dbd4;--accent:#ff8400;--green:#149a54;--shadow:0 20px 60px rgba(26,28,24,.10)}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:"Avenir Next","PingFang SC","Microsoft YaHei",sans-serif;-webkit-font-smoothing:antialiased}.page{max-width:1440px;margin:auto;padding:42px 34px 70px}.eyebrow{font-size:10px;font-weight:750;letter-spacing:.18em;color:var(--accent);text-transform:uppercase}.intro{display:grid;grid-template-columns:1fr 440px;gap:60px;align-items:end;margin:10px 0 32px}.intro h1{font-size:36px;line-height:1.12;letter-spacing:-.035em;margin:0;max-width:720px}.intro p{font-size:13px;line-height:1.7;color:var(--muted);margin:0}.principles{display:flex;gap:7px;flex-wrap:wrap;margin:18px 0 30px}.chip{font-size:10px;padding:7px 10px;border:1px solid var(--line);background:rgba(255,255,255,.65);border-radius:999px}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.card{background:var(--surface);border:1px solid var(--line);border-radius:18px;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,.02);transition:.22s}.card:hover{transform:translateY(-3px);box-shadow:var(--shadow)}.preview{height:280px;background:#e1e2dd;overflow:hidden;border-bottom:1px solid var(--line);position:relative;padding:13px}.mini-app{height:100%;border:1px solid #cfd1cb;border-radius:10px;background:#fff;position:relative;overflow:hidden;box-shadow:0 12px 28px rgba(28,30,26,.10)}.mini-head{height:20px;border-bottom:1px solid #ddd;display:flex;align-items:center;padding:0 7px;gap:4px}.mini-head i{width:4px;height:4px;border-radius:50%;background:#bbb}.mini-head i:first-child{background:#ff6a65}.mini-head i:nth-child(2){background:#ffc148}.mini-head i:nth-child(3){background:#38bf55}.mini-side{position:absolute;left:0;top:20px;bottom:0;width:24%;background:#fafaf8;border-right:1px solid #ddd;padding:12px 7px}.mini-line{height:4px;border-radius:99px;background:#dfe0db;margin:7px 0}.mini-line.short{width:62%}.mini-chat{position:absolute;left:24%;right:0;top:20px;bottom:0;padding:25px 12%}.mini-bubble{height:18px;width:45%;margin-left:auto;border:1px solid #d8d9d4;background:#efefec;border-radius:6px}.mini-copy{margin-top:26px}.mini-copy .mini-line{margin:9px 0}.mini-compose{position:absolute;left:34%;right:8%;bottom:9px;height:32px;background:#fff;border:1px solid #d0d2cc;border-radius:8px}.mini-float{position:absolute;left:26%;top:44px;width:34%;height:58%;background:rgba(255,255,255,.98);border:1px solid #d5d7d0;border-radius:8px;box-shadow:0 10px 24px rgba(0,0,0,.11);padding:8px}.mini-float .mini-line{height:3px}.mini-float .progress{height:3px;background:#e2e3de;margin:9px 0}.mini-float .progress i{display:block;width:72%;height:100%;background:#222}.mini-rail{position:absolute;right:0;top:20px;bottom:0;width:24px;border-left:1px solid #ddd;background:#fafaf8;display:grid;align-content:start;justify-items:center;gap:9px;padding-top:10px}.mini-rail i{width:11px;height:11px;border:1px solid #aaa;border-radius:3px}.mini-sheet{position:absolute;right:24px;top:20px;bottom:0;width:34%;background:#fff;border-left:1px solid #ddd;box-shadow:-9px 0 20px rgba(0,0,0,.07);padding:10px}.mini-workspace{position:absolute;right:0;top:20px;bottom:0;width:42%;background:#fff;border-left:1px solid #d4d6d0;box-shadow:-8px 0 18px rgba(0,0,0,.06)}.mini-workspace-head{height:24px;border-bottom:1px solid #ddd;display:flex;align-items:center;gap:4px;padding:0 6px}.mini-pill{height:13px;width:35px;border:1px solid #d6d8d1;background:#f7f7f4;border-radius:4px}.mini-plus{width:13px;height:13px;border:1px solid #d6d8d1;border-radius:4px}.mini-workspace-body{padding:10px}.mini-workspace-body .mini-line{height:3px}.mini-workspace-body .progress{height:3px;background:#e2e3de;margin:9px 0}.mini-workspace-body .progress i{display:block;width:72%;height:100%;background:#222}.mini-modal{position:absolute;left:18%;right:7%;top:47px;bottom:23px;background:#fff;border:1px solid #d3d5cf;border-radius:9px;box-shadow:0 14px 30px rgba(0,0,0,.12);display:grid;grid-template-columns:22% 1fr}.mini-modal nav{background:#fafaf8;border-right:1px solid #ddd;padding:8px}.mini-modal section{padding:10px}.body{padding:16px}.kicker{display:flex;align-items:center;gap:8px;font-size:10px;color:var(--muted)}.num{width:23px;height:23px;display:grid;place-items:center;border-radius:50%;background:#20211f;color:#fff}.body h2{font-size:17px;margin:10px 0 7px}.body p{font-size:11px;line-height:1.65;color:var(--muted);min-height:54px;margin:0}.meta{display:flex;align-items:center;gap:8px;margin-top:14px;font-size:10px}.meta b{color:var(--green)}.open{margin-left:auto;text-decoration:none;color:var(--ink);height:30px;border:1px solid var(--line);border-radius:9px;padding:0 10px;display:flex;align-items:center;gap:6px;background:#fff}.open:hover{border-color:#aaa}.recommend{display:inline-block;color:#7b4300;background:#fff1dc;border-radius:5px;padding:2px 6px;margin-left:4px}@media(max-width:1050px){.intro{grid-template-columns:1fr}.grid{grid-template-columns:1fr}.preview{height:420px}.body p{min-height:0}}@media(max-width:650px){.page{padding:28px 18px}.intro h1{font-size:28px}.preview{height:260px}}
</style>
</head>
<body><main class="page"><div class="eyebrow">JCODE DESKTOP · INTERACTION STUDY</div><section class="intro"><h1>让工具随时可达,<br>但不再永久占据画布。</h1><p>三套方案都从参考图提取同一套空间原则:工具从固定侧栏变成可关闭的工作面板;运行状态收回成悬浮卡或胶囊;终端与浏览器只在需要时出现。</p></section><div class="principles"><span class="chip">可关闭标签页</span><span class="chip">悬浮任务进度</span><span class="chip">对话宽度稳定</span><span class="chip">终端按需出现</span><span class="chip">保留工作区导航</span></div><section class="grid">
<article class="card"><div class="preview"><div class="mini-app"><div class="mini-head"><i></i><i></i><i></i></div><div class="mini-side"><div class="mini-line"></div><div class="mini-line short"></div><div class="mini-line"></div><div class="mini-line short"></div></div><div class="mini-chat" style="right:42%;padding-left:8%;padding-right:8%"><div class="mini-bubble"></div><div class="mini-copy"><div class="mini-line short"></div><div class="mini-line"></div><div class="mini-line"></div></div></div><div class="mini-float" style="left:28%;top:48px;width:27%;height:55%;z-index:2"><div class="mini-line short"></div><div class="progress"><i></i></div><div class="mini-line"></div><div class="mini-line"></div><div class="mini-line short"></div></div><div class="mini-workspace"><div class="mini-workspace-head"><span class="mini-pill"></span><span class="mini-plus"></span></div><div class="mini-workspace-body"><div class="mini-line short"></div><div class="mini-line"></div><div class="mini-line"></div></div></div><div class="mini-compose" style="right:45%"></div></div></div><div class="body"><div class="kicker"><span class="num">A</span>双区域信息架构</div><h2>浮动状态 + 右侧工作窗口</h2><p>浮动面板汇总 Todo、历史 Plan 与 Artifacts;输入框上方保留当前计划。右侧窗口负责辅助工作,终端从底部展开。</p><div class="meta"><b>职责完全分离</b><a class="open" href="jcode-desktop-redesign-v1-floating.html">打开完整原型 ↗</a></div></div></article>
<article class="card"><div class="preview"><div class="mini-app"><div class="mini-head"><i></i><i></i><i></i></div><div class="mini-side"><div class="mini-line"></div><div class="mini-line short"></div><div class="mini-line"></div><div class="mini-line short"></div></div><div class="mini-chat"><div class="mini-bubble"></div><div class="mini-copy"><div class="mini-line short"></div><div class="mini-line"></div><div class="mini-line"></div></div></div><div class="mini-sheet"><div class="mini-line short"></div><div class="mini-line"></div><div class="mini-line"></div><div class="mini-line short"></div><div class="mini-line"></div></div><div class="mini-rail"><i></i><i></i><i></i><i></i></div><div class="mini-compose"></div></div></div><div class="body"><div class="kicker"><span class="num">B</span>工具切换效率优先</div><h2>轨道式工具坞</h2><p>右侧保留 56px 工具轨道,所有面板覆盖打开、不触发布局重排。适合文件、变更和终端之间高频切换。</p><div class="meta"><b>效率最高</b><a class="open" href="jcode-desktop-redesign-v2-dock.html">打开完整原型 ↗</a></div></div></article>
<article class="card"><div class="preview"><div class="mini-app"><div class="mini-head"><i></i><i></i><i></i></div><div class="mini-chat" style="left:0"><div class="mini-bubble"></div><div class="mini-copy"><div class="mini-line short"></div><div class="mini-line"></div><div class="mini-line"></div></div></div><div class="mini-modal"><nav><div class="mini-line"></div><div class="mini-line short"></div><div class="mini-line"></div></nav><section><div class="mini-line short"></div><div class="mini-line"></div><div class="mini-line"></div><div class="mini-line"></div></section></div><div class="mini-compose"></div></div></div><div class="body"><div class="kicker"><span class="num">C</span>内容空间优先 <span class="recommend">当前推荐</span></div><h2>聚焦画布 + 聚合状态</h2><p>工作面板只保留计划、文件、变更,终端固定底部;Todo、Plan 历史和 Artifacts 直接汇总在同一个状态浮窗。</p><div class="meta"><b>空间与职责最清晰</b><a class="open" href="jcode-desktop-redesign-v3-focus.html">打开完整原型 ↗</a></div></div></article>
</section></main></body></html>
34 changes: 27 additions & 7 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { CloudSyncToggle } from './components/CloudSyncToggle'
import { DesktopTitlebar } from './components/DesktopTitlebar'
import { ComputerShotPiP } from './components/ComputerShotPiP'
import { RightPanel } from './components/RightPanel'
import { StatusPanel } from './components/StatusPanel'
import { TerminalPanel } from './components/TerminalPanel'
import { RemoteConnectWizard } from './components/RemoteConnectWizard'
import type { RemotePrefill } from './lib/remote'
Expand Down Expand Up @@ -184,10 +185,12 @@ function Shell({ activeView }: { activeView: 'chat' | 'automations' | 'cloud-mob
const isRunning = useAppSelector((s) => s.chat.isRunning)
const wsConnected = useAppSelector((s) => s.session.wsConnected)

// Panel state — a right panel (files/changes/plan/artifacts) and a
// bottom panel (terminal) that can be open simultaneously.
// Two independent regions: a floating status panel (todos/plan history/
// artifacts), plus a temporary workspace overlay (plan/files/changes).
// Terminal stays bottom-docked and keeps its size when either overlay opens.
const [rightPanelOpen, setRightPanelOpen] = useState(false)
const [rightPanelTab, setRightPanelTab] = useState<'files' | 'changes' | 'plan' | 'artifacts'>('files')
const [rightPanelTab, setRightPanelTab] = useState<'plan' | 'files' | 'changes'>('plan')
const [statusPanelOpen, setStatusPanelOpen] = useState(false)
const [bottomPanel, setBottomPanel] = useState<'none' | 'terminal'>('none')
const [bottomPanelHeight, setBottomPanelHeight] = useState(260)
const [activeRun, setActiveRun] = useState<AutomationRun | null>(null)
Expand All @@ -210,6 +213,10 @@ function Shell({ activeView }: { activeView: 'chat' | 'automations' | 'cloud-mob
setBottomPanel((p) => (p === 'terminal' ? 'none' : 'terminal'))
return
}
if (panel === 'artifacts') {
setStatusPanelOpen((open) => !open)
return
}
setRightPanelTab((current) => {
if (rightPanelOpen && current === panel) {
setRightPanelOpen(false)
Expand Down Expand Up @@ -237,16 +244,18 @@ function Shell({ activeView }: { activeView: 'chat' | 'automations' | 'cloud-mob
// ⌘` never reaches the page on macOS (OS window cycling), so ⌘J is the
// alias shown in the UI. `!e.shiftKey` keeps ⇧⌘J (DevTools) intact.
e.preventDefault(); togglePanel('terminal')
} else if (e.key === 'Escape') {
if (rightPanelOpen) setRightPanelOpen(false)
else if (statusPanelOpen) setStatusPanelOpen(false)
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [togglePanel])
}, [rightPanelOpen, statusPanelOpen, togglePanel])

useEffect(() => {
function onArtifactUpserted() {
setRightPanelTab('artifacts')
setRightPanelOpen(true)
setStatusPanelOpen(true)
}
window.addEventListener('jcode:artifact-upserted', onArtifactUpserted)
return () => window.removeEventListener('jcode:artifact-upserted', onArtifactUpserted)
Expand Down Expand Up @@ -334,6 +343,16 @@ function Shell({ activeView }: { activeView: 'chat' | 'automations' | 'cloud-mob
{/* M18: settings is a first-class view, not an overlay dialog. */}
{activeView === 'settings' && <SettingsView />}

{activeView === 'chat' && (
<StatusPanel
open={statusPanelOpen}
isRunning={isRunning}
bottomOffset={bottomPanel === 'terminal' ? bottomPanelHeight + 18 : 18}
onOpen={() => setStatusPanelOpen(true)}
onClose={() => setStatusPanelOpen(false)}
/>
)}

{/* Bottom panel (terminal) — lives under the inset surface. */}
{bottomPanel === 'terminal' && (
<div className="relative shrink-0 border-t border-[var(--color-border)]" style={{ height: bottomPanelHeight }}>
Expand All @@ -348,7 +367,8 @@ function Shell({ activeView }: { activeView: 'chat' | 'automations' | 'cloud-mob
)}
</main>

{/* Right panel (files/changes/plan/artifacts) — sibling of main. */}
{/* Temporary workspace overlay (plan/files/changes). It is absolutely
positioned, so opening it never reflows the conversation. */}
{rightPanelOpen && (
<RightPanel
activeTab={rightPanelTab}
Expand Down
57 changes: 56 additions & 1 deletion web/src/app/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,66 @@
*/

import { describe, it, expect, afterEach } from 'vitest'
import { store, uiActions } from './store'
import { buildPlanHistory, chatActions, store, uiActions } from './store'

afterEach(() => {
store.dispatch(uiActions.setView('chat'))
store.dispatch(uiActions.setSettingsTab('general'))
store.dispatch(chatActions.clearChat())
})

describe('plan history', () => {
it('reconstructs durable todo snapshots and legacy plans without duplicate revisions', () => {
const history = buildPlanHistory([
{
type: 'todo_snapshot',
timestamp: '2026-08-04T08:00:00Z',
todos: [{ id: 1, title: 'Inspect layout', status: 'in_progress' }],
},
{
type: 'todo_snapshot',
timestamp: '2026-08-04T08:01:00Z',
todos: [{ id: 1, title: 'Inspect layout', status: 'in_progress' }],
},
{
type: 'plan_update',
timestamp: '2026-08-04T08:02:00Z',
plan_status: 'approved',
plan_title: 'Desktop redesign',
plan_content: '1. Inspect\n2. Implement',
},
{ type: 'todo_snapshot', timestamp: '2026-08-04T08:03:00Z', todos: [] },
])

expect(history).toHaveLength(2)
expect(history[0].title).toBe('Inspect layout')
expect(history[1]).toMatchObject({ title: 'Desktop redesign', status: 'approved' })
})

it('merges status revisions of the same todo plan into its latest snapshot', () => {
const history = buildPlanHistory([
{
type: 'todo_snapshot',
timestamp: '2026-08-04T08:00:00Z',
todos: [
{ id: 1, title: 'Create tests', status: 'in_progress' },
{ id: 2, title: 'Verify state', status: 'pending' },
],
},
{
type: 'todo_snapshot',
timestamp: '2026-08-04T08:05:00Z',
todos: [
{ id: 2, title: 'Verify state', status: 'completed' },
{ id: 1, title: 'Create tests', status: 'completed' },
],
},
])

expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({ status: 'completed', timestamp: Date.parse('2026-08-04T08:05:00Z') })
expect(history[0].todos.every((todo) => todo.status === 'completed')).toBe(true)
})
})

describe('ui routing (M18 settings view)', () => {
Expand Down
Loading
Loading