Skip to content

Commit 9394b31

Browse files
committed
feat(app): move IM into session side panel
1 parent 200cf4c commit 9394b31

13 files changed

Lines changed: 338 additions & 157 deletions

File tree

packages/app/src/app.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ const HomeRoute = lazy(() => import("@/pages/home"))
5656
const Session = lazy(() => import("@/pages/session"))
5757
const AgentSystemRoute = lazy(() => import("@/pages/agent-system"))
5858
const ReviewRoute = lazy(() => import("@/pages/review"))
59-
const IMRoute = lazy(() => import("@/pages/im"))
6059

6160
const SessionRoute = Object.assign(
6261
() => (
@@ -343,7 +342,6 @@ export function AppInterface(props: {
343342
<Route path="/agent" component={AgentSystemRoute} />
344343
<Route path="/review" component={ReviewRoute} />
345344
<Route path="/session/:id?" component={SessionRoute} />
346-
<Route path="/im" component={IMRoute} />
347345
</Route>
348346
</Dynamic>
349347
</ConnectionGate>

packages/app/src/components/im/im-sidebar.tsx

Lines changed: 0 additions & 102 deletions
This file was deleted.

packages/app/src/components/settings-v2/servers.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ export const SettingsServersV2: Component = () => {
6363
<ButtonV2 variant="ghost-muted" icon="plus" onClick={openAdd}>
6464
{language.t("dialog.server.add.button")}
6565
</ButtonV2>
66-
<ButtonV2 variant="ghost-muted" icon="cloud" onClick={openConnectServer}>
67-
Connect to Server
66+
<ButtonV2 variant="ghost-muted" icon="server" onClick={openConnectServer}>
67+
{language.t("dialog.server.connect.button")}
6868
</ButtonV2>
6969
<WslAddServerButton />
7070
</div>

packages/app/src/context/layout-helpers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
import type { Accessor } from "solid-js"
22

3+
// Pure reducer for the right-side-panel mode. Kept out of the provider so the
4+
// open/close/toggle contract — most importantly "toggling the active tab closes
5+
// the panel" — is unit-testable without constructing the full LayoutProvider.
6+
// `undefined` means the panel is closed.
7+
export function toggledPanelMode<Mode extends string>(current: Mode | undefined, mode: Mode): Mode | undefined {
8+
return current === mode ? undefined : mode
9+
}
10+
11+
export function isPanelOpen(current: string | undefined): boolean {
12+
return current !== undefined
13+
}
14+
315
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
416
touch(key)
517
seed(key)

packages/app/src/context/layout.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,39 @@
11
import { describe, expect, test } from "bun:test"
22
import { createRoot, createSignal } from "solid-js"
3-
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
3+
import {
4+
createSessionKeyReader,
5+
ensureSessionKey,
6+
isPanelOpen,
7+
pruneSessionKeys,
8+
toggledPanelMode,
9+
} from "./layout-helpers"
10+
11+
describe("right-side-panel mode reducer", () => {
12+
// Regression guard for the IM panel "进去出不来" bug: opening a panel must be
13+
// reversible. These exercise the exact reducer the LayoutProvider's
14+
// rightPanel.open/close/toggle delegate to.
15+
test("opening a mode from closed sets that mode", () => {
16+
expect(toggledPanelMode(undefined, "im")).toBe("im")
17+
})
18+
19+
test("toggling the active mode closes the panel", () => {
20+
// open IM, then toggle IM again → closed. This is the click-to-open,
21+
// click-to-close contract the IM tab relies on.
22+
const opened = toggledPanelMode(undefined, "im")
23+
expect(opened).toBe("im")
24+
expect(toggledPanelMode(opened, "im")).toBeUndefined()
25+
})
26+
27+
test("toggling a different mode switches instead of closing", () => {
28+
expect(toggledPanelMode("review", "im")).toBe("im")
29+
})
30+
31+
test("isPanelOpen reflects presence of a mode", () => {
32+
expect(isPanelOpen(undefined)).toBe(false)
33+
expect(isPanelOpen("im")).toBe(true)
34+
expect(isPanelOpen("review")).toBe(true)
35+
})
36+
})
437

538
describe("layout session-key helpers", () => {
639
test("couples touch and scroll seed in order", () => {

packages/app/src/context/layout.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
1515
import { createPathHelpers } from "./file/path"
1616
import type { ProjectAvatarVariant } from "@deepagent-code/ui/v2/project-avatar-v2"
1717
import { migrateLegacySessionStateKeys, ServerScope, SessionStateKey } from "@/utils/server-scope"
18-
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
18+
import {
19+
createSessionKeyReader,
20+
ensureSessionKey,
21+
isPanelOpen,
22+
pruneSessionKeys,
23+
toggledPanelMode,
24+
} from "./layout-helpers"
1925

2026
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
2127

@@ -68,7 +74,18 @@ type SessionView = {
6874
reviewOpen?: string[]
6975
// U3/U4/U7: added "worktree" (isolated worktree diff/merge), "subagents" (child-session list),
7076
// "browser" (isolated WebContentsView).
71-
rightPanelMode?: "menu" | "review" | "files" | "status" | "worktree" | "subagents" | "browser" | "plugins" | "profile" | "debug"
77+
rightPanelMode?:
78+
| "menu"
79+
| "review"
80+
| "files"
81+
| "status"
82+
| "worktree"
83+
| "subagents"
84+
| "browser"
85+
| "plugins"
86+
| "profile"
87+
| "debug"
88+
| "im"
7289
pendingMessage?: string
7390
pendingMessageAt?: number
7491
todoCollapsed?: boolean
@@ -883,15 +900,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
883900
},
884901
rightPanel: {
885902
mode: rightPanelMode,
886-
opened: createMemo(() => rightPanelMode() !== undefined),
903+
opened: createMemo(() => isPanelOpen(rightPanelMode())),
887904
open(mode: NonNullable<SessionView["rightPanelMode"]>) {
888905
setRightPanelMode(mode)
889906
},
890907
close() {
891908
setRightPanelMode(undefined)
892909
},
893910
toggle(mode: NonNullable<SessionView["rightPanelMode"]>) {
894-
setRightPanelMode(rightPanelMode() === mode ? undefined : mode)
911+
setRightPanelMode(toggledPanelMode(rightPanelMode(), mode))
895912
},
896913
},
897914
review: {

packages/app/src/i18n/en.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ export const dict = {
404404
"dialog.server.add.error": "Could not connect to server",
405405
"dialog.server.add.checking": "Checking...",
406406
"dialog.server.add.button": "Add server",
407+
"dialog.server.connect.button": "Connect to server",
407408
"dialog.server.add.name": "Server name (optional)",
408409
"dialog.server.add.namePlaceholder": "Localhost",
409410
"dialog.server.add.username": "Username (optional)",
@@ -673,7 +674,13 @@ export const dict = {
673674

674675
"session.tab.session": "Session",
675676
"session.tab.review": "Review",
677+
"session.tab.im": "Messages",
676678
"session.tab.context": "Context",
679+
"im.loading": "Loading…",
680+
"im.group.empty": "No groups yet",
681+
"im.group.create": "New group",
682+
"im.group.create.placeholder": "Group name, press Enter",
683+
"im.group.create.failed": "Failed to create group",
677684
"session.panel.reviewAndFiles": "Review and files",
678685
"session.review.filesChanged": "{{count}} Files Changed",
679686
"session.review.change.one": "Change",

packages/app/src/i18n/zh.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ export const dict = {
418418
"dialog.server.add.error": "无法连接到服务器",
419419
"dialog.server.add.checking": "检查中...",
420420
"dialog.server.add.button": "添加服务器",
421+
"dialog.server.connect.button": "连接到服务端",
421422
"dialog.server.add.name": "服务器名称(可选)",
422423
"dialog.server.add.namePlaceholder": "Localhost",
423424
"dialog.server.add.username": "用户名(可选)",
@@ -594,7 +595,13 @@ export const dict = {
594595

595596
"session.tab.session": "会话",
596597
"session.tab.review": "审查",
598+
"session.tab.im": "消息",
597599
"session.tab.context": "上下文",
600+
"im.loading": "加载中…",
601+
"im.group.empty": "还没有群组",
602+
"im.group.create": "新建群组",
603+
"im.group.create.placeholder": "群组名称,回车创建",
604+
"im.group.create.failed": "创建群组失败",
598605
"session.panel.reviewAndFiles": "审查和文件",
599606
"session.review.filesChanged": "{{count}} 个文件变更",
600607
"session.review.change.one": "更改",

packages/app/src/pages/im.tsx

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, test, mock } from "bun:test"
2+
import { submitCreateGroup } from "./im-panel-helpers"
3+
import type { IMGroup } from "@/components/im/types"
4+
5+
const group = (id: string, name: string): IMGroup => ({
6+
id,
7+
workspaceID: "ws",
8+
projectID: null,
9+
type: "project",
10+
name,
11+
createdBy: "server",
12+
createdAt: 0,
13+
updatedAt: 0,
14+
deletedAt: null,
15+
})
16+
17+
describe("submitCreateGroup", () => {
18+
test("creates a group via the client (never window.prompt)", async () => {
19+
const createGroup = mock(async (p: { name: string; type: "project" | "system" }) => group("grp_1", p.name))
20+
const result = await submitCreateGroup(" Design ", createGroup)
21+
22+
expect(createGroup).toHaveBeenCalledTimes(1)
23+
// name is trimmed before sending
24+
expect(createGroup.mock.calls[0][0]).toEqual({ name: "Design", type: "project" })
25+
expect(result).toEqual({ group: group("grp_1", "Design") })
26+
})
27+
28+
test("blank / whitespace name is a no-op and never calls the client", async () => {
29+
const createGroup = mock(async () => group("grp_x", "x"))
30+
expect(await submitCreateGroup(" ", createGroup)).toEqual({ skipped: true })
31+
expect(await submitCreateGroup("", createGroup)).toEqual({ skipped: true })
32+
expect(createGroup).not.toHaveBeenCalled()
33+
})
34+
35+
test("surfaces a client failure as an error string", async () => {
36+
const createGroup = mock(async () => {
37+
throw new Error("boom")
38+
})
39+
const result = await submitCreateGroup("Team", createGroup)
40+
expect(result).toEqual({ error: "boom" })
41+
})
42+
})

0 commit comments

Comments
 (0)