ikanban-web 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/assets/{ghostty-web-CkHg_Jfr.js → ghostty-web-CEQVZrGO.js} +1 -1
  2. package/dist/assets/home-BfGWAM3p.js +1 -0
  3. package/dist/assets/{index-DbuHW0gF.css → index-7dPULZd1.css} +1 -1
  4. package/dist/assets/{index-n2BMSLCz.js → index-WJDzUYhg.js} +106 -106
  5. package/dist/assets/session-BDqUCVwy.js +24 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/app.tsx +82 -61
  9. package/src/components/dialog-command-palette.tsx +70 -0
  10. package/src/components/dialog-fork.tsx +1 -1
  11. package/src/components/dialog-select-file.tsx +1 -1
  12. package/src/components/dialog-select-server.test.ts +16 -0
  13. package/src/components/dialog-select-server.tsx +59 -51
  14. package/src/components/prompt-input/submit.ts +1 -1
  15. package/src/components/server-list-item-frame.tsx +15 -0
  16. package/src/components/titlebar.tsx +12 -31
  17. package/src/context/global-sync/bootstrap-mode.ts +17 -0
  18. package/src/context/global-sync/bootstrap.ts +18 -10
  19. package/src/context/global-sync.test.ts +57 -0
  20. package/src/context/global-sync.tsx +5 -0
  21. package/src/context/notification.tsx +2 -2
  22. package/src/i18n/en.ts +13 -13
  23. package/src/i18n/zh.ts +13 -13
  24. package/src/pages/directory-layout.tsx +2 -2
  25. package/src/pages/home.tsx +104 -19
  26. package/src/pages/layout/sidebar-items.tsx +3 -3
  27. package/src/pages/layout/sidebar-project.tsx +1 -3
  28. package/src/pages/layout/sidebar-shell-helpers.ts +1 -1
  29. package/src/pages/layout/sidebar-shell.test.ts +2 -2
  30. package/src/pages/layout/sidebar-workspace.tsx +1 -1
  31. package/src/pages/layout.tsx +89 -179
  32. package/src/pages/session/message-timeline.tsx +4 -4
  33. package/src/pages/session/use-session-commands.tsx +1 -1
  34. package/src/pages/session.tsx +1 -1
  35. package/dist/assets/home-BTCNns6Z.js +0 -1
  36. package/dist/assets/session-S93i-VnN.js +0 -24
@@ -5,6 +5,8 @@ import {
5
5
  loadRootSessionsWithFallback,
6
6
  pickDirectoriesToEvict,
7
7
  } from "./global-sync"
8
+ import { bootstrapGlobal } from "./global-sync/bootstrap"
9
+ import { shouldLoadProjectsOnBootstrap } from "./global-sync/bootstrap-mode"
8
10
 
9
11
  describe("pickDirectoriesToEvict", () => {
10
12
  test("keeps pinned stores and evicts idle stores", () => {
@@ -124,3 +126,58 @@ describe("canDisposeDirectory", () => {
124
126
  ).toBe(true)
125
127
  })
126
128
  })
129
+
130
+ describe("bootstrapGlobal", () => {
131
+ test("does not call project list when project loading is disabled", async () => {
132
+ let projectListCalls = 0
133
+ const project = {
134
+ list: async () => {
135
+ projectListCalls += 1
136
+ return { data: [] }
137
+ },
138
+ }
139
+
140
+ const updates: Array<[string, unknown]> = []
141
+
142
+ await bootstrapGlobal({
143
+ globalSDK: {
144
+ global: {
145
+ health: async () => ({ data: { healthy: true } }),
146
+ config: { get: async () => ({ data: {} }) },
147
+ },
148
+ path: { get: async () => ({ data: { state: "", config: "", worktree: "", directory: "", home: "" } }) },
149
+ project,
150
+ provider: {
151
+ list: async () => ({ data: { all: [], connected: [], default: {} } }),
152
+ auth: async () => ({ data: {} }),
153
+ },
154
+ } as never,
155
+ connectErrorTitle: "connect",
156
+ connectErrorDescription: "connect desc",
157
+ requestFailedTitle: "request",
158
+ unknownError: "unknown",
159
+ invalidConfigurationError: "invalid",
160
+ formatMoreCount: () => "",
161
+ setGlobalStore: ((key: string, value: unknown) => {
162
+ updates.push([key, value])
163
+ return value
164
+ }) as never,
165
+ loadProjects: false,
166
+ } as never)
167
+
168
+ expect(projectListCalls).toBe(0)
169
+ expect(updates.some(([key]) => key === "ready")).toBe(true)
170
+ })
171
+ })
172
+
173
+ describe("shouldLoadProjectsOnBootstrap", () => {
174
+ test("skips project loading on the home route", () => {
175
+ expect(shouldLoadProjectsOnBootstrap("/", "/")).toBe(false)
176
+ expect(shouldLoadProjectsOnBootstrap("/ikanban", "/ikanban/")).toBe(false)
177
+ })
178
+
179
+ test("loads projects away from the home route", () => {
180
+ expect(shouldLoadProjectsOnBootstrap("/dGVzdA", "/")).toBe(true)
181
+ expect(shouldLoadProjectsOnBootstrap("/ikanban/dGVzdA", "/ikanban/")).toBe(true)
182
+ })
183
+ })
@@ -26,6 +26,7 @@ import { Persist, persisted } from "@/utils/persist"
26
26
  import type { InitError } from "../pages/error"
27
27
  import { useGlobalSDK } from "./global-sdk"
28
28
  import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
29
+ import { shouldLoadProjectsOnBootstrap } from "./global-sync/bootstrap-mode"
29
30
  import { createChildStoreManager } from "./global-sync/child-store"
30
31
  import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
31
32
  import { createRefreshQueue } from "./global-sync/queue"
@@ -334,6 +335,10 @@ function createGlobalSync() {
334
335
  unknownError: language.t("error.chain.unknown"),
335
336
  invalidConfigurationError: language.t("error.server.invalidConfiguration"),
336
337
  formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
338
+ loadProjects: shouldLoadProjectsOnBootstrap(
339
+ globalThis.location?.pathname ?? "/",
340
+ import.meta.env.BASE_URL ?? "/",
341
+ ),
337
342
  setGlobalStore: setBootStore,
338
343
  })
339
344
  }
@@ -245,7 +245,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
245
245
  session: sessionID,
246
246
  })
247
247
 
248
- const href = `/${base64Encode(directory)}/session/${sessionID}`
248
+ const href = `/${base64Encode(directory)}/${sessionID}`
249
249
  if (settings.notifications.agent()) {
250
250
  void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href)
251
251
  }
@@ -278,7 +278,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
278
278
  const description =
279
279
  session?.title ??
280
280
  (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
281
- const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
281
+ const href = sessionID ? `/${base64Encode(directory)}/${sessionID}` : `/${base64Encode(directory)}`
282
282
  if (settings.notifications.errors()) {
283
283
  void platform.notify(language.t("notification.session.error.title"), description, href)
284
284
  }
package/src/i18n/en.ts CHANGED
@@ -133,7 +133,7 @@ export const dict = {
133
133
  "Customize which models appear in the model selector.",
134
134
  "dialog.model.manage.provider.toggle": "Toggle all {{provider}} models",
135
135
 
136
- "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
136
+ "dialog.model.unpaid.freeModels.title": "Free models provided by iKanban",
137
137
  "dialog.model.unpaid.addMore.title": "Add more models from popular providers",
138
138
 
139
139
  "dialog.provider.viewAll": "Show more providers",
@@ -146,7 +146,7 @@ export const dict = {
146
146
  "provider.connect.status.waiting": "Waiting for authorization...",
147
147
  "provider.connect.status.failed": "Authorization failed: {{error}}",
148
148
  "provider.connect.apiKey.description":
149
- "Enter your {{provider}} API key to connect your account and use {{provider}} models in OpenCode.",
149
+ "Enter your {{provider}} API key to connect your account and use {{provider}} models in iKanban.",
150
150
  "provider.connect.apiKey.label": "{{provider}} API key",
151
151
  "provider.connect.apiKey.placeholder": "API key",
152
152
  "provider.connect.apiKey.required": "API key is required",
@@ -160,7 +160,7 @@ export const dict = {
160
160
  "provider.connect.oauth.code.visit.prefix": "Visit ",
161
161
  "provider.connect.oauth.code.visit.link": "this link",
162
162
  "provider.connect.oauth.code.visit.suffix":
163
- " to collect your authorization code to connect your account and use {{provider}} models in OpenCode.",
163
+ " to collect your authorization code to connect your account and use {{provider}} models in iKanban.",
164
164
  "provider.connect.oauth.code.label": "{{method}} authorization code",
165
165
  "provider.connect.oauth.code.placeholder": "Authorization code",
166
166
  "provider.connect.oauth.code.required": "Authorization code is required",
@@ -168,7 +168,7 @@ export const dict = {
168
168
  "provider.connect.oauth.auto.visit.prefix": "Visit ",
169
169
  "provider.connect.oauth.auto.visit.link": "this link",
170
170
  "provider.connect.oauth.auto.visit.suffix":
171
- " and enter the code below to connect your account and use {{provider}} models in OpenCode.",
171
+ " and enter the code below to connect your account and use {{provider}} models in iKanban.",
172
172
  "provider.connect.oauth.auto.confirmationCode": "Confirmation code",
173
173
  "provider.connect.toast.connected.title": "{{provider}} connected",
174
174
  "provider.connect.toast.connected.description":
@@ -486,7 +486,7 @@ export const dict = {
486
486
 
487
487
  "toast.update.title": "Update available",
488
488
  "toast.update.description":
489
- "A new version of OpenCode ({{version}}) is now available to install.",
489
+ "A new version of iKanban ({{version}}) is now available to install.",
490
490
  "toast.update.action.installRestart": "Install and restart",
491
491
  "toast.update.action.notYet": "Not yet",
492
492
 
@@ -497,7 +497,7 @@ export const dict = {
497
497
  "error.page.action.checking": "Checking...",
498
498
  "error.page.action.checkUpdates": "Check for updates",
499
499
  "error.page.action.updateTo": "Update to {{version}}",
500
- "error.page.report.prefix": "Please report this error to the OpenCode team",
500
+ "error.page.report.prefix": "Please report this error to the iKanban team",
501
501
  "error.page.report.discord": "on Discord",
502
502
  "error.page.version": "Version: {{version}}",
503
503
 
@@ -550,7 +550,7 @@ export const dict = {
550
550
  "notification.session.error.fallbackDescription": "An error occurred",
551
551
 
552
552
  "home.recentProjects": "Recent projects",
553
- "home.sessionBoard": "Session board",
553
+ "home.sessionBoard": "Ikanban",
554
554
  "home.sessionBoard.description":
555
555
  "Live session activity grouped into Progress and Idle.",
556
556
  "home.sessionBoard.progress": "Progress",
@@ -667,7 +667,7 @@ export const dict = {
667
667
  "sidebar.workspaces.disable": "Disable workspaces",
668
668
  "sidebar.gettingStarted.title": "Getting started",
669
669
  "sidebar.gettingStarted.line1":
670
- "OpenCode includes free models so you can start immediately.",
670
+ "iKanban includes free models so you can start immediately.",
671
671
  "sidebar.gettingStarted.line2":
672
672
  "Connect any provider to use models, inc. Claude, GPT, Gemini etc.",
673
673
  "sidebar.project.recentSessions": "Recent sessions",
@@ -695,12 +695,12 @@ export const dict = {
695
695
 
696
696
  "settings.general.row.language.title": "Language",
697
697
  "settings.general.row.language.description":
698
- "Change the display language for OpenCode",
698
+ "Change the display language for iKanban",
699
699
  "settings.general.row.appearance.title": "Appearance",
700
700
  "settings.general.row.appearance.description":
701
- "Customise how OpenCode looks on your device",
701
+ "Customise how iKanban looks on your device",
702
702
  "settings.general.row.theme.title": "Theme",
703
- "settings.general.row.theme.description": "Customise how OpenCode is themed.",
703
+ "settings.general.row.theme.description": "Customise how iKanban is themed.",
704
704
  "settings.general.row.font.title": "Font",
705
705
  "settings.general.row.font.description":
706
706
  "Customise the mono font used in code blocks",
@@ -731,7 +731,7 @@ export const dict = {
731
731
 
732
732
  "settings.updates.row.startup.title": "Check for updates on startup",
733
733
  "settings.updates.row.startup.description":
734
- "Automatically check for updates when OpenCode launches",
734
+ "Automatically check for updates when iKanban launches",
735
735
  "settings.updates.row.check.title": "Check for updates",
736
736
  "settings.updates.row.check.description":
737
737
  "Manually check for updates and install if available",
@@ -739,7 +739,7 @@ export const dict = {
739
739
  "settings.updates.action.checking": "Checking...",
740
740
  "settings.updates.toast.latest.title": "You're up to date",
741
741
  "settings.updates.toast.latest.description":
742
- "You're running the latest version of OpenCode.",
742
+ "You're running the latest version of iKanban.",
743
743
  "font.option.ibmPlexMono": "IBM Plex Mono",
744
744
  "font.option.cascadiaCode": "Cascadia Code",
745
745
  "font.option.firaCode": "Fira Code",
package/src/i18n/zh.ts CHANGED
@@ -145,7 +145,7 @@ export const dict = {
145
145
  "dialog.model.recent": "最近",
146
146
  "dialog.model.manage.description": "自定义模型选择器中显示的模型。",
147
147
  "dialog.model.manage.provider.toggle": "切换 {{provider}} 的所有模型",
148
- "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
148
+ "dialog.model.unpaid.freeModels.title": "iKanban 提供的免费模型",
149
149
  "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
150
150
 
151
151
  "dialog.provider.viewAll": "查看更多提供商",
@@ -158,7 +158,7 @@ export const dict = {
158
158
  "provider.connect.status.waiting": "等待授权...",
159
159
  "provider.connect.status.failed": "授权失败:{{error}}",
160
160
  "provider.connect.apiKey.description":
161
- "输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。",
161
+ "输入你的 {{provider}} API 密钥以连接帐户,并在 iKanban 中使用 {{provider}} 模型。",
162
162
  "provider.connect.apiKey.label": "{{provider}} API 密钥",
163
163
  "provider.connect.apiKey.placeholder": "API 密钥",
164
164
  "provider.connect.apiKey.required": "API 密钥为必填项",
@@ -172,7 +172,7 @@ export const dict = {
172
172
  "provider.connect.oauth.code.visit.prefix": "访问 ",
173
173
  "provider.connect.oauth.code.visit.link": "此链接",
174
174
  "provider.connect.oauth.code.visit.suffix":
175
- " 获取授权码,以连接你的帐户并在 OpenCode 中使用 {{provider}} 模型。",
175
+ " 获取授权码,以连接你的帐户并在 iKanban 中使用 {{provider}} 模型。",
176
176
  "provider.connect.oauth.code.label": "{{method}} 授权码",
177
177
  "provider.connect.oauth.code.placeholder": "授权码",
178
178
  "provider.connect.oauth.code.required": "授权码为必填项",
@@ -180,7 +180,7 @@ export const dict = {
180
180
  "provider.connect.oauth.auto.visit.prefix": "访问 ",
181
181
  "provider.connect.oauth.auto.visit.link": "此链接",
182
182
  "provider.connect.oauth.auto.visit.suffix":
183
- " 并输入以下代码,以连接你的帐户并在 OpenCode 中使用 {{provider}} 模型。",
183
+ " 并输入以下代码,以连接你的帐户并在 iKanban 中使用 {{provider}} 模型。",
184
184
  "provider.connect.oauth.auto.confirmationCode": "确认码",
185
185
  "provider.connect.toast.connected.title": "{{provider}} 已连接",
186
186
  "provider.connect.toast.connected.description":
@@ -461,7 +461,7 @@ export const dict = {
461
461
  "toast.session.unshare.failed.description": "取消分享会话时发生错误",
462
462
  "toast.session.listFailed.title": "无法加载 {{project}} 的会话",
463
463
  "toast.update.title": "有可用更新",
464
- "toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。",
464
+ "toast.update.description": "iKanban 有新版本 ({{version}}) 可安装。",
465
465
  "toast.update.action.installRestart": "安装并重启",
466
466
  "toast.update.action.notYet": "稍后",
467
467
 
@@ -472,7 +472,7 @@ export const dict = {
472
472
  "error.page.action.checking": "检查中...",
473
473
  "error.page.action.checkUpdates": "检查更新",
474
474
  "error.page.action.updateTo": "更新到 {{version}}",
475
- "error.page.report.prefix": "请将此错误报告给 OpenCode 团队",
475
+ "error.page.report.prefix": "请将此错误报告给 iKanban 团队",
476
476
  "error.page.report.discord": "在 Discord 上",
477
477
  "error.page.version": "版本:{{version}}",
478
478
  "error.dev.rootNotFound":
@@ -520,7 +520,7 @@ export const dict = {
520
520
  "notification.session.error.fallbackDescription": "发生错误",
521
521
 
522
522
  "home.recentProjects": "最近项目",
523
- "home.sessionBoard": "会话看板",
523
+ "home.sessionBoard": "Ikanban",
524
524
  "home.sessionBoard.description": "按 Progress 和 Idle 分组。",
525
525
  "home.sessionBoard.progress": "Progress",
526
526
  "home.sessionBoard.idle": "Idle",
@@ -624,7 +624,7 @@ export const dict = {
624
624
  "sidebar.workspaces.enable": "启用工作区",
625
625
  "sidebar.workspaces.disable": "禁用工作区",
626
626
  "sidebar.gettingStarted.title": "入门",
627
- "sidebar.gettingStarted.line1": "OpenCode 提供免费模型,你可以立即开始使用。",
627
+ "sidebar.gettingStarted.line1": "iKanban 提供免费模型,你可以立即开始使用。",
628
628
  "sidebar.gettingStarted.line2":
629
629
  "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。",
630
630
  "sidebar.project.recentSessions": "最近会话",
@@ -652,12 +652,12 @@ export const dict = {
652
652
  "settings.general.section.feed": "动态",
653
653
  "settings.general.section.display": "显示",
654
654
  "settings.general.row.language.title": "语言",
655
- "settings.general.row.language.description": "更改 OpenCode 的显示语言",
655
+ "settings.general.row.language.description": "更改 iKanban 的显示语言",
656
656
  "settings.general.row.appearance.title": "外观",
657
657
  "settings.general.row.appearance.description":
658
- "自定义 OpenCode 在你的设备上的外观",
658
+ "自定义 iKanban 在你的设备上的外观",
659
659
  "settings.general.row.theme.title": "主题",
660
- "settings.general.row.theme.description": "自定义 OpenCode 的主题。",
660
+ "settings.general.row.theme.description": "自定义 iKanban 的主题。",
661
661
  "settings.general.row.font.title": "字体",
662
662
  "settings.general.row.font.description": "自定义代码块使用的等宽字体",
663
663
  "settings.general.row.reasoningSummaries.title": "显示推理摘要",
@@ -682,14 +682,14 @@ export const dict = {
682
682
  "settings.general.action.releaseNotesCurrent": "查看变更",
683
683
 
684
684
  "settings.updates.row.startup.title": "启动时检查更新",
685
- "settings.updates.row.startup.description": "在 OpenCode 启动时自动检查更新",
685
+ "settings.updates.row.startup.description": "在 iKanban 启动时自动检查更新",
686
686
  "settings.updates.row.check.title": "检查更新",
687
687
  "settings.updates.row.check.description": "手动检查更新并在有更新时安装",
688
688
  "settings.updates.action.checkNow": "立即检查",
689
689
  "settings.updates.action.checking": "正在检查...",
690
690
  "settings.updates.toast.latest.title": "已是最新版本",
691
691
  "settings.updates.toast.latest.description":
692
- "你正在使用最新版本的 OpenCode。",
692
+ "你正在使用最新版本的 iKanban。",
693
693
 
694
694
  "font.option.ibmPlexMono": "IBM Plex Mono",
695
695
  "font.option.cascadiaCode": "Cascadia Code",
@@ -19,8 +19,8 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
19
19
  <DataProvider
20
20
  data={sync.data}
21
21
  directory={props.directory}
22
- onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/session/${sessionID}`)}
23
- onSessionHref={(sessionID: string) => `/${params.dir}/session/${sessionID}`}
22
+ onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/${sessionID}`)}
23
+ onSessionHref={(sessionID: string) => `/${params.dir}/${sessionID}`}
24
24
  >
25
25
  <LocalProvider>{props.children}</LocalProvider>
26
26
  </DataProvider>
@@ -1,4 +1,4 @@
1
- import { createMemo, createEffect, For, Match, Switch } from "solid-js";
1
+ import { createMemo, createEffect, createResource, For, Match, Switch } from "solid-js";
2
2
  import { Button } from "ikanban-ui/button";
3
3
  import { Logo } from "ikanban-ui/logo";
4
4
  import { useLayout } from "@/context/layout";
@@ -15,7 +15,6 @@ import { useServer } from "@/context/server";
15
15
  import { useGlobalSync } from "@/context/global-sync";
16
16
  import { useLanguage } from "@/context/language";
17
17
  import { IconButton } from "ikanban-ui/icon-button";
18
- import { sortedRootSessions } from "@/pages/layout/helpers";
19
18
  import type { Session } from "@opencode-ai/sdk/v2/client";
20
19
 
21
20
  type BoardColumn = "progress" | "idle";
@@ -26,6 +25,34 @@ type BoardCard = {
26
25
  updatedAt: number;
27
26
  };
28
27
 
28
+ const homeStyles = {
29
+ border: { border: "1px solid var(--border-weak-base)" },
30
+ heroIcon: {
31
+ border: "1px solid var(--border-weak-base)",
32
+ background:
33
+ "linear-gradient(135deg, color-mix(in srgb, var(--text-interactive-base) 12%, var(--background-stronger)), var(--background-stronger))",
34
+ },
35
+ emptyPanel: {
36
+ "border-color": "var(--border-weak-base)",
37
+ background:
38
+ "linear-gradient(180deg, color-mix(in srgb, var(--surface-base-hover) 35%, var(--background-stronger)), var(--background-stronger))",
39
+ },
40
+ emptyIcon: {
41
+ border: "1px solid var(--border-weak-base)",
42
+ background:
43
+ "linear-gradient(135deg, color-mix(in srgb, var(--text-interactive-base) 10%, var(--surface-inset-base)), var(--surface-inset-base))",
44
+ },
45
+ card: {
46
+ border: "1px solid var(--border-weak-base)",
47
+ background: "var(--background-base)",
48
+ },
49
+ emptyState: {
50
+ "border-color": "var(--border-weak-base)",
51
+ background:
52
+ "linear-gradient(180deg, color-mix(in srgb, var(--surface-base-hover) 30%, var(--surface-inset-base)), var(--surface-inset-base))",
53
+ },
54
+ } as const;
55
+
29
56
  export default function Home() {
30
57
  const sync = useGlobalSync();
31
58
  const layout = useLayout();
@@ -35,26 +62,49 @@ export default function Home() {
35
62
  const globalSDK = useGlobalSDK();
36
63
  const server = useServer();
37
64
  const language = useLanguage();
38
- const recent = createMemo(() => {
65
+ const projects = createMemo(() => {
39
66
  return sync.data.project
40
67
  .slice()
41
68
  .sort(
42
69
  (a, b) =>
43
70
  (b.time.updated ?? b.time.created) -
44
71
  (a.time.updated ?? a.time.created),
45
- )
46
- .slice(0, 5);
72
+ );
47
73
  });
74
+
75
+ const [rootSessions, { mutate: mutateRootSessions }] = createResource(
76
+ () => projects().map((project) => project.worktree),
77
+ async (directories) => {
78
+ const entries = await Promise.all(
79
+ directories.map(async (directory) => {
80
+ const result = await globalSDK.client.session.list({
81
+ directory,
82
+ roots: true,
83
+ });
84
+
85
+ const sessions = (result.data ?? []).filter(
86
+ (session): session is Session => !!session?.id && !session.time?.archived,
87
+ );
88
+
89
+ return [directory, sessions] as const;
90
+ }),
91
+ );
92
+
93
+ return Object.fromEntries(entries) as Record<string, Session[]>;
94
+ },
95
+ );
96
+
48
97
  const boardColumns = createMemo<Record<BoardColumn, BoardCard[]>>(() => {
49
98
  const now = Date.now();
50
99
  const columns: Record<BoardColumn, BoardCard[]> = {
51
100
  progress: [],
52
101
  idle: [],
53
102
  };
103
+ const sessionsByProject = rootSessions() ?? {};
54
104
 
55
- for (const project of recent()) {
105
+ for (const project of projects()) {
56
106
  const [store] = sync.child(project.worktree);
57
- const sessions = sortedRootSessions(store, now);
107
+ const sessions = sessionsByProject[project.worktree] ?? [];
58
108
 
59
109
  for (const session of sessions) {
60
110
  const status = store.session_status[session.id];
@@ -82,7 +132,7 @@ export default function Home() {
82
132
  });
83
133
 
84
134
  createEffect(() => {
85
- for (const project of recent()) {
135
+ for (const project of projects()) {
86
136
  sync.child(project.worktree);
87
137
  }
88
138
  });
@@ -97,13 +147,13 @@ export default function Home() {
97
147
  function openProject(directory: string) {
98
148
  layout.projects.open(directory);
99
149
  server.projects.touch(directory);
100
- navigate(`/${base64Encode(directory)}/session`);
150
+ navigate(`/${base64Encode(directory)}`);
101
151
  }
102
152
 
103
153
  function openSession(directory: string, sessionID: string) {
104
154
  layout.projects.open(directory);
105
155
  server.projects.touch(directory);
106
- navigate(`/${base64Encode(directory)}/session/${sessionID}`);
156
+ navigate(`/${base64Encode(directory)}/${sessionID}`);
107
157
  }
108
158
 
109
159
  async function archiveSession(directory: string, sessionID: string) {
@@ -112,6 +162,13 @@ export default function Home() {
112
162
  sessionID,
113
163
  time: { archived: Date.now() },
114
164
  });
165
+ mutateRootSessions((current) => {
166
+ if (!current?.[directory]) return current;
167
+ return {
168
+ ...current,
169
+ [directory]: current[directory].filter((session) => session.id !== sessionID),
170
+ };
171
+ });
115
172
  setStore(
116
173
  "session",
117
174
  (sessions) => sessions.filter((session) => session.id !== sessionID),
@@ -148,7 +205,10 @@ export default function Home() {
148
205
  <div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
149
206
  <div class="min-w-0">
150
207
  <div class="flex items-center gap-3">
151
- <div class="flex size-10 items-center justify-center rounded-2xl border border-border-weak bg-background-base/80">
208
+ <div
209
+ class="flex size-10 items-center justify-center rounded-2xl border shadow-xs-border-base"
210
+ style={homeStyles.heroIcon}
211
+ >
152
212
  <Logo class="w-6 opacity-90" />
153
213
  </div>
154
214
  <div>
@@ -189,7 +249,7 @@ export default function Home() {
189
249
  </div>
190
250
 
191
251
  <Switch>
192
- <Match when={sync.data.project.length > 0}>
252
+ <Match when={projects().length > 0}>
193
253
  <div class="mt-6 flex w-full flex-col gap-6">
194
254
  <section class="flex flex-col gap-4">
195
255
  <div class="grid gap-3 lg:grid-cols-2">
@@ -216,9 +276,15 @@ export default function Home() {
216
276
  </div>
217
277
  </Match>
218
278
  <Match when={true}>
219
- <div class="mt-6 rounded-2xl border border-dashed border-border-weak bg-background-base/60 px-6 py-10">
279
+ <div
280
+ class="mt-6 rounded-2xl border border-dashed px-6 py-10 shadow-xs-border-base"
281
+ style={homeStyles.emptyPanel}
282
+ >
220
283
  <div class="mx-auto flex max-w-md flex-col items-center gap-3 text-center">
221
- <div class="flex size-12 items-center justify-center rounded-2xl border border-border-weak bg-background-base/80">
284
+ <div
285
+ class="flex size-12 items-center justify-center rounded-2xl border shadow-xs-border-base"
286
+ style={homeStyles.emptyIcon}
287
+ >
222
288
  <Icon name="folder-add-left" size="large" />
223
289
  </div>
224
290
  <div class="flex flex-col gap-1 items-center justify-center">
@@ -251,12 +317,25 @@ function BoardColumnView(props: {
251
317
  }) {
252
318
  const toneClass = () => {
253
319
  if (props.tone === "progress")
254
- return "bg-emerald-500/12 text-emerald-300 border-emerald-500/20";
255
- return "bg-sky-500/12 text-sky-300 border-sky-500/20";
320
+ return "bg-surface-base-hover text-icon-success-base border-border-weak-base";
321
+ return "bg-surface-inset-base text-icon-base border-border-weak-base";
256
322
  };
257
323
 
324
+ const sectionStyle = () =>
325
+ props.tone === "progress"
326
+ ? {
327
+ ...homeStyles.border,
328
+ background:
329
+ "linear-gradient(180deg, color-mix(in srgb, var(--text-interactive-base) 8%, var(--background-stronger)), var(--background-stronger))",
330
+ }
331
+ : {
332
+ ...homeStyles.border,
333
+ background:
334
+ "linear-gradient(180deg, color-mix(in srgb, var(--surface-inset-base) 55%, var(--background-stronger)), var(--background-stronger))",
335
+ };
336
+
258
337
  return (
259
- <section class="rounded-2xl border border-border-weak bg-background-base/80 p-3 md:p-4 min-h-72">
338
+ <section class="rounded-2xl p-3 md:p-4 min-h-72 shadow-xs-border-base" style={sectionStyle()}>
260
339
  <div class="flex items-center justify-between gap-3">
261
340
  <div class="flex items-center gap-2 min-w-0">
262
341
  <div
@@ -274,7 +353,10 @@ function BoardColumnView(props: {
274
353
  <Match when={props.cards.length > 0}>
275
354
  <For each={props.cards}>
276
355
  {(card) => (
277
- <div class="overflow-hidden rounded-xl border border-border-weak bg-background-base/70 hover:border-border-strong">
356
+ <div
357
+ class="overflow-hidden rounded-xl transition-colors"
358
+ style={homeStyles.card}
359
+ >
278
360
  <div class="flex items-start justify-between gap-3 px-3 pt-3">
279
361
  <div class="min-w-0 flex-1 pt-1">
280
362
  <div class="truncate text-14-medium leading-tight text-text-strong">
@@ -320,7 +402,10 @@ function BoardColumnView(props: {
320
402
  </For>
321
403
  </Match>
322
404
  <Match when={true}>
323
- <div class="rounded-xl border border-dashed border-border-weak px-3 py-8 text-center text-12-regular text-text-weak">
405
+ <div
406
+ class="rounded-xl border border-dashed px-3 py-8 text-center text-12-regular text-text-weak"
407
+ style={homeStyles.emptyState}
408
+ >
324
409
  {props.empty}
325
410
  </div>
326
411
  </Match>
@@ -101,7 +101,7 @@ const SessionRow = (props: {
101
101
  cancelHoverPrefetch: () => void
102
102
  }): JSX.Element => (
103
103
  <A
104
- href={`/${props.slug}/session/${props.session.id}`}
104
+ href={`/${props.slug}/${props.session.id}`}
105
105
  class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
106
106
  onPointerEnter={props.scheduleHoverPrefetch}
107
107
  onPointerLeave={props.cancelHoverPrefetch}
@@ -310,7 +310,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
310
310
  onMessageSelect={(message) => {
311
311
  if (!isActive()) {
312
312
  layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
313
- navigate(`${props.slug}/session/${props.session.id}`)
313
+ navigate(`/${props.slug}/${props.session.id}`)
314
314
  return
315
315
  }
316
316
  window.history.replaceState(null, "", `#message-${message.id}`)
@@ -360,7 +360,7 @@ export const NewSessionItem = (props: {
360
360
  const tooltip = () => props.mobile || !props.sidebarExpanded()
361
361
  const item = (
362
362
  <A
363
- href={`/${props.slug}/session`}
363
+ href={`/${props.slug}`}
364
364
  end
365
365
  class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
366
366
  onClick={() => {
@@ -8,7 +8,7 @@ import { Icon } from "ikanban-ui/icon"
8
8
  import { IconButton } from "ikanban-ui/icon-button"
9
9
  import { Tooltip } from "ikanban-ui/tooltip"
10
10
  import { createSortable } from "@thisbeyond/solid-dnd"
11
- import { useLayout, type LocalProject } from "@/context/layout"
11
+ import { type LocalProject } from "@/context/layout"
12
12
  import { useGlobalSync } from "@/context/global-sync"
13
13
  import { useLanguage } from "@/context/language"
14
14
  import { useNotification } from "@/context/notification"
@@ -77,7 +77,6 @@ const ProjectTile = (props: {
77
77
  language: ReturnType<typeof useLanguage>
78
78
  }): JSX.Element => {
79
79
  const notification = useNotification()
80
- const layout = useLayout()
81
80
  const unseenCount = createMemo(() =>
82
81
  props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
83
82
  )
@@ -127,7 +126,6 @@ const ProjectTile = (props: {
127
126
  onClick={() => {
128
127
  if (props.selected()) {
129
128
  props.setSuppressHover(true)
130
- layout.sidebar.toggle()
131
129
  return
132
130
  }
133
131
  props.setSuppressHover(false)
@@ -1 +1 @@
1
- export const sidebarExpanded = (mobile: boolean | undefined, opened: boolean) => !!mobile || opened
1
+ export const sidebarExpanded = (mobile: boolean | undefined, _opened: boolean) => !!mobile
@@ -6,8 +6,8 @@ describe("sidebarExpanded", () => {
6
6
  expect(sidebarExpanded(true, false)).toBe(true)
7
7
  })
8
8
 
9
- test("follows desktop open state when not mobile", () => {
10
- expect(sidebarExpanded(false, true)).toBe(true)
9
+ test("stays closed on desktop now that the sidebar pane is removed", () => {
10
+ expect(sidebarExpanded(false, true)).toBe(false)
11
11
  expect(sidebarExpanded(false, false)).toBe(false)
12
12
  })
13
13
  })
@@ -437,7 +437,7 @@ export const SortableWorkspace = (props: {
437
437
  root={props.project.worktree}
438
438
  setHoverSession={props.ctx.setHoverSession}
439
439
  clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
440
- navigateToNewSession={() => navigate(`/${slug()}/session`)}
440
+ navigateToNewSession={() => navigate(`/${slug()}`)}
441
441
  />
442
442
  </div>
443
443
  </div>