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
2 changes: 2 additions & 0 deletions .agents/skills/run-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ The runners install locked dependencies, resolve GhosttyKit when needed, and kee

Keep at most one runner active per worktree. Track and stop the parent process you started before rebuilding. Never stop or modify another worktree's instance; if this worktree already has an instance you do not own, reuse it or report that it is running.

For a permission-grant restart that must preserve the existing signed macOS binary, use `bun run dev:macos --reuse-macos-build` (container flags still apply). This verifies the worktree app's identity and strict signature, skips native building/signing, and starts the usual managed services and app. It deliberately does not incorporate native source edits. Missing, mismatched, or invalid artifacts fail without falling back to a rebuild. Normal launches continue to build. This option is macOS-only.

For website-only development, use `bun run dev:web`.
47 changes: 47 additions & 0 deletions .github/workflows/webrtc-artifact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Native WebRTC Artifact

# Manual until the source build and generated notices have been reviewed.
# This job uploads a candidate; it does not publish or change SwiftPM pins.
on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: native-webrtc-artifact
cancel-in-progress: false

jobs:
build:
runs-on: [self-hosted, macOS, ARM64, codevisor-macos-arm64]
timeout-minutes: 240
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Read pinned tools
id: tools
run: |
node -e 'const p=JSON.parse(require("fs").readFileSync("scripts/webrtc-build.lock.json")); console.log("python="+p.pythonVersion); console.log("xcode="+p.xcodeVersion)' >> "$GITHUB_OUTPUT"
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ steps.tools.outputs.python }}
- name: Build frameworks and dependency notices
shell: bash
run: |
developer_dir="/Applications/Xcode_${{ steps.tools.outputs.xcode }}.app/Contents/Developer"
if [[ ! -x "$developer_dir/usr/bin/xcodebuild" ]]; then
developer_dir="$(scripts/release/resolve-xcode.sh)"
fi
# The recipe still enforces the exact pinned version before fetching.
DEVELOPER_DIR="$developer_dir" node scripts/build-webrtc.mjs
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: native-webrtc-candidate
retention-days: 14
if-no-files-found: error
path: |
tmp/webrtc-source/*/artifacts/WebRTC.xcframework.zip
tmp/webrtc-source/*/artifacts/manifest.json
tmp/webrtc-source/*/artifacts/revisions.txt
tmp/webrtc-source/*/artifacts/*-gn-args.txt
tmp/webrtc-source/*/artifacts/notices-*/LICENSE.md
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@
[submodule ".repos/cef"]
path = .repos/cef
url = https://github.com/chromiumembedded/cef.git
[submodule ".repos/WebRTC"]
path = .repos/WebRTC
url = https://github.com/stasel/WebRTC.git
[submodule ".repos/acp-registry"]
path = .repos/acp-registry
url = https://github.com/agentclientprotocol/registry.git
1 change: 1 addition & 0 deletions .repos/WebRTC
Submodule WebRTC added at 1d0469

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/ios/Codevisor/Features/Home/HomeSidebarSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ struct HomeSidebarTabRow: Identifiable, Equatable {
case browser(favicon: UIImage?)
case plugin(pluginId: String, paneType: String?)
case document
case screenSharing
case newTab
}

Expand Down
4 changes: 3 additions & 1 deletion apps/ios/Codevisor/Features/Home/HomeView+Sidebar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ extension HomeView {
return "New Tab"
case .browser:
return BrowserPaneCache.shared.localTitle(paneId: pane.id) ?? pane.name
case .terminal, .plugin, .document:
case .terminal, .plugin, .document, .screenSharing:
return pane.name
}
}
Expand All @@ -133,6 +133,8 @@ extension HomeView {
.plugin(pluginId: pane.pluginId ?? "", paneType: pane.pluginPaneType)
case .document:
.document
case .screenSharing:
.screenSharing
case .newTab:
.newTab
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ struct HomeSidebarTabIcon: View {
} else {
symbol("puzzlepiece.extension")
}
case .screenSharing:
symbol("display")
case .document:
symbol("doc.richtext")
case .newTab:
Expand All @@ -71,6 +73,7 @@ extension HomeSidebarTabRow.Icon {
case let .terminal(isAgentOwned): isAgentOwned ? "Agent terminal" : "Terminal"
case .browser: "Browser"
case .plugin: "Plugin"
case .screenSharing: "Screen Sharing"
case .document: "Document"
case .newTab: "New Tab"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ struct WorkspacePaneContentView: View {
.task(id: environment.machines.httpConnectionState(forMachineId: machineId)) {
await browserPaneModel(pane).connectionDidChange()
}
case .screenSharing:
ContentUnavailableView(
"Screen Sharing", systemImage: "display",
description: Text("Screen Sharing currently requires Codevisor on a Mac."))
case .document:
if let path = pane.documentPath {
MarkdownDocumentView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ extension WorkspaceScreen {
return lhs.chatSessionId != nil && lhs.chatSessionId == rhs.chatSessionId
case .terminal:
return lhs.terminalKey.caseInsensitiveCompare(rhs.terminalKey) == .orderedSame
case .newTab, .plugin, .document, .browser:
case .newTab, .plugin, .document, .browser, .screenSharing:
return false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ struct WorkspaceScreen: View {
return "New Tab"
case .browser:
return BrowserPaneCache.shared.localTitle(paneId: pane.id) ?? pane.name
case .terminal, .plugin, .document:
case .terminal, .plugin, .document, .screenSharing:
return pane.name
}
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions apps/macos/Codevisor/CloudAuthDeeplinkHandling.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import SwiftUI
import CodevisorCore

/// codevisor://cloud-auth?ott=… deeplink handling: completes a cloud sign-in
/// that came back through the default browser (the in-app
/// ASWebAuthenticationSession path never leaves the app) and routes to the
/// Account settings tab so the result is visible.
struct CloudAuthDeeplinkHandling: ViewModifier {
@Environment(AppEnvironment.self) private var environment
@Environment(\.openSettings) private var openSettings

func body(content: Content) -> some View {
content
// No confirmation gate: the one-time token proves a sign-in this
// user just performed, is single-use, and expires in minutes.
.onOpenURL { url in
guard let deeplink = CloudAuthDeeplink.parse(url) else { return }
Task { await environment.cloud.completeSignIn(ott: deeplink.ott) }
SettingsRouter.shared.showMachines()
openSettings()
}
}
}
85 changes: 43 additions & 42 deletions apps/macos/Codevisor/CodevisorApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ struct RootView: View {
switch selection {
case let .session(serverId, sessionId):
sessionDetail(store, serverId: serverId, sessionId: sessionId)
case let .workspace(serverId, workspaceId):
workspaceDetail(store, serverId: serverId, workspaceId: workspaceId)
case let .newChat(target):
newChat(store, target: target)
case .none:
Expand All @@ -507,10 +509,9 @@ struct RootView: View {
{
let controller = store.controller(for: session, project: project)
SessionContainerView(
session: session,
mount: .chat(session, controller),
project: project,
store: store,
controller: controller,
onFocusedChatChanged: { chatId in
self.selection = .session(serverId: serverId, id: chatId)
}
Expand All @@ -533,6 +534,42 @@ struct RootView: View {
}
}

/// A workspace shown without a chat: the same container, mounted on the
/// workspace itself. Its panes, splits, toolbar and New Tab page are the
/// shared ones; nothing here creates a session, a worktree or an agent.
@ViewBuilder
private func workspaceDetail(
_ store: SessionStore,
serverId: String,
workspaceId: UUID
) -> some View {
if let workspace = environment.workspaces.workspace(id: workspaceId),
workspace.serverId == serverId,
let project = environment.projectList.projects.first(where: {
$0.serverId == serverId && $0.id == workspace.projectId
})
{
SessionContainerView(
mount: .workspace(workspace),
project: project,
store: store,
// The moment a chat exists in this workspace (New Tab → New Chat), the
// selection moves to it: the container remounts as `.chat`, which is
// what upgrades the cached leaf group and restores chat affordances.
onFocusedChatChanged: { chatId in
self.selection = .session(serverId: serverId, id: chatId)
}
)
.id("\(serverId):\(workspaceId.uuidString)")
} else {
ContentUnavailableView(
"Workspace Unavailable",
systemImage: "rectangle.on.rectangle.slash",
description: Text("This workspace is no longer available on its machine.")
)
}
}

/// The standalone new-chat page. Creates NOTHING until the first message
/// is sent — sending resolves the picked directory (project folder or a
/// fresh worktree) and materializes the workspace around the started
Expand All @@ -553,6 +590,10 @@ struct RootView: View {
/// Identifies the current sidebar selection.
enum SidebarSelection: Hashable {
case session(serverId: String, id: UUID)
/// A workspace shown on its own. Workspaces own their layout and server
/// identity independently of any chat, so one that has never hosted a chat
/// is still somewhere the user can be.
case workspace(serverId: String, id: UUID)
case newChat(NewChatTarget?)
}

Expand All @@ -578,43 +619,3 @@ struct NewChatTarget: Hashable {
.environment(AppEnvironment.preview())
.frame(width: 1100, height: 720)
}

/// codevisor://cloud-auth?ott=… deeplink handling: completes a cloud sign-in
/// that came back through the default browser (the in-app
/// ASWebAuthenticationSession path never leaves the app) and routes to the
/// Account settings tab so the result is visible.
private struct CloudAuthDeeplinkHandling: ViewModifier {
@Environment(AppEnvironment.self) private var environment
@Environment(\.openSettings) private var openSettings

func body(content: Content) -> some View {
content
// No confirmation gate: the one-time token proves a sign-in this
// user just performed, is single-use, and expires in minutes.
.onOpenURL { url in
guard let deeplink = CloudAuthDeeplink.parse(url) else { return }
Task { await environment.cloud.completeSignIn(ott: deeplink.ott) }
SettingsRouter.shared.showMachines()
openSettings()
}
}
}

/// codevisor://install-plugin deeplink handling: routes to the selected
/// machine's Plugins settings page with the linked repo staged as a pending
/// install. Never auto-installs — the plugins pane opens the standard
/// discover→consent sheet, so the verbatim commands are always shown before
/// anything runs.
private struct PluginInstallDeeplinkHandling: ViewModifier {
@Environment(\.openSettings) private var openSettings

func body(content: Content) -> some View {
content
.onOpenURL { url in
guard let deeplink = PluginInstallDeeplink.parse(url) else { return }
SettingsRouter.shared.pendingPluginInstallSource = deeplink.repo
SettingsRouter.shared.showPlugins()
openSettings()
}
}
}
17 changes: 12 additions & 5 deletions apps/macos/Codevisor/Features/Panes/MarkdownDocumentPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ final class MarkdownDocumentPane: Pane {

init(context: PaneContext, descriptor: PaneDescriptorState) {
self.id = descriptor.id
self.document = MarkdownDocumentModel(
path: descriptor.documentPath ?? "",
sessionId: context.sessionId,
client: context.client ?? CodevisorServerClient(config: context.machine.serverConfig)
)
let path = descriptor.documentPath ?? ""
// Reading a file goes through a chat session's file API. A pane group
// without one reports the document as unavailable rather than fetching
// under some other session's identity.
self.document =
context.sessionId.map { sessionId in
MarkdownDocumentModel(
path: path,
sessionId: sessionId,
client: context.client ?? CodevisorServerClient(config: context.machine.serverConfig)
)
} ?? MarkdownDocumentModel(unavailablePath: path)
}

func makeView() -> AnyView {
Expand Down
Loading