diff --git a/.TermindPresentationQA.xcodeproj/project.pbxproj b/.TermindPresentationQA.xcodeproj/project.pbxproj new file mode 100644 index 00000000..20afa7c7 --- /dev/null +++ b/.TermindPresentationQA.xcodeproj/project.pbxproj @@ -0,0 +1,2692 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 0202B16D630CC57432748F0C /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + 03892FFD3F1E351BD2C296E4 /* TermindWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 32414DBD2157FBAD4D00056D /* TermindWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 082994E88792AF7D1F99B147 /* TermindPersistence.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */; }; + 0861B3AA103F736DEE93983B /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; }; + 08620C447AFE3884EA26ADCC /* TermindPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F2CD72AE76428E896F81B64 /* TermindPresentation.framework */; }; + 0C77A0D7A810A78DA1F6CD93 /* CSSH2 in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100011 /* CSSH2 */; }; + 0D54DF6508C7918D7D5B9D5A /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100014 /* OpenSSL */; }; + 10DD764492A94453449B3484 /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; }; + 1D479CE3B3FF74B0C23BA3F2 /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; + 20ADB83C9DB7F78EDF8BCBDF /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; + 23E272F32F769A979F3ABF42 /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; + 25C0658E2DD9B2FE28023DA7 /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + 26C689ECB8B41A357765CCF1 /* TermindRemoteInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 21951230A1FF30987ADC3B6B /* TermindRemoteInfrastructure.framework */; }; + 38462C442F34CFDE6FF4661D /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; + 38B690B07BB71C9BAA6B45B0 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + 3CEFDFDB25F7D9C9A795C601 /* TermindPersistence.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */; }; + 5310B1DD3FA3FE77185B3F4A /* TermindRemoteInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 21951230A1FF30987ADC3B6B /* TermindRemoteInfrastructure.framework */; }; + 640E0A8248C5231A7424BE56 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + 6995A68F3132E9C67B0DA01E /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + 6DF0C216374DA144B1FC2030 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + 6FB449BC0FE9518E6E22B442 /* TermindPersistence.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */; }; + 76535271991F0D93E7277E6F /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + 7B23538CFDAF459CA4CB4351 /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + 7B51DD6FD59A860DDA7F80EE /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; + 7DE0BDBF5E7A76602DDD443D /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + 81A97F4DC317A6940A7A2CA5 /* TermindPersistence.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */; }; + 88E7F9CE7BC5631B487E2BC5 /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + 8BFD569E84CA09FDEFF87698 /* TermindPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F2CD72AE76428E896F81B64 /* TermindPresentation.framework */; }; + 8D2404FE022681C084DE0E23 /* TermindPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F2CD72AE76428E896F81B64 /* TermindPresentation.framework */; }; + 8F73CAE12C350F5FDB033EE0 /* GhosttyKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100016 /* GhosttyKit */; }; + 8FCAF8FD0FF78E38C58ED887 /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + 9AF99D2C7AF5E879598A5972 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + A04F3C049C318FA491B42039 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + A671EB6598B90BA03EF86CE7 /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + A9A947EA53E15BD04CE319C1 /* TermindPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F2CD72AE76428E896F81B64 /* TermindPresentation.framework */; }; + AA1200000000000000000001 /* GhosttyTerminal in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100012 /* GhosttyTerminal */; }; + AA1200000000000000000002 /* GhosttyTheme in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100013 /* GhosttyTheme */; }; + AA1200000000000000000005 /* GhosttyTerminal in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100012 /* GhosttyTerminal */; }; + AA1200000000000000000006 /* GhosttyTheme in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100013 /* GhosttyTheme */; }; + AA1200000000000000000009 /* GhosttyKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100016 /* GhosttyKit */; }; + AA1200000000000000000010 /* MarkdownView in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100017 /* MarkdownView */; }; + AA1200000000000000000013 /* TermindPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F2CD72AE76428E896F81B64 /* TermindPresentation.framework */; }; + AA1300000000000000000001 /* ListViewKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100018 /* ListViewKit */; }; + AB2210ABB10F707CBFB01C78 /* GhosttyTheme in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100013 /* GhosttyTheme */; }; + ABCDEF010000000000000001 /* TermindPersistence.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */; }; + ABCDEF020000000000000001 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100015 /* ZIPFoundation */; }; + ABCDEF020000000000000002 /* AgentKitScrubber in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001C /* AgentKitScrubber */; }; + ABCDEF020000000000000003 /* MarkdownView in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100017 /* MarkdownView */; }; + B08DD88AB7C2444E5AEC19BC /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + B1821FD31E7D009C900B5557 /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + B616FF213FC31346E6BB3817 /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; }; + BAAD00000000000000000003 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = BAAD00000000000000000002 /* GRDB */; }; + BAAD00000000000000000010 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = BAAD00000000000000000002 /* GRDB */; }; + BAAD00000000000000000011 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = BAAD00000000000000000002 /* GRDB */; }; + BAAD00000000000000000012 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = BAAD00000000000000000002 /* GRDB */; }; + BAAD00000000000000000013 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = BAAD00000000000000000002 /* GRDB */; }; + BAAD00000000000000000014 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = BAAD00000000000000000002 /* GRDB */; }; + C2D4B372BA5A040DDBCEFC85 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + C4AD00000000000000000001 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100015 /* ZIPFoundation */; }; + C4AD00000000000000000002 /* MarkdownView in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100017 /* MarkdownView */; }; + CF863393BB8C2B38A9EEE18A /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; }; + D7D1BBB65020F61F7EFDDFD7 /* GhosttyTerminal in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100012 /* GhosttyTerminal */; }; + E635C54CA9F1A63D909CFCE6 /* TermindPersistence.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */; }; + EF2F88E4809C845C3F1BBF63 /* TermindDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; }; + F6ACDF3012081771F6C8F659 /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; + F996BFD771BC77E67AB91FFB /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; }; + FACB579B671967F922A33CD9 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; }; + FB01141D5DD3036A40889B76 /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; }; + TRMD00000000000000030001 /* termind-mcp in Embed MCP Bridge */ = {isa = PBXBuildFile; fileRef = TRMD00000000000000030002 /* termind-mcp */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + TRMD00000000000000100002 /* GhosttyTerminal in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100012 /* GhosttyTerminal */; }; + TRMD00000000000000100003 /* GhosttyTheme in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100013 /* GhosttyTheme */; }; + TRMD00000000000000100005 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100015 /* ZIPFoundation */; }; + TRMD00000000000000100006 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100015 /* ZIPFoundation */; }; + TRMD00000000000000100007 /* GhosttyKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100016 /* GhosttyKit */; }; + TRMD00000000000000100008 /* MarkdownView in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100017 /* MarkdownView */; }; + TRMD00000000000000100009 /* ListViewKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD00000000000000100018 /* ListViewKit */; }; + TRMD0000000000000010000B /* GlyphixTextFx in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001A /* GlyphixTextFx */; }; + TRMD0000000000000010000C /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; + TRMD0000000000000010000D /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; }; + TRMD0000000000000010000E /* AgentKitScrubber in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001C /* AgentKitScrubber */; }; + TRMD0000000000000010000F /* AgentKitScrubber in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001C /* AgentKitScrubber */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0A35D3902E3320DBDB756D0B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = Framework; + }; + 25EB7B3558E87335BF52EBFD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ADAD84AB61B493DA1D22B54C; + remoteInfo = Framework; + }; + 266FBAED0BB986F8F0D1000B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 727F565FD44C8D180B283598; + remoteInfo = Framework; + }; + 2B98C090049C0DB38AA2199B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 727F565FD44C8D180B283598; + remoteInfo = Framework; + }; + 2F5BAF1F7EA44EC2E1F6EA73 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = Framework; + }; + 81596FC81A193992EB68BB60 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BDA6B93652DE90AEBDA7090E; + remoteInfo = Framework; + }; + 892065B84B7200DAAE43F5F2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BDA6B93652DE90AEBDA7090E; + remoteInfo = Framework; + }; + 8A1D0EA6890B1E7D3BDFE747 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B34524427DC3DF7A5835776C; + remoteInfo = TermindWidgets; + }; + 8D3CBDCC65F0ED530DC28977 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = Framework; + }; + AA1200000000000000000011 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 727F565FD44C8D180B283598; + remoteInfo = TermindPresentation; + }; + B2A0F8D7119DC7A994252B95 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = Framework; + }; + B9648C248A9D35BAD9BFBD82 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 727F565FD44C8D180B283598; + remoteInfo = Framework; + }; + BBDB8DE077D7709FC87EAD49 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = Framework; + }; + DB8AB3CC3053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = TermindDomain; + }; + DB8AB3CD3053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = TermindApplication; + }; + DB8AB3CE3053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = TermindDomain; + }; + DB8AB3CF3053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ADAD84AB61B493DA1D22B54C; + remoteInfo = TermindInfrastructure; + }; + DB8AB3D03053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = TermindDomain; + }; + DB8AB3D13053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = TermindApplication; + }; + DB8AB3D23053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB005287D48C7756E15B6830; + remoteInfo = TermindPersistence; + }; + DB8AB3D33053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = TermindDomain; + }; + DB8AB3D43053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = TermindDomain; + }; + DB8AB3D53053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = TermindApplication; + }; + DB8AB3D63053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ADAD84AB61B493DA1D22B54C; + remoteInfo = TermindInfrastructure; + }; + DB8AB3D73053A07800C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB005287D48C7756E15B6830; + remoteInfo = TermindPersistence; + }; + DB8AB3D83053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = TermindDomain; + }; + DB8AB3D93053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = TermindApplication; + }; + DB8AB3DA3053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ADAD84AB61B493DA1D22B54C; + remoteInfo = TermindInfrastructure; + }; + DB8AB3DB3053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB005287D48C7756E15B6830; + remoteInfo = TermindPersistence; + }; + DB8AB3DC3053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = TermindDomain; + }; + DB8AB3DD3053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = TermindApplication; + }; + DB8AB3DE3053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ADAD84AB61B493DA1D22B54C; + remoteInfo = TermindInfrastructure; + }; + DB8AB3DF3053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB005287D48C7756E15B6830; + remoteInfo = TermindPersistence; + }; + DB8AB3E03053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1EAB6B47E04D61C8CFC54762; + remoteInfo = TFI; + }; + DB8AB3E13053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B243FDB536F5111B356353DA; + remoteInfo = TermindDomain; + }; + DB8AB3E23053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5F3ED08556B8962920EF788; + remoteInfo = TermindApplication; + }; + DB8AB3E33053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ADAD84AB61B493DA1D22B54C; + remoteInfo = TermindInfrastructure; + }; + DB8AB3E43053A07900C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB005287D48C7756E15B6830; + remoteInfo = TermindPersistence; + }; + DB8AE7623053CEF500C7EFAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB005287D48C7756E15B6830; + remoteInfo = TermindPersistence; + }; + DDD7C2043B64225E19B0D941 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 727F565FD44C8D180B283598; + remoteInfo = Framework; + }; + TRMD00000000000000020090 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = TRMD00000000000000000050; + remoteInfo = TFM; + }; + TRMD00000000000000030090 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = TRMD00000000000000000090 /* Project object */; + proxyType = 1; + remoteGlobalIDString = TRMD00000000000000030050; + remoteInfo = TermindMCPBridge; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + B7FBAAD7AF587EA9ADB523C5 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 03892FFD3F1E351BD2C296E4 /* TermindWidgets.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000000085 /* Embed MCP Bridge */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 6; + files = ( + TRMD00000000000000030001 /* termind-mcp in Embed MCP Bridge */, + ); + name = "Embed MCP Bridge"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 01FFEA64B9D6E13E50664FB8 /* Termind.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Termind.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 10111AE8F047FB59B318813A /* TermindApplication.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TermindApplication.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 13753997381D54B4D92FDDD5 /* TFITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TFITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 21951230A1FF30987ADC3B6B /* TermindRemoteInfrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TermindRemoteInfrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TermindPersistence.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 32414DBD2157FBAD4D00056D /* TermindWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; path = TermindWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TermindInfrastructure.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6F2CD72AE76428E896F81B64 /* TermindPresentation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TermindPresentation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7A62058825D6607920F1E2A1 /* TermindDomain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TermindDomain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8C9A1D5C026A485801BF7908 /* FrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DBE378B130478CFB00B16AA1 /* Termind.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = Termind.storekit; path = StoreKit/Termind.storekit; sourceTree = ""; }; + TRMD00000000000000000001 /* Termind.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Termind.app; sourceTree = BUILT_PRODUCTS_DIR; }; + TRMD00000000000000020001 /* TFMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TFMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + TRMD00000000000000030002 /* termind-mcp */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "termind-mcp"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 20115C9164DA003CDD91B7BE /* Exceptions for "TFIWidgets" folder in "TermindWidgets" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = B34524427DC3DF7A5835776C /* TermindWidgets */; + }; + E26B867E4820091C8A63C738 /* Exceptions for "TFI" folder in "TFI" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 1EAB6B47E04D61C8CFC54762 /* TFI */; + }; + TRMD00000000000000000011 /* Exceptions for "TFM" folder in "TFM" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = TRMD00000000000000000050 /* TFM */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 0FE0DF713B1C0832D76696AF /* TFI */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + E26B867E4820091C8A63C738 /* Exceptions for "TFI" folder in "TFI" target */, + ); + path = TFI; + sourceTree = ""; + }; + 107C3841D2F5B0D215D798F0 /* TermindDomain */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TermindDomain; + sourceTree = ""; + }; + 1352C351013C9A37E7DABE9E /* TermindApplication */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TermindApplication; + sourceTree = ""; + }; + 46E0A2AA823D19093256BE94 /* Resources */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Resources; + sourceTree = ""; + }; + 735894552DE200D22AA043F6 /* TermindPresentation */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TermindPresentation; + sourceTree = ""; + }; + 92C81B33B00182BDC162D7C9 /* TermindInfrastructure */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TermindInfrastructure; + sourceTree = ""; + }; + A102A7EA2C3532D7FA88B0A4 /* TFITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TFITests; + sourceTree = ""; + }; + A93D181DCED7ACF38C25FA9E /* TermindPersistence */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TermindPersistence; + sourceTree = ""; + }; + BA4A4EAD7B8B3A380C9ED1C2 /* FrameworkTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = FrameworkTests; + sourceTree = ""; + }; + CB622EAF077B1253532F1E18 /* TFIWidgets */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 20115C9164DA003CDD91B7BE /* Exceptions for "TFIWidgets" folder in "TermindWidgets" target */, + ); + path = TFIWidgets; + sourceTree = ""; + }; + E58718D8105B0F87D5986B4F /* TermindRemoteInfrastructure */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TermindRemoteInfrastructure; + sourceTree = ""; + }; + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = WorkspaceActivitySupport; + sourceTree = ""; + }; + TRMD00000000000000000010 /* TFM */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + TRMD00000000000000000011 /* Exceptions for "TFM" folder in "TFM" target */, + ); + path = TFM; + sourceTree = ""; + }; + TRMD00000000000000020010 /* TFMTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TFMTests; + sourceTree = ""; + }; + TRMD00000000000000030010 /* TermindMCPBridge */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = TermindMCPBridge; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 250F72DDA471D831C5727455 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ABCDEF020000000000000003 /* MarkdownView in Frameworks */, + AA1200000000000000000009 /* GhosttyKit in Frameworks */, + AA1200000000000000000002 /* GhosttyTheme in Frameworks */, + AA1200000000000000000001 /* GhosttyTerminal in Frameworks */, + A671EB6598B90BA03EF86CE7 /* TermindDomain.framework in Frameworks */, + 38B690B07BB71C9BAA6B45B0 /* TermindApplication.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2F8570B27A408F55FC6F56E3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B51DD6FD59A860DDA7F80EE /* AgentKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 626F4E8C20AF0B18068B185B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 38462C442F34CFDE6FF4661D /* AgentKit in Frameworks */, + EF2F88E4809C845C3F1BBF63 /* TermindDomain.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 75CB445EB4CF63B7BCBB2495 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ABCDEF020000000000000002 /* AgentKitScrubber in Frameworks */, + 0C77A0D7A810A78DA1F6CD93 /* CSSH2 in Frameworks */, + 0D54DF6508C7918D7D5B9D5A /* OpenSSL in Frameworks */, + 0861B3AA103F736DEE93983B /* TermindInfrastructure.framework in Frameworks */, + 88E7F9CE7BC5631B487E2BC5 /* TermindDomain.framework in Frameworks */, + FACB579B671967F922A33CD9 /* TermindApplication.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7B09CFC5B32584951BD4C8F9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAD00000000000000000011 /* GRDB in Frameworks */, + AA1300000000000000000001 /* ListViewKit in Frameworks */, + C4AD00000000000000000002 /* MarkdownView in Frameworks */, + C4AD00000000000000000001 /* ZIPFoundation in Frameworks */, + 5310B1DD3FA3FE77185B3F4A /* TermindRemoteInfrastructure.framework in Frameworks */, + 8F73CAE12C350F5FDB033EE0 /* GhosttyKit in Frameworks */, + AB2210ABB10F707CBFB01C78 /* GhosttyTheme in Frameworks */, + D7D1BBB65020F61F7EFDDFD7 /* GhosttyTerminal in Frameworks */, + 8D2404FE022681C084DE0E23 /* TermindPresentation.framework in Frameworks */, + 8FCAF8FD0FF78E38C58ED887 /* TermindDomain.framework in Frameworks */, + 9AF99D2C7AF5E879598A5972 /* TermindApplication.framework in Frameworks */, + CF863393BB8C2B38A9EEE18A /* TermindInfrastructure.framework in Frameworks */, + 3CEFDFDB25F7D9C9A795C601 /* TermindPersistence.framework in Frameworks */, + 20ADB83C9DB7F78EDF8BCBDF /* AgentKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A911B92D071D716A3CCA771E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ABCDEF020000000000000001 /* ZIPFoundation in Frameworks */, + ABCDEF010000000000000001 /* TermindPersistence.framework in Frameworks */, + 7DE0BDBF5E7A76602DDD443D /* TermindDomain.framework in Frameworks */, + 6DF0C216374DA144B1FC2030 /* TermindApplication.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BD0AED00305B5F77687B8B74 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAD00000000000000000003 /* GRDB in Frameworks */, + A04F3C049C318FA491B42039 /* TermindApplication.framework in Frameworks */, + 1D479CE3B3FF74B0C23BA3F2 /* AgentKit in Frameworks */, + 25C0658E2DD9B2FE28023DA7 /* TermindDomain.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E9D10112240438404117D4ED /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAD00000000000000000014 /* GRDB in Frameworks */, + AA1200000000000000000006 /* GhosttyTheme in Frameworks */, + AA1200000000000000000005 /* GhosttyTerminal in Frameworks */, + 8BFD569E84CA09FDEFF87698 /* TermindPresentation.framework in Frameworks */, + 76535271991F0D93E7277E6F /* TermindDomain.framework in Frameworks */, + B08DD88AB7C2444E5AEC19BC /* TermindApplication.framework in Frameworks */, + 10DD764492A94453449B3484 /* TermindInfrastructure.framework in Frameworks */, + 6FB449BC0FE9518E6E22B442 /* TermindPersistence.framework in Frameworks */, + F6ACDF3012081771F6C8F659 /* AgentKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F2C7E760B2B6A0B5EE1DEA77 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAD00000000000000000012 /* GRDB in Frameworks */, + AA1200000000000000000010 /* MarkdownView in Frameworks */, + AA1200000000000000000013 /* TermindPresentation.framework in Frameworks */, + 0202B16D630CC57432748F0C /* TermindDomain.framework in Frameworks */, + 6995A68F3132E9C67B0DA01E /* TermindApplication.framework in Frameworks */, + B616FF213FC31346E6BB3817 /* TermindInfrastructure.framework in Frameworks */, + 082994E88792AF7D1F99B147 /* TermindPersistence.framework in Frameworks */, + 23E272F32F769A979F3ABF42 /* AgentKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FB0D6E6D0B8DBACFC8EA2B2B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A9A947EA53E15BD04CE319C1 /* TermindPresentation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000000020 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAD00000000000000000010 /* GRDB in Frameworks */, + 26C689ECB8B41A357765CCF1 /* TermindRemoteInfrastructure.framework in Frameworks */, + 08620C447AFE3884EA26ADCC /* TermindPresentation.framework in Frameworks */, + TRMD0000000000000010000C /* AgentKit in Frameworks */, + TRMD0000000000000010000E /* AgentKitScrubber in Frameworks */, + TRMD0000000000000010000B /* GlyphixTextFx in Frameworks */, + TRMD00000000000000100005 /* ZIPFoundation in Frameworks */, + TRMD00000000000000100008 /* MarkdownView in Frameworks */, + TRMD00000000000000100009 /* ListViewKit in Frameworks */, + TRMD00000000000000100007 /* GhosttyKit in Frameworks */, + TRMD00000000000000100002 /* GhosttyTerminal in Frameworks */, + TRMD00000000000000100003 /* GhosttyTheme in Frameworks */, + 7B23538CFDAF459CA4CB4351 /* TermindDomain.framework in Frameworks */, + 640E0A8248C5231A7424BE56 /* TermindApplication.framework in Frameworks */, + F996BFD771BC77E67AB91FFB /* TermindInfrastructure.framework in Frameworks */, + E635C54CA9F1A63D909CFCE6 /* TermindPersistence.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000020020 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BAAD00000000000000000013 /* GRDB in Frameworks */, + TRMD0000000000000010000D /* AgentKit in Frameworks */, + TRMD0000000000000010000F /* AgentKitScrubber in Frameworks */, + TRMD00000000000000100006 /* ZIPFoundation in Frameworks */, + B1821FD31E7D009C900B5557 /* TermindDomain.framework in Frameworks */, + C2D4B372BA5A040DDBCEFC85 /* TermindApplication.framework in Frameworks */, + FB01141D5DD3036A40889B76 /* TermindInfrastructure.framework in Frameworks */, + 81A97F4DC317A6940A7A2CA5 /* TermindPersistence.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000030020 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CDDB75F14F753E46BB14D972 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 46E0A2AA823D19093256BE94 /* Resources */, + 107C3841D2F5B0D215D798F0 /* TermindDomain */, + 1352C351013C9A37E7DABE9E /* TermindApplication */, + E58718D8105B0F87D5986B4F /* TermindRemoteInfrastructure */, + 735894552DE200D22AA043F6 /* TermindPresentation */, + 92C81B33B00182BDC162D7C9 /* TermindInfrastructure */, + A93D181DCED7ACF38C25FA9E /* TermindPersistence */, + ); + path = Frameworks; + sourceTree = ""; + }; + TRMD00000000000000000030 = { + isa = PBXGroup; + children = ( + CB622EAF077B1253532F1E18 /* TFIWidgets */, + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */, + DBE378B130478CFB00B16AA1 /* Termind.storekit */, + CDDB75F14F753E46BB14D972 /* Frameworks */, + TRMD00000000000000000010 /* TFM */, + TRMD00000000000000020010 /* TFMTests */, + TRMD00000000000000030010 /* TermindMCPBridge */, + TRMD00000000000000000040 /* Products */, + 0FE0DF713B1C0832D76696AF /* TFI */, + BA4A4EAD7B8B3A380C9ED1C2 /* FrameworkTests */, + A102A7EA2C3532D7FA88B0A4 /* TFITests */, + ); + sourceTree = ""; + }; + TRMD00000000000000000040 /* Products */ = { + isa = PBXGroup; + children = ( + 32414DBD2157FBAD4D00056D /* TermindWidgets.appex */, + TRMD00000000000000000001 /* Termind.app */, + TRMD00000000000000020001 /* TFMTests.xctest */, + TRMD00000000000000030002 /* termind-mcp */, + 7A62058825D6607920F1E2A1 /* TermindDomain.framework */, + 10111AE8F047FB59B318813A /* TermindApplication.framework */, + 21951230A1FF30987ADC3B6B /* TermindRemoteInfrastructure.framework */, + 6F2CD72AE76428E896F81B64 /* TermindPresentation.framework */, + 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */, + 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */, + 01FFEA64B9D6E13E50664FB8 /* Termind.app */, + 8C9A1D5C026A485801BF7908 /* FrameworkTests.xctest */, + 13753997381D54B4D92FDDD5 /* TFITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1EAB6B47E04D61C8CFC54762 /* TFI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 20CE6E8A93D697985F372EA6 /* Build configuration list for PBXNativeTarget "TFI" */; + buildPhases = ( + B7FBAAD7AF587EA9ADB523C5 /* Embed Foundation Extensions */, + AC89B83FA8E4154383D9AC81 /* Sources */, + 7B09CFC5B32584951BD4C8F9 /* Frameworks */, + 8F0C5245592A0E2AFB85E50F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E954DDEB2251D5DDF5B5E4D0 /* PBXTargetDependency */, + 969DB5ED9186D2B471A0BD11 /* PBXTargetDependency */, + 78A8722D62CD30AC7A4E0E02 /* PBXTargetDependency */, + D6444C208365A08727ACC139 /* PBXTargetDependency */, + 29FDCE2C09E8F0A257C594A5 /* PBXTargetDependency */, + C2CB1C43BCBD82FF680630A0 /* PBXTargetDependency */, + 99D1BE807E1DA1B24CBA115B /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 0FE0DF713B1C0832D76696AF /* TFI */, + 46E0A2AA823D19093256BE94 /* Resources */, + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */, + ); + name = TFI; + packageProductDependencies = ( + BAAD00000000000000000002 /* GRDB */, + TRMD00000000000000100018 /* ListViewKit */, + TRMD00000000000000100017 /* MarkdownView */, + TRMD00000000000000100015 /* ZIPFoundation */, + TRMD00000000000000100016 /* GhosttyKit */, + TRMD00000000000000100013 /* GhosttyTheme */, + TRMD00000000000000100012 /* GhosttyTerminal */, + TRMD0000000000000010001B /* AgentKit */, + ); + productName = TFI; + productReference = 01FFEA64B9D6E13E50664FB8 /* Termind.app */; + productType = "com.apple.product-type.application"; + }; + 4E03E735D4992AEB794812A1 /* FrameworkTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 76E7A969963CB90646CC1B06 /* Build configuration list for PBXNativeTarget "FrameworkTests" */; + buildPhases = ( + F1FF4852C800F21D4AE3E8C6 /* Sources */, + F2C7E760B2B6A0B5EE1DEA77 /* Frameworks */, + 19B97D4999ACF4791837A994 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AA1200000000000000000012 /* PBXTargetDependency */, + 1DD69C71517BBB3B34BBBA0C /* PBXTargetDependency */, + C81555991C1262931C1ED5A3 /* PBXTargetDependency */, + BAFE3F1B3AA7972D0C429218 /* PBXTargetDependency */, + FBE9CCCAB149BECFFB7B8567 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 46E0A2AA823D19093256BE94 /* Resources */, + BA4A4EAD7B8B3A380C9ED1C2 /* FrameworkTests */, + ); + name = FrameworkTests; + packageProductDependencies = ( + BAAD00000000000000000002 /* GRDB */, + TRMD0000000000000010001B /* AgentKit */, + TRMD00000000000000100017 /* MarkdownView */, + ); + productName = FrameworkTests; + productReference = 8C9A1D5C026A485801BF7908 /* FrameworkTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 727F565FD44C8D180B283598 /* TermindPresentation */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5187A7B8CCE63A51982AB904 /* Build configuration list for PBXNativeTarget "TermindPresentation" */; + buildPhases = ( + 7F30AE9837284D35881E071C /* Sources */, + 250F72DDA471D831C5727455 /* Frameworks */, + 4C3532C482B667F9A7CBB31C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6FEC2C90F6E3A33FB42096D8 /* PBXTargetDependency */, + 3AEDD6414F1B16E4308F01FB /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 735894552DE200D22AA043F6 /* TermindPresentation */, + ); + name = TermindPresentation; + packageProductDependencies = ( + TRMD00000000000000100017 /* MarkdownView */, + TRMD00000000000000100016 /* GhosttyKit */, + TRMD00000000000000100012 /* GhosttyTerminal */, + TRMD00000000000000100013 /* GhosttyTheme */, + ); + productName = TermindPresentation; + productReference = 6F2CD72AE76428E896F81B64 /* TermindPresentation.framework */; + productType = "com.apple.product-type.framework"; + }; + A5F3ED08556B8962920EF788 /* TermindApplication */ = { + isa = PBXNativeTarget; + buildConfigurationList = 976EB3B3A2F3583E74DF1E13 /* Build configuration list for PBXNativeTarget "TermindApplication" */; + buildPhases = ( + 01260326BA2579B3E3586B26 /* Sources */, + 626F4E8C20AF0B18068B185B /* Frameworks */, + 978461AD29C4066E4C394835 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 710D3CAF3582192E63670379 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 1352C351013C9A37E7DABE9E /* TermindApplication */, + ); + name = TermindApplication; + packageProductDependencies = ( + TRMD0000000000000010001B /* AgentKit */, + ); + productName = TermindApplication; + productReference = 10111AE8F047FB59B318813A /* TermindApplication.framework */; + productType = "com.apple.product-type.framework"; + }; + ADAD84AB61B493DA1D22B54C /* TermindInfrastructure */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5658F2D4BC8AAEC899C2300F /* Build configuration list for PBXNativeTarget "TermindInfrastructure" */; + buildPhases = ( + 9134CC60C9D2F547DC97DAF6 /* Sources */, + A911B92D071D716A3CCA771E /* Frameworks */, + 9215EE77E44C37EC54EA3FAC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EA931847718F5BD9BCEBB63A /* PBXTargetDependency */, + ABCDEF010000000000000002 /* PBXTargetDependency */, + 81CC5F621EFB712B902CD692 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 92C81B33B00182BDC162D7C9 /* TermindInfrastructure */, + ); + name = TermindInfrastructure; + packageProductDependencies = ( + TRMD00000000000000100015 /* ZIPFoundation */, + ); + productName = TermindInfrastructure; + productReference = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; + productType = "com.apple.product-type.framework"; + }; + B243FDB536F5111B356353DA /* TermindDomain */ = { + isa = PBXNativeTarget; + buildConfigurationList = B15822D25CFA7C3167647626 /* Build configuration list for PBXNativeTarget "TermindDomain" */; + buildPhases = ( + A2E672E66CF74C027D07828C /* Sources */, + 2F8570B27A408F55FC6F56E3 /* Frameworks */, + 77D58C1ACE18922CD4E48C73 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 107C3841D2F5B0D215D798F0 /* TermindDomain */, + ); + name = TermindDomain; + packageProductDependencies = ( + TRMD0000000000000010001B /* AgentKit */, + ); + productName = TermindDomain; + productReference = 7A62058825D6607920F1E2A1 /* TermindDomain.framework */; + productType = "com.apple.product-type.framework"; + }; + B34524427DC3DF7A5835776C /* TermindWidgets */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1A55575DB0A0A07D5A5B38BE /* Build configuration list for PBXNativeTarget "TermindWidgets" */; + buildPhases = ( + 324CB4A18F9FD467A1BB8C8C /* Sources */, + FB0D6E6D0B8DBACFC8EA2B2B /* Frameworks */, + 2CA3E6071DF7C233129681FA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7694F0D5535EF9F706E23705 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 46E0A2AA823D19093256BE94 /* Resources */, + CB622EAF077B1253532F1E18 /* TFIWidgets */, + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */, + ); + name = TermindWidgets; + packageProductDependencies = ( + ); + productName = TermindWidgets; + productReference = 32414DBD2157FBAD4D00056D /* TermindWidgets.appex */; + productType = "com.apple.product-type.app-extension"; + }; + BDA6B93652DE90AEBDA7090E /* TermindRemoteInfrastructure */ = { + isa = PBXNativeTarget; + buildConfigurationList = 12F5D040E2844B6FD884BF65 /* Build configuration list for PBXNativeTarget "TermindRemoteInfrastructure" */; + buildPhases = ( + F599E47BAC4A976A849F668D /* Sources */, + 75CB445EB4CF63B7BCBB2495 /* Frameworks */, + E816FA2491D173780D089045 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 677C5AADCEEE4708387B0AFE /* PBXTargetDependency */, + 120C1C1E71C276EF090C7DC0 /* PBXTargetDependency */, + 6DB5920C1BFFC4D9795463D0 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + E58718D8105B0F87D5986B4F /* TermindRemoteInfrastructure */, + ); + name = TermindRemoteInfrastructure; + packageProductDependencies = ( + TRMD0000000000000010001C /* AgentKitScrubber */, + TRMD00000000000000100011 /* CSSH2 */, + TRMD00000000000000100014 /* OpenSSL */, + ); + productName = TermindRemoteInfrastructure; + productReference = 21951230A1FF30987ADC3B6B /* TermindRemoteInfrastructure.framework */; + productType = "com.apple.product-type.framework"; + }; + DB005287D48C7756E15B6830 /* TermindPersistence */ = { + isa = PBXNativeTarget; + buildConfigurationList = BD8BF81D53731B72A867D811 /* Build configuration list for PBXNativeTarget "TermindPersistence" */; + buildPhases = ( + C99C8316C00B8A01F2128F1B /* Sources */, + BD0AED00305B5F77687B8B74 /* Frameworks */, + 6418B7148055671AAB2FB492 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7FF186C9DC087748DDFB9EAF /* PBXTargetDependency */, + 00758A789ECB9643122A60EC /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + A93D181DCED7ACF38C25FA9E /* TermindPersistence */, + ); + name = TermindPersistence; + packageProductDependencies = ( + BAAD00000000000000000002 /* GRDB */, + TRMD0000000000000010001B /* AgentKit */, + ); + productName = TermindPersistence; + productReference = 244632CCDB1CA43F45C0EB54 /* TermindPersistence.framework */; + productType = "com.apple.product-type.framework"; + }; + F815C7E534B763A0E88A5CBD /* TFITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7676DA2F42FAE8E2D331D93F /* Build configuration list for PBXNativeTarget "TFITests" */; + buildPhases = ( + 7A66ED3CE1F6BC55177F5B13 /* Sources */, + E9D10112240438404117D4ED /* Frameworks */, + 1627D22C6A1816AB5712F093 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4D3EA05C89BC456F30784AA5 /* PBXTargetDependency */, + 0EF002FE2F91FCC695C1D40F /* PBXTargetDependency */, + 4EB5324D41D2FA6D5726D73D /* PBXTargetDependency */, + 191B454FB44C6FB85FA289A6 /* PBXTargetDependency */, + B5CB96BC5381BEF3FF47AED5 /* PBXTargetDependency */, + 7643FA54701782DDD29BF1A7 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + A102A7EA2C3532D7FA88B0A4 /* TFITests */, + FD8DC40513A4A80A71C41FCA /* WorkspaceActivitySupport */, + ); + name = TFITests; + packageProductDependencies = ( + BAAD00000000000000000002 /* GRDB */, + TRMD0000000000000010001B /* AgentKit */, + TRMD00000000000000100012 /* GhosttyTerminal */, + TRMD00000000000000100013 /* GhosttyTheme */, + ); + productName = TFITests; + productReference = 13753997381D54B4D92FDDD5 /* TFITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + TRMD00000000000000000050 /* TFM */ = { + isa = PBXNativeTarget; + buildConfigurationList = TRMD00000000000000000060 /* Build configuration list for PBXNativeTarget "TFM" */; + buildPhases = ( + TRMD00000000000000000070 /* Sources */, + TRMD00000000000000000020 /* Frameworks */, + TRMD00000000000000000080 /* Resources */, + TRMD00000000000000000085 /* Embed MCP Bridge */, + ); + buildRules = ( + ); + dependencies = ( + 360EC5FE18C1E44595290420 /* PBXTargetDependency */, + 33C62E8FC4C6680501A68A25 /* PBXTargetDependency */, + TRMD00000000000000030091 /* PBXTargetDependency */, + F98F58C51DE326AC4AF95F66 /* PBXTargetDependency */, + C0B7F56205F86D0178F32100 /* PBXTargetDependency */, + 97B15C378163C5A05C822115 /* PBXTargetDependency */, + 521C5CC104E7B21A365A7F89 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 46E0A2AA823D19093256BE94 /* Resources */, + TRMD00000000000000000010 /* TFM */, + ); + name = TFM; + packageProductDependencies = ( + BAAD00000000000000000002 /* GRDB */, + TRMD0000000000000010001B /* AgentKit */, + TRMD0000000000000010001C /* AgentKitScrubber */, + TRMD0000000000000010001A /* GlyphixTextFx */, + TRMD00000000000000100016 /* GhosttyKit */, + TRMD00000000000000100012 /* GhosttyTerminal */, + TRMD00000000000000100013 /* GhosttyTheme */, + TRMD00000000000000100015 /* ZIPFoundation */, + TRMD00000000000000100017 /* MarkdownView */, + TRMD00000000000000100018 /* ListViewKit */, + ); + productName = Termind; + productReference = TRMD00000000000000000001 /* Termind.app */; + productType = "com.apple.product-type.application"; + }; + TRMD00000000000000020050 /* TFMTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = TRMD00000000000000020060 /* Build configuration list for PBXNativeTarget "TFMTests" */; + buildPhases = ( + TRMD00000000000000020070 /* Sources */, + TRMD00000000000000020020 /* Frameworks */, + TRMD00000000000000020080 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + TRMD00000000000000020091 /* PBXTargetDependency */, + C436B3D8B1BF8204D50F3848 /* PBXTargetDependency */, + 4FAF81350D21B55CD76B9E57 /* PBXTargetDependency */, + 1BE462834539F50116A77177 /* PBXTargetDependency */, + 438F9D0971977DE55A9319FE /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + TRMD00000000000000020010 /* TFMTests */, + ); + name = TFMTests; + packageProductDependencies = ( + BAAD00000000000000000002 /* GRDB */, + TRMD0000000000000010001B /* AgentKit */, + TRMD0000000000000010001C /* AgentKitScrubber */, + TRMD00000000000000100015 /* ZIPFoundation */, + ); + productName = TFMTests; + productReference = TRMD00000000000000020001 /* TFMTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + TRMD00000000000000030050 /* TermindMCPBridge */ = { + isa = PBXNativeTarget; + buildConfigurationList = TRMD00000000000000030060 /* Build configuration list for PBXNativeTarget "TermindMCPBridge" */; + buildPhases = ( + TRMD00000000000000030070 /* Sources */, + TRMD00000000000000030020 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + TRMD00000000000000030010 /* TermindMCPBridge */, + ); + name = TermindMCPBridge; + packageProductDependencies = ( + ); + productName = "termind-mcp"; + productReference = TRMD00000000000000030002 /* termind-mcp */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + TRMD00000000000000000090 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2660; + LastUpgradeCheck = 2700; + TargetAttributes = { + 1EAB6B47E04D61C8CFC54762 = { + SystemCapabilities = { + com.apple.Push = { + enabled = 1; + }; + com.apple.iCloud = { + enabled = 1; + }; + }; + }; + F815C7E534B763A0E88A5CBD = { + CreatedOnToolsVersion = 26.6; + TestTargetID = 1EAB6B47E04D61C8CFC54762; + }; + TRMD00000000000000000050 = { + CreatedOnToolsVersion = 26.6; + SystemCapabilities = { + com.apple.InAppPurchase = { + enabled = 1; + }; + com.apple.Push = { + enabled = 1; + }; + com.apple.iCloud = { + enabled = 1; + }; + }; + }; + TRMD00000000000000020050 = { + CreatedOnToolsVersion = 26.6; + TestTargetID = TRMD00000000000000000050; + }; + TRMD00000000000000030050 = { + CreatedOnToolsVersion = 26.6; + }; + }; + }; + buildConfigurationList = TRMD000000000000000000A0 /* Build configuration list for PBXProject "Termind" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + "zh-Hans", + "zh-Hant", + Base, + ); + mainGroup = TRMD00000000000000000030; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + BAAD00000000000000000001 /* XCRemoteSwiftPackageReference "GRDB.swift" */, + TRMD00000000000000100029 /* XCRemoteSwiftPackageReference "AgentKit" */, + TRMD00000000000000100021 /* XCRemoteSwiftPackageReference "libssh2-spm" */, + TRMD00000000000000100028 /* XCRemoteSwiftPackageReference "GlyphixTextFx" */, + TRMD00000000000000100022 /* XCRemoteSwiftPackageReference "libghostty-spm" */, + TRMD00000000000000100023 /* XCRemoteSwiftPackageReference "openssl-spm" */, + TRMD00000000000000100024 /* XCRemoteSwiftPackageReference "ZIPFoundation" */, + TRMD00000000000000100025 /* XCRemoteSwiftPackageReference "MarkdownView" */, + TRMD00000000000000100026 /* XCRemoteSwiftPackageReference "ListViewKit" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = TRMD00000000000000000040 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BDA6B93652DE90AEBDA7090E /* TermindRemoteInfrastructure */, + B34524427DC3DF7A5835776C /* TermindWidgets */, + 727F565FD44C8D180B283598 /* TermindPresentation */, + TRMD00000000000000000050 /* TFM */, + TRMD00000000000000020050 /* TFMTests */, + TRMD00000000000000030050 /* TermindMCPBridge */, + B243FDB536F5111B356353DA /* TermindDomain */, + A5F3ED08556B8962920EF788 /* TermindApplication */, + ADAD84AB61B493DA1D22B54C /* TermindInfrastructure */, + DB005287D48C7756E15B6830 /* TermindPersistence */, + 1EAB6B47E04D61C8CFC54762 /* TFI */, + 4E03E735D4992AEB794812A1 /* FrameworkTests */, + F815C7E534B763A0E88A5CBD /* TFITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1627D22C6A1816AB5712F093 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 19B97D4999ACF4791837A994 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2CA3E6071DF7C233129681FA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C3532C482B667F9A7CBB31C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6418B7148055671AAB2FB492 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 77D58C1ACE18922CD4E48C73 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8F0C5245592A0E2AFB85E50F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9215EE77E44C37EC54EA3FAC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 978461AD29C4066E4C394835 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E816FA2491D173780D089045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000000080 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000020080 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 01260326BA2579B3E3586B26 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 324CB4A18F9FD467A1BB8C8C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7A66ED3CE1F6BC55177F5B13 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7F30AE9837284D35881E071C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9134CC60C9D2F547DC97DAF6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A2E672E66CF74C027D07828C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AC89B83FA8E4154383D9AC81 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C99C8316C00B8A01F2128F1B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F1FF4852C800F21D4AE3E8C6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F599E47BAC4A976A849F668D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000000070 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000020070 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + TRMD00000000000000030070 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00758A789ECB9643122A60EC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = DB8AB3D33053A07800C7EFAE /* PBXContainerItemProxy */; + }; + 0EF002FE2F91FCC695C1D40F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1EAB6B47E04D61C8CFC54762 /* TFI */; + targetProxy = DB8AB3E03053A07900C7EFAE /* PBXContainerItemProxy */; + }; + 120C1C1E71C276EF090C7DC0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = 8D3CBDCC65F0ED530DC28977 /* PBXContainerItemProxy */; + }; + 191B454FB44C6FB85FA289A6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = DB8AB3E23053A07900C7EFAE /* PBXContainerItemProxy */; + }; + 1BE462834539F50116A77177 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ADAD84AB61B493DA1D22B54C /* TermindInfrastructure */; + targetProxy = DB8AB3DA3053A07900C7EFAE /* PBXContainerItemProxy */; + }; + 1DD69C71517BBB3B34BBBA0C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = DB8AB3DC3053A07900C7EFAE /* PBXContainerItemProxy */; + }; + 29FDCE2C09E8F0A257C594A5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = DB8AB3CD3053A07800C7EFAE /* PBXContainerItemProxy */; + }; + 33C62E8FC4C6680501A68A25 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 727F565FD44C8D180B283598 /* TermindPresentation */; + targetProxy = 2B98C090049C0DB38AA2199B /* PBXContainerItemProxy */; + }; + 360EC5FE18C1E44595290420 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BDA6B93652DE90AEBDA7090E /* TermindRemoteInfrastructure */; + targetProxy = 892065B84B7200DAAE43F5F2 /* PBXContainerItemProxy */; + }; + 3AEDD6414F1B16E4308F01FB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = 2F5BAF1F7EA44EC2E1F6EA73 /* PBXContainerItemProxy */; + }; + 438F9D0971977DE55A9319FE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB005287D48C7756E15B6830 /* TermindPersistence */; + targetProxy = DB8AB3DB3053A07900C7EFAE /* PBXContainerItemProxy */; + }; + 4D3EA05C89BC456F30784AA5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 727F565FD44C8D180B283598 /* TermindPresentation */; + targetProxy = 266FBAED0BB986F8F0D1000B /* PBXContainerItemProxy */; + }; + 4EB5324D41D2FA6D5726D73D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = DB8AB3E13053A07900C7EFAE /* PBXContainerItemProxy */; + }; + 4FAF81350D21B55CD76B9E57 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = DB8AB3D93053A07900C7EFAE /* PBXContainerItemProxy */; + }; + 521C5CC104E7B21A365A7F89 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB005287D48C7756E15B6830 /* TermindPersistence */; + targetProxy = DB8AB3D73053A07800C7EFAE /* PBXContainerItemProxy */; + }; + 677C5AADCEEE4708387B0AFE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ADAD84AB61B493DA1D22B54C /* TermindInfrastructure */; + targetProxy = 25EB7B3558E87335BF52EBFD /* PBXContainerItemProxy */; + }; + 6DB5920C1BFFC4D9795463D0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = BBDB8DE077D7709FC87EAD49 /* PBXContainerItemProxy */; + }; + 6FEC2C90F6E3A33FB42096D8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = B2A0F8D7119DC7A994252B95 /* PBXContainerItemProxy */; + }; + 710D3CAF3582192E63670379 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = DB8AB3CE3053A07800C7EFAE /* PBXContainerItemProxy */; + }; + 7643FA54701782DDD29BF1A7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB005287D48C7756E15B6830 /* TermindPersistence */; + targetProxy = DB8AB3E43053A07900C7EFAE /* PBXContainerItemProxy */; + }; + 7694F0D5535EF9F706E23705 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 727F565FD44C8D180B283598 /* TermindPresentation */; + targetProxy = DDD7C2043B64225E19B0D941 /* PBXContainerItemProxy */; + }; + 78A8722D62CD30AC7A4E0E02 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 727F565FD44C8D180B283598 /* TermindPresentation */; + targetProxy = B9648C248A9D35BAD9BFBD82 /* PBXContainerItemProxy */; + }; + 7FF186C9DC087748DDFB9EAF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = 0A35D3902E3320DBDB756D0B /* PBXContainerItemProxy */; + }; + 81CC5F621EFB712B902CD692 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = DB8AB3D13053A07800C7EFAE /* PBXContainerItemProxy */; + }; + 969DB5ED9186D2B471A0BD11 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B34524427DC3DF7A5835776C /* TermindWidgets */; + targetProxy = 8A1D0EA6890B1E7D3BDFE747 /* PBXContainerItemProxy */; + }; + 97B15C378163C5A05C822115 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ADAD84AB61B493DA1D22B54C /* TermindInfrastructure */; + targetProxy = DB8AB3D63053A07800C7EFAE /* PBXContainerItemProxy */; + }; + 99D1BE807E1DA1B24CBA115B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB005287D48C7756E15B6830 /* TermindPersistence */; + targetProxy = DB8AB3D23053A07800C7EFAE /* PBXContainerItemProxy */; + }; + AA1200000000000000000012 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 727F565FD44C8D180B283598 /* TermindPresentation */; + targetProxy = AA1200000000000000000011 /* PBXContainerItemProxy */; + }; + ABCDEF010000000000000002 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB005287D48C7756E15B6830 /* TermindPersistence */; + targetProxy = DB8AE7623053CEF500C7EFAE /* PBXContainerItemProxy */; + }; + B5CB96BC5381BEF3FF47AED5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ADAD84AB61B493DA1D22B54C /* TermindInfrastructure */; + targetProxy = DB8AB3E33053A07900C7EFAE /* PBXContainerItemProxy */; + }; + BAFE3F1B3AA7972D0C429218 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ADAD84AB61B493DA1D22B54C /* TermindInfrastructure */; + targetProxy = DB8AB3DE3053A07900C7EFAE /* PBXContainerItemProxy */; + }; + C0B7F56205F86D0178F32100 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = DB8AB3D53053A07800C7EFAE /* PBXContainerItemProxy */; + }; + C2CB1C43BCBD82FF680630A0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ADAD84AB61B493DA1D22B54C /* TermindInfrastructure */; + targetProxy = DB8AB3CF3053A07800C7EFAE /* PBXContainerItemProxy */; + }; + C436B3D8B1BF8204D50F3848 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = DB8AB3D83053A07900C7EFAE /* PBXContainerItemProxy */; + }; + C81555991C1262931C1ED5A3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5F3ED08556B8962920EF788 /* TermindApplication */; + targetProxy = DB8AB3DD3053A07900C7EFAE /* PBXContainerItemProxy */; + }; + D6444C208365A08727ACC139 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = DB8AB3CC3053A07800C7EFAE /* PBXContainerItemProxy */; + }; + E954DDEB2251D5DDF5B5E4D0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BDA6B93652DE90AEBDA7090E /* TermindRemoteInfrastructure */; + targetProxy = 81596FC81A193992EB68BB60 /* PBXContainerItemProxy */; + }; + EA931847718F5BD9BCEBB63A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = DB8AB3D03053A07800C7EFAE /* PBXContainerItemProxy */; + }; + F98F58C51DE326AC4AF95F66 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B243FDB536F5111B356353DA /* TermindDomain */; + targetProxy = DB8AB3D43053A07800C7EFAE /* PBXContainerItemProxy */; + }; + FBE9CCCAB149BECFFB7B8567 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB005287D48C7756E15B6830 /* TermindPersistence */; + targetProxy = DB8AB3DF3053A07900C7EFAE /* PBXContainerItemProxy */; + }; + TRMD00000000000000020091 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = TRMD00000000000000000050 /* TFM */; + targetProxy = TRMD00000000000000020090 /* PBXContainerItemProxy */; + }; + TRMD00000000000000030091 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = TRMD00000000000000030050 /* TermindMCPBridge */; + targetProxy = TRMD00000000000000030090 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 00A459BF9EBF49731B530B7A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindremoteinfrastructure; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 171123A06EEDC2EE73AD3FC8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.tfitests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Termind.app/Termind"; + }; + name = Release; + }; + 26CFA808AF64A9ACDD4DF923 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindpersistence; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2E96D702E8EE688FB9B4DC71 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindinfrastructure; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 30D03D452305AD59ECD3C460 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindapplication; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3891D69CE8B2E67E07EFE802 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APS_ENVIRONMENT = development; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 16; + GENERATE_INFOPLIST_FILE = YES; + ICLOUD_CONTAINER_ENVIRONMENT = Development; + INFOPLIST_FILE = TFI/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Termind; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; + INFOPLIST_KEY_NSCameraUsageDescription = "Termind uses the camera to attach photos to your chat."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Authenticate to use your SSH keys."; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Termind connects to servers and model providers on your local network."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Termind uses the microphone to transcribe speech into your chat draft."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "Termind uses Apple Speech Recognition to turn your recording into text. Depending on system settings and language support, Apple may process the audio online."; + INFOPLIST_KEY_NSSupportsLiveActivities = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.9.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind; + PRODUCT_MODULE_NAME = TermindIOS; + PRODUCT_NAME = Termind; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 443D26104FCB9DFBF49EEFD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.frameworktests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 51206A2D54AE88E0247CD9C6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindpresentation; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 56B29D693660C76F4121622D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.frameworktests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 61A9E8DD25722DF762DBC92A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindremoteinfrastructure; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 67308C9998F6AA19BBC68898 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 6; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TFIWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Termind Workspaces"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.6.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.workspaces; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 925DF91139ED491057366958 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindpersistence; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 9A9993A9405331488B140C6A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.terminddomain; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + A1DAF8C420A41777983ECE89 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindpresentation; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BD9E47297AE0FA3235A4633E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.terminddomain; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C4ED38FD24B69F063AC1FCC7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindapplication; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D76D79A8E58FBD72935F61F2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APS_ENVIRONMENT = production; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 16; + GENERATE_INFOPLIST_FILE = YES; + ICLOUD_CONTAINER_ENVIRONMENT = Production; + INFOPLIST_FILE = TFI/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Termind; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; + INFOPLIST_KEY_NSCameraUsageDescription = "Termind uses the camera to attach photos to your chat."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Authenticate to use your SSH keys."; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Termind connects to servers and model providers on your local network."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Termind uses the microphone to transcribe speech into your chat draft."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "Termind uses Apple Speech Recognition to turn your recording into text. Depending on system settings and language support, Apple may process the audio online."; + INFOPLIST_KEY_NSSupportsLiveActivities = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.9.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind; + PRODUCT_MODULE_NAME = TermindIOS; + PRODUCT_NAME = Termind; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D9AB222931EA3C8F960F429A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 6; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TFIWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Termind Workspaces"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.6.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.workspaces; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E072A089DE54B923933916DA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.termindinfrastructure; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INSTALL_OBJC_HEADER = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F661272902C5911A4C1DECCA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.tfitests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Termind.app/Termind"; + }; + name = Debug; + }; + TRMD000000000000000000B0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 6; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = F6SUNDBBN9; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.6.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + TRMD000000000000000000C0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 6; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = F6SUNDBBN9; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.6.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; + SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES; + SWIFT_VERSION = 6.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + TRMD000000000000000000D0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APS_ENVIRONMENT = development; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TFM/TFM.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 25; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + ICLOUD_CONTAINER_ENVIRONMENT = Development; + INFOPLIST_FILE = TFM/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Termind; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LM_SKIP_METADATA_EXTRACTION = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.9.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind; + PRODUCT_MODULE_NAME = Termind; + PRODUCT_NAME = Termind; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + }; + name = Debug; + }; + TRMD000000000000000000E0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APS_ENVIRONMENT = production; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TFM/TFM.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 25; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + EXCLUDED_SOURCE_FILE_NAMES = ( + "$(inherited)", + DeveloperSettingsPage.swift, + ); + GENERATE_INFOPLIST_FILE = NO; + ICLOUD_CONTAINER_ENVIRONMENT = Production; + INFOPLIST_FILE = TFM/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Termind; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LM_SKIP_METADATA_EXTRACTION = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.9.0; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind; + PRODUCT_MODULE_NAME = Termind; + PRODUCT_NAME = Termind; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + }; + name = Release; + }; + TRMD000000000000000200D0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -Wno-error=deprecated-declarations"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.tests; + PRODUCT_NAME = TFMTests; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = macosx; + SWIFT_VERSION = 6.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Termind.app/Contents/MacOS/Termind"; + }; + name = Debug; + }; + TRMD000000000000000200E0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -Wno-error=deprecated-declarations"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.tests; + PRODUCT_NAME = TFMTests; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = macosx; + SWIFT_VERSION = 6.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Termind.app/Contents/MacOS/Termind"; + }; + name = Release; + }; + TRMD000000000000000300D0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = TermindMCPBridge/TermindMCPBridge.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Automatic; + CREATE_INFOPLIST_SECTION_IN_BINARY = YES; + DEAD_CODE_STRIPPING = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + OTHER_SWIFT_FLAGS = "-parse-as-library"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.mcpbridge; + PRODUCT_NAME = "termind-mcp"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + TRMD000000000000000300E0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = TermindMCPBridge/TermindMCPBridge.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Automatic; + CREATE_INFOPLIST_SECTION_IN_BINARY = YES; + DEAD_CODE_STRIPPING = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + OTHER_SWIFT_FLAGS = "-parse-as-library"; + PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind.mcpbridge; + PRODUCT_NAME = "termind-mcp"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; + SWIFT_VERSION = 6.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 12F5D040E2844B6FD884BF65 /* Build configuration list for PBXNativeTarget "TermindRemoteInfrastructure" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 61A9E8DD25722DF762DBC92A /* Debug */, + 00A459BF9EBF49731B530B7A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1A55575DB0A0A07D5A5B38BE /* Build configuration list for PBXNativeTarget "TermindWidgets" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D9AB222931EA3C8F960F429A /* Debug */, + 67308C9998F6AA19BBC68898 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 20CE6E8A93D697985F372EA6 /* Build configuration list for PBXNativeTarget "TFI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3891D69CE8B2E67E07EFE802 /* Debug */, + D76D79A8E58FBD72935F61F2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5187A7B8CCE63A51982AB904 /* Build configuration list for PBXNativeTarget "TermindPresentation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 51206A2D54AE88E0247CD9C6 /* Debug */, + A1DAF8C420A41777983ECE89 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5658F2D4BC8AAEC899C2300F /* Build configuration list for PBXNativeTarget "TermindInfrastructure" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E072A089DE54B923933916DA /* Debug */, + 2E96D702E8EE688FB9B4DC71 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7676DA2F42FAE8E2D331D93F /* Build configuration list for PBXNativeTarget "TFITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F661272902C5911A4C1DECCA /* Debug */, + 171123A06EEDC2EE73AD3FC8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 76E7A969963CB90646CC1B06 /* Build configuration list for PBXNativeTarget "FrameworkTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 443D26104FCB9DFBF49EEFD4 /* Debug */, + 56B29D693660C76F4121622D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 976EB3B3A2F3583E74DF1E13 /* Build configuration list for PBXNativeTarget "TermindApplication" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 30D03D452305AD59ECD3C460 /* Debug */, + C4ED38FD24B69F063AC1FCC7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B15822D25CFA7C3167647626 /* Build configuration list for PBXNativeTarget "TermindDomain" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BD9E47297AE0FA3235A4633E /* Debug */, + 9A9993A9405331488B140C6A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BD8BF81D53731B72A867D811 /* Build configuration list for PBXNativeTarget "TermindPersistence" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 925DF91139ED491057366958 /* Debug */, + 26CFA808AF64A9ACDD4DF923 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + TRMD00000000000000000060 /* Build configuration list for PBXNativeTarget "TFM" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + TRMD000000000000000000D0 /* Debug */, + TRMD000000000000000000E0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + TRMD000000000000000000A0 /* Build configuration list for PBXProject "Termind" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + TRMD000000000000000000B0 /* Debug */, + TRMD000000000000000000C0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + TRMD00000000000000020060 /* Build configuration list for PBXNativeTarget "TFMTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + TRMD000000000000000200D0 /* Debug */, + TRMD000000000000000200E0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + TRMD00000000000000030060 /* Build configuration list for PBXNativeTarget "TermindMCPBridge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + TRMD000000000000000300D0 /* Debug */, + TRMD000000000000000300E0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + BAAD00000000000000000001 /* XCRemoteSwiftPackageReference "GRDB.swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/groue/GRDB.swift.git"; + requirement = { + kind = exactVersion; + version = 7.11.1; + }; + }; + TRMD00000000000000100021 /* XCRemoteSwiftPackageReference "libssh2-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Lakr233/libssh2-spm.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.3; + }; + }; + TRMD00000000000000100022 /* XCRemoteSwiftPackageReference "libghostty-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Lakr233/libghostty-spm"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.6.20260909; + }; + }; + TRMD00000000000000100023 /* XCRemoteSwiftPackageReference "openssl-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Lakr233/openssl-spm.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.6.2; + }; + }; + TRMD00000000000000100024 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/weichsel/ZIPFoundation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.20; + }; + }; + TRMD00000000000000100025 /* XCRemoteSwiftPackageReference "MarkdownView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Lakr233/MarkdownView"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.3.2; + }; + }; + TRMD00000000000000100026 /* XCRemoteSwiftPackageReference "ListViewKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Lakr233/ListViewKit"; + requirement = { + kind = exactVersion; + version = 4.3.2; + }; + }; + TRMD00000000000000100028 /* XCRemoteSwiftPackageReference "GlyphixTextFx" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Lakr233/GlyphixTextFx.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.3.6; + }; + }; + TRMD00000000000000100029 /* XCRemoteSwiftPackageReference "AgentKit" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../AgentKit"; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + BAAD00000000000000000002 /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + package = BAAD00000000000000000001 /* XCRemoteSwiftPackageReference "GRDB.swift" */; + productName = GRDB; + }; + TRMD00000000000000100011 /* CSSH2 */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100021 /* XCRemoteSwiftPackageReference "libssh2-spm" */; + productName = CSSH2; + }; + TRMD00000000000000100012 /* GhosttyTerminal */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100022 /* XCRemoteSwiftPackageReference "libghostty-spm" */; + productName = GhosttyTerminal; + }; + TRMD00000000000000100013 /* GhosttyTheme */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100022 /* XCRemoteSwiftPackageReference "libghostty-spm" */; + productName = GhosttyTheme; + }; + TRMD00000000000000100014 /* OpenSSL */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100023 /* XCRemoteSwiftPackageReference "openssl-spm" */; + productName = OpenSSL; + }; + TRMD00000000000000100015 /* ZIPFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100024 /* XCRemoteSwiftPackageReference "ZIPFoundation" */; + productName = ZIPFoundation; + }; + TRMD00000000000000100016 /* GhosttyKit */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100022 /* XCRemoteSwiftPackageReference "libghostty-spm" */; + productName = GhosttyKit; + }; + TRMD00000000000000100017 /* MarkdownView */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100025 /* XCRemoteSwiftPackageReference "MarkdownView" */; + productName = MarkdownView; + }; + TRMD00000000000000100018 /* ListViewKit */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100026 /* XCRemoteSwiftPackageReference "ListViewKit" */; + productName = ListViewKit; + }; + TRMD0000000000000010001A /* GlyphixTextFx */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100028 /* XCRemoteSwiftPackageReference "GlyphixTextFx" */; + productName = GlyphixTextFx; + }; + TRMD0000000000000010001B /* AgentKit */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100029 /* XCRemoteSwiftPackageReference "AgentKit" */; + productName = AgentKit; + }; + TRMD0000000000000010001C /* AgentKitScrubber */ = { + isa = XCSwiftPackageProductDependency; + package = TRMD00000000000000100029 /* XCRemoteSwiftPackageReference "AgentKit" */; + productName = AgentKitScrubber; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = TRMD00000000000000000090 /* Project object */; +} diff --git a/.TermindPresentationQA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/.TermindPresentationQA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..2de1333b --- /dev/null +++ b/.TermindPresentationQA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,195 @@ +{ + "originHash" : "6aeb5447857612da7b6960feb17ce0fc14e8688afe3bd4e0b56c885ecc186b9f", + "pins" : [ + { + "identity" : "agentkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AkinoKaede/AgentKit.git", + "state" : { + "revision" : "bbc233cf6cf4dc28c3f6e7d9b7b06d096f7b27f7", + "version" : "0.13.1" + } + }, + { + "identity" : "choreographer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/unixzii/Choreographer", + "state" : { + "revision" : "04dc84bfe380b8c5910ed7d0607765fddedd0460", + "version" : "0.2.0" + } + }, + { + "identity" : "glyphixtextfx", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/GlyphixTextFx.git", + "state" : { + "revision" : "e1d0aa71c1849363a1bbef061d3d92f10481e62c", + "version" : "2.3.6" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift.git", + "state" : { + "revision" : "b83108d10f42680d78f23fe4d4d80fc88dab3212", + "version" : "7.11.1" + } + }, + { + "identity" : "highlightr", + "kind" : "remoteSourceControl", + "location" : "https://github.com/raspu/Highlightr", + "state" : { + "revision" : "05e7fcc63b33925cd0c1faaa205cdd5681e7bbef", + "version" : "2.3.0" + } + }, + { + "identity" : "libghostty-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/libghostty-spm", + "state" : { + "revision" : "7e45d27160f9b34aca9ca5c9820e9207482f9f04", + "version" : "1.6.20260909" + } + }, + { + "identity" : "libssh2-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/libssh2-spm.git", + "state" : { + "revision" : "d211fdf8587532cd952927d9f9f3f5167ce6a77f", + "version" : "2.0.3" + } + }, + { + "identity" : "listviewkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/ListViewKit", + "state" : { + "revision" : "c6a067ba837758a50612f88dd1d6bb025175df6c", + "version" : "4.3.2" + } + }, + { + "identity" : "litext", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/Litext", + "state" : { + "revision" : "130b4eef642d76a3d2dcf07ab966f32f08e14b90", + "version" : "2.2.1" + } + }, + { + "identity" : "lrucache", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/LRUCache", + "state" : { + "revision" : "f0570e943ea4445d258113ba73f50cb2b7e564ac", + "version" : "1.3.0" + } + }, + { + "identity" : "markdownview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/MarkdownView", + "state" : { + "revision" : "4abc4817fa5211f05e25fbf7a6295fb8e9fcfd90", + "version" : "4.3.2" + } + }, + { + "identity" : "msdisplaylink", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/MSDisplayLink.git", + "state" : { + "revision" : "87eb0af130744c8cbe2e31b6e1a5bcd659f1c220", + "version" : "2.2.0" + } + }, + { + "identity" : "openssl-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/openssl-spm.git", + "state" : { + "revision" : "9f3b525d960fe71e534482310e96cd9c4f2faa17", + "version" : "3.6.2" + } + }, + { + "identity" : "respring", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ktiays/Respring", + "state" : { + "revision" : "2148e8fce312b4dcf76b8f909226b19f3ae38186", + "version" : "1.1.0" + } + }, + { + "identity" : "scrubberkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/ScrubberKit.git", + "state" : { + "revision" : "b69932dc44461dd4a73aafeb78d514222f68d3e1", + "version" : "0.2.0" + } + }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark", + "state" : { + "revision" : "924936d0427cb25a61169739a7660230bffa6ea6", + "version" : "0.8.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" + } + }, + { + "identity" : "swiftmath", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mgriebling/SwiftMath", + "state" : { + "revision" : "fa8244ed032f4a1ade4cb0571bf87d2f1a9fd2d7", + "version" : "1.7.3" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup", + "state" : { + "revision" : "18b80329749eca5ea29fc50211dca5c7eff5bfec", + "version" : "2.13.9" + } + }, + { + "identity" : "with", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ktiays/With", + "state" : { + "revision" : "98edf55b91436b2c13a76fc2b66dc3b34627d8d4", + "version" : "2.1.5" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation.git", + "state" : { + "revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d", + "version" : "0.9.20" + } + } + ], + "version" : 3 +} diff --git a/.TermindPresentationQA.xcodeproj/xcshareddata/xcschemes/TFI.xcscheme b/.TermindPresentationQA.xcodeproj/xcshareddata/xcschemes/TFI.xcscheme new file mode 100644 index 00000000..ad700897 --- /dev/null +++ b/.TermindPresentationQA.xcodeproj/xcshareddata/xcschemes/TFI.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.TermindPresentationQA.xcodeproj/xcshareddata/xcschemes/TFM.xcscheme b/.TermindPresentationQA.xcodeproj/xcshareddata/xcschemes/TFM.xcscheme new file mode 100644 index 00000000..0ee79b56 --- /dev/null +++ b/.TermindPresentationQA.xcodeproj/xcshareddata/xcschemes/TFM.xcscheme @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FrameworkTests/ModelProviderCredentialTests.swift b/FrameworkTests/ModelProviderCredentialTests.swift index dd570b7e..6b0d166c 100644 --- a/FrameworkTests/ModelProviderCredentialTests.swift +++ b/FrameworkTests/ModelProviderCredentialTests.swift @@ -44,6 +44,7 @@ struct ModelProviderCredentialTests { name: "Vertex", apiFormat: .generateContent, credentialRef: "ordinary", usesVertex: true) secrets.values["ordinary"] = "wrong-key" #expect(ai.modelSecret(for: provider) == nil) + provider.vertex.projectID = "test-project" provider.vertex.credentialRef = "service-account" #expect(ai.modelSecret(for: provider) == nil) secrets.values["service-account"] = "service-account-json" diff --git a/FrameworkTests/ModelProviderFormTests.swift b/FrameworkTests/ModelProviderFormTests.swift new file mode 100644 index 00000000..bef5a20f --- /dev/null +++ b/FrameworkTests/ModelProviderFormTests.swift @@ -0,0 +1,65 @@ +import AgentKit +import Foundation +import TermindApplication +import TermindDomain +import Testing + +@Suite @MainActor +struct ModelProviderFormTests { + @Test(arguments: ModelAPIType.allCases) + func emptyInputsSaveConcreteDefaultsWithoutChangingTheDraft(_ type: ModelAPIType) { + let draft = ModelProvider(name: " \n") + let endpoint = ProviderEndpoint(apiType: type, url: " \n") + let ai = AIConfigurationModel(persistsSecrets: false) + #expect(ai.saveProvider(draft, endpoint: endpoint)) + let stored = ai.modelProvider(draft.id) + #expect(stored?.name == type.defaultProviderName) + #expect(ai.endpoint(for: draft.id).url == type.defaultURL) + #expect(stored?.inferenceURL == ProviderEndpoint(apiType: type, url: type.defaultURL).requestURL) + #expect(endpoint.url == " \n") + #expect(draft.name == " \n") + } + + @Test func automaticUsesResponsesOnlyForAnEmptyFormURL() { + let empty = ModelProviderForm.normalized(ModelProvider(name: ""), endpoint: .init()) + #expect(empty.provider.name == "OpenAI") + #expect(empty.provider.apiFormat == .responses) + #expect(empty.endpoint.url == "https://api.openai.com/v1/responses") + #expect(empty.provider.resolvedModelsURL == "https://api.openai.com/v1/models") + #expect(ProviderEndpoint().requestURL.isEmpty) + #expect(ProviderEndpoint(url: "https://local.test/v1").format == .chatCompletions) + } + + @Test func explicitValuesAreNeverReplacedEvenWhenTheyMatchAnotherDefault() { + let value = ModelProviderForm.normalized( + ModelProvider(name: "OpenAI"), + endpoint: .init(apiType: .messages, url: ModelAPIType.automatic.defaultURL)) + #expect(value.provider.name == "OpenAI") + #expect(value.provider.apiFormat == .messages) + #expect(value.endpoint.url == ModelAPIType.automatic.defaultURL) + let bad = ModelProviderForm.normalized(ModelProvider(name: "Private"), endpoint: .init(url: "bad URL")) + #expect(!ModelProviderForm.hasValidAddress(bad.provider)) + #expect(bad.endpoint.url == "bad URL") + } + + @Test func vertexNormalizesLocationAndKeepsItsSeparateDestination() { + var provider = ModelProvider(name: "", usesVertex: true) + provider.vertex = VertexConfig(projectID: "project", location: " ", credentialRef: "account") + let value = ModelProviderForm.normalized(provider, endpoint: .init(apiType: .generateContent)) + #expect(value.provider.name == "Google") + #expect(value.provider.vertex.location == "global") + #expect(ModelProviderForm.hasValidAddress(value.provider)) + #expect( + value.provider.requestURL() + == "https://aiplatform.googleapis.com/v1/projects/project/locations/global/publishers/google/models/{model}:generateContent" + ) + #expect(!value.provider.canFetchModels) + let switched = ModelProviderForm.normalized(provider, endpoint: .init(apiType: .messages)).provider + #expect(!switched.usesVertex) + #expect(switched.vertex.credentialRef == "account") + provider.vertex.projectID = "project/escape" + #expect( + !ModelProviderForm.hasValidAddress( + ModelProviderForm.normalized(provider, endpoint: .init(apiType: .generateContent)).provider)) + } +} diff --git a/FrameworkTests/ModelProviderVertexTests.swift b/FrameworkTests/ModelProviderVertexTests.swift new file mode 100644 index 00000000..c771d6a2 --- /dev/null +++ b/FrameworkTests/ModelProviderVertexTests.swift @@ -0,0 +1,238 @@ +import AgentKit +import Foundation +import Security +import SwiftData +import TermindApplication +import TermindDomain +import TermindInfrastructure +import TermindPersistence +import Testing + +@Suite @MainActor +struct ModelProviderVertexTests { + private func vertexProvider() -> ModelProvider { + ModelProvider( + name: "Google", apiFormat: .generateContent, credentialRef: "api-key", usesVertex: true, + vertex: VertexConfig( + projectID: "test-project", location: "us-central1", credentialRef: "service-account", + clientEmail: "fixture@example.invalid"), models: [AIModel(id: "gemini-test")]) + } + + @Test func vertexPersistsAlongsideTheFrozenProviderRowAndDeletesWithIt() throws { + let container = try TermindPersistence.makeContainer(inMemory: true) + let persistence = AIPersistence(context: container.mainContext) + let ai = AIConfigurationModel(persistence: persistence, persistsSecrets: false) + let provider = vertexProvider() + #expect(ai.saveProvider(provider, endpoint: .init(apiType: .generateContent))) + let reopened = AIConfigurationModel(persistence: persistence, persistsSecrets: false) + let saved = try #require(reopened.modelProvider(provider.id)) + #expect(saved.usesVertex) + #expect(saved.vertex == provider.vertex) + #expect(saved.models == provider.models) + #expect(saved.requestURL().hasPrefix("https://us-central1-aiplatform.googleapis.com/")) + #expect(reopened.endpoint(for: provider.id).url == ModelAPIType.generateContent.defaultURL) + reopened.delete(modelProviderID: provider.id) + #expect(try container.mainContext.fetch(FetchDescriptor()).isEmpty) + } + + @Test func v18MigratesWithoutChangingExistingProviderData() throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + let url = directory.appendingPathComponent("Store.sqlite") + let schema = Schema(versionedSchema: TermindSchemaV18.self) + let provider = ModelProvider(name: "Legacy", models: [AIModel(id: "private")]) + let endpoint = ProviderEndpoint(apiType: .messages, url: "https://private.invalid/v1") + do { + let legacy = try ModelContainer( + for: schema, + configurations: [ + ModelConfiguration("Termind", schema: schema, url: url, cloudKitDatabase: .none) + ]) + legacy.mainContext.insert(PersistentModelProvider(provider, endpoint: endpoint, sortIndex: 0)) + legacy.mainContext.insert(PersistentAIModel(provider.models[0], providerID: provider.id, sortIndex: 0)) + try legacy.mainContext.save() + } + let container = try TermindPersistence.makeContainer(url: url) + let snapshot = try AIPersistence(context: container.mainContext).load() + #expect(snapshot.providers.first?.name == "Legacy") + #expect(snapshot.providers.first?.models == provider.models) + #expect(snapshot.endpoints[provider.id] == endpoint) + #expect(snapshot.providers.first?.usesVertex == false) + } + + @Test func syncRoundTripsVertexAndPreservesItWhenAnOlderWriterOmitsIt() throws { + let container = try TermindPersistence.makeContainer(inMemory: true) + let defaults = UserDefaults(suiteName: "ProviderVertexTests.\(UUID())")! + let store = SettingsSyncPersistence(context: container.mainContext, defaults: defaults) + try store.prepare() + let provider = vertexProvider() + let endpoint = ProviderEndpoint(apiType: .generateContent, url: ModelAPIType.generateContent.defaultURL) + var value = SyncedModelProvider(provider, endpoint: endpoint, sortIndex: 0) + func envelope(_ value: SyncedModelProvider, time: TimeInterval) -> SettingsEnvelope { + let payload = SettingsRecordPayload.provider(value) + return SettingsEnvelope( + key: payload.key, + version: .next(after: nil, writerID: UUID(), now: Date(timeIntervalSinceNow: time)), payload: payload) + } + let first = envelope(value, time: 10) + let roundTrip = try JSONDecoder().decode(SettingsEnvelope.self, from: JSONEncoder().encode(first)) + #expect(roundTrip == first) + let received = try store.applyRemote([roundTrip]).snapshot.configuration.providers[0] + #expect(received.vertex == provider.vertex) + #expect(received.usesVertex) + value.name = "Old client rename" + value.vertex = nil + let legacyData = try JSONEncoder().encode(value) + #expect(try JSONDecoder().decode(SyncedModelProvider.self, from: legacyData).vertex == nil) + let legacy = try store.applyRemote([envelope(value, time: 20)]) + #expect(legacy.snapshot.configuration.providers[0].name == "Old client rename") + #expect(legacy.snapshot.configuration.providers[0].vertex == provider.vertex) + #expect(legacy.snapshot.configuration.providers[0].usesVertex) + #expect(legacy.needsUpload.contains(first.key)) + value.vertex = SyncedProviderVertex(provider) + value.vertex?.isEnabled = false + value.vertex?.credentialRef = "" + let cleared = try store.applyRemote([envelope(value, time: 30)]).snapshot.configuration.providers[0] + #expect(!cleared.usesVertex) + #expect(cleared.vertex.credentialRef.isEmpty) + } + + @Test func failedMetadataSaveRollsBackBothCredentialsAndKeepsDraftSecrets() throws { + let provider = vertexProvider() + let secrets = VertexSecrets() + secrets.values = ["api-key": "old-key", "service-account": "old-json"] + let persistence = FailingProviderPersistence(provider: provider) + let ai = AIConfigurationModel(persistence: persistence, aiSecrets: secrets) + let edits: [ModelProviderCredentialEdit] = [ + .init(kind: .apiKey, reference: "api-key", previous: "old-key", replacement: "new-key"), + .init(kind: .serviceAccount, reference: "service-account", previous: "old-json", replacement: "new-json"), + ] + #expect(throws: (any Error).self) { + try ai.saveProvider(provider, endpoint: .init(apiType: .generateContent), credentials: edits) + } + #expect(secrets.values == ["api-key": "old-key", "service-account": "old-json"]) + #expect(ai.modelProvider(provider.id) == provider) + #expect(edits[1].replacement == "new-json") + } + + @Test func credentialConflictsFailBeforeWritingAndDeletionCleansBothReferences() throws { + let provider = vertexProvider() + let secrets = VertexSecrets() + secrets.values = ["api-key": "key", "service-account": "newer-json"] + let ai = AIConfigurationModel(modelProviders: [provider], aiSecrets: secrets) + #expect(throws: SettingsDraftMerge.Conflict.self) { + try ai.saveProvider( + provider, endpoint: .init(apiType: .generateContent), + credentials: [ + .init( + kind: .serviceAccount, reference: "service-account", previous: "old-json", replacement: "stale") + ]) + } + #expect(secrets.writes.isEmpty) + ai.delete(modelProviderID: provider.id) + #expect(secrets.values.isEmpty) + #expect(Set(secrets.writes) == ["api-key", "service-account"]) + } + + @Test func fileAndPastedAccountsShareValidationAndRejectUntrustedTokenHosts() async throws { + // Ephemeral test-only key: never written to the repository, Keychain, or a real Google account. + let key = try #require( + SecKeyCreateRandomKey( + [ + kSecAttrKeyType: kSecAttrKeyTypeRSA, kSecAttrKeySizeInBits: 1024, + ] as CFDictionary, nil)) + let rsa = try #require(SecKeyCopyExternalRepresentation(key, nil)) as Data + func der(_ tag: UInt8, _ data: Data) -> Data { + let length = data.count + return Data([tag, 0x82, UInt8(length >> 8), UInt8(length & 255)]) + data + } + let algorithm = Data([ + 0x02, 0x01, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, + ]) + let pem = + "-----BEGIN PRIVATE KEY-----\n" + der(0x30, algorithm + der(0x04, rsa)).base64EncodedString() + + "\n-----END PRIVATE KEY-----\n" + let object = [ + "type": "service_account", "project_id": "test-project", "client_email": "fixture@example.invalid", + "private_key": pem, "token_uri": "https://oauth2.googleapis.com/token", + ] + let json = String(decoding: try JSONEncoder().encode(object), as: UTF8.self) + let account = try ModelProviderServiceAccount(json: json) + #expect(account.projectID == "test-project") + #expect(account.clientEmail == "fixture@example.invalid") + let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + let url = directory.appendingPathComponent("fixture.json") + try Data(json.utf8).write(to: url) + #expect(try await ModelProviderServiceAccountFile.read(url) == account.json) + #expect(throws: (any Error).self) { + try ModelProviderServiceAccount( + json: json.replacingOccurrences(of: "oauth2.googleapis.com", with: "evil.invalid")) + } + try Data(repeating: 32, count: ModelProviderServiceAccount.maximumBytes + 1).write(to: url) + await #expect(throws: (any Error).self) { try await ModelProviderServiceAccountFile.read(url) } + } + + @Test func clearingAnUncommittedCredentialDoesNotMintAKeychainReference() throws { + let secrets = VertexSecrets() + let ai = AIConfigurationModel(aiSecrets: secrets) + let provider = ModelProvider(name: "") + try ai.saveProvider( + provider, endpoint: .init(), + credentials: [ + .init(kind: .serviceAccount, reference: "", previous: nil, replacement: nil) + ]) + #expect(secrets.writes.isEmpty) + #expect(ai.modelProvider(provider.id)?.vertex.credentialRef == "") + #expect(ai.modelProvider(provider.id)?.name == "OpenAI") + } + + @Test func failedCredentialCleanupRemainsVisibleAfterTheOtherReferenceSucceeds() { + let provider = vertexProvider() + let secrets = VertexSecrets() + secrets.values = ["api-key": "key", "service-account": "json"] + secrets.failingReference = "service-account" + let ai = AIConfigurationModel(modelProviders: [provider], aiSecrets: secrets) + ai.delete(modelProviderID: provider.id) + #expect(ai.modelProvider(provider.id) == nil) + #expect(secrets.values["api-key"] == nil) + #expect(secrets.values["service-account"] == "json") + #expect(ai.secretStorageError != nil) + #expect(Set(secrets.writes) == ["api-key", "service-account"]) + } + + @Test func invalidServiceAccountErrorsNeverEchoInput() { + for json in ["private-secret", "{}", String(repeating: "private-secret", count: 100_000)] { + do { + _ = try ModelProviderServiceAccount(json: json) + Issue.record("Invalid service account accepted") + } catch { + #expect(!error.localizedDescription.contains("private-secret")) + } + } + } +} + +@MainActor private final class VertexSecrets: AISecretStoring { + var values: [String: String] = [:] + var writes: [String] = [] + var failingReference: String? + enum Failure: Error { case inaccessible } + func string(for reference: String) throws -> String? { values[reference] } + func setString(_ value: String?, for reference: String) throws { + writes.append(reference) + if reference == failingReference { throw Failure.inaccessible } + values[reference] = value + } +} + +@MainActor private final class FailingProviderPersistence: AIConfigurationPersisting { + let provider: ModelProvider + init(provider: ModelProvider) { self.provider = provider } + func load() throws -> AIConfigurationSnapshot { .init(providers: [provider], roles: [:]) } + func save(_ snapshot: AIConfigurationSnapshot) throws { throw Failure.failed } + enum Failure: Error { case failed } +} diff --git a/FrameworkTests/PersistenceCompatibilityTests.swift b/FrameworkTests/PersistenceCompatibilityTests.swift index ea997658..a81fc9db 100644 --- a/FrameworkTests/PersistenceCompatibilityTests.swift +++ b/FrameworkTests/PersistenceCompatibilityTests.swift @@ -65,7 +65,8 @@ struct PersistenceCompatibilityTests { try first.mainContext.save() #expect(try second.mainContext.fetchCount(FetchDescriptor()) == 0) #expect(Set(first.schema.entities.map(\.name)) == Set(TermindPersistence.schema.entities.map(\.name))) - #expect(TermindMigrationPlan.schemas.count == 18) + #expect(TermindMigrationPlan.schemas.count == 19) + #expect(first.schema.entities.contains { $0.name == "PersistentModelProviderVertexConfiguration" }) } @Test diff --git a/Frameworks/Resources/Localizable.xcstrings b/Frameworks/Resources/Localizable.xcstrings index f5d2b6d5..06f34535 100644 --- a/Frameworks/Resources/Localizable.xcstrings +++ b/Frameworks/Resources/Localizable.xcstrings @@ -1,23 +1,25 @@ { "sourceLanguage" : "en", "strings" : { - "This skill name is reserved for a built-in skill.": { - "extractionState": "manual", - "localizations": { - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此名称保留给内置技能使用。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "此名稱保留給內建技能使用。" - } - } - } - }, + "Google Vertex AI" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"Google Vertex AI"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"Google Vertex AI"}}}}, + "Vertex AI" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"Vertex AI"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"Vertex AI"}}}}, + "Project ID" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"项目ID"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"專案ID"}}}}, + "Location" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"区域"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"區域"}}}}, + "Service Account" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"服务账号"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"服務帳戶"}}}}, + "Remove Service Account" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"移除服务账号"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"移除服務帳戶"}}}}, + "Paste Service Account JSON" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"粘贴服务账号JSON"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"貼上服務帳戶JSON"}}}}, + "Enter a valid project and location to configure Vertex AI." : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"输入有效的项目和区域以配置Vertex AI。"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"輸入有效的專案與區域以設定Vertex AI。"}}}}, + "Vertex AI requires a service account. Add model IDs manually; model discovery is unavailable." : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"Vertex AI需要服务账号。不支持模型发现,请手动添加模型ID。"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"Vertex AI需要服務帳戶。不支援模型探索,請手動新增模型ID。"}}}}, + "Enter a valid provider URL, or a Vertex project and location." : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"请输入有效的提供方URL,或Vertex项目和区域。"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"請輸入有效的供應商URL,或Vertex專案與區域。"}}}}, + "Choose a valid Google service account JSON file smaller than 1 MiB." : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"请选择小于1 MiB的有效Google服务账号JSON文件。"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"請選擇小於1 MiB的有效Google服務帳戶JSON檔案。"}}}}, + "Show more" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"显示更多"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"顯示更多"}}}}, + "Show less" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"收起"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"收合"}}}}, + "install" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"安装"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"安裝"}}}}, + "skills" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"技能"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"技能"}}}}, + "persistent memory" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"持久化记忆"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"持久化記憶"}}}}, + "saved conversations" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"历史会话"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"歷史對話"}}}}, + "skill files" : {"extractionState":"manual","localizations":{"zh-Hans":{"stringUnit":{"state":"translated","value":"技能文件"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"技能檔案"}}}}, + "%@ (%lld operations)" : {"extractionState":"manual","localizations":{"en":{"variations":{"plural":{"one":{"stringUnit":{"state":"translated","value":"%1$@ (%2$lld operation)"}},"other":{"stringUnit":{"state":"translated","value":"%1$@ (%2$lld operations)"}}}}},"zh-Hans":{"variations":{"plural":{"other":{"stringUnit":{"state":"translated","value":"%1$@(%2$lld项操作)"}}}}},"zh-Hant":{"variations":{"plural":{"other":{"stringUnit":{"state":"translated","value":"%1$@(%2$lld項操作)"}}}}}}}, "" : { "localizations" : { "zh-Hans" : { @@ -2100,6 +2102,7 @@ } }, "Add a key first. This endpoint requires one to list models." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2116,6 +2119,7 @@ } }, "Add a key, then press Fetch" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -7466,22 +7470,6 @@ } } }, - "Delete Provider" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "删除提供商" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "刪除供應商" - } - } - } - }, "Delete saved memory, snapshots, learning progress, and search indexes, then turn off Memory. Skills and chat history are kept. Re-enabling Memory can rebuild memories from old chats." : { "extractionState" : "manual", "localizations" : { @@ -18403,6 +18391,7 @@ } }, "Rebuild" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -19179,7 +19168,6 @@ } } }, - "Replace Key" : { "extractionState" : "manual", "localizations" : { @@ -19413,6 +19401,7 @@ }, "Reset" : { "comment" : "A button that deletes all skills.", + "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "zh-Hans" : { @@ -21593,7 +21582,6 @@ } } }, - "Skip" : { "localizations" : { "zh-Hans" : { @@ -25654,6 +25642,23 @@ } } }, + "This skill name is reserved for a built-in skill." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此名称保留给内置技能使用。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此名稱保留給內建技能使用。" + } + } + } + }, "This SSH record is not supported." : { "extractionState" : "stale", "localizations" : { @@ -28429,4 +28434,4 @@ } }, "version" : "1.2" -} +} \ No newline at end of file diff --git a/Frameworks/TermindApplication/FeatureModels/AIConfigurationModel.swift b/Frameworks/TermindApplication/FeatureModels/AIConfigurationModel.swift index a66412ca..8d09dcb6 100644 --- a/Frameworks/TermindApplication/FeatureModels/AIConfigurationModel.swift +++ b/Frameworks/TermindApplication/FeatureModels/AIConfigurationModel.swift @@ -376,6 +376,7 @@ public final class AIConfigurationModel { /// a configured credential is unavailable; callers must not send anonymously. /// Vertex always requires a service account, even without a stored reference. public func modelSecret(for provider: ModelProvider) -> String? { + if provider.usesVertexEndpoint && !ModelProviderForm.hasValidAddress(provider) { return nil } let reference = provider.effectiveCredentialRef if reference.isEmpty { return provider.usesVertexEndpoint ? nil : "" } guard let value = secret(for: reference), !value.isEmpty else { return nil } @@ -451,10 +452,7 @@ public final class AIConfigurationModel { @discardableResult public func saveProvider(_ draft: ModelProvider, endpoint: ProviderEndpoint) -> Bool { mutatePersistedState { - var value = draft - value.inferenceURL = endpoint.requestURL - value.baseURL = endpoint.baseURL - value.apiFormat = endpoint.format + let (value, endpoint) = ModelProviderForm.normalized(draft, endpoint: endpoint) let removed = Set((modelProvider(value.id)?.models ?? []).map(\.id)).subtracting(value.models.map(\.id)) for role in AIRole.allCases { if let ref = roles[role], ref.modelProviderID == value.id, removed.contains(ref.modelID) { @@ -500,7 +498,12 @@ public final class AIConfigurationModel { endpoints[modelProviderID] = nil } guard deleted else { return } - setSecret(nil, for: provider.credentialRef) + var failures: [String] = [] + for reference in Set([provider.credentialRef, provider.vertex.credentialRef]) where !reference.isEmpty { + setSecret(nil, for: reference) + if let error = secretStorageError { failures.append(error) } + } + secretStorageError = failures.isEmpty ? nil : failures.joined(separator: "\n") } public func setModelProviderEnabled(_ enabled: Bool, for id: ModelProvider.ID) { diff --git a/Frameworks/TermindApplication/ModelProviderServiceAccount.swift b/Frameworks/TermindApplication/ModelProviderServiceAccount.swift new file mode 100644 index 00000000..d46e12a0 --- /dev/null +++ b/Frameworks/TermindApplication/ModelProviderServiceAccount.swift @@ -0,0 +1,121 @@ +import AgentKit +import Foundation +import TermindDomain + +/// Validated transient input. Never put this value in a configuration snapshot or sync payload. +public nonisolated struct ModelProviderServiceAccount: Sendable { + public static let maximumBytes = 1_048_576 + public let json: String + public let clientEmail: String + public let projectID: String + + public init(json: String) throws { + guard json.utf8.count <= Self.maximumBytes, + let object = try? JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any], + object["type"] as? String == "service_account", + let tokenURI = object["token_uri"] as? String, + ["https://oauth2.googleapis.com/token", "https://accounts.google.com/o/oauth2/token"].contains(tokenURI), + let key = object["private_key"] as? String, + key.hasPrefix("-----BEGIN PRIVATE KEY-----"), + key.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix("-----END PRIVATE KEY-----"), + let der = Data( + base64Encoded: key.replacingOccurrences(of: "-----BEGIN PRIVATE KEY-----", with: "") + .replacingOccurrences(of: "-----END PRIVATE KEY-----", with: "") + .components(separatedBy: .whitespacesAndNewlines).joined()), + der.count > 256, der.first == 0x30, + let email = GoogleServiceAccountAuth.clientEmail(in: json), email.contains("@"), + let project = GoogleServiceAccountAuth.projectID(in: json), !project.isEmpty + else { throw ValidationError.invalid } + self.json = json + clientEmail = email + projectID = project + } + + public enum ValidationError: LocalizedError { + case invalid + public var errorDescription: String? { + String(localized: "Choose a valid Google service account JSON file smaller than 1 MiB.") + } + } +} + +public nonisolated enum ModelProviderCredentialKind: Sendable { + case apiKey, serviceAccount + + public func reference(in provider: ModelProvider) -> String { + switch self { + case .apiKey: provider.credentialRef + case .serviceAccount: provider.vertex.credentialRef + } + } + + public func setReference(_ reference: String, in provider: inout ModelProvider) { + switch self { + case .apiKey: provider.credentialRef = reference + case .serviceAccount: provider.vertex.credentialRef = reference + } + } +} + +/// A compare-and-swap edit held only for the duration of an editor transaction. +public nonisolated struct ModelProviderCredentialEdit { + public let kind: ModelProviderCredentialKind + public let reference: String + public let previous: String? + public let replacement: String? + + public init(kind: ModelProviderCredentialKind, reference: String, previous: String?, replacement: String?) { + self.kind = kind + self.reference = reference + self.previous = previous + self.replacement = replacement + } +} + +extension AIConfigurationModel { + /// Writes both secrets before metadata, rolling every changed secret back if any step fails. + public func saveProvider( + _ draft: ModelProvider, endpoint: ProviderEndpoint, credentials: [ModelProviderCredentialEdit] + ) throws { + var value = draft + let current = modelProvider(draft.id) + for edit in credentials { + guard edit.kind.reference(in: current ?? draft) == edit.reference else { + throw SettingsDraftMerge.Conflict.changed + } + let previous = secret(for: edit.reference) + if let error = secretStorageError, !edit.reference.isEmpty { throw CredentialError.message(error) } + guard previous == edit.previous else { throw SettingsDraftMerge.Conflict.changed } + } + var rollback: [(String, String?)] = [] + do { + for edit in credentials { + if edit.reference.isEmpty && edit.replacement == nil { continue } + let reference = edit.reference.isEmpty ? Self.makeCredentialRef("provider") : edit.reference + rollback.append((reference, edit.previous)) + setSecret(edit.replacement, for: reference) + if let error = secretStorageError { throw CredentialError.message(error) } + edit.kind.setReference(edit.replacement == nil ? "" : reference, in: &value) + } + guard saveProvider(value, endpoint: endpoint) else { + throw CredentialError.message(storageError ?? String(localized: "Unable to Save")) + } + } catch { + var message = error.localizedDescription + for (reference, previous) in rollback.reversed() { + setSecret(previous, for: reference) + if let failure = secretStorageError { message += "\n" + failure } + } + throw CredentialError.message(message) + } + } + + public enum CredentialError: LocalizedError { + case message(String) + public var errorDescription: String? { + switch self { + case .message(let message): message + } + } + } +} diff --git a/Frameworks/TermindDomain/AI/ModelProviderForm.swift b/Frameworks/TermindDomain/AI/ModelProviderForm.swift new file mode 100644 index 00000000..3df5e8f9 --- /dev/null +++ b/Frameworks/TermindDomain/AI/ModelProviderForm.swift @@ -0,0 +1,73 @@ +import AgentKit +import Foundation + +/// Form hints are also concrete save defaults, never implicit runtime destinations. +nonisolated extension ModelAPIType { + public var defaultProviderName: String { + switch self { + case .automatic, .chatCompletions, .responses: "OpenAI" + case .messages: "Anthropic" + case .generateContent: "Google" + } + } + + public var defaultURL: String { + switch self { + case .automatic: "https://api.openai.com/v1/responses" + case .chatCompletions, .responses: "https://api.openai.com/v1" + case .messages: "https://api.anthropic.com/v1" + case .generateContent: "https://generativelanguage.googleapis.com/v1beta" + } + } +} + +nonisolated extension ProviderEndpoint { + public var withDefaultURL: ProviderEndpoint { + var value = self + if value.url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + value.url = apiType.defaultURL + } + return value + } +} + +public nonisolated enum ModelProviderForm { + public static func normalized(_ provider: ModelProvider, endpoint: ProviderEndpoint) -> ( + provider: ModelProvider, endpoint: ProviderEndpoint + ) { + var provider = provider + let endpoint = endpoint.withDefaultURL + provider.name = provider.name.trimmingCharacters(in: .whitespacesAndNewlines) + if provider.name.isEmpty { provider.name = endpoint.apiType.defaultProviderName } + provider.inferenceURL = endpoint.requestURL + provider.baseURL = endpoint.baseURL + provider.apiFormat = endpoint.format + provider.usesVertex = provider.usesVertex && endpoint.apiType == .generateContent + provider.vertex.projectID = provider.vertex.projectID.trimmingCharacters(in: .whitespacesAndNewlines) + provider.vertex.location = provider.vertex.location.trimmingCharacters(in: .whitespacesAndNewlines) + if provider.vertex.location.isEmpty { provider.vertex.location = "global" } + return (provider, endpoint) + } + + public static func hasValidAddress(_ provider: ModelProvider) -> Bool { + if provider.usesVertexEndpoint { + return isPathIdentifier(provider.vertex.projectID) && isPathIdentifier(provider.vertex.location) + } + return isValidURL(provider.inferenceURL) + } + + public static func isValidURL(_ value: String) -> Bool { + guard let url = URL(string: value), + ["http", "https"].contains(url.scheme?.lowercased() ?? ""), + let host = url.host, !host.isEmpty + else { return false } + return true + } + + private static func isPathIdentifier(_ value: String) -> Bool { + !value.isEmpty + && value.utf8.allSatisfy { + (48...57).contains($0) || (65...90).contains($0) || (97...122).contains($0) || $0 == 45 + } + } +} diff --git a/Frameworks/TermindDomain/Settings/SettingsSyncTypes.swift b/Frameworks/TermindDomain/Settings/SettingsSyncTypes.swift index 9fe24425..6c6d4571 100644 --- a/Frameworks/TermindDomain/Settings/SettingsSyncTypes.swift +++ b/Frameworks/TermindDomain/Settings/SettingsSyncTypes.swift @@ -60,12 +60,34 @@ public nonisolated struct SyncedAIModel: Codable, Hashable, Sendable { } } +public nonisolated struct SyncedProviderVertex: Codable, Hashable, Sendable { + public var isEnabled: Bool + public var projectID: String + public var location: String + public var credentialRef: String + public var clientEmail: String + + public init(_ provider: ModelProvider) { + isEnabled = provider.usesVertex + projectID = provider.vertex.projectID + location = provider.vertex.location + credentialRef = provider.vertex.credentialRef + clientEmail = provider.vertex.clientEmail + } + + public var configuration: VertexConfig { + VertexConfig(projectID: projectID, location: location, credentialRef: credentialRef, clientEmail: clientEmail) + } +} + public nonisolated struct SyncedModelProvider: Codable, Hashable, Sendable { public var id: UUID public var name: String public var endpoint: ProviderEndpoint public var isEnabled: Bool public var credentialRef: String + /// Absence identifies an older writer, not an explicit removal. + public var vertex: SyncedProviderVertex? public var models: [SyncedAIModel] public var modelsFetchedAt: Date? public var sortIndex: Int @@ -76,6 +98,7 @@ public nonisolated struct SyncedModelProvider: Codable, Hashable, Sendable { self.endpoint = endpoint isEnabled = provider.isEnabled credentialRef = provider.credentialRef + vertex = SyncedProviderVertex(provider) models = provider.models.map(SyncedAIModel.init) modelsFetchedAt = provider.modelsFetchedAt self.sortIndex = sortIndex @@ -85,7 +108,9 @@ public nonisolated struct SyncedModelProvider: Codable, Hashable, Sendable { ModelProvider( id: id, name: name, apiFormat: endpoint.format, isEnabled: isEnabled, inferenceURL: endpoint.requestURL, baseURL: endpoint.baseURL, - credentialRef: credentialRef, models: models.map(\.value), modelsFetchedAt: modelsFetchedAt) + credentialRef: credentialRef, usesVertex: vertex?.isEnabled ?? false, + vertex: vertex?.configuration ?? VertexConfig(), + models: models.map(\.value), modelsFetchedAt: modelsFetchedAt) } } diff --git a/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift b/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift index 7dda60c7..52e17d60 100644 --- a/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift +++ b/Frameworks/TermindInfrastructure/Cloud/SettingsSyncCoordinator.swift @@ -250,7 +250,10 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc private func migrateSecrets() throws { let config = try persistence.load().configuration - try secrets.migrate(Set(config.providers.map(\.credentialRef) + config.mcpServers.map(\.credentialRef))) + try secrets.migrate( + Set( + config.providers.flatMap { [$0.credentialRef, $0.vertex.credentialRef] } + + config.mcpServers.map(\.credentialRef))) } private func applyRemote(_ envelopes: [SettingsEnvelope]) throws @@ -260,11 +263,11 @@ public final class SettingsSyncCoordinator: NSObject, CKSyncEngineDelegate, @unc ) { let result = try persistence.applyRemote(envelopes) - let references = envelopes.filter { result.acceptedKeys.contains($0.key) }.compactMap { envelope -> String? in + let references = envelopes.filter { result.acceptedKeys.contains($0.key) }.flatMap { envelope -> [String] in switch envelope.payload { - case .provider(let value): value.credentialRef - case .mcpServer(let value): value.credentialRef - default: nil + case .provider(let value): [value.credentialRef, value.vertex?.credentialRef ?? ""] + case .mcpServer(let value): [value.credentialRef] + default: [] } } try secrets.acceptRemoteReferences(Set(references)) diff --git a/Frameworks/TermindInfrastructure/ModelProviderServiceAccountFile.swift b/Frameworks/TermindInfrastructure/ModelProviderServiceAccountFile.swift new file mode 100644 index 00000000..244bd498 --- /dev/null +++ b/Frameworks/TermindInfrastructure/ModelProviderServiceAccountFile.swift @@ -0,0 +1,19 @@ +import Foundation +import TermindApplication + +public enum ModelProviderServiceAccountFile { + public static func read(_ url: URL) async throws -> String { + try await Task.detached { + let scoped = url.startAccessingSecurityScopedResource() + defer { if scoped { url.stopAccessingSecurityScopedResource() } } + try Task.checkCancellation() + let file = try FileHandle(forReadingFrom: url) + defer { try? file.close() } + let data = try file.read(upToCount: ModelProviderServiceAccount.maximumBytes + 1) ?? Data() + guard data.count <= ModelProviderServiceAccount.maximumBytes, + let json = String(data: data, encoding: .utf8) + else { throw ModelProviderServiceAccount.ValidationError.invalid } + return try ModelProviderServiceAccount(json: json).json + }.value + } +} diff --git a/Frameworks/TermindPersistence/HostPersistence.swift b/Frameworks/TermindPersistence/HostPersistence.swift index 9c3669f5..86bce800 100644 --- a/Frameworks/TermindPersistence/HostPersistence.swift +++ b/Frameworks/TermindPersistence/HostPersistence.swift @@ -686,6 +686,13 @@ public enum TermindSchemaV18: VersionedSchema { } } +public enum TermindSchemaV19: VersionedSchema { + public static let versionIdentifier = Schema.Version(19, 0, 0) + public static var models: [any PersistentModel.Type] { + TermindSchemaV18.models + [PersistentModelProviderVertexConfiguration.self] + } +} + public enum TermindMigrationPlan: SchemaMigrationPlan { public static var schemas: [any VersionedSchema.Type] { [ @@ -694,7 +701,7 @@ public enum TermindMigrationPlan: SchemaMigrationPlan { TermindSchemaV7.self, TermindSchemaV8.self, TermindSchemaV9.self, TermindSchemaV10.self, TermindSchemaV11.self, TermindSchemaV12.self, TermindSchemaV13.self, TermindSchemaV14.self, TermindSchemaV15.self, TermindSchemaV16.self, TermindSchemaV17.self, - TermindSchemaV18.self, + TermindSchemaV18.self, TermindSchemaV19.self, ] } @@ -754,6 +761,7 @@ public enum TermindMigrationPlan: SchemaMigrationPlan { MigrationStage.lightweight(fromVersion: TermindSchemaV15.self, toVersion: TermindSchemaV16.self), MigrationStage.lightweight(fromVersion: TermindSchemaV16.self, toVersion: TermindSchemaV17.self), MigrationStage.lightweight(fromVersion: TermindSchemaV17.self, toVersion: TermindSchemaV18.self), + MigrationStage.lightweight(fromVersion: TermindSchemaV18.self, toVersion: TermindSchemaV19.self), ] } } diff --git a/Frameworks/TermindPersistence/PersistentModelProviderVertexConfiguration.swift b/Frameworks/TermindPersistence/PersistentModelProviderVertexConfiguration.swift new file mode 100644 index 00000000..d46fdbfe --- /dev/null +++ b/Frameworks/TermindPersistence/PersistentModelProviderVertexConfiguration.swift @@ -0,0 +1,29 @@ +import AgentKit +import Foundation +import SwiftData + +/// Independent backing preserves every installed provider entity hash through V18. +@Model +public final class PersistentModelProviderVertexConfiguration { + @Attribute(.unique) public var providerID: UUID = UUID() + public var isEnabled = false + public var projectID = "" + public var location = "global" + public var credentialRef = "" + public var clientEmail = "" + + public init(_ provider: ModelProvider) { + providerID = provider.id + isEnabled = provider.usesVertex + projectID = provider.vertex.projectID + location = provider.vertex.location + credentialRef = provider.vertex.credentialRef + clientEmail = provider.vertex.clientEmail + } + + public func apply(to provider: inout ModelProvider) { + provider.usesVertex = isEnabled + provider.vertex = VertexConfig( + projectID: projectID, location: location, credentialRef: credentialRef, clientEmail: clientEmail) + } +} diff --git a/Frameworks/TermindPersistence/SettingsSyncStore.swift b/Frameworks/TermindPersistence/SettingsSyncStore.swift index e3685968..969cf8c7 100644 --- a/Frameworks/TermindPersistence/SettingsSyncStore.swift +++ b/Frameworks/TermindPersistence/SettingsSyncStore.swift @@ -132,8 +132,25 @@ public final class SettingsSyncPersistence { uniqueKeysWithValues: try skills.load().compactMap { skill in skill.package.map { (skill.id, $0) } }) + let existingProviders = Dictionary( + uniqueKeysWithValues: snapshot.configuration.providers.map { ($0.id, $0) }) + let legacyProviderIDs = Set( + try context.fetch(FetchDescriptor()).compactMap { + row -> UUID? in + guard let envelope = row.envelope, !envelope.isDeleted, + case .provider(let value) = envelope.payload, value.vertex == nil + else { return nil } + return value.id + }) snapshot = try materialize() let remoteSnapshot = snapshot + for index in snapshot.configuration.providers.indices { + let id = snapshot.configuration.providers[index].id + if legacyProviderIDs.contains(id), let old = existingProviders[id] { + snapshot.configuration.providers[index].usesVertex = old.usesVertex + snapshot.configuration.providers[index].vertex = old.vertex + } + } for index in snapshot.skills.indices where snapshot.skills[index].package == nil { snapshot.skills[index].package = existingPackages[snapshot.skills[index].id] } diff --git a/Frameworks/TermindPersistence/SwiftDataAIConfigurationStore.swift b/Frameworks/TermindPersistence/SwiftDataAIConfigurationStore.swift index 515a4c1b..b46989bd 100644 --- a/Frameworks/TermindPersistence/SwiftDataAIConfigurationStore.swift +++ b/Frameworks/TermindPersistence/SwiftDataAIConfigurationStore.swift @@ -24,6 +24,9 @@ public final class AIPersistence { let providerRows = try context.fetch( FetchDescriptor(sortBy: [SortDescriptor(\.sortIndex)]) ) + let vertexRows = try context.fetch(FetchDescriptor()) + let vertexByProvider = Dictionary( + vertexRows.map { ($0.providerID, $0) }, uniquingKeysWith: { first, _ in first }) var seenProviders: Set = [] var endpoints: [ModelProvider.ID: ProviderEndpoint] = [:] let providers = providerRows.compactMap { row -> ModelProvider? in @@ -34,7 +37,9 @@ public final class AIPersistence { return modelRow.value } endpoints[row.id] = row.endpoint - return row.value(models: models) + var provider = row.value(models: models) + vertexByProvider[row.id]?.apply(to: &provider) + return provider } func valid(_ ref: AIModelRef) -> Bool { @@ -84,6 +89,7 @@ public final class AIPersistence { public func replace(_ snapshot: AIConfigurationSnapshot) throws { try context.delete(model: PersistentAIModel.self) try context.delete(model: PersistentModelProvider.self) + try context.delete(model: PersistentModelProviderVertexConfiguration.self) try context.delete(model: PersistentAIRoleAssignment.self) try context.delete(model: PersistentAISelection.self) try context.delete(model: PersistentMCPServer.self) @@ -97,6 +103,7 @@ public final class AIPersistence { context.insert( PersistentModelProvider(provider, endpoint: endpoint, sortIndex: providerIndex) ) + context.insert(PersistentModelProviderVertexConfiguration(provider)) for (modelIndex, model) in provider.models.enumerated() { context.insert( PersistentAIModel(model, providerID: provider.id, sortIndex: modelIndex) diff --git a/Frameworks/TermindPersistence/TermindPersistence.swift b/Frameworks/TermindPersistence/TermindPersistence.swift index 8c8b4225..4ecee767 100644 --- a/Frameworks/TermindPersistence/TermindPersistence.swift +++ b/Frameworks/TermindPersistence/TermindPersistence.swift @@ -5,7 +5,7 @@ import SwiftData /// choose storage and own the container lifetime, never the model set. @MainActor public enum TermindPersistence { - public static let schema = Schema(versionedSchema: TermindSchemaV18.self) + public static let schema = Schema(versionedSchema: TermindSchemaV19.self) public static func makeContainer(inMemory: Bool = false, url: URL? = nil) throws -> ModelContainer { precondition(!inMemory || url == nil, "An in-memory store cannot also have a file URL") diff --git a/Frameworks/TermindPresentation/AgentKnowledgeToolPresentation.swift b/Frameworks/TermindPresentation/AgentKnowledgeToolPresentation.swift new file mode 100644 index 00000000..044cef58 --- /dev/null +++ b/Frameworks/TermindPresentation/AgentKnowledgeToolPresentation.swift @@ -0,0 +1,89 @@ +import AgentKit +import Foundation +import TermindApplication + +/// Render-time repair also covers cards persisted with the original, incorrect skill fallback. +/// A remote tool or an unknown explicit presenter ID can never opt into this policy by name alone. +nonisolated enum AgentKnowledgeToolPresentation { + enum Kind: String { + case skillsList = "skills_list" + case skillRead = "skill_read_file" + case skillManage = "skill_manage" + case skillInstall = "skill_install" + case memory + case sessionSearch = "session_search" + case legacySkill = "load_skill" + } + + struct Operation { + let action: AgentToolDescriptor.Presentation.ActionKind + let activity: String + let batchActivity: String + var isArgument = false + } + + static func kind(for descriptor: AgentToolDescriptor) -> Kind? { + guard descriptor.target == .local, let kind = Kind(rawValue: descriptor.name) else { return nil } + if let id = descriptor.presentation?.presenterID, id != "builtin.\(kind.rawValue)" { return nil } + return kind + } + + static func operation(for card: AgentToolCard, locale: Locale, approval: Bool = false) -> Operation? { + guard let kind = kind(for: card.descriptor) else { return nil } + let args = card.invocation.call.arguments.objectValue ?? [:] + func localized(_ key: String.LocalizationValue) -> String { AppLocalization.string(key, locale: locale) } + func string(_ key: String) -> String? { + AgentToolDetailFormatting.nonempty(args[key]?.stringValue) + } + let skills = localized("skills") + let memory = localized("persistent memory") + let sessions = localized("saved conversations") + switch kind { + case .skillsList: + return .init(action: .list, activity: skills, batchActivity: skills) + case .skillRead, .legacySkill: + let listing = string("path") == "." + let fallback = listing ? localized("skill files") : localized("a skill") + guard !approval, let name = string("name") else { + return .init(action: listing ? .list : .read, activity: fallback, batchActivity: skills) + } + let path = string("path") ?? "SKILL.md" + return .init( + action: listing ? .list : .read, activity: listing ? name : "\(name) / \(path)", batchActivity: skills, + isArgument: true) + case .skillManage: + let operations = args["operations"]?.arrayValue ?? [] + let names = Set( + operations.compactMap { AgentToolDetailFormatting.nonempty($0.objectValue?["name"]?.stringValue) }) + let name = !approval && names.count == 1 ? names.first : nil + let activity = name ?? skills + let counted = operations.count > 1 ? localized("\(activity) (\(operations.count) operations)") : activity + return .init(action: .update, activity: counted, batchActivity: skills, isArgument: name != nil) + case .skillInstall: + let name = approval ? nil : string("name") + return .init( + action: .install, activity: name ?? localized("a skill"), batchActivity: skills, + isArgument: name != nil + ) + case .memory: + let operations = args["operations"]?.arrayValue + let reading = operations?.isEmpty == true + let count = operations?.count ?? 0 + let activity = count > 1 ? localized("\(memory) (\(count) operations)") : memory + return .init(action: reading ? .read : .update, activity: activity, batchActivity: memory) + case .sessionSearch: + if let query = string("query") { + return .init( + action: .search, activity: approval ? sessions : query, batchActivity: sessions, + isArgument: !approval) + } + return .init( + action: string("conversation_id") == nil ? .list : .read, activity: sessions, batchActivity: sessions) + } + } + + static func title(_ text: String) -> String { + let oneLine = text.split(whereSeparator: \.isWhitespace).joined(separator: " ") + return oneLine.count > 80 ? String(oneLine.prefix(79)) + "…" : oneLine + } +} diff --git a/Frameworks/TermindPresentation/AgentToolDetailPreview.swift b/Frameworks/TermindPresentation/AgentToolDetailPreview.swift new file mode 100644 index 00000000..22b015c7 --- /dev/null +++ b/Frameworks/TermindPresentation/AgentToolDetailPreview.swift @@ -0,0 +1,119 @@ +import AgentKit +import Foundation + +/// A whole-card budget, not one budget per block. Full values remain untouched for expansion and Copy. +nonisolated public struct AgentToolDetailPreview: Equatable, Sendable { + public static let characterLimit = 600 + public static let rowLimit = 5 + public let items: [AgentToolDetail.Item] + public let fullItems: [AgentToolDetail.Item] + public var isShortened: Bool { items != fullItems } + + public init(_ detail: AgentToolDetail) { + fullItems = detail.items + var budget = Budget() + items = budget.project(detail.items) + } + + public var fullText: String { Self.text(fullItems) } + + private static func text(_ items: [AgentToolDetail.Item]) -> String { + items.map { item in + switch item { + case .field(let field): return field.label + ": " + field.value + case .message(let value, _): return value + case .text(let block): return [block.title, block.text].compactMap { $0 }.joined(separator: "\n") + case .list(let list): + return + ([list.title].compactMap { $0 } + list.rows.map(rowText) + + (list.rows.isEmpty ? [list.emptyMessage].compactMap { $0 } : [])).joined(separator: "\n") + case .groupedList(let list): + return + ([list.summary] + + list.sections.map { + ([$0.title] + $0.rows.map(rowText)).joined(separator: "\n") + }).joined(separator: "\n") + } + }.joined(separator: "\n\n") + } + + private static func rowText(_ row: AgentToolDetail.ListRow) -> String { + ([row.title, row.subtitle, row.detail].compactMap { $0 } + row.badges).joined(separator: "\n") + } + + private struct Budget { + var characters = AgentToolDetailPreview.characterLimit + var rows = AgentToolDetailPreview.rowLimit + var hasRoom: Bool { characters > 0 && rows > 0 } + + mutating func take(_ value: String) -> String { + guard characters > 0 else { return "" } + let result = value.count > characters ? String(value.prefix(max(0, characters - 1))) + "…" : value + characters -= result.count + return result + } + + mutating func optional(_ value: String?) -> String? { + value.map { take($0) } + } + + mutating func project(_ items: [AgentToolDetail.Item]) -> [AgentToolDetail.Item] { + var result: [AgentToolDetail.Item] = [] + for item in items { + guard hasRoom else { break } + switch item { + case .field(var field): + rows -= 1 + field.label = take(field.label) + field.value = take(field.value) + result.append(.field(field)) + case .message(let value, let tint): + rows -= 1 + result.append(.message(take(value), tint)) + case .text(var block): + rows -= 1 + block.title = optional(block.title) + block.text = take(block.text) + result.append(.text(block)) + case .list(var list): + list.title = optional(list.title) + list.rows = projectRows(list.rows) + if list.rows.isEmpty { + rows -= 1 + list.emptyMessage = optional(list.emptyMessage) + } + result.append(.list(list)) + case .groupedList(var list): + list.summary = take(list.summary) + var sections: [AgentToolDetail.ListSection] = [] + for var section in list.sections { + guard hasRoom else { break } + section.title = take(section.title) + section.rows = projectRows(section.rows) + sections.append(section) + } + list.sections = sections + result.append(.groupedList(list)) + } + } + return result + } + + mutating func projectRows(_ values: [AgentToolDetail.ListRow]) -> [AgentToolDetail.ListRow] { + var result: [AgentToolDetail.ListRow] = [] + for var row in values { + guard hasRoom else { break } + rows -= 1 + row.title = take(row.title) + row.subtitle = optional(row.subtitle) + row.detail = optional(row.detail) + row.badges = row.badges.compactMap { badge in + guard characters > 0 else { return nil } + return take(badge) + } + result.append(row) + } + return result + } + } +} diff --git a/Frameworks/TermindPresentation/AgentToolPresentation.swift b/Frameworks/TermindPresentation/AgentToolPresentation.swift index 17e43ba2..ab3b837e 100644 --- a/Frameworks/TermindPresentation/AgentToolPresentation.swift +++ b/Frameworks/TermindPresentation/AgentToolPresentation.swift @@ -19,6 +19,11 @@ nonisolated public enum AgentToolPresentation { public let omittedCharacters: Int } + public static func usesCompactDetails(for descriptor: AgentToolDescriptor) -> Bool { + guard let kind = AgentKnowledgeToolPresentation.kind(for: descriptor) else { return false } + return kind != .legacySkill + } + public static func symbol(for card: AgentToolCard) -> String { return card.descriptor.presentation?.symbol ?? "wrench.and.screwdriver" } @@ -59,6 +64,15 @@ nonisolated public enum AgentToolPresentation { hostName: String?, locale: Locale ) -> String { + if let operation = AgentKnowledgeToolPresentation.operation(for: card, locale: locale, approval: true) { + let action = localizedActionVerb(operation.action, locale: locale) + let title = + isChinese(locale) + ? localized("Approval tool action: \(action) \(operation.activity)", locale: locale) + : uppercaseFirst(actionName(operation.action, form: .infinitive, locale: locale)) + " " + + operation.activity + return AgentKnowledgeToolPresentation.title(title) + } let activity = approvalActivity(for: card, locale: locale) guard let kind = actionKind(for: card) else { let action = localized("Run \(activity)", locale: locale) @@ -147,6 +161,12 @@ nonisolated public enum AgentToolPresentation { hostName: String? = nil, locale: Locale = .current ) -> String { + if let operation = AgentKnowledgeToolPresentation.operation(for: card, locale: locale) { + return AgentKnowledgeToolPresentation.title( + actionSummary( + operation.action, state: card.state, activity: operation.activity, + isArgumentValue: operation.isArgument, target: nil, locale: locale)) + } let operation = operation(for: card, locale: locale) let phase: String if card.state == .running { @@ -215,12 +235,15 @@ nonisolated public enum AgentToolPresentation { struct Key: Hashable { let name: String let state: AgentToolCard.State + let knowledgeAction: AgentToolDescriptor.Presentation.ActionKind? } var order: [Key] = [] var grouped: [Key: [AgentToolCard]] = [:] for card in cards { - let key = Key(name: card.descriptor.qualifiedName, state: card.state) + let key = Key( + name: card.descriptor.qualifiedName, state: card.state, + knowledgeAction: AgentKnowledgeToolPresentation.operation(for: card, locale: locale)?.action) if grouped[key] == nil { order.append(key) } grouped[key, default: []].append(card) } @@ -292,6 +315,16 @@ nonisolated public enum AgentToolPresentation { ]) } + // A failed/denied historical result may still contain success-shaped fields. + // Only successful execution is allowed to select a knowledge success presenter. + if usesCompactDetails(for: card.descriptor), + result.isError || card.state == .failed || card.state == .denied || card.state == .interrupted + { + if detailOutput(from: result, presentation: card.descriptor.presentation, review: card.review) == nil { + return nil + } + return AgentToolDetail(items: AgentToolDetailFormatting.genericItems(value, locale: locale)) + } guard let presenter else { return AgentToolDetail( items: AgentToolDetailFormatting.genericItems(value, locale: locale) @@ -609,6 +642,14 @@ nonisolated public enum AgentToolPresentation { _ cards: [AgentToolCard], locale: Locale ) -> String { guard let first = cards.first else { return "" } + if let operation = AgentKnowledgeToolPresentation.operation(for: first, locale: locale) { + if cards.count == 1 { return summary(for: first, locale: locale) } + let activity = operation.batchActivity + " · " + toolCallCount(cards.count, locale: locale) + return AgentKnowledgeToolPresentation.title( + actionSummary( + operation.action, state: first.state, activity: activity, + isArgumentValue: false, target: nil, locale: locale)) + } guard let presentation = first.descriptor.presentation else { let activity = cards.count == 1 @@ -1012,6 +1053,7 @@ nonisolated public enum AgentToolPresentation { case .edit: .init(infinitive: "edit", progressive: "Editing", past: "Edited") case .fetch: .init(infinitive: "fetch", progressive: "Fetching", past: "Fetched") case .inspect: .init(infinitive: "inspect", progressive: "Inspecting", past: "Inspected") + case .install: .init(infinitive: "install", progressive: "Installing", past: "Installed") case .list: .init(infinitive: "list", progressive: "Listing", past: "Listed") case .move: .init(infinitive: "move", progressive: "Moving", past: "Moved") case .present: .init(infinitive: "present", progressive: "Writing", past: "Presented") @@ -1056,6 +1098,7 @@ nonisolated public enum AgentToolPresentation { case .edit: localized("edit", locale: locale) case .fetch: localized("fetch", locale: locale) case .inspect: localized("inspect", locale: locale) + case .install: localized("install", locale: locale) case .list: localized("list", locale: locale) case .move: localized("move", locale: locale) case .present: localized("present", locale: locale) @@ -1096,6 +1139,9 @@ nonisolated public enum AgentToolPresentation { case .scratchWorkspace: localized("the scratch workspace", locale: locale) case .secret: localized("a secret", locale: locale) case .skill: localized("a skill", locale: locale) + case .skills: localized("skills", locale: locale) + case .memory: localized("persistent memory", locale: locale) + case .sessions: localized("saved conversations", locale: locale) case .sshCommand: localized("the SSH command", locale: locale) case .taskList: localized("the task list", locale: locale) case .url: localized("a URL", locale: locale) diff --git a/Frameworks/TermindPresentation/AssistantCompactToolDetails.swift b/Frameworks/TermindPresentation/AssistantCompactToolDetails.swift new file mode 100644 index 00000000..44ccee99 --- /dev/null +++ b/Frameworks/TermindPresentation/AssistantCompactToolDetails.swift @@ -0,0 +1,45 @@ +import AgentKit +import SwiftUI + +/// The compact knowledge-tool body. Expansion belongs to the transcript, never to a recycled row. +public struct AssistantCompactToolDetails: View { + private let preview: AgentToolDetailPreview + private let isError: Bool + private let copy: (String) -> Void + @Binding private var expanded: Bool + @ScaledMetric(relativeTo: .caption) private var previewHeight: CGFloat = 180 + @State private var contentHeight: CGFloat = 0 + + public init(detail: AgentToolDetail, isError: Bool, expanded: Binding, copy: @escaping (String) -> Void) { + preview = AgentToolDetailPreview(detail) + self.isError = isError + _expanded = expanded + self.copy = copy + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + AssistantToolDetails(items: expanded ? preview.fullItems : preview.items, isError: isError, copy: copy) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + .onGeometryChange(for: CGFloat.self) { + $0.size.height + } action: { + contentHeight = $0 + } + .frame(maxHeight: expanded ? nil : previewHeight, alignment: .top) + .clipped() + if expanded || preview.isShortened || contentHeight > previewHeight { + HStack(spacing: 16) { + Button(expanded ? "Show less" : "Show more") { expanded.toggle() } + .accessibilityIdentifier("AssistantTool.More") + Button("Copy", systemImage: "doc.on.doc") { copy(preview.fullText) } + .accessibilityIdentifier("AssistantTool.Copy") + } + .font(.caption) + .frame(minHeight: 44) + } + } + .accessibilityIdentifier("AssistantTool.CompactDetails") + } +} diff --git a/Frameworks/TermindPresentation/AssistantTranscriptPresentationState.swift b/Frameworks/TermindPresentation/AssistantTranscriptPresentationState.swift index c6183904..6e77e1a2 100644 --- a/Frameworks/TermindPresentation/AssistantTranscriptPresentationState.swift +++ b/Frameworks/TermindPresentation/AssistantTranscriptPresentationState.swift @@ -5,6 +5,7 @@ import TermindApplication nonisolated public enum AssistantTranscriptDisclosureID: Hashable, Sendable { case work(UUID) case tool(UUID) + case toolDetail(UUID) case toolBatch(UUID) case reasoning(UUID) case plan(UUID) @@ -13,7 +14,7 @@ nonisolated public enum AssistantTranscriptDisclosureID: Hashable, Sendable { fileprivate var sourceID: UUID { switch self { - case .work(let id), .tool(let id), .toolBatch(let id), .reasoning(let id), .plan(let id), + case .work(let id), .tool(let id), .toolDetail(let id), .toolBatch(let id), .reasoning(let id), .plan(let id), .compactionActivity(let id), .failure(let id): id } diff --git a/Frameworks/TermindPresentation/AssistantTranscriptPreview+Knowledge.swift b/Frameworks/TermindPresentation/AssistantTranscriptPreview+Knowledge.swift new file mode 100644 index 00000000..63f9b5b2 --- /dev/null +++ b/Frameworks/TermindPresentation/AssistantTranscriptPreview+Knowledge.swift @@ -0,0 +1,100 @@ +#if DEBUG + import AgentKit + import Foundation + import TermindApplication + + extension AssistantTranscriptPreview { + /// No live store, account, network, or model service is involved in these rendering fixtures. + public static func knowledgeCards(language: AppLanguage) -> [AgentToolCard] { + let prose: String + switch language { + case .simplifiedChinese: prose = "部署之前先检查状态,并保留可复用的验证步骤。" + case .traditionalChinese: prose = "部署之前先檢查狀態,並保留可重用的驗證步驟。" + case .english, .system: prose = "Check status before deploying and retain reusable verification steps." + } + let body = Array(repeating: prose, count: 80).joined(separator: "\n") + let operations: AgentJSONValue = .array( + (0..<8).map { index in + .object(["action": .string("update"), "name": .string("deploy-\(index)"), "content": .string(body)]) + }) + struct Sample { + let name: String + let symbol: String + let arguments: [String: AgentJSONValue] + let result: AgentJSONValue + init(_ name: String, _ symbol: String, _ arguments: [String: AgentJSONValue], _ result: AgentJSONValue) + { + self.name = name + self.symbol = symbol + self.arguments = arguments + self.result = result + } + } + let samples: [Sample] = [ + .init( + "skills_list", "books.vertical", [:], + .object([ + "skills": .array( + (0..<15).map { index in + .object([ + "name": .string("deploy-\(index)"), "description": .string(prose), + "enabled": .bool(index.isMultiple(of: 2)), "read_only": .bool(index == 0), + ]) + }) + ])), + .init( + "skill_read_file", "book.closed", ["name": .string("deploy")], + .object([ + "skill": .string("deploy"), "path": .string("SKILL.md"), "title": .string("Deploy"), + "content": .string(body), + ])), + .init( + "skill_manage", "book.closed", ["operations": operations], + .object([ + "saved": .bool(true), "available": .string("next_run"), + ])), + .init( + "skill_install", "square.and.arrow.down", ["name": .string("deploy")], + .object([ + "saved": .bool(true), "name": .string("deploy"), + "source": .string("https://github.com/example/skills/tree/main/deploy"), + "available": .string("next_run"), + ])), + .init( + "memory", "brain", + [ + "operations": .array([ + .object([ + "action": .string("add"), "target": .string("user"), "content": .string(prose), + ]) + ]) + ], .object(["memory": .string("MEMORY\n" + body)])), + .init( + "session_search", "magnifyingglass", ["query": .string("deploy")], + .object([ + "messages": .array( + (0..<8).map { index in + .object([ + "title": .string("Deploy \(index)"), + "role": .string(index.isMultiple(of: 2) ? "user" : "assistant"), + "content": .string(body), "conversation_id": .string("not-visible"), + ]) + }), "next_offset": .number(8), + ])), + ] + return samples.map { sample in + let name = sample.name + var card = AgentToolCard( + invocation: .init( + runID: UUID(), call: .init(id: name, name: name, arguments: .object(sample.arguments))), + descriptor: .init( + name: name, summary: name, inputSchema: .object([:]), target: .local, approvalPolicy: .approve, + presentation: .init( + symbol: sample.symbol, activity: .semanticLabel(.skill), + presenterID: "builtin.\(name)", actionKind: .write)), state: .completed) + card.result = AgentToolResult(callID: name, content: sample.result.encodedString) + return card + } + } + } +#endif diff --git a/Frameworks/TermindPresentation/ModelProviderConnectionFields.swift b/Frameworks/TermindPresentation/ModelProviderConnectionFields.swift index 3395aa21..da1038d6 100644 --- a/Frameworks/TermindPresentation/ModelProviderConnectionFields.swift +++ b/Frameworks/TermindPresentation/ModelProviderConnectionFields.swift @@ -6,38 +6,57 @@ public struct ModelProviderConnectionFields: View { @Binding private var isEnabled: Bool @Binding private var name: String @Binding private var endpoint: ProviderEndpoint + private let showsEndpoint: Bool - public init(name: Binding, endpoint: Binding, isEnabled: Binding) { + public init( + name: Binding, endpoint: Binding, isEnabled: Binding, + showsEndpoint: Bool = true + ) { _isEnabled = isEnabled _name = name _endpoint = endpoint + self.showsEndpoint = showsEndpoint } + private var preview: ProviderEndpoint { endpoint.withDefaultURL } + public var body: some View { Section { LabeledContent("Name") { - TextField("Name", text: $name.singleLineText, prompt: Text("New Provider"), axis: .horizontal) - .singleLinePaste().lineLimit(1).labelsHidden().multilineTextAlignment(.trailing) + TextField( + "Name", text: $name.singleLineText, prompt: Text(verbatim: endpoint.apiType.defaultProviderName), + axis: .horizontal + ) + .singleLinePaste().lineLimit(1).labelsHidden().multilineTextAlignment(.trailing) } Toggle("Enabled", isOn: $isEnabled) Picker("API Type", selection: $endpoint.apiType) { ForEach(ModelAPIType.allCases) { Text($0.displayName).tag($0) } } } - Section("Endpoint") { - LabeledContent("URL") { - TextField("URL", text: $endpoint.url.singleLineText, axis: .horizontal).singleLinePaste().lineLimit(1) - .labelsHidden() - .autocorrectionDisabled() - .multilineTextAlignment(.trailing) - } - if endpoint.isConfigured { - LabeledContent("Full URL") { - Text(endpoint.requestURL).font(.caption.monospaced()).textSelection(.enabled) - .multilineTextAlignment(.trailing).fixedSize(horizontal: false, vertical: true) + if showsEndpoint { + Section("Endpoint") { + LabeledContent("URL") { + TextField( + "URL", text: $endpoint.url.singleLineText, prompt: Text(verbatim: endpoint.apiType.defaultURL), + axis: .horizontal + ).singleLinePaste().lineLimit(1) + .labelsHidden() + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + } + if !ModelProviderForm.isValidURL(preview.requestURL) { + Text("Enter a valid provider URL, or a Vertex project and location.") + .font(.caption).foregroundStyle(.red) + } + if preview.isConfigured { + LabeledContent("Full URL") { + Text(preview.requestURL).font(.caption.monospaced()).textSelection(.enabled) + .multilineTextAlignment(.trailing).fixedSize(horizontal: false, vertical: true) + } + Text("Requests use the \(preview.format.displayName) format.") + .font(.caption).foregroundStyle(.secondary) } - Text("Requests use the \(endpoint.format.displayName) format.") - .font(.caption).foregroundStyle(.secondary) } } } diff --git a/Frameworks/TermindPresentation/ModelProviderVertexFields.swift b/Frameworks/TermindPresentation/ModelProviderVertexFields.swift new file mode 100644 index 00000000..2daa75bd --- /dev/null +++ b/Frameworks/TermindPresentation/ModelProviderVertexFields.swift @@ -0,0 +1,157 @@ +import SwiftUI +import UniformTypeIdentifiers + +/// Platform-independent fields; hosts own file access, secret transactions, and request lifetimes. +public struct ModelProviderVertexFields: View { + @Binding private var isEnabled: Bool + @Binding private var projectID: String + @Binding private var location: String + private let clientEmail: String + private let hasCredential: Bool + private let requestURL: String? + private let readFile: (URL) async throws -> String + private let importJSON: (String) throws -> Void + private let remove: () throws -> Void + @State private var importingFile = false + @State private var pasting = false + @State private var json = "" + @State private var error: String? + @State private var importTask: Task? + + public init( + isEnabled: Binding, projectID: Binding, location: Binding, + clientEmail: String, hasCredential: Bool, requestURL: String?, + readFile: @escaping (URL) async throws -> String, + importJSON: @escaping (String) throws -> Void, remove: @escaping () throws -> Void + ) { + _isEnabled = isEnabled + _projectID = projectID + _location = location + self.clientEmail = clientEmail + self.hasCredential = hasCredential + self.requestURL = requestURL + self.readFile = readFile + self.importJSON = importJSON + self.remove = remove + } + + public var body: some View { + Section("Google Vertex AI") { + Toggle("Vertex AI", isOn: $isEnabled) + if isEnabled { + LabeledContent("Project ID") { + TextField("Project ID", text: $projectID.singleLineText, axis: .horizontal) + .singleLinePaste().labelsHidden().multilineTextAlignment(.trailing) + .autocorrectionDisabled().lineLimit(1) + } + LabeledContent("Location") { + TextField( + "Location", text: $location.singleLineText, prompt: Text(verbatim: "global"), axis: .horizontal + ) + .singleLinePaste().labelsHidden().multilineTextAlignment(.trailing) + .autocorrectionDisabled().lineLimit(1) + } + LabeledContent("Service Account") { + if hasCredential { Text(clientEmail).textSelection(.enabled) } else { Text("Required") } + } + ViewThatFits(in: .horizontal) { + HStack { credentialActions } + VStack(alignment: .leading, spacing: 12) { credentialActions } + } + .buttonStyle(.bordered) + if let requestURL { + LabeledContent("Full URL") { + Text(requestURL).font(.caption.monospaced()).textSelection(.enabled) + .multilineTextAlignment(.trailing).fixedSize(horizontal: false, vertical: true) + } + } else { + Text("Enter a valid project and location to configure Vertex AI.") + .font(.caption).foregroundStyle(.secondary) + } + Text("Vertex AI requires a service account. Add model IDs manually; model discovery is unavailable.") + .font(.caption).foregroundStyle(.secondary) + if let error { Text(error).foregroundStyle(.red) } + } + } + .fileImporter(isPresented: $importingFile, allowedContentTypes: [.json], allowsMultipleSelection: false) { + result in + switch result { + case .success(let urls): + guard let url = urls.first else { return } + importTask?.cancel() + importTask = Task { + do { + let value = try await readFile(url) + try Task.checkCancellation() + try importJSON(value) + } catch { + if !Task.isCancelled { self.error = error.localizedDescription } + } + } + case .failure(let error): self.error = error.localizedDescription + } + } + .sheet(isPresented: $pasting, onDismiss: { json = "" }) { + VStack(alignment: .leading, spacing: 12) { + Text("Paste Service Account JSON").font(.headline) + TextEditor(text: $json).font(.body.monospaced()).autocorrectionDisabled() + #if os(iOS) + .textInputAutocapitalization(.never) + #endif + .frame(minHeight: 220) + if let error { Text(error).foregroundStyle(.red) } + HStack { + Button("Cancel") { + json = "" + pasting = false + } + Spacer() + Button("Import") { + perform { + try importJSON(json) + json = "" + pasting = false + } + }.disabled(json.isEmpty) + } + } + .padding().frame(minWidth: 300, idealWidth: 520) + } + .onDisappear { + importTask?.cancel() + importTask = nil + json = "" + } + .onChange(of: isEnabled) { _, _ in + importTask?.cancel() + importTask = nil + json = "" + pasting = false + } + } + + @ViewBuilder private var credentialActions: some View { + Button("Import") { + importTask?.cancel() + error = nil + importingFile = true + } + Button("Paste") { + importTask?.cancel() + error = nil + json = "" + pasting = true + } + if hasCredential { + Button("Remove Service Account", role: .destructive) { perform(remove) } + } + } + + private func perform(_ action: () throws -> Void) { + importTask?.cancel() + do { + try action() + error = nil + } catch { self.error = error.localizedDescription } + } +} diff --git a/Frameworks/TermindRemoteInfrastructure/Agent/TermindAgentTools.swift b/Frameworks/TermindRemoteInfrastructure/Agent/TermindAgentTools.swift index b5d20cd9..c265c57a 100644 --- a/Frameworks/TermindRemoteInfrastructure/Agent/TermindAgentTools.swift +++ b/Frameworks/TermindRemoteInfrastructure/Agent/TermindAgentTools.swift @@ -157,6 +157,6 @@ nonisolated public enum TermindAgentTools { "sftp_move", "sftp_chmod", "sftp_delete", "fetch", "web_search", "request_user_input", "request_user_secret", "scratch_list", "scratch_read", "scratch_write", "scratch_edit", "scratch_diff", "scratch_delete", - "scratch_pull", "scratch_push", "present_plan", "manage_tasks", + "scratch_pull", "scratch_push", "present_plan", "manage_tasks", "load_skill", ] } diff --git a/Memory/ARCHITECTURE.md b/Memory/ARCHITECTURE.md index 064ca314..6d3bedb4 100644 --- a/Memory/ARCHITECTURE.md +++ b/Memory/ARCHITECTURE.md @@ -21,7 +21,7 @@ The agent runtime is the separately versioned `AgentKit` package. - `TermindPresentation` owns reusable SwiftUI content, presentation policy, Ghostty themes, and the `TerminalPreferences` bridge. It depends only on Application and Domain; native surfaces, transports, and lifetimes stay in apps. -- `TermindPersistence` exclusively owns every SwiftData model, frozen V1-V17 +- `TermindPersistence` exclusively owns every SwiftData model, frozen V1-V18 schema, migration plan, and container factory. It never depends on an app target. - `TFM` and `TFI` own platform UI, adapters, and lifecycle orchestration. Both use `App`, `Application`, `Design`, `DesignSystem`, `Features`, `Infrastructure`, @@ -301,8 +301,31 @@ empty, or unreadable remains an error. `AIConfigurationModel.modelSecret(for:)` shares this distinction across discovery, probes, Chat, Guardian, titles, compact, Command Generator, and Memory review. Anonymous resolution never reads secret storage or inherits another provider's storage error. Vertex still requires its -service account. AgentKit omits empty authentication headers for ordinary providers. -No authentication mode or persistence schema is added. +service account and a valid project/location. AgentKit omits empty authentication headers for ordinary providers. + +Model-provider forms show protocol-specific Name and URL hints, not prefilled input. +A shared Domain policy materializes blank/whitespace fields at the Application save +boundary: Automatic stores OpenAI's full Responses URL; explicit protocols store +OpenAI/Anthropic v1 or Google v1beta bases. Nonempty input never falls back or changes +on a type switch. Runtime reads never infer public destinations for legacy empty rows. +Desktop keeps page-owned input buffers while saving concrete defaults immediately; +mobile previews a normalized copy and normalizes again after conflict merging at Save. +Reopening shows saved values. Merely viewing or editing a form does not contact a provider. + +Explicit generateContent offers a Vertex AI toggle. Project, location (blank saves +as global), and service-account identity are independent of the retained ordinary +URL/API key. Leaving that type disables Vertex without deleting its configuration. +Missing Vertex credentials never fall back to an API key or anonymous access; Vertex +models are manually added. AgentKit owns address derivation and OAuth token exchange. +File/paste import validates bounded service-account JSON and trusted Google token URLs; +plaintext stays in transient drafts and AISecretStore. Two-reference credential edits +compare their original values and roll back all changed secrets on metadata failure. +Deletion removes both references; disabling Vertex removes neither. +V19 adds independent PersistentModelProviderVertexConfiguration backing without changing +V1-V18 entities. Settings sync carries only metadata/references, preserves existing +Vertex metadata when an older payload omits it, and respects explicit clears and deletes. +The existing Keychain migration covers both references; no private key enters CloudKit +configuration. Vertex data already discarded by V12 is not reconstructed. Both apps accept user-selected HTTP and HTTPS model endpoints. Their ATS arbitrary loads exception supports unknown user-configured domains, including local servers; @@ -701,7 +724,7 @@ selection, menus, and clipboard behavior. TermindPresentation owns tool summaries, batch status semantics, bounded result previews, timestamp formatting, Markdown sanitization/soft-break policy and the shared disclosure/deadline state. Hosts inject a resolved AgentToolDetailPresenter from TermindRemoteInfrastructure; Presentation never imports the registry's transport -module. Tool protocol results, AgentKit versions and persisted schemas are unchanged. +module. Tool protocol results and persisted schemas are unchanged. TFM retains native AppKit rows, Markdown caches and message editing state. TFI retains SwiftUI content inside its UIKit ListViewKit bridge. A row's presentation revision can change without its data changing: the mobile @@ -714,3 +737,12 @@ after the whole row leaves the viewport, unless the reader has already chosen it The direct approval callback stages a tool card idempotently before accepting a response. Queued tool-proposed and approval-requested notifications update that identity without erasing an already recorded denial. This keeps UI state independent of whether the event channel or the direct interaction reaches MainActor first. + +Skill and memory tools own their UI-neutral result and approval projections in AgentKit. +Presentation repairs their historical summary semantics only for recognized local tool/presenter +identities. Their compact result preview shares a 600-character/five-row budget across the entire +card and a scaled 180-point viewport. Full-detail expansion is independent, window-owned transcript +state and survives row reuse; Copy retains the full already-sanitized projection. Unknown presenter +IDs and MCP tools never opt into this policy by name. Long approvals instead retain all live +arguments in a scaled 240-point scroll region with actions outside it. Neither preview path reads +live skill/memory storage or changes the original protocol result, authorization, or persisted data. diff --git a/TFI/Application/FeatureModels/MobileProviderEditorModel.swift b/TFI/Application/FeatureModels/MobileProviderEditorModel.swift index d08dba0c..c8043c48 100644 --- a/TFI/Application/FeatureModels/MobileProviderEditorModel.swift +++ b/TFI/Application/FeatureModels/MobileProviderEditorModel.swift @@ -12,6 +12,7 @@ final class MobileProviderEditorModel { var draft: ModelProvider var endpoint: ProviderEndpoint let key = MobileSecretDraft() + let serviceAccount = MobileSecretDraft() var keyAction: KeyAction { get { key.action } set { key.action = newValue } @@ -25,43 +26,71 @@ final class MobileProviderEditorModel { init(ai: AIConfigurationModel, provider: ModelProvider) { self.ai = ai - original = provider - draft = provider wasPersisted = ai.modelProvider(provider.id) != nil + var initial = provider + if !wasPersisted && provider.vertex == VertexConfig() { initial.vertex.location = "" } + original = initial + draft = initial originalEndpoint = wasPersisted ? ai.endpoint(for: provider.id) : ProviderEndpoint(url: provider.inferenceURL) endpoint = originalEndpoint key.begin(in: ai, reference: provider.credentialRef) + serviceAccount.begin(in: ai, reference: provider.vertex.credentialRef) } var hasExistingKey: Bool { wasPersisted && !original.credentialRef.isEmpty } - var isDirty: Bool { draft != original || endpoint != originalEndpoint || key.isDirty } + var isDirty: Bool { draft != original || endpoint != originalEndpoint || key.isDirty || serviceAccount.isDirty } + var hasServiceAccount: Bool { + serviceAccount.action == .replace || (serviceAccount.action == .preserve && !draft.vertex.credentialRef.isEmpty) + } var canSave: Bool { - let url = URL(string: endpoint.requestURL) - return !draft.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - && ["http", "https"].contains(url?.scheme?.lowercased() ?? "") - && url?.host?.isEmpty == false && key.isValid + ModelProviderForm.hasValidAddress(requestProvider) && key.isValid && serviceAccount.isValid + } + + func importServiceAccount(_ json: String) throws { + let account = try ModelProviderServiceAccount(json: json) + serviceAccount.text = account.json + serviceAccount.action = .replace + draft.vertex.clientEmail = account.clientEmail + if draft.vertex.projectID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + draft.vertex.projectID = account.projectID + } + } + + func removeServiceAccount() { + serviceAccount.text = "" + serviceAccount.action = .clear + draft.vertex.clientEmail = "" + } + + func discardPlaintext() { + key.discardPlaintext() + serviceAccount.discardPlaintext() } var requestProvider: ModelProvider { - var provider = draft - provider.inferenceURL = endpoint.requestURL - provider.baseURL = endpoint.baseURL - provider.apiFormat = endpoint.format - return provider + ModelProviderForm.normalized(draft, endpoint: endpoint).provider } func requestSecret() throws -> String { guard !wasPersisted || ai.modelProvider(original.id) != nil else { throw MobileEditorError.message(String(localized: "Provider Deleted")) } - if key.action == .preserve { - guard let secret = ai.modelSecret(for: ai.modelProvider(original.id) ?? requestProvider) else { - throw MobileEditorError.message( - ai.secretStorageError ?? String(localized: "The provider's credential is unavailable.")) - } - return secret + let provider = requestProvider + guard ModelProviderForm.hasValidAddress(provider) else { + throw MobileEditorError.message( + String(localized: "Enter a valid provider URL, or a Vertex project and location.")) } - return try key.effective(in: ai, reference: draft.effectiveCredentialRef) + let credential = provider.usesVertexEndpoint ? serviceAccount : key + let reference = provider.effectiveCredentialRef + let secret = try credential.effective(in: ai, reference: reference) + guard + !secret.isEmpty || (!provider.usesVertexEndpoint && reference.isEmpty && credential.action != .replace) + || (!provider.usesVertexEndpoint && credential.action == .clear) + else { + throw MobileEditorError.message( + String(localized: "The provider's credential is unavailable.")) + } + return secret } func add(_ models: [AIModel]) { @@ -93,6 +122,10 @@ final class MobileProviderEditorModel { var fields = SettingsFields(original, draft, current) try fields.merge(\.name) try fields.merge(\.isEnabled) + try fields.merge(\.usesVertex) + try fields.merge(\.vertex.projectID) + try fields.merge(\.vertex.location) + try fields.merge(\.vertex.clientEmail) value = fields.result value.models = try SettingsDraftMerge.collection( original.models, draft.models, current.models, merge: Self.mergeModel) @@ -103,11 +136,17 @@ final class MobileProviderEditorModel { try connection.merge(\.apiType) savedEndpoint = connection.result } - value.name = value.name.trimmingCharacters(in: .whitespacesAndNewlines) - try key.commit(in: ai, reference: value.credentialRef) { reference in - value.credentialRef = reference - return ai.saveProvider(value, endpoint: savedEndpoint) + guard ModelProviderForm.hasValidAddress(ModelProviderForm.normalized(value, endpoint: savedEndpoint).provider) + else { + throw MobileEditorError.message( + String(localized: "Enter a valid provider URL, or a Vertex project and location.")) } + let edits = try [ + key.providerEdit(reference: value.credentialRef, kind: .apiKey), + serviceAccount.providerEdit(reference: value.vertex.credentialRef, kind: .serviceAccount), + ].compactMap { $0 } + try ai.saveProvider(value, endpoint: savedEndpoint, credentials: edits) + discardPlaintext() } static func mergeModel(_ original: AIModel, _ draft: AIModel, _ current: AIModel) throws -> AIModel { diff --git a/TFI/Application/FeatureModels/MobileSecretDraft.swift b/TFI/Application/FeatureModels/MobileSecretDraft.swift index 3a86ce82..eab64b6e 100644 --- a/TFI/Application/FeatureModels/MobileSecretDraft.swift +++ b/TFI/Application/FeatureModels/MobileSecretDraft.swift @@ -35,6 +35,19 @@ final class MobileSecretDraft { } } + func providerEdit(reference: String, kind: ModelProviderCredentialKind) throws -> ModelProviderCredentialEdit? { + guard action != .preserve else { return nil } + if let readError { throw MobileEditorError.message(readError) } + guard reference == originalReference else { throw SettingsDraftMerge.Conflict.changed } + return .init( + kind: kind, reference: reference, previous: originalSecret, replacement: action == .clear ? nil : text) + } + + func discardPlaintext() { + text = "" + originalSecret = nil + } + func commit(in ai: AIConfigurationModel, reference: String, kind: String = "key", save: (String) -> Bool) throws { if action != .preserve { if let readError { throw MobileEditorError.message(readError) } diff --git a/TFI/Features/AI/MobileAssistantInteraction.swift b/TFI/Features/AI/MobileAssistantInteraction.swift index 8b8100b6..ec8f938a 100644 --- a/TFI/Features/AI/MobileAssistantInteraction.swift +++ b/TFI/Features/AI/MobileAssistantInteraction.swift @@ -5,6 +5,7 @@ import TermindPresentation import TermindRemoteInfrastructure struct MobileAssistantInteraction: View { + @ScaledMetric(relativeTo: .caption) private var approvalHeight: CGFloat = 240 @Bindable var conversation: ConversationModel let hosts: HostLibraryModel @FocusState private var focusedInput: InputFocus? @@ -84,6 +85,7 @@ struct MobileAssistantInteraction: View { } .scrollBounceBehavior(.basedOnSize) } + .frame(maxHeight: AgentToolPresentation.usesCompactDetails(for: approval.descriptor) ? approvalHeight : nil) HStack { Button("Deny", systemImage: "xmark", role: .destructive) { diff --git a/TFI/Features/AI/MobileAssistantWorkRow.swift b/TFI/Features/AI/MobileAssistantWorkRow.swift index a2c62d4a..62ff0103 100644 --- a/TFI/Features/AI/MobileAssistantWorkRow.swift +++ b/TFI/Features/AI/MobileAssistantWorkRow.swift @@ -108,7 +108,7 @@ struct MobileAssistantToolRow: View { ) ) .foregroundStyle(card.state == .running ? Color.secondary : .primary) - .lineLimit(expanded ? nil : 1) + .lineLimit(AgentToolPresentation.usesCompactDetails(for: card.descriptor) ? 1 : (expanded ? nil : 1)) .modifier(MobileAssistantScanning(active: card.state == .running)) if card.state == .reviewing { Image(systemName: "checkmark.shield").foregroundStyle(.orange) @@ -125,8 +125,22 @@ struct MobileAssistantToolRow: View { if let detail = AgentToolPresentation.detail( for: card, presenter: TermindAgentTools.presenter(for: card.descriptor), locale: locale ) { - AssistantToolDetails(items: detail.items, isError: card.result?.isError == true) { - UIPasteboard.general.string = $0 + if AgentToolPresentation.usesCompactDetails(for: card.descriptor) { + AssistantCompactToolDetails( + detail: detail, isError: card.result?.isError == true, + expanded: Binding( + get: { presentation.isExpanded(.toolDetail(card.id)) }, + set: { value in + if value != presentation.isExpanded(.toolDetail(card.id)) { + presentation.toggleDisclosure(.toolDetail(card.id)) + } + } + ) + ) { UIPasteboard.general.string = $0 } + } else { + AssistantToolDetails(items: detail.items, isError: card.result?.isError == true) { + UIPasteboard.general.string = $0 + } } } if card.result?.isTruncated == true { diff --git a/TFI/Features/Settings/MobileAISettingsView.swift b/TFI/Features/Settings/MobileAISettingsView.swift index 6defacf1..cf8ec921 100644 --- a/TFI/Features/Settings/MobileAISettingsView.swift +++ b/TFI/Features/Settings/MobileAISettingsView.swift @@ -2,6 +2,7 @@ import AgentKit import SwiftUI import TermindApplication import TermindDomain +import TermindInfrastructure import TermindPresentation struct MobileAISettingsView: View { @@ -86,7 +87,7 @@ struct MobileModelsSettingsView: View { Image(systemName: "brain").foregroundStyle(.secondary).accessibilityHidden(true) VStack(alignment: .leading, spacing: 4) { Text(provider.name).foregroundStyle(.primary) - Text(provider.inferenceURL).font(.caption).foregroundStyle(.secondary) + Text(provider.requestURL()).font(.caption).foregroundStyle(.secondary) .lineLimit(1).truncationMode(.middle) Text("\(provider.models.count) Models").font(.caption).foregroundStyle(.secondary) if !provider.isEnabled { @@ -120,11 +121,11 @@ struct MobileModelsSettingsView: View { } .sheet(item: $editing) { provider in MobileProviderEditor(ai: ai, provider: provider) } .confirmationDialog( - "Delete Provider", + "Delete", isPresented: Binding( get: { deleting != nil }, set: { if !$0 { deleting = nil } }), titleVisibility: .visible ) { - Button("Delete Provider", role: .destructive) { + Button("Delete", role: .destructive) { if let deleting { ai.delete(modelProviderID: deleting.id) } deleting = nil } @@ -206,16 +207,27 @@ struct MobileProviderEditor: View { var body: some View { MobileEditorSheet( title: String(localized: "Model Provider"), isDirty: model.isDirty, canSave: model.canSave, + onCancel: { model.discardPlaintext() }, save: { try model.save() }, content: { ModelProviderConnectionFields( - name: $model.draft.name, endpoint: $model.endpoint, isEnabled: $model.draft.isEnabled + name: $model.draft.name, endpoint: $model.endpoint, isEnabled: $model.draft.isEnabled, + showsEndpoint: !model.requestProvider.usesVertexEndpoint ) .textInputAutocapitalization(.never) - MobileSecretFields( - key: model.key, - hasExistingCredential: model.hasExistingKey - ) + if !model.requestProvider.usesVertexEndpoint { + MobileSecretFields(key: model.key, hasExistingCredential: model.hasExistingKey) + } + if model.endpoint.apiType == .generateContent { + ModelProviderVertexFields( + isEnabled: $model.draft.usesVertex, projectID: $model.draft.vertex.projectID, + location: $model.draft.vertex.location, clientEmail: model.draft.vertex.clientEmail, + hasCredential: model.hasServiceAccount, + requestURL: ModelProviderForm.hasValidAddress(model.requestProvider) + ? model.requestProvider.requestURL() : nil, + readFile: { try await ModelProviderServiceAccountFile.read($0) }, + importJSON: model.importServiceAccount, remove: model.removeServiceAccount) + } modelSection.buttonStyle(.borderless) if let error { Text(error).foregroundStyle(.red) } } @@ -237,6 +249,11 @@ struct MobileProviderEditor: View { .onChange(of: model.requestProvider) { _, _ in cancelRequests() } .onChange(of: model.key.action) { _, _ in cancelRequests() } .onChange(of: model.key.text) { _, _ in cancelRequests() } + .onChange(of: model.serviceAccount.action) { _, _ in cancelRequests() } + .onChange(of: model.serviceAccount.text) { _, _ in cancelRequests() } + .onChange(of: model.endpoint.apiType) { _, type in + if type != .generateContent { model.draft.usesVertex = false } + } .onChange(of: ai.modelProvider(model.original.id)) { _, _ in cancelRequests() } .onDisappear { cancelRequests() } } @@ -284,7 +301,7 @@ struct MobileProviderEditor: View { presentedSheet = .fetch } label: { Label("Fetch", systemImage: "arrow.down.circle").frame(maxWidth: .infinity, minHeight: 44) - }.disabled(!model.canSave) + }.disabled(!model.canSave || !model.requestProvider.canFetchModels) Button { presentedSheet = .add } label: { diff --git a/TFI/Features/Settings/MobileTerminalSettingsView.swift b/TFI/Features/Settings/MobileTerminalSettingsView.swift index 4e846500..30a0c927 100644 --- a/TFI/Features/Settings/MobileTerminalSettingsView.swift +++ b/TFI/Features/Settings/MobileTerminalSettingsView.swift @@ -18,7 +18,7 @@ struct MobileTerminalSettingsView: View { } themeSection(dark: false) themeSection(dark: true) - Section { Toggle("Shell Integration", isOn: binding(\.shellIntegration)) } + Section("Integration") { Toggle("Shell Integration", isOn: binding(\.shellIntegration)) } if let error = model.storageError { Text(error).foregroundStyle(.red) } } .navigationTitle("Terminal") diff --git a/TFITests/MobileAISettingsRenderingTests.swift b/TFITests/MobileAISettingsRenderingTests.swift index db017fbb..34cde6a7 100644 --- a/TFITests/MobileAISettingsRenderingTests.swift +++ b/TFITests/MobileAISettingsRenderingTests.swift @@ -2,11 +2,10 @@ import AgentKit import Foundation import SwiftUI import TermindApplication +import TermindDomain import Testing import UIKit -import struct TermindDomain.Host - @testable import TermindIOS /// Opt-in native snapshots: set TERMIND_AI_SCREENSHOTS in the test runner environment. @@ -22,6 +21,25 @@ struct MobileAISettingsRenderingTests { view: MobileProviderEditor(ai: ai, provider: provider).environment(\.locale, Locale(identifier: language))) } + @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + func newHintsAndVertexEditorRender(_ language: String) async throws { + let ai = AIConfigurationModel(persistsSecrets: false) + try await capture( + "provider-hints", language: language, + view: MobileProviderEditor(ai: ai, provider: ModelProvider(name: "")) + .environment(\.locale, Locale(identifier: language))) + let provider = ModelProvider( + name: "Google", usesVertex: true, + vertex: VertexConfig( + projectID: "fixture-project", credentialRef: "fixture-reference", + clientEmail: "fixture@example.invalid")) + #expect(ai.saveProvider(provider, endpoint: .init(apiType: .generateContent))) + try await capture( + "provider-vertex", language: language, + view: MobileProviderEditor(ai: ai, provider: try #require(ai.modelProvider(provider.id))) + .environment(\.locale, Locale(identifier: language))) + } + @Test func renderSettingsEditors() async throws { guard let language = ProcessInfo.processInfo.environment["TERMIND_AI_SCREENSHOTS"] else { return } let app = try MobileAppModel(inMemory: true) @@ -207,6 +225,9 @@ struct MobileAISettingsRenderingTests { let directory = FileManager.default.temporaryDirectory.appending(path: "ai-settings-screenshots") try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) let device = UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone" + if ["provider-hints", "provider-vertex"].contains(name) { + Attachment.record([UInt8](data), named: "\(device)-\(language)-\(name).png") + } try data.write(to: directory.appending(path: "\(device)-\(language)-\(name).png")) print("AI settings snapshot: \(directory.path)/\(device)-\(language)-\(name).png") } diff --git a/TFITests/MobileAISettingsTests.swift b/TFITests/MobileAISettingsTests.swift index 4157e790..822df74c 100644 --- a/TFITests/MobileAISettingsTests.swift +++ b/TFITests/MobileAISettingsTests.swift @@ -41,7 +41,7 @@ struct MobileAISettingsTests { #expect(probes.results["test"] == "OK") } - @Test(arguments: ["", "file:///tmp/model", "ftp://example.com/model", "http:///", "example.com/v1"]) + @Test(arguments: ["file:///tmp/model", "ftp://example.com/model", "http:///", "example.com/v1"]) func invalidProviderAddressesCannotBeSaved(_ url: String) { let editor = MobileProviderEditorModel(ai: AIConfigurationModel(), provider: ModelProvider(name: "Invalid")) editor.endpoint.url = url diff --git a/TFITests/MobileKnowledgeToolRenderingTests.swift b/TFITests/MobileKnowledgeToolRenderingTests.swift new file mode 100644 index 00000000..c6dc657a --- /dev/null +++ b/TFITests/MobileKnowledgeToolRenderingTests.swift @@ -0,0 +1,69 @@ +import AgentKit +import Foundation +import SwiftUI +import TermindApplication +import TermindPresentation +import Testing +import UIKit + +@testable import TermindIOS + +@Suite(.serialized) @MainActor +struct MobileKnowledgeToolRenderingTests { + @Test func knowledgeRowsStayCompactAtPhoneWidthAndExpandOnDemand() async throws { + for language in [AppLanguage.english, .simplifiedChinese, .traditionalChinese] { + for card in AssistantTranscriptPreview.knowledgeCards(language: language) { + let state = AssistantTranscriptPresentationState() + state.followDefault(true, for: .tool(card.id)) + let controller = UIHostingController( + rootView: + MobileAssistantToolRow(card: card, presentation: state, hostName: { _ in "Unused" }) + .environment(\.locale, language.locale) + .environment(\.mobileListReduceMotion, true) + .padding(16) + .background(Color(uiColor: .systemBackground))) + let size = CGSize(width: 390, height: 30_000) + let compact = controller.sizeThatFits(in: size) + #expect(compact.height <= 360, "\(card.descriptor.name): \(compact.height)") + #expect(compact.height > 40) + controller.view.frame = CGRect(origin: .zero, size: compact) + controller.view.layoutIfNeeded() + if ProcessInfo.processInfo.environment["TERMIND_KNOWLEDGE_SCREENSHOTS"] == "1" { + try snapshot(controller.view, name: "ios-\(language.rawValue)-\(card.descriptor.name)") + } + state.toggleDisclosure(.toolDetail(card.id)) + try await Task.sleep(for: .milliseconds(20)) + controller.view.setNeedsLayout() + controller.view.layoutIfNeeded() + let expanded = controller.sizeThatFits(in: size) + #expect(expanded.height >= compact.height) + if card.descriptor.name != "skill_install" { #expect(expanded.height > compact.height) } + #expect(state.isExpanded(.toolDetail(card.id))) + } + } + } + + @Test func largerTextScalesThePreviewRatherThanHidingItsControls() { + let detail = AgentToolDetail(items: [ + .text(.init(text: String(repeating: "Readable memory\n", count: 100), style: .plain)) + ]) + let controller = UIHostingController( + rootView: + AssistantCompactToolDetails(detail: detail, isError: false, expanded: .constant(false), copy: { _ in }) + .environment(\.dynamicTypeSize, .accessibility2)) + let size = controller.sizeThatFits(in: CGSize(width: 390, height: 30_000)) + #expect(size.height > 180) + #expect(size.height < 800) + } + + private func snapshot(_ view: UIView, name: String) throws { + let image = UIGraphicsImageRenderer(bounds: view.bounds).image { context in + view.layer.render(in: context.cgContext) + } + let directory = FileManager.default.temporaryDirectory.appending(path: "knowledge-screenshots") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + let url = directory.appending(path: name + ".png") + try #require(image.pngData()).write(to: url) + print("Knowledge snapshot: \(url.path)") + } +} diff --git a/TFITests/MobileProviderDefaultsTests.swift b/TFITests/MobileProviderDefaultsTests.swift new file mode 100644 index 00000000..a3be2ba2 --- /dev/null +++ b/TFITests/MobileProviderDefaultsTests.swift @@ -0,0 +1,101 @@ +import AgentKit +import Foundation +import TermindApplication +import TermindDomain +import Testing + +@testable import TermindIOS + +@Suite @MainActor +struct MobileProviderDefaultsTests { + @Test(arguments: ModelAPIType.allCases) + func blankDraftsPreviewDefaultsAndPersistConcreteValues(_ type: ModelAPIType) throws { + let ai = AIConfigurationModel(persistsSecrets: false) + let editor = MobileProviderEditorModel(ai: ai, provider: ModelProvider(name: "")) + #expect(!editor.isDirty) + editor.endpoint.apiType = type + #expect(editor.canSave) + #expect(editor.draft.name.isEmpty) + #expect(editor.endpoint.url.isEmpty) + #expect(editor.requestProvider.name == type.defaultProviderName) + #expect(try editor.requestSecret() == "") + #expect(ai.modelProviders.isEmpty) + try editor.save() + let saved = try #require(ai.modelProvider(editor.original.id)) + #expect(saved.name == type.defaultProviderName) + #expect(ai.endpoint(for: saved.id).url == type.defaultURL) + let reopened = MobileProviderEditorModel(ai: ai, provider: saved) + #expect(reopened.draft.name == type.defaultProviderName) + #expect(reopened.endpoint.url == type.defaultURL) + #expect(!reopened.isDirty) + } + + @Test func typeSwitchDoesNotReplaceExplicitValuesAndDefaultsUseMergedType() throws { + let ai = AIConfigurationModel(persistsSecrets: false) + let provider = ModelProvider(name: "Original") + #expect(ai.saveProvider(provider, endpoint: .init())) + let saved = try #require(ai.modelProvider(provider.id)) + let editor = MobileProviderEditorModel(ai: ai, provider: saved) + editor.draft.name = "" + editor.endpoint.url = "" + ai.setEndpoint(.init(apiType: .messages, url: ModelAPIType.automatic.defaultURL), for: provider.id) + try editor.save() + #expect(ai.modelProvider(provider.id)?.name == "Anthropic") + #expect(ai.endpoint(for: provider.id).url == ModelAPIType.messages.defaultURL) + let explicit = MobileProviderEditorModel(ai: ai, provider: try #require(ai.modelProvider(provider.id))) + explicit.endpoint.apiType = .generateContent + #expect(explicit.draft.name == "Anthropic") + #expect(explicit.endpoint.url == ModelAPIType.messages.defaultURL) + } + + @Test func vertexPreviewSelectsItsOwnCredentialAndCanSaveAfterClearingIt() throws { + let ai = AIConfigurationModel(persistsSecrets: false) + let provider = ModelProvider(name: "Google", credentialRef: "key") + #expect(ai.saveProvider(provider, endpoint: .init(apiType: .generateContent))) + ai.setSecret("ordinary-key", for: "key") + let editor = MobileProviderEditorModel(ai: ai, provider: try #require(ai.modelProvider(provider.id))) + editor.draft.usesVertex = true + #expect(!editor.canSave) + editor.draft.vertex.projectID = "project" + editor.draft.vertex.location = "" + #expect(editor.canSave) + #expect(throws: (any Error).self) { try editor.requestSecret() } + editor.serviceAccount.action = .replace + editor.serviceAccount.text = "draft-service-account-json" + editor.draft.vertex.clientEmail = "fixture@example.invalid" + #expect(try editor.requestSecret() == "draft-service-account-json") + #expect(ai.modelProvider(provider.id)?.usesVertex == false) + try editor.save() + let saved = try #require(ai.modelProvider(provider.id)) + #expect(saved.vertex.location == "global") + #expect(saved.credentialRef == "key") + #expect(!saved.vertex.credentialRef.isEmpty) + #expect(ai.modelSecret(for: saved) == "draft-service-account-json") + let reopened = MobileProviderEditorModel(ai: ai, provider: saved) + reopened.removeServiceAccount() + #expect(reopened.canSave) + #expect(throws: (any Error).self) { try reopened.requestSecret() } + try reopened.save() + #expect(ai.modelProvider(provider.id)?.vertex.credentialRef == "") + #expect(ai.secret(for: saved.vertex.credentialRef) == nil) + #expect(ai.secret(for: "key") == "ordinary-key") + } + + @Test func vertexProjectConflictsRejectSaveAndInvalidImportsDoNotAlterDraft() throws { + let ai = AIConfigurationModel(persistsSecrets: false) + let provider = ModelProvider( + name: "Google", usesVertex: true, + vertex: VertexConfig(projectID: "original", credentialRef: "service")) + #expect(ai.saveProvider(provider, endpoint: .init(apiType: .generateContent))) + let saved = try #require(ai.modelProvider(provider.id)) + let editor = MobileProviderEditorModel(ai: ai, provider: saved) + #expect(throws: (any Error).self) { try editor.importServiceAccount("private-invalid") } + #expect(editor.draft == saved) + editor.draft.vertex.projectID = "local" + var remote = saved + remote.vertex.projectID = "remote" + ai.upsert(remote) + #expect(throws: SettingsDraftMerge.Conflict.self) { try editor.save() } + #expect(ai.modelProvider(provider.id)?.vertex.projectID == "remote") + } +} diff --git a/TFM/Features/AI/ConversationTranscriptView.swift b/TFM/Features/AI/ConversationTranscriptView.swift index f355608e..116d6468 100644 --- a/TFM/Features/AI/ConversationTranscriptView.swift +++ b/TFM/Features/AI/ConversationTranscriptView.swift @@ -532,6 +532,7 @@ struct AgentTaskCapsuleView: View { /// transcript that is waiting on a person, and the de-emphasis the rest of this /// view got would make it read like history. private struct AgentApprovalCard: View { + @ScaledMetric(relativeTo: .caption) private var approvalHeight: CGFloat = 240 let request: AgentApprovalRequest let conversation: ConversationModel @@ -571,6 +572,7 @@ private struct AgentApprovalCard: View { } .scrollBounceBehavior(.basedOnSize) } + .frame(maxHeight: AgentToolPresentation.usesCompactDetails(for: request.descriptor) ? approvalHeight : nil) HStack { Button("Deny", role: .destructive) { diff --git a/TFM/Features/AI/TranscriptNativeRows.swift b/TFM/Features/AI/TranscriptNativeRows.swift index c7eacf7e..4ef65b02 100644 --- a/TFM/Features/AI/TranscriptNativeRows.swift +++ b/TFM/Features/AI/TranscriptNativeRows.swift @@ -789,7 +789,7 @@ private enum NativeTranscriptFactory { reviewing: card.state == .reviewing, reduceMotion: context.configuration.reduceMotion, measurement: context.measurement, - lineLimit: expanded ? 0 : 1 + lineLimit: AgentToolPresentation.usesCompactDetails(for: card.descriptor) ? 1 : (expanded ? 0 : 1) ) header.alphaValue = card.state == .interrupted ? 0.5 : 1 header.toolTip = expanded ? String(localized: "Hide details") : String(localized: "Show details") @@ -819,10 +819,12 @@ private enum NativeTranscriptFactory { for: card, presenter: TermindAgentTools.presenter(for: card.descriptor), locale: context.configuration.locale ) { - for item in projected.items { - detail.addArrangedSubview( - toolDetail(item, isError: card.result?.isError == true) - ) + if AgentToolPresentation.usesCompactDetails(for: card.descriptor) { + detail.addArrangedSubview(compactToolDetail(projected, card: card, context: context)) + } else { + for item in projected.items { + detail.addArrangedSubview(toolDetail(item, isError: card.result?.isError == true)) + } } } if card.result?.isTruncated == true { @@ -1392,6 +1394,46 @@ private enum NativeTranscriptFactory { return card } + private static func compactToolDetail( + _ detail: AgentToolDetail, card: AgentToolCard, context: NativeTranscriptContext + ) -> NSView { + let preview = AgentToolDetailPreview(detail) + let id = AssistantTranscriptDisclosureID.toolDetail(card.id) + let expanded = context.presentation.disclosures.isExpanded(id) + let body = NativeStack.vertical() + for item in expanded ? preview.fullItems : preview.items { + body.addArrangedSubview(toolDetail(item, isError: card.result?.isError == true)) + } + let scale = max(1, NSFont.preferredFont(forTextStyle: .caption1).pointSize / NSFont.smallSystemFontSize) + let height = 180 * scale + let width = body.widthAnchor.constraint(equalToConstant: max(1, context.contentWidth - 48)) + width.isActive = true + body.layoutSubtreeIfNeeded() + let needsMore = preview.isShortened || body.fittingSize.height > height + width.isActive = false + let clipped = NativePlanPreview(content: body, expanded: expanded, previewHeight: height) + let toggle = { + context.presentation.disclosures.toggleDisclosure(id) + context.invalidate(context.entryID, false) + } + clipped.handler = needsMore ? toggle : nil + let stack = NativeStack.vertical() + stack.addArrangedSubview(clipped) + if expanded || needsMore { + let actions = NativeStack.horizontal() + let more = NativeButton( + title: AppLocalization.string( + expanded ? "Show less" : "Show more", locale: context.configuration.locale)) + more.identifier = NSUserInterfaceItemIdentifier("AssistantTool.More") + more.handler = toggle + actions.addArrangedSubview(more) + actions.addArrangedSubview(NativeCopyButton(text: preview.fullText)) + stack.addArrangedSubview(actions) + } + stack.identifier = NSUserInterfaceItemIdentifier("AssistantTool.CompactDetails") + return stack + } + private static func toolDetail(_ item: AgentToolDetail.Item, isError: Bool) -> NSView { switch item { case .field(let field): @@ -2138,10 +2180,12 @@ private final class NativePlanPreview: NSControl { var handler: (() -> Void)? private let content: NSView private let expanded: Bool + private let previewHeight: CGFloat - init(content: NSView, expanded: Bool) { + init(content: NSView, expanded: Bool, previewHeight: CGFloat = Metrics.Chat.planPreviewHeight) { self.content = content self.expanded = expanded + self.previewHeight = previewHeight super.init(frame: .zero) wantsLayer = true layer?.masksToBounds = true @@ -2155,7 +2199,7 @@ private final class NativePlanPreview: NSControl { if expanded { content.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true } else { - heightAnchor.constraint(lessThanOrEqualToConstant: Metrics.Chat.planPreviewHeight).isActive = true + heightAnchor.constraint(lessThanOrEqualToConstant: previewHeight).isActive = true } } @@ -2164,15 +2208,16 @@ private final class NativePlanPreview: NSControl { override func layout() { super.layout() - guard !expanded else { + guard !expanded, content.fittingSize.height > previewHeight else { layer?.mask = nil return } let mask = CAGradientLayer() mask.colors = [NSColor.black.cgColor, NSColor.black.cgColor, NSColor.clear.cgColor] mask.locations = [0, 0.72, 1] - mask.startPoint = CGPoint(x: 0.5, y: 0) - mask.endPoint = CGPoint(x: 0.5, y: 1) + // AppKit layer coordinates run upwards even though the transcript view is flipped. + mask.startPoint = CGPoint(x: 0.5, y: 1) + mask.endPoint = CGPoint(x: 0.5, y: 0) mask.frame = bounds layer?.mask = mask } @@ -2185,7 +2230,7 @@ private final class NativePlanPreview: NSControl { let size = content.fittingSize return NSSize( width: NSView.noIntrinsicMetric, - height: expanded ? size.height : min(size.height, Metrics.Chat.planPreviewHeight) + height: expanded ? size.height : min(size.height, previewHeight) ) } } @@ -2339,6 +2384,9 @@ private final class NativeBadge: NSView { font: .systemFont(ofSize: NSFont.smallSystemFontSize - 1), color: .secondaryLabelColor ) + label.maximumNumberOfLines = 1 + label.lineBreakMode = .byTruncatingTail + label.setContentCompressionResistancePriority(.required, for: .horizontal) label.translatesAutoresizingMaskIntoConstraints = false addSubview(label) NSLayoutConstraint.activate([ diff --git a/TFM/Features/Settings/ModelProviderEditorSheet.swift b/TFM/Features/Settings/ModelProviderEditorSheet.swift index c516d08d..20a71998 100644 --- a/TFM/Features/Settings/ModelProviderEditorSheet.swift +++ b/TFM/Features/Settings/ModelProviderEditorSheet.swift @@ -2,16 +2,26 @@ import AgentKit import SwiftUI import TermindApplication import TermindDomain +import TermindInfrastructure import TermindPresentation @Observable final class ModelProviderEditorSession { var secret = "" + var name = "" + var endpoint = ProviderEndpoint() + var projectID = "" + var location = "" + var error: String? private var didBegin = false - func begin(with provider: ModelProvider, in store: AIConfigurationModel) { + func begin(with provider: ModelProvider, in store: AIConfigurationModel, isNew: Bool = false) { guard !didBegin else { return } didBegin = true + name = isNew ? "" : provider.name + endpoint = isNew ? ProviderEndpoint() : store.endpoint(for: provider.id) + projectID = provider.vertex.projectID + location = isNew ? "" : provider.vertex.location secret = store.secret(for: provider.credentialRef) ?? "" } } @@ -24,6 +34,7 @@ final class ModelProviderEditorSession { struct ModelProviderEditorPage: View { @Environment(\.locale) private var locale let modelProviderID: ModelProvider.ID + var isNew = false let showModels: () -> Void let deleted: () -> Void @@ -66,13 +77,15 @@ struct ModelProviderEditorPage: View { private func settings(_ provider: ModelProvider) -> some View { Form { ModelProviderConnectionFields( - name: binding(\.name), - endpoint: Binding( - get: { endpoint }, set: { app.aiConfiguration.setEndpoint($0, for: modelProviderID) }), + name: Binding(get: { session.name }, set: saveName), + endpoint: Binding(get: { session.endpoint }, set: saveEndpoint), isEnabled: Binding( get: { app.aiConfiguration.modelProvider(modelProviderID)?.isEnabled ?? false }, - set: { app.aiConfiguration.setModelProviderEnabled($0, for: modelProviderID) })) - credentialSection + set: { value in updateProvider { $0.isEnabled = value } }), + showsEndpoint: !provider.usesVertexEndpoint) + if !provider.usesVertexEndpoint { credentialSection } + if session.endpoint.apiType == .generateContent { vertexSection(provider) } + if let error = session.error { Text(error).foregroundStyle(.red) } Section("Models") { Button(action: showModels) { @@ -92,7 +105,7 @@ struct ModelProviderEditorPage: View { } Section { - Button("Delete Provider", role: .destructive) { + Button("Delete", role: .destructive) { app.aiConfiguration.delete(modelProviderID: modelProviderID) deleted() } @@ -115,11 +128,14 @@ struct ModelProviderEditorPage: View { SecureField( "API Key", text: Binding( - get: { session.secret }, set: { session.secret = $0 } + get: { session.secret }, + set: { + session.secret = $0 + store(secret: $0) + } ), prompt: Text("Optional") ) - .onChange(of: session.secret) { _, value in store(secret: value) } Text( "The key is stored in this Mac's Data Protection Keychain." @@ -134,7 +150,7 @@ struct ModelProviderEditorPage: View { private func loadSecrets() { guard let provider else { return } - session.begin(with: provider, in: app.aiConfiguration) + session.begin(with: provider, in: app.aiConfiguration, isNew: isNew) } /// Mints a reference the first time something is typed, and clears the @@ -145,41 +161,82 @@ struct ModelProviderEditorPage: View { /// entry that holds nothing is not credentialled, and it should not look /// like it is. private func store(secret value: String) { + guard let provider else { return } + do { + try saveCredential(provider, kind: .apiKey, replacement: value.isEmpty ? nil : value) + session.error = nil + } catch { session.error = error.localizedDescription } + } + + private func saveName(_ value: String) { + session.name = value guard var provider else { return } - if value.isEmpty { - app.aiConfiguration.setSecret(nil, for: provider.credentialRef) - provider.credentialRef = "" - } else { - if provider.credentialRef.isEmpty { - provider.credentialRef = AIConfigurationModel.makeCredentialRef() - } - app.aiConfiguration.setSecret(value, for: provider.credentialRef) - } - app.aiConfiguration.upsert(provider) + provider.name = value + app.aiConfiguration.saveProvider(provider, endpoint: app.aiConfiguration.endpoint(for: modelProviderID)) } - private var endpoint: ProviderEndpoint { - app.aiConfiguration.endpoint(for: modelProviderID) + private func saveEndpoint(_ value: ProviderEndpoint) { + session.endpoint = value + guard var provider else { return } + if session.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { provider.name = "" } + app.aiConfiguration.saveProvider(provider, endpoint: value) } - /// A two-way binding into the stored provider. - /// - /// Reads and writes go through `AIConfigurationModel` rather than through a local copy, - /// so the row behind the sheet updates as you type and there is no "apply" - /// step that could be skipped. The `?? ModelProvider(name: "")` is unreachable - /// while the sheet is up — the `if let` in `body` has already established - /// the provider exists. - private func binding(_ keyPath: WritableKeyPath) -> Binding { - Binding( - get: { - app.aiConfiguration.modelProvider(modelProviderID)?[keyPath: keyPath] - ?? ModelProvider(name: "")[keyPath: keyPath] + private func vertexSection(_ provider: ModelProvider) -> some View { + ModelProviderVertexFields( + isEnabled: Binding( + get: { provider.usesVertex }, + set: { value in + updateProvider { $0.usesVertex = value } + }), + projectID: Binding( + get: { session.projectID }, + set: { value in + session.projectID = value + updateProvider { $0.vertex.projectID = value } + }), + location: Binding( + get: { session.location }, + set: { value in + session.location = value + updateProvider { $0.vertex.location = value } + }), + clientEmail: provider.vertex.clientEmail, hasCredential: !provider.vertex.credentialRef.isEmpty, + requestURL: ModelProviderForm.hasValidAddress(provider) ? provider.requestURL() : nil, + readFile: { try await ModelProviderServiceAccountFile.read($0) }, + importJSON: { json in + let account = try ModelProviderServiceAccount(json: json) + guard var current = self.provider else { return } + current.vertex.clientEmail = account.clientEmail + if current.vertex.projectID.isEmpty { current.vertex.projectID = account.projectID } + try saveCredential(current, kind: .serviceAccount, replacement: account.json) + session.projectID = current.vertex.projectID }, - set: { newValue in - guard var provider = app.aiConfiguration.modelProvider(modelProviderID) else { return } - provider[keyPath: keyPath] = newValue - app.aiConfiguration.upsert(provider) - } - ) + remove: { + guard var current = self.provider else { return } + current.vertex.clientEmail = "" + try saveCredential(current, kind: .serviceAccount, replacement: nil) + }) + } + + private func updateProvider(_ change: (inout ModelProvider) -> Void) { + guard var provider else { return } + change(&provider) + app.aiConfiguration.saveProvider(provider, endpoint: app.aiConfiguration.endpoint(for: modelProviderID)) + } + + private func saveCredential(_ value: ModelProvider, kind: ModelProviderCredentialKind, replacement: String?) throws + { + let ai = app.aiConfiguration + let reference = kind.reference(in: value) + let previous = ai.secret(for: reference) + if !reference.isEmpty, let error = ai.secretStorageError { + throw AIConfigurationModel.CredentialError.message(error) + } + try ai.saveProvider( + value, endpoint: ai.endpoint(for: value.id), + credentials: [ + .init(kind: kind, reference: reference, previous: previous, replacement: replacement) + ]) } } diff --git a/TFM/Features/Settings/ModelProviderSettings.swift b/TFM/Features/Settings/ModelProviderSettings.swift index 403a11c5..04df940a 100644 --- a/TFM/Features/Settings/ModelProviderSettings.swift +++ b/TFM/Features/Settings/ModelProviderSettings.swift @@ -27,8 +27,9 @@ struct ModelProviderSettings: View { // provider living in a view's `@State`. Cancelling takes it back // out again — which is what `isNew` below is for. let provider = ModelProvider(name: "") - app.aiConfiguration.upsert(provider) - edit(provider.id, true) + if app.aiConfiguration.saveProvider(provider, endpoint: ProviderEndpoint()) { + edit(provider.id, true) + } } Text( @@ -70,7 +71,9 @@ private struct ModelProviderRow: View { Text(provider.name) .lineLimit(1) Group { - if endpoint.isConfigured { + if provider.usesVertexEndpoint { + Text(ModelProviderForm.hasValidAddress(provider) ? provider.requestURL() : "Vertex AI") + } else if endpoint.isConfigured { Text(endpoint.url) } else { Text("Needs a URL") diff --git a/TFM/Features/Settings/SettingsPageView.swift b/TFM/Features/Settings/SettingsPageView.swift index 6dfd539a..cc47ed27 100644 --- a/TFM/Features/Settings/SettingsPageView.swift +++ b/TFM/Features/Settings/SettingsPageView.swift @@ -93,12 +93,14 @@ enum SettingsRoute: Hashable { struct SettingsPageView: View { @Environment(AppModel.self) private var app @Environment(WindowModel.self) private var window + @State private var newlyCreatedProviderID: ModelProvider.ID? var body: some View { detail.frame(maxWidth: .infinity, maxHeight: .infinity) .background(TermindColor.windowSurface) .onChange(of: window.settingsRoute) { oldRoute, newRoute in cleanUpDraft(from: oldRoute, movingTo: newRoute) + if case .modelProvider = oldRoute { newlyCreatedProviderID = nil } } .onDisappear { cleanUpDraft(from: window.settingsRoute, movingTo: .index) @@ -118,6 +120,7 @@ struct SettingsPageView: View { case .modelProvider(let id): ModelProviderEditorPage( modelProviderID: id, + isNew: newlyCreatedProviderID == id, showModels: { navigate(to: .providerModels(id)) }, deleted: { navigate(to: .category(.ai)) } ) @@ -162,7 +165,8 @@ struct SettingsPageView: View { SettingsFormPage { SFTPSettings() } case .ai: SettingsFormPage { - ModelProviderSettings { id, _ in + ModelProviderSettings { id, isNew in + newlyCreatedProviderID = isNew ? id : nil navigate(to: .modelProvider(id)) } ModelRoleSettings() diff --git a/TFMTests/AgentKnowledgeToolPresentationTests.swift b/TFMTests/AgentKnowledgeToolPresentationTests.swift new file mode 100644 index 00000000..2781c089 --- /dev/null +++ b/TFMTests/AgentKnowledgeToolPresentationTests.swift @@ -0,0 +1,178 @@ +import AgentKit +import Foundation +import TermindApplication +import TermindPresentation +import Testing + +@Suite struct AgentKnowledgeToolPresentationTests { + private let english = Locale(identifier: "en") + + @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + func oldMemoryMetadataIsRepairedInEveryState(_ language: String) { + let locale = Locale(identifier: language) + for state: AgentToolCard.State in [ + .proposed, .waitingForApproval, .waitingForUser, .reviewing, .running, + .completed, .failed, .denied, .interrupted, + ] { + var value = card( + "memory", + arguments: [ + "operations": .array([ + .object([ + "action": .string("add"), "target": .string("user"), "content": .string("private-body"), + ]) + ]) + ]) + value.state = state + let title = AgentToolPresentation.summary(for: value, locale: locale) + #expect(!title.contains("skill")) + #expect(!title.contains("技能")) + #expect(!title.contains("private-body")) + #expect(title.contains(language == "en" ? "memory" : language == "zh-Hans" ? "记忆" : "記憶")) + } + var value = card("memory", arguments: ["operations": .array([])]) + value.state = .completed + if language == "en" { + #expect(AgentToolPresentation.summary(for: value, locale: locale) == "Read persistent memory") + } + } + + @Test func eachKnowledgeModeHasTheCorrectActionAndTarget() { + let cases: [(String, [String: AgentJSONValue], String)] = [ + ("skills_list", [:], "Listed skills"), + ("skill_read_file", ["name": .string("guide")], "Read guide / SKILL.md"), + ("skill_read_file", ["name": .string("guide"), "path": .string(".")], "Listed guide"), + ("skill_install", ["name": .string("guide")], "Installed guide"), + ( + "skill_manage", + ["operations": .array([.object(["action": .string("delete"), "name": .string("guide")])])], + "Updated guide" + ), + ("memory", ["operations": .array([.object(["action": .string("add")])])], "Updated persistent memory"), + ("session_search", [:], "Listed saved conversations"), + ("session_search", ["query": .string("deploy")], "Searched deploy"), + ("session_search", ["conversation_id": .string("hidden-id")], "Read saved conversations"), + ] + for (name, args, expected) in cases { + let value = card(name, arguments: args) + #expect(AgentToolPresentation.summary(for: value, locale: english) == expected) + } + } + + @Test func mixedMemoryReadAndWriteBatchesKeepTheirActionsAndFailuresSeparate() { + let read = card("memory", arguments: ["operations": .array([])]) + var write = card("memory", arguments: ["operations": .array([.object(["action": .string("add")])])]) + write.state = .failed + let title = AgentToolPresentation.batchSummary(for: [read, read, write], locale: english) + #expect(title.contains("Read persistent memory")) + #expect(title.contains("2 tool calls")) + #expect(title.contains("failed to update persistent memory")) + #expect(!title.contains("skill")) + } + + @Test func unknownPresenterOrRemoteIdentityCannotSelectKnowledgePolicy() { + var value = card("memory") + value.descriptor.presentation?.presenterID = "remote.memory" + #expect(!AgentToolPresentation.usesCompactDetails(for: value.descriptor)) + value.descriptor.presentation?.presenterID = "builtin.memory" + value.descriptor.target = .mcp + #expect(!AgentToolPresentation.usesCompactDetails(for: value.descriptor)) + } + + @Test func approvalKeepsBodyOutOfItsTitleAndProjectsTheCompleteArguments() { + let body = String(repeating: "approval-only-content\n", count: 1_000) + let value = card( + "skill_manage", + arguments: [ + "operations": .array([ + .object([ + "action": .string("create"), "name": .string("example"), "content": .string(body), + ]) + ]) + ]) + let request = AgentApprovalRequest( + invocation: value.invocation, descriptor: value.descriptor, userIntent: "Create") + let approval = AgentToolPresentation.approval( + for: request, presenter: SkillManageTool.presenter, locale: english) + #expect(approval.title == "Update skills") + #expect(approval.detail.items.contains(.text(.init(title: "Content", text: body, style: .monospaced)))) + } + + @Test func failedKnowledgeResultsCannotDrawSuccessMessages() throws { + var value = card("skill_install") + value.state = .failed + value.result = AgentToolResult(callID: "call", content: "{\"saved\":true}", isError: true) + let detail = try #require( + AgentToolPresentation.detail(for: value, presenter: SkillInstallTool.presenter, locale: english)) + #expect( + !detail.items.contains { + if case .message(_, .success) = $0 { return true } + return false + }) + } + + @Test func titleLimitsDoNotChangeOriginalArguments() { + let query = "开始\n" + String(repeating: "🧭", count: 160) + let value = card("session_search", arguments: ["query": .string(query)]) + let title = AgentToolPresentation.summary(for: value, locale: english) + #expect(title.count <= 80) + #expect(!title.contains("\n")) + #expect(title.hasSuffix("…")) + #expect(value.invocation.call.arguments.objectValue?["query"]?.stringValue == query) + } + + @Test func wholeCardPreviewSharesItsBudgetAndPreservesFullCopy() { + let text = String(repeating: "中🧭", count: 900) + let detail = AgentToolDetail( + items: (0..<32).map { .text(.init(title: "Block \($0)", text: text, style: .plain)) }) + let preview = AgentToolDetailPreview(detail) + #expect(preview.isShortened) + #expect(preview.fullItems == detail.items) + let rendered = AgentToolDetailPreview(AgentToolDetail(items: preview.items)).fullText + // Separators are formatting, not source text; all source strings share the 600-character budget. + #expect(rendered.count <= 610) + #expect(preview.fullText.contains("Block 31")) + #expect(preview.fullText.hasSuffix(text)) + } + + @Test func listsShareFiveRowsAcrossSectionsAndShortDetailsRemainUnchanged() { + let rows = (0..<4).map { AgentToolDetail.ListRow(title: "Skill \($0)") } + let detail = AgentToolDetail(items: [ + .list(.init(title: "First", rows: rows)), .list(.init(title: "Second", rows: rows)), + ]) + let preview = AgentToolDetailPreview(detail) + let count = preview.items.reduce(0) { count, item in + if case .list(let list) = item { return count + list.rows.count } + return count + } + #expect(count == 5) + #expect(preview.isShortened) + let short = AgentToolDetail(items: [.list(.init(rows: [.init(title: "One")], emptyMessage: "None"))]) + #expect(!AgentToolDetailPreview(short).isShortened) + } + + @Test @MainActor func detailExpansionIsIndependentAndSurvivesReconfiguration() { + let state = AssistantTranscriptPresentationState() + let id = UUID() + state.toggleDisclosure(.toolDetail(id)) + let revision = state.layoutRevision(for: [id]) + state.followDefault(false, for: .tool(id)) + state.prune(validTimelineIDs: [id]) + #expect(state.isExpanded(.toolDetail(id))) + #expect(!state.isExpanded(.tool(id))) + #expect(state.layoutRevision(for: [id]) >= revision) + state.prune(validTimelineIDs: []) + #expect(!state.isExpanded(.toolDetail(id))) + } + + private func card(_ name: String, arguments: [String: AgentJSONValue] = [:]) -> AgentToolCard { + AgentToolCard( + invocation: .init(runID: UUID(), call: .init(id: "call", name: name, arguments: .object(arguments))), + descriptor: .init( + name: name, summary: name, inputSchema: .object([:]), target: .local, + approvalPolicy: .ask, + presentation: .init( + symbol: "brain", activity: .semanticArgument(key: "target", fallback: .skill), + presenterID: "builtin.\(name)", actionKind: .write)), state: .completed) + } +} diff --git a/TFMTests/KnowledgeToolRenderingTests.swift b/TFMTests/KnowledgeToolRenderingTests.swift new file mode 100644 index 00000000..1d948d99 --- /dev/null +++ b/TFMTests/KnowledgeToolRenderingTests.swift @@ -0,0 +1,74 @@ +import AgentKit +import AppKit +import Foundation +import SwiftUI +import TermindApplication +import TermindPresentation +import Testing + +@testable import Termind + +@Suite(.serialized) @MainActor +struct KnowledgeToolRenderingTests { + @Test(arguments: [380.0, 720.0]) + func knowledgeRowsStayCompactAndExpandWithoutLosingReaderState(_ width: Double) throws { + for language in [AppLanguage.english, .simplifiedChinese, .traditionalChinese] { + for card in AssistantTranscriptPreview.knowledgeCards(language: language) { + let state = TranscriptPresentationState() + state.disclosures.followDefault(true, for: .tool(card.id)) + let row = TranscriptNativeRow.make(for: .tool) + func configure() { + row.configure( + entry: .init(id: card.id, content: .tool(card)), proposedWidth: width, + configuration: .init( + isCompact: true, showsTerminalActions: false, isConversationStreaming: false, + locale: language.locale, reduceMotion: true, colorScheme: .light), + actions: .init(), presentation: state, markdownCache: TranscriptMarkdownCache(limit: 4), + measurement: false, invalidate: { _, _ in }) + } + configure() + let compact = row.measuredHeight(for: width) + #expect(compact > 20) + #expect(compact <= 320, "\(card.descriptor.name): \(compact)") + if width == 720, ProcessInfo.processInfo.environment["TERMIND_KNOWLEDGE_SCREENSHOTS"] == "1" { + try snapshot( + row, width: width, height: compact, name: "mac-\(language.rawValue)-\(card.descriptor.name)") + } + state.disclosures.toggleDisclosure(.toolDetail(card.id)) + configure() + let expanded = row.measuredHeight(for: width) + #expect(expanded >= compact) + if card.descriptor.name != "skill_install" { #expect(expanded > compact) } + configure() + #expect(state.disclosures.isExpanded(.toolDetail(card.id))) + } + } + } + + private func snapshot(_ row: NSView, width: Double, height: Double, name: String) throws { + let canvas = NSView(frame: CGRect(x: 0, y: 0, width: width + 32, height: ceil(height) + 32)) + canvas.wantsLayer = true + canvas.layer?.backgroundColor = NSColor.textBackgroundColor.cgColor + row.translatesAutoresizingMaskIntoConstraints = true + row.frame = CGRect(x: 16, y: 16, width: width, height: ceil(height)) + canvas.addSubview(row) + let window = NSWindow(contentRect: canvas.frame, styleMask: [.borderless], backing: .buffered, defer: false) + window.appearance = NSAppearance(named: .aqua) + window.contentView = canvas + window.orderFront(nil) + defer { + window.orderOut(nil) + window.contentView = nil + } + canvas.layoutSubtreeIfNeeded() + let bitmap = try #require(canvas.bitmapImageRepForCachingDisplay(in: canvas.bounds)) + canvas.cacheDisplay(in: canvas.bounds, to: bitmap) + let data = try #require(bitmap.representation(using: .png, properties: [:])) + let directory = FileManager.default.temporaryDirectory.appending(path: "knowledge-screenshots") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + let url = directory.appending(path: name + ".png") + try data.write(to: url) + row.removeFromSuperview() + print("Knowledge snapshot: \(url.path)") + } +} diff --git a/TFMTests/ModelProviderRenderingTests.swift b/TFMTests/ModelProviderRenderingTests.swift index abb5efd9..348ba7e4 100644 --- a/TFMTests/ModelProviderRenderingTests.swift +++ b/TFMTests/ModelProviderRenderingTests.swift @@ -2,6 +2,7 @@ import AgentKit import AppKit import SwiftUI import TermindApplication +import TermindDomain import Testing @testable import Termind @@ -26,6 +27,43 @@ struct ModelProviderRenderingTests { language: language, name: "provider-list") } + @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + func newProviderHintsAndVertexFieldsRender(_ language: String) async throws { + let ai = AIConfigurationModel(persistsSecrets: false) + let provider = ModelProvider(name: "") + #expect(ai.saveProvider(provider, endpoint: .init())) + let app = AppModel(hosts: HostLibraryModel(), ai: ai) + defer { app.shutdownForTermination() } + try await capture( + ModelProviderEditorPage(modelProviderID: provider.id, isNew: true, showModels: {}, deleted: {}).environment( + app), + language: language, name: "new-hints") + var vertex = ModelProvider(name: "Google", usesVertex: true) + vertex.vertex = VertexConfig( + projectID: "fixture-project", location: "global", + credentialRef: "fixture-reference", clientEmail: "fixture@example.invalid") + #expect(ai.saveProvider(vertex, endpoint: .init(apiType: .generateContent))) + try await capture( + ModelProviderEditorPage(modelProviderID: vertex.id, showModels: {}, deleted: {}).environment(app), + language: language, name: "vertex") + } + + @Test func newEditorKeepsEmptyBuffersWhileReopeningReadsSavedDefaults() throws { + let ai = AIConfigurationModel(persistsSecrets: false) + let provider = ModelProvider(name: "") + #expect(ai.saveProvider(provider, endpoint: .init())) + let saved = try #require(ai.modelProvider(provider.id)) + let newSession = ModelProviderEditorSession() + newSession.begin(with: saved, in: ai, isNew: true) + #expect(newSession.name.isEmpty) + #expect(newSession.endpoint.url.isEmpty) + let reopened = ModelProviderEditorSession() + reopened.begin(with: saved, in: ai) + #expect(reopened.name == "OpenAI") + #expect(reopened.endpoint.url == ModelAPIType.automatic.defaultURL) + #expect(!SettingsDraftPolicy.shouldDiscard(saved)) + } + private func capture(_ view: some View, language: String, name: String) async throws { let content = view.environment(\.locale, Locale(identifier: language)).frame(width: 680, height: 700) let controller = NSHostingController(rootView: content) diff --git a/TFMTests/SettingsSyncTests.swift b/TFMTests/SettingsSyncTests.swift index 737f696d..c64e0c71 100644 --- a/TFMTests/SettingsSyncTests.swift +++ b/TFMTests/SettingsSyncTests.swift @@ -100,9 +100,10 @@ struct SettingsSyncTests { var legacy: ModelContainer? = try ModelContainer( for: oldSchema, configurations: localModelConfiguration("Migration", schema: oldSchema, url: url)) - try AIPersistence(context: legacy!.mainContext).save( - AIConfigurationSnapshot( - providers: [provider], endpoints: [provider.id: endpoint], roles: [:])) + legacy!.mainContext.insert(PersistentModelProvider(provider, endpoint: endpoint, sortIndex: 0)) + for (index, model) in provider.models.enumerated() { + legacy!.mainContext.insert(PersistentAIModel(model, providerID: provider.id, sortIndex: index)) + } legacy!.mainContext.insert(PersistentAgentSkill(skill, sortIndex: 0)) try legacy!.mainContext.save() legacy = nil diff --git a/Termind.xcodeproj/project.pbxproj b/Termind.xcodeproj/project.pbxproj index eb6ff268..a4226969 100644 --- a/Termind.xcodeproj/project.pbxproj +++ b/Termind.xcodeproj/project.pbxproj @@ -2624,7 +2624,7 @@ repositoryURL = "https://github.com/AkinoKaede/AgentKit.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.13.1; + minimumVersion = 0.13.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Termind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Termind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2de1333b..072323e7 100644 --- a/Termind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Termind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/AkinoKaede/AgentKit.git", "state" : { - "revision" : "bbc233cf6cf4dc28c3f6e7d9b7b06d096f7b27f7", - "version" : "0.13.1" + "revision" : "3f293bfe76ea8693cb3dcf452a65d44421989cde", + "version" : "0.13.2" } }, {