kanna-code 0.26.3 → 0.26.5

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Kanna</title>
8
- <script type="module" crossorigin src="/assets/index-QXLrswKX.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BaEB4jie.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-C5RBxQW4.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kanna-code",
3
3
  "type": "module",
4
- "version": "0.26.3",
4
+ "version": "0.26.5",
5
5
  "description": "A beautiful web UI for Claude Code",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test"
2
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
3
3
  import { tmpdir } from "node:os"
4
4
  import path from "node:path"
5
5
  import { appendGitIgnoreEntry, DiffStore, extractGitHubRepoSlug, fetchGitHubPullRequests } from "./diff-store"
@@ -148,6 +148,52 @@ describe("DiffStore", () => {
148
148
  expect((await run(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot)).trim()).toBe("origin/feature/publish-me")
149
149
  })
150
150
 
151
+ test("commit_and_push degrades to a local commit when origin is missing", async () => {
152
+ const repoRoot = await createRepo()
153
+ tempDirs.push(repoRoot)
154
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
155
+ await run(["git", "add", "."], repoRoot)
156
+ await run(["git", "commit", "-m", "init"], repoRoot)
157
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
158
+
159
+ const store = new DiffStore(repoRoot)
160
+ await store.initialize()
161
+ await store.refreshSnapshot("project-1", repoRoot)
162
+
163
+ const result = await store.commitFiles({
164
+ projectId: "project-1",
165
+ projectPath: repoRoot,
166
+ paths: ["app.txt"],
167
+ summary: "Local only",
168
+ mode: "commit_and_push",
169
+ })
170
+
171
+ expect(result).toMatchObject({
172
+ ok: true,
173
+ mode: "commit_and_push",
174
+ pushed: false,
175
+ })
176
+ expect((await run(["git", "log", "-1", "--pretty=%s"], repoRoot)).trim()).toBe("Local only")
177
+ })
178
+
179
+ test("refreshSnapshot reports origin presence before the first commit", async () => {
180
+ const repoRoot = await createRepo()
181
+ tempDirs.push(repoRoot)
182
+ await run(["git", "remote", "add", "origin", "https://github.com/jakemor/test224.git"], repoRoot)
183
+ await writeFile(path.join(repoRoot, "poem.md"), "rose\n", "utf8")
184
+
185
+ const store = new DiffStore(repoRoot)
186
+ await store.initialize()
187
+ await store.refreshSnapshot("project-1", repoRoot)
188
+
189
+ expect(store.getProjectSnapshot("project-1")).toMatchObject({
190
+ status: "ready",
191
+ branchName: "main",
192
+ hasOriginRemote: true,
193
+ originRepoSlug: "jakemor/test224",
194
+ })
195
+ })
196
+
151
197
  test("detects renamed files", async () => {
152
198
  const repoRoot = await createRepo()
153
199
  tempDirs.push(repoRoot)
@@ -273,6 +319,27 @@ describe("DiffStore", () => {
273
319
  expect(await readFile(path.join(repoRoot, ".gitignore"), "utf8")).toBe("scratch.log\n")
274
320
  })
275
321
 
322
+ test("ignoreFile accepts a folder entry for an untracked diff", async () => {
323
+ const repoRoot = await createRepo()
324
+ tempDirs.push(repoRoot)
325
+ await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
326
+ await run(["git", "add", "."], repoRoot)
327
+ await run(["git", "commit", "-m", "init"], repoRoot)
328
+ await mkdir(path.join(repoRoot, "tmp/cache"), { recursive: true })
329
+ await writeFile(path.join(repoRoot, "tmp/cache/output.log"), "tmp\n", "utf8")
330
+
331
+ const store = new DiffStore(repoRoot)
332
+ await store.initialize()
333
+ await store.refreshSnapshot("project-1", repoRoot)
334
+ await store.ignoreFile({
335
+ projectId: "project-1",
336
+ projectPath: repoRoot,
337
+ path: "tmp/cache/",
338
+ })
339
+
340
+ expect(await readFile(path.join(repoRoot, ".gitignore"), "utf8")).toBe("tmp/cache/\n")
341
+ })
342
+
276
343
  test("appendGitIgnoreEntry does not duplicate an existing identical entry", () => {
277
344
  expect(appendGitIgnoreEntry("scratch.log\n", "scratch.log")).toBe("scratch.log\n")
278
345
  expect(appendGitIgnoreEntry("scratch.log", "scratch.log")).toBe("scratch.log\n")
@@ -37,6 +37,7 @@ function createEmptyState(): StoredChatDiffState {
37
37
  status: "unknown",
38
38
  branchName: undefined,
39
39
  defaultBranchName: undefined,
40
+ hasOriginRemote: undefined,
40
41
  originRepoSlug: undefined,
41
42
  hasUpstream: undefined,
42
43
  aheadCount: undefined,
@@ -50,6 +51,7 @@ function createEmptyState(): StoredChatDiffState {
50
51
  function branchMetadataEqual(left: BranchMetadata, right: BranchMetadata) {
51
52
  return left.branchName === right.branchName
52
53
  && left.defaultBranchName === right.defaultBranchName
54
+ && left.hasOriginRemote === right.hasOriginRemote
53
55
  && left.originRepoSlug === right.originRepoSlug
54
56
  && left.hasUpstream === right.hasUpstream
55
57
  }
@@ -196,6 +198,9 @@ function createPushFailure(mode: DiffCommitMode, detail: string, snapshotChanged
196
198
  if (normalized.includes("non-fast-forward") || normalized.includes("fetch first")) {
197
199
  title = "Branch is not up to date"
198
200
  message = "Your branch is behind its remote. Pull or rebase, then try pushing again."
201
+ } else if (normalized.includes("does not appear to be a git repository")) {
202
+ title = "No origin remote configured"
203
+ message = "This repository does not have an origin remote configured."
199
204
  } else if (normalized.includes("has no upstream branch") || normalized.includes("set-upstream")) {
200
205
  title = "No upstream branch configured"
201
206
  message = "This branch does not have an upstream remote branch configured yet."
@@ -1317,6 +1322,7 @@ export class DiffStore {
1317
1322
  status: state.status,
1318
1323
  branchName: state.branchName,
1319
1324
  defaultBranchName: state.defaultBranchName,
1325
+ hasOriginRemote: state.hasOriginRemote,
1320
1326
  originRepoSlug: state.originRepoSlug,
1321
1327
  hasUpstream: state.hasUpstream,
1322
1328
  aheadCount: state.aheadCount,
@@ -1339,6 +1345,7 @@ export class DiffStore {
1339
1345
  status: "no_repo",
1340
1346
  branchName: undefined,
1341
1347
  defaultBranchName: undefined,
1348
+ hasOriginRemote: undefined,
1342
1349
  originRepoSlug: undefined,
1343
1350
  hasUpstream: undefined,
1344
1351
  aheadCount: undefined,
@@ -1356,6 +1363,7 @@ export class DiffStore {
1356
1363
  const branchName = await getBranchName(repo.repoRoot)
1357
1364
  const defaultBranchName = await resolveDefaultBranchName(repo.repoRoot)
1358
1365
  const originRemoteUrl = await getOriginRemoteUrl(repo.repoRoot)
1366
+ const hasOriginRemote = originRemoteUrl !== null
1359
1367
  const originRepoSlug = extractGitHubRepoSlug(originRemoteUrl) ?? undefined
1360
1368
  const hasUpstream = await hasUpstreamBranch(repo.repoRoot)
1361
1369
  const { aheadCount, behindCount } = hasUpstream
@@ -1373,6 +1381,7 @@ export class DiffStore {
1373
1381
  status: "ready",
1374
1382
  branchName,
1375
1383
  defaultBranchName,
1384
+ hasOriginRemote,
1376
1385
  originRepoSlug,
1377
1386
  hasUpstream,
1378
1387
  aheadCount,
@@ -1808,7 +1817,11 @@ export class DiffStore {
1808
1817
  throw new Error("Project is not in a git repository")
1809
1818
  }
1810
1819
 
1811
- const hasUpstream = await hasUpstreamBranch(repo.repoRoot)
1820
+ const [hasUpstream, originRemoteUrl] = await Promise.all([
1821
+ hasUpstreamBranch(repo.repoRoot),
1822
+ getOriginRemoteUrl(repo.repoRoot),
1823
+ ])
1824
+ const hasOriginRemote = originRemoteUrl !== null
1812
1825
  if (args.action === "publish") {
1813
1826
  const publishResult = await runGit(["push", "-u", "origin", "HEAD"], repo.repoRoot)
1814
1827
  if (publishResult.exitCode !== 0) {
@@ -2002,7 +2015,11 @@ export class DiffStore {
2002
2015
  if (!repo) {
2003
2016
  throw new Error("Project is not in a git repository")
2004
2017
  }
2005
- const hasUpstream = await hasUpstreamBranch(repo.repoRoot)
2018
+ const [hasUpstream, originRemoteUrl] = await Promise.all([
2019
+ hasUpstreamBranch(repo.repoRoot),
2020
+ getOriginRemoteUrl(repo.repoRoot),
2021
+ ])
2022
+ const hasOriginRemote = originRemoteUrl !== null
2006
2023
 
2007
2024
  const currentDirtyPaths = new Set((await listDirtyPaths(repo.repoRoot)).map((entry) => entry.path))
2008
2025
  const missingPaths = normalizedPaths.filter((relativePath) => !currentDirtyPaths.has(relativePath))
@@ -2039,6 +2056,16 @@ export class DiffStore {
2039
2056
  } satisfies DiffCommitResult
2040
2057
  }
2041
2058
 
2059
+ if (!hasUpstream && !hasOriginRemote) {
2060
+ return {
2061
+ ok: true,
2062
+ mode: args.mode,
2063
+ branchName,
2064
+ pushed: false,
2065
+ snapshotChanged,
2066
+ } satisfies DiffCommitResult
2067
+ }
2068
+
2042
2069
  const pushResult = hasUpstream
2043
2070
  ? await runGit(["push"], repo.repoRoot)
2044
2071
  : await runGit(["push", "-u", "origin", "HEAD"], repo.repoRoot)
@@ -2103,23 +2130,26 @@ export class DiffStore {
2103
2130
  projectPath: string
2104
2131
  path: string
2105
2132
  }) {
2106
- const relativePath = normalizeRepoRelativePath(args.path)
2133
+ const ignoreEntry = normalizeRepoRelativePath(args.path)
2107
2134
  const repo = await resolveRepo(args.projectPath)
2108
2135
  if (!repo) {
2109
2136
  throw new Error("Project is not in a git repository")
2110
2137
  }
2111
2138
 
2112
- const entry = await findDirtyPath(repo.repoRoot, relativePath)
2113
- if (!entry) {
2114
- throw new Error(`File is no longer changed: ${relativePath}`)
2115
- }
2116
- if (!entry.isUntracked) {
2139
+ const dirtyPaths = await listDirtyPaths(repo.repoRoot)
2140
+ const exactEntry = dirtyPaths.find((candidate) => candidate.path === ignoreEntry)
2141
+ if (exactEntry && !exactEntry.isUntracked) {
2117
2142
  throw new Error("Only untracked files can be ignored from the diff viewer")
2118
2143
  }
2119
2144
 
2145
+ const entry = dirtyPaths.find((candidate) => candidate.isUntracked && (candidate.path === ignoreEntry || candidate.path.startsWith(ignoreEntry)))
2146
+ if (!entry) {
2147
+ throw new Error(`File is no longer changed: ${ignoreEntry}`)
2148
+ }
2149
+
2120
2150
  const gitignorePath = path.join(repo.repoRoot, ".gitignore")
2121
2151
  const currentContents = await readFile(gitignorePath, "utf8").catch(() => null)
2122
- const nextContents = appendGitIgnoreEntry(currentContents, relativePath)
2152
+ const nextContents = appendGitIgnoreEntry(currentContents, ignoreEntry)
2123
2153
  if (nextContents !== currentContents) {
2124
2154
  await writeFile(gitignorePath, nextContents, "utf8")
2125
2155
  }
@@ -15,8 +15,8 @@ import { createWsRouter, type ClientState } from "./ws-router"
15
15
  import { deleteProjectUpload, inferAttachmentContentType, inferProjectFileContentType, persistProjectUpload } from "./uploads"
16
16
  import { getProjectUploadDir } from "./paths"
17
17
 
18
- const MAX_UPLOAD_FILES = 10
19
- const MAX_UPLOAD_SIZE_BYTES = 25 * 1024 * 1024
18
+ const MAX_UPLOAD_FILES = 50
19
+ const MAX_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024
20
20
  const STALE_EMPTY_CHAT_PRUNE_INTERVAL_MS = 60 * 1000
21
21
 
22
22
  export async function persistUploadedFiles(args: {
@@ -182,7 +182,7 @@ describe("uploads", () => {
182
182
  try {
183
183
  const project = await server.store.openProject(projectDir, "Project")
184
184
  const formData = new FormData()
185
- formData.append("files", new File([new Uint8Array(25 * 1024 * 1024 + 1)], "big.bin", { type: "application/octet-stream" }))
185
+ formData.append("files", new File([new Uint8Array(100 * 1024 * 1024 + 1)], "big.bin", { type: "application/octet-stream" }))
186
186
 
187
187
  const response = await fetch(`http://localhost:${server.port}/api/projects/${project.id}/uploads`, {
188
188
  method: "POST",
@@ -191,7 +191,7 @@ describe("uploads", () => {
191
191
 
192
192
  expect(response.status).toBe(413)
193
193
  expect(await response.json()).toEqual({
194
- error: "File \"big.bin\" exceeds the 25 MB limit.",
194
+ error: "File \"big.bin\" exceeds the 100 MB limit.",
195
195
  })
196
196
  } finally {
197
197
  await server.stop()
@@ -57,7 +57,7 @@ export function createWsRouter({
57
57
  }: CreateWsRouterArgs) {
58
58
  const sockets = new Set<ServerWebSocket<ClientState>>()
59
59
  const resolvedDiffStore = diffStore ?? {
60
- getProjectSnapshot: () => ({ status: "unknown", branchName: undefined, defaultBranchName: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] as const } }),
60
+ getProjectSnapshot: () => ({ status: "unknown", branchName: undefined, defaultBranchName: undefined, hasOriginRemote: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] as const } }),
61
61
  refreshSnapshot: async () => false,
62
62
  initializeGit: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
63
63
  getGitHubPublishInfo: async () => ({ ghInstalled: false, authenticated: false, activeAccountLogin: undefined, owners: [], suggestedRepoName: "my-repo" }),
@@ -544,6 +544,7 @@ export interface GitHubRepoAvailabilityResult {
544
544
  export interface BranchMetadata {
545
545
  branchName?: string
546
546
  defaultBranchName?: string
547
+ hasOriginRemote?: boolean
547
548
  originRepoSlug?: string
548
549
  hasUpstream?: boolean
549
550
  }